The Ultimate Showdown: MethodChannel vs. JNI - Unraveling the Secrets of Native Integration in Flutter
Tags: flutter, dart, jnigen, google
Published: 2023-06-14 15:02:22

We recently had **Google I/O 2023** and with them we received incredible news for **Flutter**, in one of them, we were introduced to a new approach to System Interoperability with Native Android, more specific: **JNIgen**.

## What is JNIgen?

For developers familiar with **Java**, the name **JNI** is not really new, **JNI **being a **Java Native Interface** that allows Java/Kotlin code to interact with written native code in C/C++ and even Assembly in some cases.

In a more visual way, in the image below we can see that the **JNI** works as a bridge between the codes:
![JNI Bridge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/clrzaqnmr9v8k1yshhtx.png)

Therefore, **JNIgen** follows the same principle plus the generator, that is, this Dart Interoperability tool was designed to generate all the necessary bindings by analyzing the java code, generating the native code in C/C++ and the bindings in Dart, all this automatically.

## Ok, but why use JNIgen and not just use MethodChannel?

To answer this question, we first need to see the differences between each in practice.

For this example, we will use the [ML Kit Google Code Scanner API](https://developers.google.com/ml-kit/vision/barcode-scanning?hl=en). This should be the end result:
![GIF of final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4x70iyb5gbtrp2ueqqc.gif)

So we'll do it both ways: **MethodChannel** and **JNIgen**.

## 1 - MethodChannel

There is no need to define these basic steps:
1. Creating a flutter project([Read more here](https://docs.flutter.dev/get-started/test-drive?tab=androidstudio))
2. Adding the necessary gradle dependencies for the ML Kit API ([Read more here](https://developers.google.com/ml-kit/vision/barcode-scanning/code-scanner?hl=pt-br) )

After following the basic steps, this should be your MainActivity:
```
class MainActivity: FlutterActivity() {

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
  }
}
```
_I used Kotlin as my native language, for some it might look different([More here](https://docs.flutter.dev/platform-integration/platform-channels?tab=type-mappings-kotlin-tab))_

Let's continue.

## Steps:
**1 - Creating the communication channel:**
Now we must define the name of our channel like this:
```
private val CHANNEL = "com.example.com/Scanner"
```

Then we need to define the function responsible for the handler that the method will call, like this:
```
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }
```

In the end, the code will look like this;

```
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.com/Scanner"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }
  }
}
```


**2- Adding barcode function:**
Now, if you've read the ML Kit guide above, the code below is self-explanatory:
```
val scanner = GmsBarcodeScanning.getClient(this)
scanner.startScan()
    .addOnSuccessListener { barcode ->
        // Task completed successfully
    }
    .addOnCanceledListener {
        // Task canceled
    }
    .addOnFailureListener { e ->
        // Task failed with an exception
    }
```

Well, we know that the response we will get from this method is not synchronous, so we will launch this method inside a new Thread so that we can handle the response:
```
val scanner = GmsBarcodeScanning.getClient(this)
object : Thread() {
scanner.startScan()
    .addOnSuccessListener { barcode ->
        // Task completed successfully
    }
    .addOnCanceledListener {
        // Task canceled
    }
    .addOnFailureListener { e ->
        // Task failed with an exception
    }
}.start()
```

Now we can get the response and return it as a response from the channel we created:
```
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.com/Scanner"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      if (call.method == "getBarCode") {
                val scanner = GmsBarcodeScanning.getClient(this)
                object : Thread() {
                    override fun run() {
                        scanner.startScan()
                            .addOnSuccessListener { barcode ->
                                result.success(barcode.rawValue)
                            }
                            .addOnCanceledListener { ->
                                result.success("")
                            }
                            .addOnFailureListener { e ->
                                result.error("Exception", "Found Exception", e)
                            };
                    }
                }.start()
            }
    }
  }
}
```

Almost forgot, **getBarCode** is the method we'll call to access the Barcode Scanner from our dart code.

**3- Configuring the communication Dart Channel:**
You probably got a main.dart file with a default example code, you can remove any unnecessary code, for this case we will work with the StatefulWidget class, first let's define the methodChannel with the same channel name as we gave it in our native code:
```
static const platform = MethodChannel('com.example.com/Scanner');
```

So let's create a method that will handle the communication, we can call this function **_getBarCode**, this method must be a **Future**, as we mentioned before, the scanner is not synchronous, so the return will not be immediate:
```
Future<void> _getScanner() async {}
```

Within this method we have the code responsible for calling and handling the response from the channel:
```
await platform.invokeMethod('getBarCode');
```

Inside the **invokeMethod** we pass as an argument the method that we added in the native code that calls the Barcode function. Now, we know that the method will return a string with the code value we read, so we can wrap it in a String:
```
String scanned = await platform.invokeMethod('getBarCode');
```

Now we must wrap this inside a function and we must add a way to handle any exceptions that may occur, in the end this should be the end result:
```
void _getScanner() async {
  String? scanned;
  try {
    scanned = await platform.invokeMethod('getBarCode');
  } on PlatformException catch (e) {
    scanned = "";
  }

  setState(() {
    result = scanned ?? "";
  });
}
```

Then we can call this function in our code, which should look like this:
```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyScannerPage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyScannerPage extends StatefulWidget {
  const MyScannerPage({super.key, required this.title});

  final String title;

  @override
  State<MyScannerPage> createState() => _MyScannerPageState();
}

class _MyScannerPageState extends State<MyScannerPage> {
  static const platform = MethodChannel('com.example.com/Scanner');
  String result = "";

  void _getScanner() async {
    String? scanned;
    try {
      scanned = await platform.invokeMethod('getBarCode');
    } on PlatformException catch (e) {
      scanned = "";
    }

    setState(() {
      result = scanned ?? "";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              "ScanResult: ",
              style: TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
              ),
            ),
            Text(
              result,
              style: const TextStyle(color: Colors.black),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _getScanner();
        },
        tooltip: 'Scan',
        child: const Icon(Icons.qr_code_scanner),
      ), 
    );
  }
}
```

And with that we arrive at the result we were looking for:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4x70iyb5gbtrp2ueqqc.gif)

## 2- JNIGen

Now, let's get the same result but with a different approach, again, no need to explain the basic steps:
1. Creating a flutter project([Read more here](https://docs.flutter.dev/get-started/test-drive?tab=androidstudio))
2. Adding the necessary gradle dependencies for the ML Kit API ([Read more here](https://developers.google.com/ml-kit/vision/barcode-scanning/code-scanner?hl=pt-br) )

## Steps:
**1- Creating a class with scanner code**
As with **MethodChannel**, you'll create a native class with the code that will handle the QrCode scan, something like this:
```
class Scanner {
    suspend fun getCode(context: Context): String? {
        val scanner = GmsBarcodeScanning.getClient(context)
        return suspendCoroutine<String?> { continuation ->
            scanner.startScan()
                .addOnSuccessListener { barcode ->
                    continuation.resume(barcode.rawValue)
                }
                .addOnCanceledListener {
                    continuation.resume("")
                }
                .addOnFailureListener { e ->
                    continuation.resumeWithException(e)
                }
        }
    }
}
```
The difference here is that we can do this in a normal class, normal function without the need for a channel or a handler.

**2- Adding JNIgen to the project**
To work with **JNIgen**, we need to add 2 libs to our **pubspec.yaml**, we can do this with just a few commands:
```
flutter pub add jni dev:jnigen
```
This will add **jni** as a dependency and **jnigen** as a dev_dependence.

**3- Creating the configuration file**
Now that we have the **JNIgen** dependency in our project, we can define the configuration we want, for this we need to create a new _yaml_ file in the root of the project, you can define the name whatever you want, like **jnigen.yaml* *, inside this file we would have the code below:
```
android_sdk_config:
  # The add_gradle_deps: true property allow the bindings to compile the Android dependencies and classpath to the jnigen
  add_gradle_deps: true

  # The suspend_fun_to_async: true property is pretty explanatory itself, but to resume, it will convert Kotlin suspend functions to Dart async functions
suspend_fun_to_async: true

output:
  # In the output we set two languages, the first will be native code in "c"  and the other will be the one we use in Flutter, meaning "Dart"
  c:
    # In this property you will set the name of the library we are creating, this way jnigen will set the generated code according to this name.
    library_name: scanner
    # For this property, you first need to create this path in your project, it will the path were the generated native code will be deployed.
    path: src/scanner/
  dart:
    # For this property, you first need to create this path in your project, it will the path were the generated bindings code will be deployed.
    path: lib/scanner.dart
    # This set the structure of the bindings that will be generated in dart, it can be file-per-class(package_structure) or all wrapped in a single file(single_file)
    structure: single_file

  # In this property we set the directory to the class we will need
source-path:
  - 'android/app/src/main/kotlin/ao/marco_domingos/scanner_api_example'

  # In this property we set the exact class we will need, we set it with his package_name only this way the jnigen will be able to find it
classes:
  - 'ao.marco_domingos.scanner_api_example.Scanner'
```
In this file you can read the comments to understand all its properties.

**4- Generating the bindings**
Now that we have the configuration file ready, all that remains is to generate the bindings, we can do it with this command:
```
dart run jnigen --config jni.yaml
```

But, jnigen needs to scan compiled **jar** files to generate the bindings, so you might get an error if you just run the above command without a compiled **jar** file, so first you need to run the below command and only after the above:
```
flutter build apk
```

If everything worked fine, you can check the path you set in the configuration file and you will find all the generated bindings.

**5- Adding generated native code**
Now we need to change our **app/build.gradle** with the following line of code inside the android property:

```
externalNativeBuild {
    cmake {
        path "../../src/scanner/CMakeLists.txt"
    }
}
```

This will add the **CMakeList** file that was generated by **JNIgen** into our **build.gradle**.

**6- Calling native function getCode**
Now we need to call our native function, but before that we need to initialize the JNI communication channel between Dart and native code, we can do this by adding the code below to our main:
```
Jni.initDLApi();
```

Now we can call our native function like this:

```
  void _getScanner() async {
    String? scanned;
    try {
      final scanner = await Scanner().getCode(JObject.fromRef(Jni.getCachedApplicationContext()));
      scanned = scanner.toDartString();
    } catch (e) {
      scanned = "";
    }

    setState(() {
      result = scanned ?? "";
    });
  }
```

Looking at our function, we can see that we can call the native class and function directly from our Dart code without having to define a channel and method as we would have to with MethodChannel. And when we run it, the result is:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4x70iyb5gbtrp2ueqqc.gif)
Exactly what we wanted!

If we go to see both codes, we can see that in both we use 50% Kotlin and 50% Dart, but the JNI facilitates the interoperation between the native code that we have and the dart without the need to create a channel, a platform to manipulate it, calling native code just like other Dart code. If that doesn't convince you, then let's talk about **Game Changer**.

## Game Changer
So with **JNIgen** we have a game changer, in MethodChannel we can't reduce the % of native code we use to work with a native Feature, we can just increase it, but with JNIgen we can reduce, like now , I'll just use 10% Kotlin and 90% Dart to get the same result.

First, we can go to our native class **Scanner.kt** and remove any traces of it and we will have our class like this:

```
class Scanner {}
```

Okay, now let's just add a small function to handle a task and convert it to a suspend function:

```
class Scanner {
    suspend fun <T> Task<T>.await(): T {
        return suspendCancellableCoroutine { continuation ->
            addOnSuccessListener { result ->
                continuation.resume(result)
            }

            addOnFailureListener { exception ->
                continuation.resumeWithException(exception)
            }
        }
    }
}
```

Now you might ask: _If there is no native code to call the scanner, how can we scan your code?_

Well, this is where the magic starts, let's go back to our jnigen configuration file and add some classes responsible for accessing the Scanner:

```
classes:
  - 'ao.marco_domingos.scanner_api_example.Scanner'
  - 'com.google.mlkit.vision.codescanner.GmsBarcodeScanning'
  - 'com.google.mlkit.vision.codescanner.GmsBarcodeScanner'
  - 'com.google.mlkit.vision.barcode.common.Barcode'
  - 'com.google.mlkit.vision.barcode.common.internal.BarcodeSource'
  - 'com.google.android.gms.tasks.Task'
```

Now, remember that jnigen only generates the bindings according to a compiled jar file, we already ran **flutter build apk** in the beginning, but we made changes in our native code, so we need to run **flutter build apk ** again to compile the latest changes in the jar file and only then run **dart run jnigen --config jnigen.yaml** again to update the bindings.

After finishing this part let's go to what really matters, our function and change our old code to this one:
```
  void _getScanner() async {
    String? scanned;
    try {
      final scanner = GmsBarcodeScanning.getClient(JObject.fromRef(Jni.getCachedApplicationContext()));
      final result = await Scanner().await0(scanner.startScan());
      scanned = result.getRawValue().toDartString();
    } catch (e) {
      scanned = "";
    }

    setState(() {
      result = scanned ?? "";
    });
  }
```

Looking at this code you can see that the code we had in our native Kotlin class is now in our Dart function and the only code we have in our native class is the function that converts the scanner task to a suspend function which is converted to an async function in our Dart code as defined in the config file.

Now you might ask _Does it work?_

Here is your answer:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xk1lc3yz3qbnx1kpzw6z.gif)

This is the next level of Dart interop tools, remembering that **JNIgen** is still actively under development, this is what the tool can do in its early stages, what will the tool be able to do in the later stages? Perhaps we can develop features with 0% native code, which is actually already possible with some API's and JNIgen.

You can see all the code used in this article in [here](https://github.com/Marco4763/scanner_api_example)

And if you are curious about the link that is returned when reading the QRCode, here it is:

{% embed https://www.youtube.com/watch?v=-P-ein58laA %}

Hope you enjoyed the reading.

Follow my profile for more articles and interesting projects:
[Github](https://github.com/Marco4763)
[LinkedIn](https://www.linkedin.com/in/marco-domingos-239756171/)