TIKI

Share this post
How to Call Dart from Android or iOS Native Code
blog.mytiki.com
Developers

How to Call Dart from Android or iOS Native Code

Add a Dart library to a native app to work seamlessly with Kotlin, Java, Swift, or Objective-C code

Ricardo Gonçalves
and
Mike Audi
Dec 6, 2022
Share this post
How to Call Dart from Android or iOS Native Code
blog.mytiki.com

Imagine discovering an awesome Dart library… the tiki-sdk-dart library, for example (which is both awesome and in Dart). If you want to use that library in an Android or iOS app, the no-brainer solution is to create/add it to a Flutter app. Then use Flutter to compile the entire app to native platform code.

But what if you already have a native app? How do you add a Dart library to a native app to work seamlessly with your Kotlin, Java, Swift, or Objective-C code?

In this case, the Dart code is compiled ahead-of-time (AOT) into an XCFramework (iOS) or AAR (Android) that can be imported into your native project. Then you wrap the Dart code in native functions to expose the required Dart APIs.

Sounds like too much work? It is! Luckily, this is precisely what Flutter does when compiling apps natively. If Flutter already does this, why not leverage Flutter to package Dart code for native libraries?

One of Flutter’s features is the ability to call native code from Dart code using Platform Channels. In Flutter, it is easy to find example documentation on how to call native APIs with Dart code. But what is not obvious is that Platform Channels are a two-way binding, making it possible to call Dart code from native code, too.

View Source Code


Dart Project

First, we set up a new Dart project called hello_tiki.

hello_tiki/lib/hello_tiki.dart
class HelloTiki {
  void hello() {
    print("hello 🍍!");
  }
}
hello_tiki/pubspec.yaml
name: hello_tiki
description: Hello TIKI library.

version: 1.0.0+1

environment:
  sdk: '>=2.18.2 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

...

Flutter Plugin

Flutter Plugins are a special type of package combining Dart code and platform-specific (native) code. Without a plugin, if you try to add your platform-specific code to the .android or .ios folders, it will be overwritten anytime you run a flutter pub get. In the parent directory of your Dart project, create a plugin.

flutter create -t plugin --platforms android,ios hello_tiki_plugin

Adding your Dart Project as a plugin dependency.

hello_tiki_plugin/pubspec.yaml
name: hello_tiki_plugin
description: A new Flutter plugin project.
version: 0.0.1

environment:
  sdk: '>=2.18.4 <3.0.0'
  flutter: ">=2.5.0"

dependencies:
  flutter:
    sdk: flutter
  hello_tiki:
    path: ../hello_tiki
  plugin_platform_interface: ^2.0.2

...

flutter:
  plugin:
    platforms:
      android:
        package: com.example.hello_tiki_plugin
        pluginClass: HelloTikiPlugin
      ios:
        pluginClass: HelloTikiPlugin

In the plugin project, we’re going to add the Platform Channel handlers for both Dart and iOS/Android. First, for the Dart side, Flutter generated 3 files (hello_tiki_plugin, hello_tiki_plugin_method_channel, and hello_tiki_plugin_platform_interaction). First, add the method channel constructor to the hello_tiki_plugin.

hello_tiki_plugin/lib/hello_tiki_plugin.dart
...

class HelloTikiPlugin {
  final MethodChannelHelloTikiPlugin methodChannelHelloTikiPlugin;

  HelloTikiPlugin()
      : methodChannelHelloTikiPlugin = MethodChannelHelloTikiPlugin();

  ...
}

Next, define your method handlers in the hello_tiki_plugin_method_channel method call handler

hello_tiki_plugin/lib/hello_tiki_plugin_method_channel.dart
...

import 'package:hello_tiki/hello_tiki.dart';

class MethodChannelHelloTikiPlugin extends HelloTikiPluginPlatform {

  ...  

  Future<void> methodHandler(MethodCall call) async {
    switch (call.method) {
      case "hello_tiki":
        HelloTiki().hello();
        break;
    }
  }
}

With our Dart side of the handlers defined, we’re going to flip over and set up the Android/iOS handlers. Plugins are constructed and registered the FlutterEngine PluginRegistry on engine construction —meaning, to explicitly call plugin functions, you need to retrieve the registered instance.

hello_tiki_plugin/android/src/main/kotlin/com/example/hello_tiki_plugin/HelloTikiPlugin.kt
class HelloTikiPlugin() : FlutterPlugin, MethodCallHandler {
    private lateinit var channel: MethodChannel

    companion object {
        fun fromEngine(flutterEngine: FlutterEngine): HelloTikiPlugin {
            return flutterEngine.plugins.get(HelloTikiPlugin::class.java) as HelloTikiPlugin
        }
    }

    ...

    fun sayHello() {
        channel.invokeMethod("hello_tiki", null)
    }
}
hello_tiki_plugin/ios/Classes/SwiftHelloTikiPlugin.swift
import Flutter
import UIKit

public class SwiftHelloTikiPlugin: NSObject, FlutterPlugin {
    var channel: FlutterMethodChannel
    
    public init(registrar: FlutterPluginRegistrar) {
        channel = FlutterMethodChannel(name: "hello_tiki_channel", binaryMessenger: registrar.messenger())
        super.init()
        registrar.addMethodCallDelegate(self, channel: channel)
    }
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = SwiftHelloTikiPlugin(registrar: registrar)
    }

    ...
    
    public func sayHello(){
        channel.invokeMethod("hello_tiki", arguments:  nil)
    }
}

Now that we have Dart code that can be called from native functions, we can package it up to run in our iOS/Android apps.

Flutter Module

The Flutter Module is a special kind of package that allows Flutter code to be imported and used inside native projects. It is mainly used in the add-to-app process —when you want to add Flutter code to an existing project without having to rewrite the whole project in Flutter.

So, if we want to import our hello-tiki example into a native project, we must first create a Flutter module with the Dart code. The Flutter module will compile our code, embed the Dart VM, and package it into a native library. Create a Flutter module in the parent directory for your plugin:

flutter create -t module hello_tiki_module

Next, we add the plugin as a dependency to the module.

hello_tiki_module/pubspec.yaml
name: hello_tiki_module
description: Flutter Module to Hello TIKI library.

version: 1.0.0+1

environment:
  sdk: '>=2.18.2 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  hello_tiki_plugin:
    path: ../hello_tiki_plugin

...

flutter:
  uses-material-design: true
  module:
    androidX: true
    androidPackage: com.example.hello_tiki_module
    iosBundleIdentifier: com.example.hello_tiki_module

Import to iOS or Android

Now we can follow the add-to-app instructions and compile our library for iOS or Android. I have chosen the simplest approaches for Android and iOS: Gradle and CocoaPods.

Note: These steps assume you already have working iOS and Android projects. If you do not, first create demo projects for each in the same parent directory as your Dart module.

Android (Gradle):

  1. Make sure that your hello_tiki_module is in the same parent directory as your Android Project. If not, adjust the file routes in #2 accordingly.

  2. Include the Flutter module as a subproject in the host app’s settings.gradle by adding to the bottom of the file:

    setBinding(new Binding([gradle: this]))
    evaluate(new File(
            settingsDir.parentFile,
            'hello_tiki_module/.android/include_flutter.groovy'
    )) 
    
    include ':hello_tiki_module'
    project (':hello_tiki_module').projectDir = new File(settingsDir.getParentFile(), 'hello_tiki_module')

    Note: If you get a “Build was configured to prefer settings repositories over project repositories but repository 'maven' was added by plugin class 'FlutterPlugin'“ error, then

    1. Remove the dependencyResolutionManagement block from settings.gradle file.

    2. Add to the project’s build.gradle

      allprojects {
          repositories {
              google()
              mavenCentral()
          }
      }
  3. Add implementation project(':flutter') to your App’s build.gradle dependencies block

  4. Run a Gradle Sync

    Note: Android Studio may warn about the "Binding" import. You can safely ignore it.

  5. Once it is synced, you need to configure the FlutterEngine. If you’re using Flutter UIs, FlutterActivity has some easy helper methods to do this. In this example, we’re not, so we’re going to init the engine manually.

    private fun initFlutter(context: Context): FlutterEngine {
            val loader = FlutterLoader()
            loader.startInitialization(context)
            loader.ensureInitializationComplete(context, null)
            val flutterEngine = FlutterEngine(context)
            flutterEngine.dartExecutor.executeDartEntrypoint(
                DartExecutor
                    .DartEntrypoint
                    .createDefault()
            )
            GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine)
            return flutterEngine
    }

    Call init on Application startup. For example, in onCreate for your main Activity

    ...
    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            
            val flutterEngine = initFlutter(this.applicationContext)
            
            ...
        }
    }
  6. Done! Now, you can use the Hello TIKI methods in Android:

HelloTikiPlugin.fromEngine(flutterEngine).sayHello()

In Logcat look for "hello 🍍!"

iOS (CocoaPods):

  1. Set the platform version to 13

    platform :ios, '13.0'
  2. Add the following lines to your Podfile:

    Note: Change flutter_application_path if the module is not in the parent directory.

    flutter_application_path = '../hello_tiki_module'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  3. For each Podfile target that needs to embed Flutter, call:

    install_all_flutter_pods(flutter_application_path)

    for example:

    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
  4. In the Podfile’s post_install block, call flutter_post_install (installer):

    post_install do |installer|
      flutter_post_install(installer) if defined?(flutter_post_install)
    end
  5. Run pod install

  6. The final step is to configure the FlutterEngine and register the plugin. We use a manual plugin registration to retain an instance of the FlutterPlugin to call the sayHello function.

    class FlutterDependencies: ObservableObject {
        let flutterEngine = FlutterEngine(name: "my flutter engine")
        var helloTikiPlugin: SwiftHelloTikiPlugin
        
        init(){
            flutterEngine.run()
            let registrar = flutterEngine.registrar(forPlugin: "HelloTikiPlugin")
            helloTikiPlugin = SwiftHelloTikiPlugin(registrar: registrar!)
        }
        
        @main
        struct D2N_ExampleApp: App {
            @StateObject var flutterDependencies = FlutterDependencies()
            ...
        }
    }
  7. Done! Now you can use Hello Tiki methods in iOS:

    helloTikiPlugin.sayHello()

View Source Code

And that’s it! You’re calling Dart functions from native code by leveraging Flutter Platform Channels.

This is a very simple example and works very well for internal code or simple dart library consumption. To learn how to build and distribute native libraries written in Dart, like we do for tiki-sdk-dart, check out our platform-specific blogs.

Please share and help a dev get some sleep.

Share

Share this post
How to Call Dart from Android or iOS Native Code
blog.mytiki.com
Previous
Next
A guest post by
Ricardo Gonçalves
Mobile lead and founding team member at TIKI.
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 TIKI Inc.
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing