Calling iOS Native Code in Flutter

In this tutorial, we will create a small Flutter function that calls iOS Swift native code to read the value of the battery level.

Flutter uses a flexible system that allows you to call platform-specific APIs whether available in Kotlin or Java code on Android, or in Swift or Objective-C code on iOS.

Flutter’s built-in platform-specific API support does not rely on code generation, but rather on a flexible message-passing style.

Basically, the Flutter part of the app sends messages to its host iOS or Android part of the app, over a platform channel. The host listens on the platform channel, receives the message and calls platform-specific API’s using the native programming language, then sends the response back to the client(Flutter part of the app).

To make it work we will need to do the following:

  1. Create a new Flutter project,
  2. Create a MethodChannel object,
  3. Implement a function that calls the native iOS code,
  4. Prepare UI to display the result,
  5. Implement a new function in Swift that reads the battery level.

Flutter

A named channel for communicating with platform plugins using asynchronous method calls. Method calls are encoded into binary before being sent, and binary results received are decoded into Dart values. The MethodCodec used must be compatible with the one used by the platform plugin.

1. Create a new Flutter project

To start off we will need to create a new Flutter project in Android Studio(or any IDE that you might be using), if you don’t know how to create a project you can refer to the https://www.appsdeveloperblog.com/hello-world-app-in-flutter/ tutorial.

2. Create the MethodChannel

Then we will construct the channel. We will use a MethodChannel with a single platform method that will return the battery level of the host by declaring the following constant:

static const platform = const MethodChannel('samples.flutter.dev/battery');

Next, we will invoke a method on the channel, where we will specify a String identifier.

static const platform = const MethodChannel('samples.flutter.dev/battery');

final int result = await platform.invokeMethod('getBatteryLevel');

3. Create a flutter function that calls the native iOS code

Below is a short code example that uses the MethodChannel and demonstrates how to write a function to get the battery level on the device.

static const platform = const MethodChannel('samples.flutter.dev/battery');
String _batteryLevel = 'Battery Level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

As you can see in the code we created a Future function called “_getBatteryLevel()” that awaits for the “platform.invokeMethod(‘getBatteryLevel’)” which is the function we will discuss further down in the iOS function section. The flutter app will first check the platform. Once the platform is confirmed, the app will listen to the platform channel and check for the function that’s being called.

4. Flutter UI to display the result

As you can see we declared a String variable _batteryLevel and gave it a value “Battery Level”. Then we have declared a Future void _getBatteryLevel() function which is asynchronous. Inside the void, we are declaring an empty String “batteryLevel”. Then we wrap the call in a try-catch statement in case the method fails. In the try part of the statement, we will check the native method check battery and pass it in the batteryLevel as a String, then catch any error that might occur. After the try-catch statement, we simply set the state of  _batteryLevel to a “batteryLevel” value.

Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(

              child: Text('Get Battery Level'),
              onPressed: _getBatteryLevel,
            ),
            Text(_batteryLevel),
          ],
        ),

Complete Flutter Code

Below is a complete code example in Flutter. But we are not done yet. We still need to work on the native iOS code for this to work.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/cupertino.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.dev/battery');
  String _batteryLevel =
      'Battery Level';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }


  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(

              child: Text('Get Battery Level'),
              onPressed: _getBatteryLevel,
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

The Flutter code is now ready. Let’s see how the native iOS code will look like.

iOS Swift. Creating the FlutterMethodChannel

In the AppDelegate.swift file override the “application:didFinishLaunchingWithOptions” function and create a “FlutterMethodChannel” that will be tied to the samples.flutter.dev/battery channel name. To find the AppDelegate.swift file, open the “ios” folder in your flutter project, open the “Runner” folder which contains the file “AppDelegate.swift” in its root. Have a look at the screenshot below for the location:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
   
              batteryChannel.setMethodCallHandler({
              [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
              
              // Note: this method is invoked on the UI thread.
              guard call.method == "getBatteryLevel" else {
               result(FlutterMethodNotImplemented)
               return
              }

            self?.receiveBatteryLevel(result: result)
          })

     GeneratedPluginRegistrant.register(with: self) 

     return super.application(application, didFinishLaunchingWithOptions: launchOptions) 
  } 
}

iOS Swift. The receiveBatteryLevel() Function

We are now ready to create the receiveBatteryLevel() Swift function that will actually read and return the buttery level value on the device.  To do that we will use the native iOS Battery API. Add the following function at the bottom of AppDelegate.swift.

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

After this, you should be able to see the output on your app, after running it.

Note: If you use an emulator you will probably get “battery info unavailable”.

I hope this Flutter tutorial was helpful for you. If you are interested in learning Flutter, please check other Flutter tutorials on this site. Some of them have video tutorials included.

Happy mobile app development with Flutter 🙂!


Leave a Reply

Your email address will not be published. Required fields are marked *