Back to Blog Home
← all posts

Combine Flutter with NativeScript

May 10, 2023 — by Technical Steering Committee (TSC)

Flutter offers yet another option for creative view development with NativeScript. Let's look at how to enrich Flutter with Bluetooth capabilities using @nativescript-community/ble.

👉 Demo Repo

This post was inspired by Sovik Biswas explorations here.

Create a Flutter module to use with NativeScript

Prerequisites:

Whether you have an existing Flutter app or just want to mix in different Flutter views throughout your NativeScript app, we can use them as a Flutter module.

1. Add Flutter to a NativeScript app

You can use Flutter in any existing NativeScript app or by creating a new one with ns create.

We can then create a Flutter module at the root of the project directory:

flutter create --template module flutter_views

Note: You can run flutter run --debug or flutter build ios from inside this flutter_views folder as any normal Flutter project to develop it.

Learn more from the Flutter documentation here.

2. Configure your Dart code to have named entry points

Named entry points allow us to use different Flutter views in our NativeScript app by matching the entry point with the view id, for example: <Flutter id="myFlutterView" />

  • main.dart
@pragma('vm:entry-point')
void myFlutterView() => runApp(const MyFlutterView());

3. Configure platforms for usage

For brevity, we'll demonstrate iOS in this post, however you can see the full example repo which includes Android as well.

App_Resources/iOS/Podfile should contain the following to reference our Flutter module.

platform :ios, '14.0'

flutter_application_path = '../../flutter_views'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)

post_install do |installer|
    flutter_post_install(installer) if defined?(flutter_post_install)
end

Add Flutter debug permissions to App_Resources/iOS/Info.plist:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow Flutter tools to debug your views.</string>
<key>NSBonjourServices</key>
<array>
  <string>_dartobservatory._tcp</string>
</array>

4. Install @nativescript/flutter

npm install @nativescript/flutter

5. Use Flutter wherever desired

Be sure to initialize the Flutter engine before bootstrapping your app, typically in app.ts or main.ts:

import { init } from '@nativescript/flutter';
init();

// bootstrap app...

Use Flutter anywhere.

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
  xmlns:ui="@nativescript/flutter">
    <ui:Flutter id="myFlutterView"></ui:Flutter>
</Page>

When using flavors, you can just register the element for usage in your markup:

import { Flutter } from '@nativescript/flutter'

// Angular
import { registerElement } from '@nativescript/angular'
registerElement('Flutter', () => Flutter)

// Solid
import { registerElement } from 'dominative';
registerElement('flutter', Flutter);

// Svelte
import { registerNativeViewElement } from 'svelte-native/dom'
registerNativeViewElement('flutter', () => Flutter);

// React
import { registerElement } from 'react-nativescript';
registerElement('flutter', () => Flutter);

// Vue
import Vue from 'nativescript-vue'
Vue.registerElement('Flutter', () => Flutter)

Prepare Flutter for bi-directional NativeScript communication

We'll be using platform channels to setup bi-directional communication between Flutter and NativeScript.

In our Dart code, we only need one single nativescript channel to handle any platform behavior we might want. For json payload messaging, we'll setup a BasicMessageChannel with the StringCodec. We can also setup a message handler in our state constructor.

  • main.dart
import 'package:flutter/services.dart';
import 'dart:convert';

class _MyPageState extends State<MyPage> {
  static const platform = BasicMessageChannel('nativescript', StringCodec());

  _MyPageState() {
    // Receive platform messages from NativeScript
    platform.setMessageHandler((String? message) async {
      Map<String, dynamic> platformMessage = jsonDecode(message!);
      switch (platformMessage['type']) {
        case 'salutation':
          // use any data from the platform
          String hello = platformMessage['data'];
          print(hello);
          break;
      }
      return 'success';
    });
  }
}
  • ns-view.html
<Flutter id="myFlutterView" loaded="loadedFlutter"></Flutter>
  • ns-view.ts
let flutter: Flutter;

export function loadedFlutter(args) {
  flutter = args.object;

  // Send Flutter messages from NativeScript
  flutter.sendMessage('salutation', 'hello');
}

Enable Bluetooth via NativeScript

npm install @nativescript-community/ble

Add Bluetooth permissions to App_Resources/iOS/Info.plist:

<key>UIRequiredDeviceCapabilities</key>
<array>
  <string>bluetooth-le</string>
  <string>location-services</string>
</array>
<key>UIBackgroundModes</key>
<array>
  <string>bluetooth-central</string>
  <string>bluetooth-peripheral</string>
  <string>location</string>
</array>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Use Bluetooth to connect to your devices</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Use Bluetooth to connect to your devices</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Use location with Bluetooth devices</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Use location with Bluetooth devices</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Use location with Bluetooth devices</string>

Setup the Bluetooth plugin to start/stop scanning while also sending discovered devices to Flutter.

import { Utils } from "@nativescript/core";
import { Bluetooth, Peripheral } from "@nativescript-community/ble";
import { Flutter, FlutterChannelType } from "@nativescript/flutter";

let flutter: Flutter;
// Flutter can call NativeScript through these channel types
const channel: FlutterChannelType = {
  startScanning: _startScanning,
  stopScanning: _stopScanning,
};

const bluetooth: Bluetooth = new Bluetooth();
const discoveredPeripherals: Array<{ name: string; address: string }> = [];
// reduce extraneous bluetooth scan results when emitting to Flutter
const throttleScanResults = Utils.throttle(_throttleScanResults, 600);

function _throttleScanResults() {
    flutter.sendMessage('scanResults', discoveredPeripherals);
}

function _startScanning() {
  bluetooth.on(Bluetooth.device_discovered_event, result => {
    const peripheral = <Peripheral>result.data;
    if (peripheral?.name) {
      if (!discoveredPeripherals.find((p) => p.address === peripheral.UUID)) {
        discoveredPeripherals.push({
          name: peripheral.name.trim(),
          address: peripheral.UUID?.trim(),
        });
      }
      throttleScanResults();
    }
  });
  bluetooth.startScanning({});
}

function _stopScanning() {
    bluetooth.stopScanning();
    bluetooth.off(Bluetooth.device_discovered_event);
}

We can bind channel to our Flutter component. This creates a contract of message types between NativeScript and Flutter to communicate.

<Flutter id="myFlutterView" channel="{{channel}}" />

Receive Bluetooth data in Flutter from NativeScript

In our main.dart file we could setup the contract of message types to receive the data.

_MyPageState() {
  platform.setMessageHandler((String? message) async {
    Map<String, dynamic> platformMessage = jsonDecode(message!);
    switch (platformMessage['type']) {
      case 'scanResults':
        // prepare results for presentation in a ListView
        List<BluetoothDevice> devices = platformMessage['data']
            .map<BluetoothDevice>((i) => BluetoothDevice.fromJson(i))
            .toList();
        setState(() {
          _devicesList = devices;
        });
        break;
    }
    return 'success';
  });
}

// Bindable methods for Flutter to communicate to NativeScript
void stopScanning() {
  Map<String, dynamic> message = {'type': 'stopScanning'};
  platform.send(jsonEncode(message));
}

void startScanning() {
  // List<BluetoothDevice> devices = [];
  Map<String, dynamic> message = {'type': 'startScanning'};
  platform.send(jsonEncode(message));
}

We can now setup our Widgets to bind to our data. Rather than showing the entire Flutter Widget tree, you can see a complete example here.

Taking things further with IoT and Arduino

This post was heavily inspired by Sovik Biswas excellent explorations here wherein you could challenge yourself to talk to an Arduino device for even more interesting capabilities.

Cheers 🍻

Just like Open Native creates possibilities for framework and platform developers to combine their strengths cross-ecosystems, Flutter developers can also utilize NativeScript plugins to enrich project possibilities.

It's productive and rewarding to collaborate with your peers; no matter their background or skillset.

Cheers to a welcoming tech community with joyful collaboration 🌸