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

CODE: https://gist.github.com/velotiotech/cb6e901ce5e19c86341f773ff7a45ede.js

  • 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 

CODE: https://gist.github.com/velotiotech/5835a2d90e588849ec5598013667a369.js

  • Brew will give you instruction like this for further setup

CODE: https://gist.github.com/velotiotech/047ffdcb88f925f45aaa813260dd2509.js

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: 

CODE: https://gist.github.com/velotiotech/7172d60ce26aa8468849e74f260b797d.js

2.2. Flutter setup: 

2.2.1 Get Dependencies

Run the following commands with Flutter:

CODE: https://gist.github.com/velotiotech/84ef114696d5c14bdd62fa4c771873e3.js

CODE: https://gist.github.com/velotiotech/4b8b8d17eb7afb3b2a250349695cf118.js

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:

CODE: https://gist.github.com/velotiotech/816535fcb76928942023e3f5512e2e11.js

Run the following command in your terminal to generate code:

CODE: https://gist.github.com/velotiotech/0a5a32fe1a4767d7b41b68bb92264287.js

2.3. Android setup: 

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

CODE: https://gist.github.com/velotiotech/06569ba26797864f4954172874f7264d.js

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:

CODE: https://gist.github.com/velotiotech/c1bb1fa99579728fffc436d88bde776c.js

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

CODE: https://gist.github.com/velotiotech/f2aa10e20a7200fdbf0ad1acb23d9cca.js

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

CODE: https://gist.github.com/velotiotech/3c4527db41b00d273684d4db50e37165.js

 HardwareUtils.java 

CODE: https://gist.github.com/velotiotech/fefc2a6d8706f806b7741a03f3ec8853.js

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 

CODE: https://gist.github.com/velotiotech/9072eb5360a01613daf2263536268477.js

3.2.3 Let's generate C & Dart code.

Execute the following command to create APK:

CODE: https://gist.github.com/velotiotech/c3e33b96e91e1b28d7c8adadd7cc39c6.js

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

CODE: https://gist.github.com/velotiotech/beab9f84ad5ebeed6ab2d85eb162f0aa.js

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

CODE: https://gist.github.com/velotiotech/9fe134263fa37c9da0926a981ef3c04c.js

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.

CODE: https://gist.github.com/velotiotech/deaabf5f04eb37b0e6723d3a08c275ba.js

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

CODE: https://gist.github.com/velotiotech/cb6e901ce5e19c86341f773ff7a45ede.js

  • 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 

CODE: https://gist.github.com/velotiotech/5835a2d90e588849ec5598013667a369.js

  • Brew will give you instruction like this for further setup

CODE: https://gist.github.com/velotiotech/047ffdcb88f925f45aaa813260dd2509.js

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: 

CODE: https://gist.github.com/velotiotech/7172d60ce26aa8468849e74f260b797d.js

2.2. Flutter setup: 

2.2.1 Get Dependencies

Run the following commands with Flutter:

CODE: https://gist.github.com/velotiotech/84ef114696d5c14bdd62fa4c771873e3.js

CODE: https://gist.github.com/velotiotech/4b8b8d17eb7afb3b2a250349695cf118.js

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:

CODE: https://gist.github.com/velotiotech/816535fcb76928942023e3f5512e2e11.js

Run the following command in your terminal to generate code:

CODE: https://gist.github.com/velotiotech/0a5a32fe1a4767d7b41b68bb92264287.js

2.3. Android setup: 

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

CODE: https://gist.github.com/velotiotech/06569ba26797864f4954172874f7264d.js

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:

CODE: https://gist.github.com/velotiotech/c1bb1fa99579728fffc436d88bde776c.js

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

CODE: https://gist.github.com/velotiotech/f2aa10e20a7200fdbf0ad1acb23d9cca.js

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

CODE: https://gist.github.com/velotiotech/3c4527db41b00d273684d4db50e37165.js

 HardwareUtils.java 

CODE: https://gist.github.com/velotiotech/fefc2a6d8706f806b7741a03f3ec8853.js

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 

CODE: https://gist.github.com/velotiotech/9072eb5360a01613daf2263536268477.js

3.2.3 Let's generate C & Dart code.

Execute the following command to create APK:

CODE: https://gist.github.com/velotiotech/c3e33b96e91e1b28d7c8adadd7cc39c6.js

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

CODE: https://gist.github.com/velotiotech/beab9f84ad5ebeed6ab2d85eb162f0aa.js

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

CODE: https://gist.github.com/velotiotech/9fe134263fa37c9da0926a981ef3c04c.js

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.

CODE: https://gist.github.com/velotiotech/deaabf5f04eb37b0e6723d3a08c275ba.js

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