• United States+1
  • United Kingdom+44
  • Afghanistan (‫افغانستان‬‎)+93
  • Albania (Shqipëri)+355
  • Algeria (‫الجزائر‬‎)+213
  • American Samoa+1684
  • Andorra+376
  • Angola+244
  • Anguilla+1264
  • Antigua and Barbuda+1268
  • Argentina+54
  • Armenia (Հայաստան)+374
  • Aruba+297
  • Australia+61
  • Austria (Österreich)+43
  • Azerbaijan (Azərbaycan)+994
  • Bahamas+1242
  • Bahrain (‫البحرين‬‎)+973
  • Bangladesh (বাংলাদেশ)+880
  • Barbados+1246
  • Belarus (Беларусь)+375
  • Belgium (België)+32
  • Belize+501
  • Benin (Bénin)+229
  • Bermuda+1441
  • Bhutan (འབྲུག)+975
  • Bolivia+591
  • Bosnia and Herzegovina (Босна и Херцеговина)+387
  • Botswana+267
  • Brazil (Brasil)+55
  • British Indian Ocean Territory+246
  • British Virgin Islands+1284
  • Brunei+673
  • Bulgaria (България)+359
  • Burkina Faso+226
  • Burundi (Uburundi)+257
  • Cambodia (កម្ពុជា)+855
  • Cameroon (Cameroun)+237
  • Canada+1
  • Cape Verde (Kabu Verdi)+238
  • Caribbean Netherlands+599
  • Cayman Islands+1345
  • Central African Republic (République centrafricaine)+236
  • Chad (Tchad)+235
  • Chile+56
  • China (中国)+86
  • Christmas Island+61
  • Cocos (Keeling) Islands+61
  • Colombia+57
  • Comoros (‫جزر القمر‬‎)+269
  • Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)+243
  • Congo (Republic) (Congo-Brazzaville)+242
  • Cook Islands+682
  • Costa Rica+506
  • Côte d’Ivoire+225
  • Croatia (Hrvatska)+385
  • Cuba+53
  • Curaçao+599
  • Cyprus (Κύπρος)+357
  • Czech Republic (Česká republika)+420
  • Denmark (Danmark)+45
  • Djibouti+253
  • Dominica+1767
  • Dominican Republic (República Dominicana)+1
  • Ecuador+593
  • Egypt (‫مصر‬‎)+20
  • El Salvador+503
  • Equatorial Guinea (Guinea Ecuatorial)+240
  • Eritrea+291
  • Estonia (Eesti)+372
  • Ethiopia+251
  • Falkland Islands (Islas Malvinas)+500
  • Faroe Islands (Føroyar)+298
  • Fiji+679
  • Finland (Suomi)+358
  • France+33
  • French Guiana (Guyane française)+594
  • French Polynesia (Polynésie française)+689
  • Gabon+241
  • Gambia+220
  • Georgia (საქართველო)+995
  • Germany (Deutschland)+49
  • Ghana (Gaana)+233
  • Gibraltar+350
  • Greece (Ελλάδα)+30
  • Greenland (Kalaallit Nunaat)+299
  • Grenada+1473
  • Guadeloupe+590
  • Guam+1671
  • Guatemala+502
  • Guernsey+44
  • Guinea (Guinée)+224
  • Guinea-Bissau (Guiné Bissau)+245
  • Guyana+592
  • Haiti+509
  • Honduras+504
  • Hong Kong (香港)+852
  • Hungary (Magyarország)+36
  • Iceland (Ísland)+354
  • India (भारत)+91
  • Indonesia+62
  • Iran (‫ایران‬‎)+98
  • Iraq (‫العراق‬‎)+964
  • Ireland+353
  • Isle of Man+44
  • Israel (‫ישראל‬‎)+972
  • Italy (Italia)+39
  • Jamaica+1876
  • Japan (日本)+81
  • Jersey+44
  • Jordan (‫الأردن‬‎)+962
  • Kazakhstan (Казахстан)+7
  • Kenya+254
  • Kiribati+686
  • Kosovo+383
  • Kuwait (‫الكويت‬‎)+965
  • Kyrgyzstan (Кыргызстан)+996
  • Laos (ລາວ)+856
  • Latvia (Latvija)+371
  • Lebanon (‫لبنان‬‎)+961
  • Lesotho+266
  • Liberia+231
  • Libya (‫ليبيا‬‎)+218
  • Liechtenstein+423
  • Lithuania (Lietuva)+370
  • Luxembourg+352
  • Macau (澳門)+853
  • Macedonia (FYROM) (Македонија)+389
  • Madagascar (Madagasikara)+261
  • Malawi+265
  • Malaysia+60
  • Maldives+960
  • Mali+223
  • Malta+356
  • Marshall Islands+692
  • Martinique+596
  • Mauritania (‫موريتانيا‬‎)+222
  • Mauritius (Moris)+230
  • Mayotte+262
  • Mexico (México)+52
  • Micronesia+691
  • Moldova (Republica Moldova)+373
  • Monaco+377
  • Mongolia (Монгол)+976
  • Montenegro (Crna Gora)+382
  • Montserrat+1664
  • Morocco (‫المغرب‬‎)+212
  • Mozambique (Moçambique)+258
  • Myanmar (Burma) (မြန်မာ)+95
  • Namibia (Namibië)+264
  • Nauru+674
  • Nepal (नेपाल)+977
  • Netherlands (Nederland)+31
  • New Caledonia (Nouvelle-Calédonie)+687
  • New Zealand+64
  • Nicaragua+505
  • Niger (Nijar)+227
  • Nigeria+234
  • Niue+683
  • Norfolk Island+672
  • North Korea (조선 민주주의 인민 공화국)+850
  • Northern Mariana Islands+1670
  • Norway (Norge)+47
  • Oman (‫عُمان‬‎)+968
  • Pakistan (‫پاکستان‬‎)+92
  • Palau+680
  • Palestine (‫فلسطين‬‎)+970
  • Panama (Panamá)+507
  • Papua New Guinea+675
  • Paraguay+595
  • Peru (Perú)+51
  • Philippines+63
  • Poland (Polska)+48
  • Portugal+351
  • Puerto Rico+1
  • Qatar (‫قطر‬‎)+974
  • Réunion (La Réunion)+262
  • Romania (România)+40
  • Russia (Россия)+7
  • Rwanda+250
  • Saint Barthélemy (Saint-Barthélemy)+590
  • Saint Helena+290
  • Saint Kitts and Nevis+1869
  • Saint Lucia+1758
  • Saint Martin (Saint-Martin (partie française))+590
  • Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)+508
  • Saint Vincent and the Grenadines+1784
  • Samoa+685
  • San Marino+378
  • São Tomé and Príncipe (São Tomé e Príncipe)+239
  • Saudi Arabia (‫المملكة العربية السعودية‬‎)+966
  • Senegal (Sénégal)+221
  • Serbia (Србија)+381
  • Seychelles+248
  • Sierra Leone+232
  • Singapore+65
  • Sint Maarten+1721
  • Slovakia (Slovensko)+421
  • Slovenia (Slovenija)+386
  • Solomon Islands+677
  • Somalia (Soomaaliya)+252
  • South Africa+27
  • South Korea (대한민국)+82
  • South Sudan (‫جنوب السودان‬‎)+211
  • Spain (España)+34
  • Sri Lanka (ශ්‍රී ලංකාව)+94
  • Sudan (‫السودان‬‎)+249
  • Suriname+597
  • Svalbard and Jan Mayen+47
  • Swaziland+268
  • Sweden (Sverige)+46
  • Switzerland (Schweiz)+41
  • Syria (‫سوريا‬‎)+963
  • Taiwan (台灣)+886
  • Tajikistan+992
  • Tanzania+255
  • Thailand (ไทย)+66
  • Timor-Leste+670
  • Togo+228
  • Tokelau+690
  • Tonga+676
  • Trinidad and Tobago+1868
  • Tunisia (‫تونس‬‎)+216
  • Turkey (Türkiye)+90
  • Turkmenistan+993
  • Turks and Caicos Islands+1649
  • Tuvalu+688
  • U.S. Virgin Islands+1340
  • Uganda+256
  • Ukraine (Україна)+380
  • United Arab Emirates (‫الإمارات العربية المتحدة‬‎)+971
  • United Kingdom+44
  • United States+1
  • Uruguay+598
  • Uzbekistan (Oʻzbekiston)+998
  • Vanuatu+678
  • Vatican City (Città del Vaticano)+39
  • Venezuela+58
  • Vietnam (Việt Nam)+84
  • Wallis and Futuna+681
  • Western Sahara (‫الصحراء الغربية‬‎)+212
  • Yemen (‫اليمن‬‎)+967
  • Zambia+260
  • Zimbabwe+263
  • Åland Islands+358
Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

JNIgen: Simplify Native Integration in Flutter

Prepare to embark on a groundbreaking journey through the realms of Flutter as we uncover the remarkable new feature—JNIgen. In this blog, we pull back the curtain to reveal JNIgen’s transformative power, from simplifying intricate tasks to amplifying scalability; this blog serves as a guiding light along the path to a seamlessly integrated Flutter ecosystem.

As Flutter continues to mesmerize developers with its constant evolution, each release unveiling a treasure trove of thrilling new features, the highly anticipated Google I/O 2023 was an extraordinary milestone. Amidst the excitement, a groundbreaking technique was unveiled: JNIgen, offering effortless access to native code like never before.

Let this blog guide you towards a future where your Flutter projects transcend limitations and manifest into awe-inspiring creations.

1. What is JNIgen?

JNIgen, which stands for Java native interface generator,  is an innovative tool that automates the process of generating Dart bindings for Android APIs accessible through Java or Kotlin code. By utilizing these generated bindings, developers can invoke Android APIs with a syntax that closely resembles native code.

With JNIgen, developers can seamlessly bridge the gap between Dart and the rich ecosystem of Android APIs. This empowers them to leverage the full spectrum of Android's functionality, ranging from system-level operations to platform-specific features. By effortlessly integrating with Android APIs through JNIgen-generated bindings, developers can harness the power of native code and build robust applications with ease.

1.1. Default approach: 

In the current Flutter framework, we rely on Platform channels to establish a seamless communication channel between Dart code and native code. These channels serve as a bridge for exchanging messages and data.

Typically, we have a Flutter app acting as the client, while the native code contains the desired methods to be executed. The Flutter app sends a message containing the method name to the native code, which then executes the requested method and sends the response back to the Flutter app.

However, this approach requires the manual implementation of handlers on both the Dart and native code sides. It entails writing code to handle method calls and manage the exchange of responses. Additionally, developers need to carefully manage method names and channel names on both sides to ensure proper communication.

1.2. Working principle of JNIgen: 

Figure 1


In JNIgen, our native code path is passed to the JNIgen generator, which initiates the generation of an intermediate layer of C code. This C code is followed by the necessary boilerplate in Dart, facilitating access to the C methods. All data binding and C files are automatically generated in the directory specified in the .yaml file, which we will explore shortly.

Consequently, as a Flutter application, our interaction is solely focused on interfacing with the newly generated Dart code, eliminating the need for direct utilization of native code.

1.3. Similar tools: 

During the Google I/O 2023 event, JNIgen was introduced as a tool for native code integration. However, it is important to note that not all external libraries available on www.pub.dev are developed exclusively using channels. Another tool, FFIgen, was introduced earlier at Google I/O 2021 and serves a similar purpose. Both FFIgen and JNIgen function similarly, converting native code into intermediate C code with corresponding Dart dependencies to establish the necessary connections.

While JNIgen primarily facilitates communication between Android native code and Dart code, FFIgen has become the preferred choice for establishing communication between iOS native code and Dart code. Both tools are specifically designed to convert native code into intermediate code, enabling seamless interoperability within their respective platforms.

2. Configuration

Prior to proceeding with the code implementation, it is essential to set up and install the necessary tools.

2.1. System setup: 

2.1.1 Install MVN

Windows

  • Download the Maven archive for Windows from the link here [download Binary zip archive]
  • After Extracting the zip file, you will get a folder with name “apache-maven-x.x.x”
  • Create a new folder with the name “ApacheMaven” in “C:\Program Files” and paste the above folder in it. [Your current path will be “C:\Program Files\ApacheMaven\apache-maven-x.x.x”]
  • Add the following entry in “Environment Variable” →  “User Variables”
    M2 ⇒ “C:\Program Files\ApacheMaven\apache-maven-x.x.x\bin”
    M2_HOME ⇒ “C:\Program Files\ApacheMaven\apache-maven-x.x.x”
  • Add a new entry “%M2_HOME%\bin” in “path” variable

Mac

  • Download Maven archive for mac from the link here [download Binary tar.gz archive]
  • Run the following command where you have downloaded the *.tar.gz file

tar -xvf apache-maven-3.8.7.bin.tar.gz
view raw .sh hosted with ❤ by GitHub

  • Add the following entry in .zshrc or .bash_profile to set Maven path “export PATH="$PATH:/Users/username/Downloads/apache-maven-x.x.x/bin”

Or

  • You can use brew to install llvm 

brew install llvm
view raw .sh hosted with ❤ by GitHub

  • Brew will give you instruction like this for further setup

==> llvm
To use the bundled libc++ please add the following LDFLAGS:
LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++"
llvm is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.
If you need to have llvm first in your PATH, run:
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
For compilers to find llvm you may need to set:
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
view raw .txt hosted with ❤ by GitHub

2.1.1 Install Clang-Format


Windows

  • Download the latest version of LLVM for windows from the link here

Mac

  • Run the following brew command: 

brew install clang-format
view raw .sh hosted with ❤ by GitHub

2.2. Flutter setup: 

2.2.1 Get Dependencies

Run the following commands with Flutter:

flutter pub add jni
view raw .sh hosted with ❤ by GitHub

flutter pub add jnigen
view raw .sh hosted with ❤ by GitHub

2.2.2 Setup configuration file

Figure 01 provides a visual representation of the .yaml file, which holds crucial configurations utilized by JNIgen. These configurations serve the purpose of identifying paths for native classes, as well as specifying the locations where JNIgen generates the resulting C and Dart files. Furthermore, the .yaml file allows for specifying Maven configurations, enabling the selection of specific third-party libraries that need to be downloaded to facilitate code generation.

By leveraging the power of the .yaml file, developers gain control over the path identification process and ensure that the generated code is placed in the desired locations. Additionally, the ability to define Maven configurations grants flexibility in managing dependencies, allowing the seamless integration of required third-party libraries into the generated code. This comprehensive approach enables precise control and customization over the code generation process, enhancing the overall efficiency and effectiveness of the development workflow.

Let's explore the properties that we have utilized within the .yaml file (Please refer "3.2.2. code implementation" section's example for better understanding):

  • android_sdk_config: 

When the value of a specific property is set to "true," it triggers the execution of a Gradle stub during the invocation of JNIgen. Additionally, it includes the Android compile classpath in the classpath of JNIgen. However, to ensure that all dependencies are cached appropriately, it is necessary to have previously performed a release build.

  • output 

As the name implies, the "output" section defines the configuration related to the generation of intermediate code. This section plays a crucial role in determining how the intermediate code will be generated and organized.

  •  c >> library_name &&  c >> path:
    Here we are setting details for c_based binding code.

  •  dart >> path &&  dart >> structure:

Here we are defining configuration for dart_based binding code.

  •  source_path:

These are specific directories that are scanned during the process of locating the relevant source files.

  •  classes:

By providing a comprehensive list of classes or packages, developers can effectively control the scope of the code generation process. This ensures that the binding code is generated only for the desired components, minimizing unnecessary code generation

By utilizing these properties within the .yaml file, developers can effectively control various aspects of the code generation process, including path identification, code organization, and dependency management. To get more in-depth information, please check out the official documentation here.

2.3. Generate bindings files:

Once this setup is complete, the final step for JNIgen is to obtain the jar file that will be scanned to generate the required bindings. To initiate the process of generating the Android APK, you can execute the following command:

flutter build apk
view raw .sh hosted with ❤ by GitHub

Run the following command in your terminal to generate code:

dart run jnigen --config jnigen.yaml
view raw .sh hosted with ❤ by GitHub

2.3. Android setup: 

Add the address of CMakeLists.txt file in your android >> app >> build.gradle file’s buildTypes section:

buildTypes {
externalNativeBuild {
cmake {
path <address of CMakeLists.txt>
}
}
}
view raw .gradle hosted with ❤ by GitHub

With this configuration, we are specifying the path for the CMake file that will been generated by JNIgen.This path declaration is crucial for identifying the location of the generated CMake file within the project structure.

With the completion of the aforementioned steps, you are now ready to run your application and leverage all the native functions that have been integrated.

3. Sample Project

To gain hands-on experience and better understand the JNIgen, let's create a small project together. Follow the steps below to get started. 

Let's start with:

3.1. Packages & directories:

3.1.1 Create a project using the following command:

flutter create jnigen_integration_project
view raw .sh hosted with ❤ by GitHub

3.1.2 Add these under dependencies of pubspec.yaml (and run command flutter pub get):

jni: ^0.5.0
jnigen: ^0.5.0
view raw .yaml hosted with ❤ by GitHub

3.1.3. Got to android >> app >> src >> main directory.

3.1.4. Create directories inside the main as show below:

Figure 02 

3.2. Code Implementation:

3.2.1 We will start with Android code. Create 2 files HardwareUtils.java & HardwareUtilsKotlin.kt inside the utils directory.

 HardwareUtilsKotlin.kt

package com.hardware.utils
import android.os.Build
class HardwareUtilsKotlin {
fun getHardwareDetails(): Map<String, String>? {
val hardwareDetails: MutableMap<String, String> = HashMap()
hardwareDetails["Language"] = "Kotlin"
hardwareDetails["Manufacture"] = Build.MANUFACTURER
hardwareDetails["Model No."] = Build.MODEL
hardwareDetails["Type"] = Build.TYPE
hardwareDetails["User"] = Build.USER
hardwareDetails["SDK"] = Build.VERSION.SDK
hardwareDetails["Board"] = Build.BOARD
hardwareDetails["Version Code"] = Build.VERSION.RELEASE
return hardwareDetails
}
}
view raw .kt hosted with ❤ by GitHub

 HardwareUtils.java 

package com.hardware.utils;
import android.os.Build;
import java.util.HashMap;
import java.util.Map;
public class HardwareUtils {
public Map<String, String> getHardwareDetails() {
Map<String, String> hardwareDetails = new HashMap<String, String>();
hardwareDetails.put("Language", "JAVA");
hardwareDetails.put("Manufacture", Build.MANUFACTURER);
hardwareDetails.put("Model No.", Build.MODEL);
hardwareDetails.put("Type", Build.TYPE);
hardwareDetails.put("User", Build.USER);
hardwareDetails.put("SDK", Build.VERSION.SDK);
hardwareDetails.put("Board", Build.BOARD);
hardwareDetails.put("Version Code", Build.VERSION.RELEASE);
return hardwareDetails;
}
public Map<String, String> getHardwareDetailsKotlin() {
return new HardwareUtilsKotlin().getHardwareDetails();
}
}
view raw .java hosted with ❤ by GitHub

3.2.2 To provide the necessary configurations to JNIGen for code generation, we will create a .yaml file named JNIgen.yaml in the root of the project.

   jnigen.yaml 

android_sdk_config:
add_gradle_deps: true
output:
c:
library_name: hardware_utils
path: src/
dart:
path: lib/hardware_utils.dart
structure: single_file
source_path:
- 'android/app/src/main/java'
classes:
- 'com.hardware.utils'
view raw .yaml hosted with ❤ by GitHub

3.2.3 Let's generate C & Dart code.

Execute the following command to create APK:

flutter build apk
view raw .sh hosted with ❤ by GitHub

After the successful execution of the above command, execute the following command:

dart run jnigen --config jnigen.yaml
view raw .sh hosted with ❤ by GitHub

3.2.4 Add the address of CMakeLists.txt in your android >> app >> build.gradle file’s buildTypes section as shown below :

buildTypes {
externalNativeBuild {
cmake {
path "../../src/CMakeLists.txt"
}
}
}
view raw .gradle hosted with ❤ by GitHub

3.2.5. Final step is to call the methods from Dart code, which was generated by JNIgen.

To do this, replace the MyHomePage class code with the below code from main.dart file.

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _hardwareDetails = '';
String _hardwareDetailsKotlin = '';
JObject activity = JObject.fromRef(Jni.getCurrentActivity());
@override
void initState() {
JMap<JString, JString> deviceHardwareDetails =
HardwareUtils().getHardwareDetails();
_hardwareDetails = 'This device details from Java class:\n';
deviceHardwareDetails.forEach((key, value) {
_hardwareDetails =
'$_hardwareDetails\n${key.toDartString()} is ${value.toDartString()}';
});
JMap<JString, JString> deviceHardwareDetailsKotlin =
HardwareUtils().getHardwareDetailsKotlin();
_hardwareDetailsKotlin = 'This device details from Kotlin class:\n';
deviceHardwareDetailsKotlin.forEach((key, value) {
_hardwareDetailsKotlin =
'$_hardwareDetailsKotlin\n${key.toDartString()} is ${value.toDartString()}';
});
setState(() {
_hardwareDetails;
_hardwareDetailsKotlin;
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
_hardwareDetails,
textAlign: TextAlign.center,
),
SizedBox(height: 20,),
Text(
_hardwareDetailsKotlin,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
view raw .dart hosted with ❤ by GitHub

After all of this, when we launch our app, we will see information about our Android device.

4. Result

For your convenience, the complete code for the project can be found here. Feel free to refer to this code repository for a comprehensive overview of the implementation details and to access the entirety of the source code.

5. Conclusion

In conclusion, we explored the limitations of the traditional approach to native API access in Flutter for mid to large-scale projects. Through our insightful exploration of JNIgen's working principles, we uncovered its remarkable potential for simplifying the native integration process.

By gaining a deep understanding of JNIgen's inner workings, we successfully developed a sample project and provided detailed guidance on the essential setup requirements. Armed with this knowledge, developers can embrace JNIgen's capabilities to streamline their native integration process effectively.

We can say that JNIgen is a valuable tool for Flutter developers seeking to combine the power of Flutter's cross-platform capabilities with the flexibility and performance benefits offered by native code. It empowers developers to build high-quality apps that seamlessly integrate platform-specific features and existing native code libraries, ultimately enhancing the overall user experience. 

Hopefully, this blog post has inspired you to explore the immense potential of JNIgen in your Flutter applications. By harnessing the JNIgen, we can open doors to new possibilities.

Thank you for taking the time to read through this blog!

6. Reference

  1. https://docs.flutter.dev/
  2. https://pub.dev/packages/jnigen
  3. https://pub.dev/packages/jni
  4. https://github.com/dart-lang/jnigen
  5. https://github.com/dart-lang/jnigen#readme
  6. https://github.com/dart-lang/jnigen/wiki/Architecture-&-Design-Notes
  7. https://medium.com/simform-engineering/jnigen-an-easy-way-to-access-platform-apis-cb1fd3101e33
  8. https://medium.com/@marcoedomingos/the-ultimate-showdown-methodchannel-vs-d83135f2392d
Get the latest engineering blogs delivered straight to your inbox.
No spam. Only expert insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings

JNIgen: Simplify Native Integration in Flutter

Prepare to embark on a groundbreaking journey through the realms of Flutter as we uncover the remarkable new feature—JNIgen. In this blog, we pull back the curtain to reveal JNIgen’s transformative power, from simplifying intricate tasks to amplifying scalability; this blog serves as a guiding light along the path to a seamlessly integrated Flutter ecosystem.

As Flutter continues to mesmerize developers with its constant evolution, each release unveiling a treasure trove of thrilling new features, the highly anticipated Google I/O 2023 was an extraordinary milestone. Amidst the excitement, a groundbreaking technique was unveiled: JNIgen, offering effortless access to native code like never before.

Let this blog guide you towards a future where your Flutter projects transcend limitations and manifest into awe-inspiring creations.

1. What is JNIgen?

JNIgen, which stands for Java native interface generator,  is an innovative tool that automates the process of generating Dart bindings for Android APIs accessible through Java or Kotlin code. By utilizing these generated bindings, developers can invoke Android APIs with a syntax that closely resembles native code.

With JNIgen, developers can seamlessly bridge the gap between Dart and the rich ecosystem of Android APIs. This empowers them to leverage the full spectrum of Android's functionality, ranging from system-level operations to platform-specific features. By effortlessly integrating with Android APIs through JNIgen-generated bindings, developers can harness the power of native code and build robust applications with ease.

1.1. Default approach: 

In the current Flutter framework, we rely on Platform channels to establish a seamless communication channel between Dart code and native code. These channels serve as a bridge for exchanging messages and data.

Typically, we have a Flutter app acting as the client, while the native code contains the desired methods to be executed. The Flutter app sends a message containing the method name to the native code, which then executes the requested method and sends the response back to the Flutter app.

However, this approach requires the manual implementation of handlers on both the Dart and native code sides. It entails writing code to handle method calls and manage the exchange of responses. Additionally, developers need to carefully manage method names and channel names on both sides to ensure proper communication.

1.2. Working principle of JNIgen: 

Figure 1


In JNIgen, our native code path is passed to the JNIgen generator, which initiates the generation of an intermediate layer of C code. This C code is followed by the necessary boilerplate in Dart, facilitating access to the C methods. All data binding and C files are automatically generated in the directory specified in the .yaml file, which we will explore shortly.

Consequently, as a Flutter application, our interaction is solely focused on interfacing with the newly generated Dart code, eliminating the need for direct utilization of native code.

1.3. Similar tools: 

During the Google I/O 2023 event, JNIgen was introduced as a tool for native code integration. However, it is important to note that not all external libraries available on www.pub.dev are developed exclusively using channels. Another tool, FFIgen, was introduced earlier at Google I/O 2021 and serves a similar purpose. Both FFIgen and JNIgen function similarly, converting native code into intermediate C code with corresponding Dart dependencies to establish the necessary connections.

While JNIgen primarily facilitates communication between Android native code and Dart code, FFIgen has become the preferred choice for establishing communication between iOS native code and Dart code. Both tools are specifically designed to convert native code into intermediate code, enabling seamless interoperability within their respective platforms.

2. Configuration

Prior to proceeding with the code implementation, it is essential to set up and install the necessary tools.

2.1. System setup: 

2.1.1 Install MVN

Windows

  • Download the Maven archive for Windows from the link here [download Binary zip archive]
  • After Extracting the zip file, you will get a folder with name “apache-maven-x.x.x”
  • Create a new folder with the name “ApacheMaven” in “C:\Program Files” and paste the above folder in it. [Your current path will be “C:\Program Files\ApacheMaven\apache-maven-x.x.x”]
  • Add the following entry in “Environment Variable” →  “User Variables”
    M2 ⇒ “C:\Program Files\ApacheMaven\apache-maven-x.x.x\bin”
    M2_HOME ⇒ “C:\Program Files\ApacheMaven\apache-maven-x.x.x”
  • Add a new entry “%M2_HOME%\bin” in “path” variable

Mac

  • Download Maven archive for mac from the link here [download Binary tar.gz archive]
  • Run the following command where you have downloaded the *.tar.gz file

tar -xvf apache-maven-3.8.7.bin.tar.gz
view raw .sh hosted with ❤ by GitHub

  • Add the following entry in .zshrc or .bash_profile to set Maven path “export PATH="$PATH:/Users/username/Downloads/apache-maven-x.x.x/bin”

Or

  • You can use brew to install llvm 

brew install llvm
view raw .sh hosted with ❤ by GitHub

  • Brew will give you instruction like this for further setup

==> llvm
To use the bundled libc++ please add the following LDFLAGS:
LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++"
llvm is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.
If you need to have llvm first in your PATH, run:
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
For compilers to find llvm you may need to set:
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
view raw .txt hosted with ❤ by GitHub

2.1.1 Install Clang-Format


Windows

  • Download the latest version of LLVM for windows from the link here

Mac

  • Run the following brew command: 

brew install clang-format
view raw .sh hosted with ❤ by GitHub

2.2. Flutter setup: 

2.2.1 Get Dependencies

Run the following commands with Flutter:

flutter pub add jni
view raw .sh hosted with ❤ by GitHub

flutter pub add jnigen
view raw .sh hosted with ❤ by GitHub

2.2.2 Setup configuration file

Figure 01 provides a visual representation of the .yaml file, which holds crucial configurations utilized by JNIgen. These configurations serve the purpose of identifying paths for native classes, as well as specifying the locations where JNIgen generates the resulting C and Dart files. Furthermore, the .yaml file allows for specifying Maven configurations, enabling the selection of specific third-party libraries that need to be downloaded to facilitate code generation.

By leveraging the power of the .yaml file, developers gain control over the path identification process and ensure that the generated code is placed in the desired locations. Additionally, the ability to define Maven configurations grants flexibility in managing dependencies, allowing the seamless integration of required third-party libraries into the generated code. This comprehensive approach enables precise control and customization over the code generation process, enhancing the overall efficiency and effectiveness of the development workflow.

Let's explore the properties that we have utilized within the .yaml file (Please refer "3.2.2. code implementation" section's example for better understanding):

  • android_sdk_config: 

When the value of a specific property is set to "true," it triggers the execution of a Gradle stub during the invocation of JNIgen. Additionally, it includes the Android compile classpath in the classpath of JNIgen. However, to ensure that all dependencies are cached appropriately, it is necessary to have previously performed a release build.

  • output 

As the name implies, the "output" section defines the configuration related to the generation of intermediate code. This section plays a crucial role in determining how the intermediate code will be generated and organized.

  •  c >> library_name &&  c >> path:
    Here we are setting details for c_based binding code.

  •  dart >> path &&  dart >> structure:

Here we are defining configuration for dart_based binding code.

  •  source_path:

These are specific directories that are scanned during the process of locating the relevant source files.

  •  classes:

By providing a comprehensive list of classes or packages, developers can effectively control the scope of the code generation process. This ensures that the binding code is generated only for the desired components, minimizing unnecessary code generation

By utilizing these properties within the .yaml file, developers can effectively control various aspects of the code generation process, including path identification, code organization, and dependency management. To get more in-depth information, please check out the official documentation here.

2.3. Generate bindings files:

Once this setup is complete, the final step for JNIgen is to obtain the jar file that will be scanned to generate the required bindings. To initiate the process of generating the Android APK, you can execute the following command:

flutter build apk
view raw .sh hosted with ❤ by GitHub

Run the following command in your terminal to generate code:

dart run jnigen --config jnigen.yaml
view raw .sh hosted with ❤ by GitHub

2.3. Android setup: 

Add the address of CMakeLists.txt file in your android >> app >> build.gradle file’s buildTypes section:

buildTypes {
externalNativeBuild {
cmake {
path <address of CMakeLists.txt>
}
}
}
view raw .gradle hosted with ❤ by GitHub

With this configuration, we are specifying the path for the CMake file that will been generated by JNIgen.This path declaration is crucial for identifying the location of the generated CMake file within the project structure.

With the completion of the aforementioned steps, you are now ready to run your application and leverage all the native functions that have been integrated.

3. Sample Project

To gain hands-on experience and better understand the JNIgen, let's create a small project together. Follow the steps below to get started. 

Let's start with:

3.1. Packages & directories:

3.1.1 Create a project using the following command:

flutter create jnigen_integration_project
view raw .sh hosted with ❤ by GitHub

3.1.2 Add these under dependencies of pubspec.yaml (and run command flutter pub get):

jni: ^0.5.0
jnigen: ^0.5.0
view raw .yaml hosted with ❤ by GitHub

3.1.3. Got to android >> app >> src >> main directory.

3.1.4. Create directories inside the main as show below:

Figure 02 

3.2. Code Implementation:

3.2.1 We will start with Android code. Create 2 files HardwareUtils.java & HardwareUtilsKotlin.kt inside the utils directory.

 HardwareUtilsKotlin.kt

package com.hardware.utils
import android.os.Build
class HardwareUtilsKotlin {
fun getHardwareDetails(): Map<String, String>? {
val hardwareDetails: MutableMap<String, String> = HashMap()
hardwareDetails["Language"] = "Kotlin"
hardwareDetails["Manufacture"] = Build.MANUFACTURER
hardwareDetails["Model No."] = Build.MODEL
hardwareDetails["Type"] = Build.TYPE
hardwareDetails["User"] = Build.USER
hardwareDetails["SDK"] = Build.VERSION.SDK
hardwareDetails["Board"] = Build.BOARD
hardwareDetails["Version Code"] = Build.VERSION.RELEASE
return hardwareDetails
}
}
view raw .kt hosted with ❤ by GitHub

 HardwareUtils.java 

package com.hardware.utils;
import android.os.Build;
import java.util.HashMap;
import java.util.Map;
public class HardwareUtils {
public Map<String, String> getHardwareDetails() {
Map<String, String> hardwareDetails = new HashMap<String, String>();
hardwareDetails.put("Language", "JAVA");
hardwareDetails.put("Manufacture", Build.MANUFACTURER);
hardwareDetails.put("Model No.", Build.MODEL);
hardwareDetails.put("Type", Build.TYPE);
hardwareDetails.put("User", Build.USER);
hardwareDetails.put("SDK", Build.VERSION.SDK);
hardwareDetails.put("Board", Build.BOARD);
hardwareDetails.put("Version Code", Build.VERSION.RELEASE);
return hardwareDetails;
}
public Map<String, String> getHardwareDetailsKotlin() {
return new HardwareUtilsKotlin().getHardwareDetails();
}
}
view raw .java hosted with ❤ by GitHub

3.2.2 To provide the necessary configurations to JNIGen for code generation, we will create a .yaml file named JNIgen.yaml in the root of the project.

   jnigen.yaml 

android_sdk_config:
add_gradle_deps: true
output:
c:
library_name: hardware_utils
path: src/
dart:
path: lib/hardware_utils.dart
structure: single_file
source_path:
- 'android/app/src/main/java'
classes:
- 'com.hardware.utils'
view raw .yaml hosted with ❤ by GitHub

3.2.3 Let's generate C & Dart code.

Execute the following command to create APK:

flutter build apk
view raw .sh hosted with ❤ by GitHub

After the successful execution of the above command, execute the following command:

dart run jnigen --config jnigen.yaml
view raw .sh hosted with ❤ by GitHub

3.2.4 Add the address of CMakeLists.txt in your android >> app >> build.gradle file’s buildTypes section as shown below :

buildTypes {
externalNativeBuild {
cmake {
path "../../src/CMakeLists.txt"
}
}
}
view raw .gradle hosted with ❤ by GitHub

3.2.5. Final step is to call the methods from Dart code, which was generated by JNIgen.

To do this, replace the MyHomePage class code with the below code from main.dart file.

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _hardwareDetails = '';
String _hardwareDetailsKotlin = '';
JObject activity = JObject.fromRef(Jni.getCurrentActivity());
@override
void initState() {
JMap<JString, JString> deviceHardwareDetails =
HardwareUtils().getHardwareDetails();
_hardwareDetails = 'This device details from Java class:\n';
deviceHardwareDetails.forEach((key, value) {
_hardwareDetails =
'$_hardwareDetails\n${key.toDartString()} is ${value.toDartString()}';
});
JMap<JString, JString> deviceHardwareDetailsKotlin =
HardwareUtils().getHardwareDetailsKotlin();
_hardwareDetailsKotlin = 'This device details from Kotlin class:\n';
deviceHardwareDetailsKotlin.forEach((key, value) {
_hardwareDetailsKotlin =
'$_hardwareDetailsKotlin\n${key.toDartString()} is ${value.toDartString()}';
});
setState(() {
_hardwareDetails;
_hardwareDetailsKotlin;
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
_hardwareDetails,
textAlign: TextAlign.center,
),
SizedBox(height: 20,),
Text(
_hardwareDetailsKotlin,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
view raw .dart hosted with ❤ by GitHub

After all of this, when we launch our app, we will see information about our Android device.

4. Result

For your convenience, the complete code for the project can be found here. Feel free to refer to this code repository for a comprehensive overview of the implementation details and to access the entirety of the source code.

5. Conclusion

In conclusion, we explored the limitations of the traditional approach to native API access in Flutter for mid to large-scale projects. Through our insightful exploration of JNIgen's working principles, we uncovered its remarkable potential for simplifying the native integration process.

By gaining a deep understanding of JNIgen's inner workings, we successfully developed a sample project and provided detailed guidance on the essential setup requirements. Armed with this knowledge, developers can embrace JNIgen's capabilities to streamline their native integration process effectively.

We can say that JNIgen is a valuable tool for Flutter developers seeking to combine the power of Flutter's cross-platform capabilities with the flexibility and performance benefits offered by native code. It empowers developers to build high-quality apps that seamlessly integrate platform-specific features and existing native code libraries, ultimately enhancing the overall user experience. 

Hopefully, this blog post has inspired you to explore the immense potential of JNIgen in your Flutter applications. By harnessing the JNIgen, we can open doors to new possibilities.

Thank you for taking the time to read through this blog!

6. Reference

  1. https://docs.flutter.dev/
  2. https://pub.dev/packages/jnigen
  3. https://pub.dev/packages/jni
  4. https://github.com/dart-lang/jnigen
  5. https://github.com/dart-lang/jnigen#readme
  6. https://github.com/dart-lang/jnigen/wiki/Architecture-&-Design-Notes
  7. https://medium.com/simform-engineering/jnigen-an-easy-way-to-access-platform-apis-cb1fd3101e33
  8. https://medium.com/@marcoedomingos/the-ultimate-showdown-methodchannel-vs-d83135f2392d

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings