Native interop with Kotlin/Java in Flutter

Native interop with Kotlin/Java in Flutter


Last year during the Fluttercon session about future of native interop I learned about this new tool called jnigen. It’s became quite stable recently and I wanted to try it out again.



Some background

In the past I used Xamarin and back then it was quite natural to invoke platform APIs from C# through something called bindings. In other words classes like Activity, Context, NSUrl, UIView were accessible directly from C# without any additional glue code. It was also possible with a bit of work to expose most of the native libraries in a similar fashion. Sort of similar thing is also available in Kotlin Multiplatform where you can access import namespaces like platform.Foundation.NSUUID. They even do some experiments with using SwiftUI views in Compose.



Current state of native interop in Dart/Flutter

Some might say that number of ways to interact with the platform from Dart is getting out of hand, sometimes leading to people choosing not to use native APIs at all or switch between random wrappers from pub.dev.1

When interacting with Java/Kotlin/Swift classes in Flutter you need to go via plugin that either includes some glue code using platform channels, or can use some type-safe approach like pigeon or protobuf. Most of the plugins are wrappers around platform-specific APIs e.g. to show notifications or access shared preferences. For some more advanced scenarios you may want to look into dart:ffi but the learning curve is quite steep for a typical mobile developer.

Even with the simplest method channel you have to write very similar code 3 times: in the platform interface, in the implementation for each platform, and then handling of it on the native side. In my case I just write it manually (+LLM) as it’s typically faster than any of the code generation methods I tried before.

From what we know Flutter team is actively working on enabling automatic direct native interop, see the short video below for some recent updates from “Flutter in Production” event (Dec 2024):




jnigen

The promise of jnigen is to generate Dart bindings from arbitrary Kotlin/Java code. How does it work:

jnigen scans compiled JAR files or Java source code to generate a description of the API, then uses it to generate Dart bindings. The Dart bindings call the C bindings, which in-turn call the Java functions through JNI. Shared functionality and base classes are provided through the support library, package:jni.

It’s a bit roundabout way, but it feels more understandable than what I remember from Xamarin bindings.



Simple example

Note: This is not meant to be a tutorial how to use jnigen, but rather an inspiration piece 🙂

A quite simple example can be found in the project repo where they call a suspend Kotlin function from Dart.

Given a simple Kotlin class:

import androidx.annotation.Keep
import kotlinx.coroutines.*

@Keep
class Example {
  public suspend fun thinkBeforeAnswering(): String {
    delay(1000L)
    return "42"
  }
}
Enter fullscreen mode

Exit fullscreen mode

after some code generation you can simply instantiate it and invoke the function with in async-await fashion:

final example = Example();
final answer = await example.thinkBeforeAnswering();
print(answer); // prints 42 after a while;
Enter fullscreen mode

Exit fullscreen mode

However, what is quite cool, you can also invoke functions synchronously. Given this Java class:


package com.example.in_app_java;

import android.app.Activity;
import android.widget.Toast;
import androidx.annotation.Keep;

@Keep
public abstract class AndroidUtils {
  public static void showToast(Activity mainActivity, CharSequence text, int duration) {
    mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, text, duration).show());
  }
}
Enter fullscreen mode

Exit fullscreen mode

A simplified invocation would look like this:

import 'package:jni/jni.dart';

final activity = JObject.fromReference(Jni.getCurrentActivity());
AndroidUtils.showToast(activity, message.toJString(), 0);
Enter fullscreen mode

Exit fullscreen mode

As you might have noticed you get some handy helpers to retrieve the current activity or context.

If you ask me, this is exactly what I was looking for.



Example with platform view

To play a bit more I wanted to rewrite some of actual production code from the app I’m working on to use jnigen instead of method channels. I’ve been using a fork of this pdf plugin2 that not only is a classic Flutter plugin, but also includes a platform view.

After getting rid of the platform channel calls I had to figure out how to control the pdfView instance that is hosted in the platform view. The simplest way I found was to keep it in a static field that gets accessed through a god-like controller class. FlutterPDFView is slightly modified platform view implementation from the original plugin.

@Keep
public class PDFViewController {
    public void setPage(int page){
        if (FlutterPDFView.pdfView == null){
            return;
        }

        FlutterPDFView.pdfView.jumpTo(page);
    }
// ...
}
Enter fullscreen mode

Exit fullscreen mode

Having my PDFViewController that can access the plugin’s platform view instance I generated the bindings with jnigen and was able to instantiate this class in Dart. What is super exciting is that when referencing the instance in Dart you get to see the exact same memory address as on the native side.

Debugging the PDFViewController class in Java

Note the same memory address when debugging PDFViewController class from Dart and Java

I needed a way to get notified about page changes from the native side. There’s no support for generating bindings for streams or similar communication channels, so you have to pass a listener that can include a callback to get invoked.

After some back and forth I learned that you can generate bindings for Java interfaces that later get implemented in Dart. It blew my mind. In other words I get to create a Dart class that in runtime is implementing a Java interface. This lets me pass it to my previously instantiated class and use it on the Java side.

Given this Java interface:

@Keep
public interface PDFStatusListener {
    void onLoaded();
    void onPageChanged(int page, int total);
    void onError(String error);
    void onLinkRequested(String uri);
    void onDisposed();
}
Enter fullscreen mode

Exit fullscreen mode

You can instantiate the implementation of it in Dart:

final listener = pdf.PDFStatusListener.implement(
    pdf.$PDFStatusListener(
        onLoaded$async: true,
        onPageChanged$async: true,
        onError$async: true,
        onLinkRequested$async: true,
        onDisposed$async: true,
        onLoaded: () {
            print('PDF Loaded');
        },
        onPageChanged: (int? page, int? total) {
            print('PDF page: $page, total: $total');
        },
        onError: (JString? string) {
            print('PDF error: ${string?.toDartString()}');
        },
        onLinkRequested: (JString? string) {
            print("Link: ${string?.toDartString()}");
        },
        onDisposed: () {
            print('PDF disposed');
            example.release();
        },
    ),
);
example.setPdfStatusListener(listener);
Enter fullscreen mode

Exit fullscreen mode

Being able to pass an instance of class as listener allows me to convert it to any form I like whether it’s a Dart stream or just a void callback. It still requires writing some wrappers and glue code, but somehow it feels more valuable than repetitive platform channels.



What about Swift

There’s experimental bindings generator for Swift: swiftgen. I tried it only once, but will definitely try to reproduce the same example and perhaps share my findings here.

Hope you enjoyed this and perhaps you’ll try it for yourself. Truth is, without Flutter developers getting interested, there’s no real incentive to push forward.

Cheers!


Other resources:


  1. I’ve heard several time how people were switching between various audio player packages just because of some missing features or little bugs, notification plugins, camera implementations etc. I wish we wouldn’t have to go through this “pick and choose” flow with fundamental capabilities like media playback or basic OS functionalities. ↩

  2. I keep a private fork that uses different implementation on iOS and builds well with my app. ↩



Source link
lol

By stp2y

Leave a Reply

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

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.