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.
This post was inspired by Sovik Biswas explorations here.
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.
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.
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());
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>
npm install @nativescript/flutter
Flutter
wherever desiredBe 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)
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');
}
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}}" />
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.
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.
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 🌸