DEV Community

Zigmas Slušnys
Zigmas Slušnys

Posted on • Updated on

Cross-platform native development differences between Flutter and NativeScript

Introduction

I am going to compare the amount of code and the ease of entry to begin developing mobile apps with Flutter and NativeScript concerning functionality that needs direct access to native platform APIs. We're not going to compare performance, plugins or programming languages.

Flutter

As we all have heard, Flutter has been a widely talked topic which is a native mobile development framework that's using Dart as its programming language.

It caught my eye as well and I decided to give it a shot and jump the hype train in order to try to ride it out and see what the fuss is all about.

NativeScript

This is another native performance like development framework using JavaScript as its programming language. The interesting approach NativeScript takes is to use JavaScript Virtual Machines - Google’s V8 for Android and WebKit’s JavaScriptCore implementation distributed with iOS 7.0+ in order to get access to the native APIs of both platforms.

Use case

While looking into flutter a question hit me to see how easy it is to work with native APIs from both Android and iOS platforms accessing platform specific functions. I've found the original flutter post about battery level access in order to display it on the application screen
(check out the post here: https://flutter.dev/docs/development/platform-integration/platform-channels )

This lead me to remember how I used to access native APIs of NativeScript and therefore got me writing this post you're reading right now.

NativeScript

In the case of NativeScript - it uses platform declaration files that have been written to easily navigate through native api's of the device. Lets see what does it take to Check and have a continuously updated level of battery on our screen.

Repository can be found at: https://github.com/slushnys/nativescript-battery-level-check

// main-view-model.ts
// Logic of the battery checking

export class BatteryLevelModel extends Observable {
    public batteryLevel: number;

    constructor() {
        super();
        this.initBatteryStatus();
    }

    private initBatteryStatus(): void {
        if (isAndroid) {
            appModule.android.registerBroadcastReceiver(
                android.content.Intent.ACTION_BATTERY_CHANGED,
                (context, intent: android.content.Intent) => {
                    const level = intent.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
                    const scale = intent.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
                    this.set("batteryLevel", Math.round((level / scale) * 100));
                }
            );
        } else {
            UIDevice.currentDevice.batteryMonitoringEnabled = true;
            this.set("batteryLevel", Math.round(UIDevice.currentDevice.batteryLevel * 100));

            appModule.ios.addNotificationObserver(UIDeviceBatteryLevelDidChangeNotification, () => {
                const newLevel = Math.round(UIDevice.currentDevice.batteryLevel * 100);
                this.set("batteryLevel", newLevel);
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So what's happening here you may ask? Its pretty simple when you think about it - we can browse through native API reference for android and ios devices and implement exactly the same functionality writing TypeScript using same namespaces, classes and functions in order to achieve our desired results.

Flutter

I'm no expert in nether Dart nor Flutter, however to my understanding in order to check the battery level on a flutter application, we would need to understand and implement objective-c/swift or java/kotlin code. Lets have a short example on how flutter implements this functionality from their example page:

Repository can be found at: https://github.com/slushnys/flutter/tree/master/examples/platform_channel_swift

Android implementation part is in Java language

package com.example.platformchannel;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
  private static final String BATTERY_CHANNEL = "samples.flutter.io/battery";
  private static final String CHARGING_CHANNEL = "samples.flutter.io/charging";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine);

    new EventChannel(flutterEngine.getDartExecutor(), CHARGING_CHANNEL).setStreamHandler(
      new StreamHandler() {
        private BroadcastReceiver chargingStateChangeReceiver;
        @Override
        public void onListen(Object arguments, EventSink events) {
          chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
          registerReceiver(
              chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        }

        @Override
        public void onCancel(Object arguments) {
          unregisterReceiver(chargingStateChangeReceiver);
          chargingStateChangeReceiver = null;
        }
      }
    );

    new MethodChannel(flutterEngine.getDartExecutor(), BATTERY_CHANNEL).setMethodCallHandler(
      new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, Result result) {
          if (call.method.equals("getBatteryLevel")) {
            int batteryLevel = getBatteryLevel();

            if (batteryLevel != -1) {
              result.success(batteryLevel);
            } else {
              result.error("UNAVAILABLE", "Battery level not available.", null);
            }
          } else {
            result.notImplemented();
          }
        }
      }
    );
  }

  private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) {
    return new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);

        if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
          events.error("UNAVAILABLE", "Charging status unavailable", null);
        } else {
          boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                               status == BatteryManager.BATTERY_STATUS_FULL;
          events.success(isCharging ? "charging" : "discharging");
        }
      }
    };
  }

  private int getBatteryLevel() {
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
      return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    } else {
      Intent intent = new ContextWrapper(getApplicationContext()).
          registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
      return (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
          intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

iOS implementation part is in Swift programming language.

import UIKit
import Flutter

enum ChannelName {
  static let battery = "samples.flutter.io/battery"
  static let charging = "samples.flutter.io/charging"
}

enum BatteryState {
  static let charging = "charging"
  static let discharging = "discharging"
}

enum MyFlutterErrorCode {
  static let unavailable = "UNAVAILABLE"
}

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
  private var eventSink: FlutterEventSink?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError("rootViewController is not type FlutterViewController")
    }
    let batteryChannel = FlutterMethodChannel(name: ChannelName.battery,
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      self?.receiveBatteryLevel(result: result)
    })

    let chargingChannel = FlutterEventChannel(name: ChannelName.charging,
                                              binaryMessenger: controller.binaryMessenger)
    chargingChannel.setStreamHandler(self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    guard device.batteryState != .unknown  else {
      result(FlutterError(code: MyFlutterErrorCode.unavailable,
                          message: "Battery info unavailable",
                          details: nil))
      return
    }
    result(Int(device.batteryLevel * 100))
  }

  public func onListen(withArguments arguments: Any?,
                       eventSink: @escaping FlutterEventSink) -> FlutterError? {
    self.eventSink = eventSink
    UIDevice.current.isBatteryMonitoringEnabled = true
    sendBatteryStateEvent()
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(AppDelegate.onBatteryStateDidChange),
      name: UIDevice.batteryStateDidChangeNotification,
      object: nil)
    return nil
  }

  @objc private func onBatteryStateDidChange(notification: NSNotification) {
    sendBatteryStateEvent()
  }

  private func sendBatteryStateEvent() {
    guard let eventSink = eventSink else {
      return
    }

    switch UIDevice.current.batteryState {
    case .full:
      eventSink(BatteryState.charging)
    case .charging:
      eventSink(BatteryState.charging)
    case .unplugged:
      eventSink(BatteryState.discharging)
    default:
      eventSink(FlutterError(code: MyFlutterErrorCode.unavailable,
                             message: "Charging status unavailable",
                             details: nil))
    }
  }

  public func onCancel(withArguments arguments: Any?) -> FlutterError? {
    NotificationCenter.default.removeObserver(self)
    eventSink = nil
    return nil
  }
}
Enter fullscreen mode Exit fullscreen mode

And that is not taken into account the part where you have to define channels so that your flutter application could communicate to the implemented native functionality. Maybe I'm being biased right now, but I feel that to implement a simple functionality in NativeScript is much more simple, user friendly and accessible to beginners with less learning curve.

Conclusion

At the end of the day, it's all about the problems that you're trying to solve. For me, flutter has the advantage using Skia as a drawing framework to represent each pixel fluently. That's what people are excited about - design. From functional point of view - if I would want to implement something platform specific I would probably choose NativeScript as I don't want to learn the intrinsics of Swift/Objective-C or Java/Kotlin in order to achieve something as simple as a battery level check.

Let me know what you think and which on would you choose and why?

Oldest comments (3)

Collapse
 
azimuthapps profile image
azimuthapps

"In the case of NativeScript - it uses platform declaration files that have been written to easily navigate through native api's of the device. Lets see what does it take to Check and have a continuously updated level of battery on our screen."

The question is, who has written these platform declaration files? Will they always be up to date? Or will a future Android API release possibly deprecate some of these features requiring an upstream maintainer to update these binding libraries?

This seems to be the same approach as what Xamarin uses, with their Android API binding libraries. They are automatically generated, I assume NativeScript is the same. Whenever there is an API platform update, these binding libraries are updated, so you will always depend on these binding libraries.

The other issue I see with this approach is that when you write your own code in .ts and use these binding libraries, if you have a bug or an error that you introduce, and it throws an exception, the exception will be a NativeScript exception, not a native library exception. I used to experience this personally with Xamarin, and googling exceptions is extremely difficult (as, logically, there will always be more people experiencing that issue in the native language than in C# or whatever, so your chances of finding a fix are reduced).

The Flutter way of doing this is to abstract away the platform specific functionality into that platforms language. It means you have to know a bit of Kotlin or Swift to get around but it also means that you are 100% in control of your own app and you're not relying on these binding libraries to be up to date. Which I think is a lot, lot better. Of course that's just my 2 cents :)

Collapse
 
slushnys profile image
Zigmas Slušnys

Thank you for your opinion. It's a valid one concerning the debugging as the errors are really like you say, hard to figure out on what's happening. This was probably most annoying thing for me to work with NS in terms of productivity.

However having the native API library definitions just seems very easy to work with when developing some native functionality requirements.

Beats knowing Swift and Kotlin by far in my eyes as it could get complex to write native platform code.

Again, thanks for commenting and have a happy Valentine's day. 🙋🏻‍♂️

Collapse
 
nathanwalker profile image
Nathan Walker • Edited

NativeScript has a metadata generator whereby platform APIs are up to date at build time (by each individual developer) - this allows anyone at anytime to generate up to date types at their own disposal for any SDK version they are targeting (eg, 'ns typings ios' or 'ns typings android') - no maintainer or third party required. You are in 100% control 100% of the time.

The author is correct here that there are two different technical problems that each address. One important point perhaps is that NativeScript developer can take advantage of Skia inside their app - not sure the same could be said for the inverse.