[MUSIC PLAYING]
HOSSEIN YOUSEFI: Hi.
My name is Hossein Yousefi, and I'm
a software engineer on the Dart Native Interop Team.
MARIAM HASNANY: Hi.
My name is Mariam Hasnany, and I'm a product manager
on the Flutter team.
Today we're going to share a new approach for system
interoperability that allows Flutter app or plugin
developers to call Android libraries directly from Dart.
Let's imagine you want your Flutter mobile app to access
the device hardware for things like user's location,
camera, notifications, and more.
Traditionally, you would use a plug-in
with a method channel, which allows
you to interact with platform APIs
by sending and receiving messages
between Dart and Native code.
There are over 20,000 plugins that
exist for the Android platform in the Flutter ecosystem
on pub.dev today.
While plugins are great for building
a reusable way for Flutter apps to access platform APIs,
developing them with method channels can be time consuming
and require a lot of code.
For example, to implement the Flutter camera plugin that
calls Android's Camera2 API, we used method channels
to write handlers to call and return
responses between the Flutter plugin
and the native code located in camera.java.
We also had to ensure that types and parameters being
sent and received matched.
To make it easier and faster for developers
who want to access the underlying native platform when
developing Flutter plugins or apps,
we have a suite of tools for Dart
interoperability with platform APIs.
Pigeon is a cogent tool for generating typesafe interop
code to communicate between Flutter and its host platform.
It allows you to define a description of your plugin's
API in a Dart file and then generate
skeleton code for Dart, Java, and Objective-C
for handling messages that you would usually write manually
with method channels.
Pigeon is great for developing Flutter plugins that
are used across multiple platforms because
of its unified interface, and it's easier
to use than method channels for adding
Flutter code to a native app.
We just used Pigeon ourselves to rewrite the Flutter camera
plugin to support Android's latest CameraX
library, which is now available for you to try on pub.dev.
But some of you may wish there was
a way you could use Dart directly
to call platform APIs so you didn't have to touch
the native side of the code.
For this scenario, we created a tool called of FFIgen
that generates Dart bindings for C, Objective-C, or SwiftUI code
and can be used to implicitly call
iOS APIs without needing to add handlers to the native code.
JNIgen is the newest tool we are actively developing
and the focus of this session.
Similar to FFIgen, JNIgen generates
Dart bindings so that you can instantiate
Java objects, call Kotlin methods, and more
without leaving Dart.
Unlike Pigeon, FFIgen and JNIgen can
be used when developing purely Dart apps in addition
to Flutter apps or Flutter plugins.
All right, Hossein.
Let's go through a concrete example
to get a better understanding of how JNIgen works.
Here's a very simple Java class called
Example that has a static method called sum
for adding two integers.
What do we need to do to generate Dart bindings so
that we can use this Java method in a Flutter app?
[MUSIC PLAYING]
HOSSEIN YOUSEFI: To generate Dart bindings for a given Java
class, we need to take four steps.
One, add that JNI and JNIgen dependencies.
Two, create the top level jnigen.yaml config file.
Three, run JNIgen to generate the bindings, and four,
add external native builds to app/build.gradle.
So let me show you how that's done in code.
First, run Flutter pub add JNI.
Dev:jnigen. This adds package JNI to dependencies and package
JNIgen to dev dependencies.
Then, let's create a file called jnigen.yaml.
This file has options for specifying
where to store the generated bindings,
where the Java source is located,
and which classes to generate from.
Now we can run JNIgen to generate the bindings.
Lastly, let's add the path to CMakeLists.txt generated
by JNIgen to add/build.gradle.
Great.
Now that the bindings are generated,
I will import the generated bindings in the main file
where my Flutter UI code is located.
The UI is basically two text fields that take in the numbers
I want to add.
Now, I can use the example.sum method to calculate their sum
and show it in a text widget.
Using the example.sum method in Dart
is identical to how you would use it in Java.
Now, I can run the example app with inputs 15 and 23
to get the output 38.
[MUSIC PLAYING]
MARIAM HASNANY: Thanks, Hossein.
Now that we have learned how to use JNIgen,
I'd like to go a little bit deeper
to explain exactly how it works.
First, what is JNI?
JNI stands for Java Native Interface,
which allows Java code that runs inside a Java virtual machine
to interoperate with applications written in other
native programming languages like C and C++.
We've created package JNI in Dart
as a support library that enables us to use
JNI to interop with Java code.
We have a similar mechanism called
Dart FFI, which stands for Foreign Function Interface,
to interop with C code.
In fact, package JNI leverages Dart FFI under the hood
so that Dart code can interop with C code,
and then we use JNI to interop C code with Java code.
We call this interop code that glues the languages together
bindings.
We could just use package JNI and write the Dart bindings
ourselves, but instead, it's easier
to let package JNIgen generate them for us.
Now that we've covered the basics,
I believe since you can compile Kotlin to Java bytecode,
you can also generate Dart bindings for Kotlin as well.
Android has a fairly new jetpack library
called Health Connect that enables
Android developers to access the health and fitness
data on a user's device.
Let's use JNIgen to leverage this Kotlin based
API to build a pedometer app.
For this app, I am thinking we show the total number of steps
I've taken for a specific day.
It seems like Health Connect can read
aggregated data, such as the total number of steps
in a given period.
Since this is more complex than adding a Java class
file from the previous example, Hossein, how do we start?
[MUSIC PLAYING]
HOSSEIN YOUSEFI: Let's take the simple Flutter application
called Step Counter.
When pressing the Refresh button,
we want the step counts to be updated
with the number of steps we've taken in the last 24 hours.
I've already added JNI and JNIgen dependencies.
To incorporate Health Connect into this, let's
check out this online documentation
and follow the steps there.
I already installed Health Connect on my phone.
I also have Google Fit which tracks
the number of steps I'm taking and stores it
in Health Connect.
I also added the necessary dependency in app/build.gradle.
The note here specifies that Health Connect SDK is
compatible with API level 26.
So I made sure the min SDK version and target SDK
version are at least 26.
Also, since we are using the latest version
of Health Connect, I updated the Kotlin version
of our application.
Following the guide, I added all necessary pieces
like permissions to AndroidManifest.xml.
With all the setup done, we need to see if Health Connect is
available on device, instantiate it, and retrieve
the aggregate data we want.
We can generate the Dart bindings for Kotlin classes
as we encounter them in the documentation incrementally
using JNIgen. First, let's see if Health Connect
is available on device or not.
For this, we'll need to generate bindings for Health Connect
clients and contexts.
To do this, we first create a JNIgen.yaml
file just like before.
Here, I said add gradle deps to true.
This way JNIgen can also generate bindings
from our gradle dependencies.
I choose the ASM backend which generates bindings
from Java bytecode instead of Java source code.
By setting suspend fun to async to true,
JNIgen converts Kotlin's suspended functions
to Dart's asynchronous methods.
I specified the path through the output files here.
And finally, I specify the classes
that I want to generate bindings from.
For now, let's add Health Connect clients and contexts.
We must run flutter build apk once
before generating the code so gradle
can actually resolve the dependencies.
Now, let's run JNIgen like before, and voila.
We successfully generated the bindings.
Again, let's not forget to add external native builds
to app/build.gradle.
Back to the documentation.
Now that we have generated the bindings,
we can replicate the HealthConnect availability
check in Dart.
First, to get the context, we can
use jni.getCachedApplicationContext
from package JNI.
This returns a pointer to the application context.
Then we can wrap this pointer in a generated context
object using Context.fromRef.
Now let's check the status of Health Connect
by calling the sdkStatus method.
The second parameter is optional,
so we can use the generated method that
only gets a context argument.
But why is the methods name sdkStatus1?
Because in Java, we can have multiple methods
with the same name that take different types
and number of arguments.
To differentiate them in the generated Dart code,
JNIgen adds a number to the end of their names.
All right, back to the code.
If Health Connect is not available,
we can show some message prompting our users
to install it.
Otherwise, let's create a client and pass it to our main widget.
Here's the Kotlin code from the documentation
that shows us how to read aggregated data from Health
Connect.
As I mentioned before, to be able to write
this code in Dart, we have to generate
the classes used like aggregates request and time range filter.
In this specific example, distance record is used,
but we want to know the total number of steps.
So we should use steps record instead.
So let's go back to JNIgen.yaml, add the other classes we need,
and regenerate the bindings.
Now we can create an aggregate request for the total count
of steps after yesterday.
You can see how I used the generated classes
like aggregate request and time range filter in my code here.
Now that we have the aggregate request,
let's call client.aggregate and await the response.
We want to make sure the return response is not null.
If so, let's take the number of steps and update the counter.
One last step before running the app,
we must call Jni.initDLApi.
This initializes the communication channels
between Dart and native platform,
which is necessary for asynchronous communication
and callbacks.
We are done.
Now let's run the app.
When we press the Refresh button,
the text gets updated with the number of steps
we've taken in the last day.
You can find the full code here, and there is also
a more complete example in this link, which
shows the chart of the steps you've taken in the last day.
The example has support for Android using
JNIgen and iOS using FFIgen.
[MUSIC PLAYING]
MARIAM HASNANY: We've now demoed two examples using the JNIgen
package.
First, we built a simple Flutter app
that uses Dart to call the sum Java method.
Next, we wrapped Health Connect Jetpack Library with JNIgen
and created a nice Dart API, exposing the classes we needed.
We then used start bindings to call the steps record
class to build a pedometer app.
Flutter has thousands of plugins that
let you access Android APIs like the Health Connect client.
While developing plugins with method channels
is still a viable approach for communicating with platform
APIs, it can be cumbersome and require a lot of manual code.
That's why we created Codegen solutions for Dart
interoperability with platform APIs that
address the same problem but with different approaches.
Let's recap what they are.
Pigeon is a faster, typesafe way of developing Flutter plugins
across multiple platforms because it creates
a unified interface that can be used to communicate
between Flutter and native Android, iOS,
and desktop platforms.
On the other hand, FFIgen automatically
generates Dart bindings to access C, Objective-C, or Swift
libraries directly with Dart.
So you don't have to touch the native side of the code.
We now have a similar tool for Android development called
JNIgen that also generates Dart bindings to call
methods and instantiate objects that
are written in Java or Kotlin.
JNIgen and FFIgen can be used together
to develop a cross-platform Flutter
app that calls iOS and Android APIs directly with Dart.
When using JNIgen, there are four steps.
First, add JNI and JNIgen dependencies.
Second, create a top level JNIgen.yaml config file.
Then, run JNIgen to generate the bindings.
And finally, add external native builds to app/build.gradle.
For more details, check out our online documentation linked
in the description below.
Currently, JNIgen is still considered experimental,
but it is ready for you to try.
We'd love your feedback on how to make
this work better for your scenarios
as we get it to stable.
Thank you so much for watching.
[MUSIC PLAYING]