Cross-platform native development differences between Flutter and NativeScript

slushnys

Zigmas Slušnys

Posted on January 9, 2020

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?

💖 💪 🙅 🚩
slushnys
Zigmas Slušnys

Posted on January 9, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related