Cross-platform native development differences between Flutter and NativeScript
Zigmas Slušnys
Posted on January 9, 2020
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);
});
}
}
}
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);
}
}
}
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
}
}
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?
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
January 9, 2020