diff --git a/firebase.json b/firebase.json index 1dd4a98080b..c343ccc4285 100644 --- a/firebase.json +++ b/firebase.json @@ -173,6 +173,7 @@ { "source": "/packages-and-plugins/plugin-api-migration", "destination": "/release/breaking-changes/plugin-api-migration", "type": 301 }, { "source": "/platform-integration/android/androidx-migration", "destination": "/release/breaking-changes/androidx-migration", "type": 301 }, { "source": "/platform-integration/android/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, + { "source": "/platform-integration/android/call-jetpack-apis", "destination": "/platform-integration/android/jnigen", "type": 301}, { "source": "/platform-integration/android/install-android", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/platform-integration/android/install-android/:rest*", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/platform-integration/android/splash-screen-migration", "destination": "/release/breaking-changes/splash-screen-migration", "type": 301 }, diff --git a/site/web/assets/images/android/jni-call-lifecycle-dark.svg b/site/web/assets/images/android/jni-call-lifecycle-dark.svg new file mode 100644 index 00000000000..f21306c8840 --- /dev/null +++ b/site/web/assets/images/android/jni-call-lifecycle-dark.svg @@ -0,0 +1,4 @@ + + +Android APIsFlutter AppBindingsC/C++NDK - Native Development KitJava Native InterfaceLifecycle of a Java Native Interface(JNI) Call \ No newline at end of file diff --git a/site/web/assets/images/android/jni-call-lifecycle-light.png b/site/web/assets/images/android/jni-call-lifecycle-light.png new file mode 100644 index 00000000000..38cbb9992b2 Binary files /dev/null and b/site/web/assets/images/android/jni-call-lifecycle-light.png differ diff --git a/site/web/assets/images/android/jni-call-lifecycle-light.svg b/site/web/assets/images/android/jni-call-lifecycle-light.svg new file mode 100644 index 00000000000..b10c537f6ec --- /dev/null +++ b/site/web/assets/images/android/jni-call-lifecycle-light.svg @@ -0,0 +1,4 @@ + + +Android APIsFlutter AppBindingsC/C++NDK - Native Development KitJava Native InterfaceLifecycle of a Java Native Interface(JNI) Call \ No newline at end of file diff --git a/sites/docs/src/content/platform-integration/android/call-jetpack-apis.md b/sites/docs/src/content/platform-integration/android/call-jetpack-apis.md deleted file mode 100644 index d2476b9145d..00000000000 --- a/sites/docs/src/content/platform-integration/android/call-jetpack-apis.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: "Calling JetPack APIs" -description: "Use the latest Android APIs from your Dart code" ---- - - - -Flutter apps running on Android can always make use of the -latest APIs on the first day they are released on Android, no -matter what. This page outlines available ways to invoke -Android-specific APIs. - -## Use an existing solution - -In most scenarios, you can use a plugin (as shown in the next section) -to invoke native APIs without writing any custom boilerplate or -glue code yourself. - -### Use a plugin - -Using a plugin is often the easiest way to access native -APIs, regardless of where your Flutter app is running. To -use plugins, visit [pub.dev][pub] and search for -the topic you need. Most native features, including accessing -common hardware like GPS, the camera, or step counters are -supported by robust plugins. - -For complete guidance on adding plugins to your Flutter app, -see the [Using packages documentation][packages]. - -[packages]: /packages-and-plugins/using-packages -[pub]: {{site.pub}} - -Not all native features are supported by plugins, especially -immediately after their release. In any scenario where -your desired native feature is not covered by a package on -[pub.dev][pub], continue on to the following sections. - -## Creating a custom solution - -Not all scenarios and APIs will be supported by -existing solutions; but luckily, you can always add whatever -support you need. The next sections describe two different -ways to call native code from Dart. - -:::note -Neither solution below is inherently better or worse than -existing plugins, because all plugins use one of the following -two options. -::: - -### Call native code directly via FFI - -The most direct and efficient way to invoke native APIs is by -calling the API directly, via FFI. This links your Dart executable -to any specified native code at compile-time, allowing you to -call it directly from the UI thread through a small amount of glue -code. In most cases, [ffigen][ffigen] or [jnigen][jnigen] are -helpful in writing this glue code. - -For complete guidance on directly calling native code from -your Flutter app, see the [FFI documentation][ffi]. - -In the coming months, the Dart team hopes to make this process -easier with direct support for calling native APIs using the -FFI approach, but without any need for the developer to write -glue code. - -[ffi]: {{site.dart-site}}/interop/c-interop -[ffigen]: {{site.pub}}/packages/ffigen -[jnigen]: {{site.pub}}/packages/jnigen - - -### Add a MethodChannel - -[`MethodChannel`][methodchannels-api-docs]s are an alternate -way Flutter apps can invoke arbitrary native code. -Unlike the FFI solution described in the previous step, -MethodChannels are always asynchronous, which -might or might not matter to you, depending on your use case. As -with FFI and direct calls to native code, using a `MethodChannel` -requires a small amount of glue code to translate your Dart objects -into native objects, and then back again. In most cases, -[`pkg:pigeon`][pigeon] is helpful in writing this glue code. - -For complete guidance on adding MethodChannels to your Flutter -app, see the [`MethodChannel`s documentation][methodchannels]. - -[methodchannels]: /platform-integration/platform-channels -[methodchannels-api-docs]: {{site.api}}/flutter/services/MethodChannel-class.html -[pigeon]: {{site.pub}}/packages/pigeon diff --git a/sites/docs/src/content/platform-integration/android/call-native-apis.md b/sites/docs/src/content/platform-integration/android/call-native-apis.md new file mode 100644 index 00000000000..8ec36b0d634 --- /dev/null +++ b/sites/docs/src/content/platform-integration/android/call-native-apis.md @@ -0,0 +1,30 @@ +--- +title: "Calling Android APIs" +description: "How to call Android-specific APIs or native libraries from Flutter code." +--- + + + +Flutter apps running on Android can always make use of the +latest APIs on the first day they are released on Android, +no matter what. + +To choose the best integration model for your app, +first review the [Platform-specific options guide](/platform-integration/native-code-options). + +## Native Android integrations + +Once you have selected an integration architecture, +use the following resources to implement it on Android: + +### Call Java or Kotlin code directly +To invoke standard Android APIs, Jetpack libraries, or custom JVM classes: +* Read our guide on using [jnigen to bind Java/Kotlin code](/platform-integration/android/jnigen). + +### Call C or C++ NDK libraries directly +To execute low-level C libraries or native engine code: +* Read our guide on using [Dart FFI](/platform-integration/bind-native-code). + +### Communicate using a message bridge +To build plugins or communicate asynchronously with platform-specific code: +* Use [Pigeon or manual MethodChannels](/platform-integration/platform-channels). diff --git a/sites/docs/src/content/platform-integration/android/compose-activity.md b/sites/docs/src/content/platform-integration/android/compose-activity.md index 1b51f5c4c2b..688cab7f0e0 100644 --- a/sites/docs/src/content/platform-integration/android/compose-activity.md +++ b/sites/docs/src/content/platform-integration/android/compose-activity.md @@ -15,8 +15,27 @@ you will have access to the full breadth of native Android functionality. Adding this functionality requires making several changes to your Flutter app and its internal, generated Android app. + +There are two ways to do it: Either using `jnigen` or Method Channels. + +## Overview of launching Activities using `jnigen` + +On the Flutter side, you will need to create Dart bindings for various Android APIs +and call them using Dart code. This will involve setting up `jni`, `jnigen`, +and a configuration file describing the generated output. + +On the Android side, you will need to make some modifications to some Android build +files to account for Compose views and a new `Activity`. + +The [code][] for this version is available on Github. + +[code]:https://github.com/flutter/demos/tree/main/native_interop_demos/android_launch_activity + +## Overview of launching Activities using Method Channels + On the Flutter side, you will need to create a new platform method channel and call its `invokeMethod` method. + On the Android side, you will need to register a matching native `MethodChannel` to receive the signal from Dart and then launch a new activity. Recall that all Flutter apps (when running on Android) exist within @@ -36,7 +55,345 @@ check out [Hosting native Android views][]. Not all Android activities use Jetpack Compose, but this tutorial assumes you want to use Compose. -## On the Dart side +## Launch Activity Using Native Interop (`jnigen`) + +### On the command line + +On the commandline, add `jnigen` as a development dependency +and `jni` as a runtime dependency. `jnigen` will be used to +generate Dart bindings and then when the app is run, `jni` +will execute them. +```sh +flutter pub add jnigen --dev +flutter pub add jni +``` + +### On the Dart side + +In your dart code, create jnigen.dart file specifying the +bindings you will be generating. + +First, run the following command to build the app and make +available the files needed to execute code generation. + +```sh +flutter build apk +``` + + +In a new file `tool/jnigen.dart`, add the following code. + +```dart +import 'dart:io'; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/android.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true), + classes: [ + // provided by Android OS + 'android.os.Bundle', + 'android.content.Intent', + 'android.content.Context' + ], + ), + ); +} +``` + +Execute `dart run tool/jnigen.dart` to generate the Dart bindings. + +With the bindings generated, we can directly + +```dart +import 'package:flutter/material.dart'; +// uses added namespace because some bindings conflict with Dart classes (Intent) +import 'package:android_launch_activity/gen/android.g.dart' as native; +import 'package:jni/jni.dart'; + +// Context.fromReference ensures we get Android Context object +// rather than the default `JObject` +var context = native.Context.fromReference(Jni.androidApplicationContext.reference); + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + // SECTION 2: START COPYING HERE + void _launchAndroidActivity() { + + var intent = native.Intent.new$1(context, native.SecondActivity.type.jClass); + intent.addFlags(native.Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra$18('message'.toJString(), 'Hello from Flutter'.toJString()); + context.startActivity(intent); + + intent.release(); + +} + // SECTION 2: END COPYING HERE + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: const Center(child: Text('Hello World!')), + floatingActionButton: FloatingActionButton( + // SECTION 3: Call `_launchAndroidActivity` somewhere. + onPressed: _launchAndroidActivity, + + // SECTION 3: End + tooltip: 'Launch Android activity', + child: const Icon(Icons.launch), + ), + ), + ); + } +} + +``` + + +### On the Android Side + +You must make changes to 4 files in the generated Android app to +ready it for launching fresh Compose activities. + +The first file requiring modifications is `android/app/build.gradle`. + + 1. Add the following to the existing `android` block: + + + + + ```kotlin title="android/app/build.gradle.kts" + android { + // Begin adding here + buildFeatures { + compose = true + } + composeOptions { + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin + kotlinCompilerExtensionVersion = "1.4.8" + } + // End adding here + } + ``` + + + + + ```groovy title="android/app/build.gradle" + android { + // Begin adding here + buildFeatures { + compose true + } + composeOptions { + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin + kotlinCompilerExtensionVersion = "1.4.8" + } + // End adding here + } + ``` + + + + + Visit the [developer.android.com][] link in the code snippet and + adjust `kotlinCompilerExtensionVersion`, as necessary. + You should only need to do this if you + receive errors during `flutter run` and those errors tell you + which versions are installed on your machine. + + [developer.android.com]: {{site.android-dev}}/jetpack/androidx/releases/compose-kotlin + + 2. Next, add the following block at the bottom of the file, at the root level: + + + + + ```kotlin title="android/app/build.gradle.kts" + dependencies { + implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") + implementation("androidx.activity:activity-compose") + implementation(platform("androidx.compose:compose-bom:2024.06.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material:material") + implementation("androidx.compose.material3:material3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2024.06.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") + } + ``` + + + + + ```groovy title="android/app/build.gradle" + dependencies { + implementation "androidx.core:core-ktx:1.10.1" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" + implementation "androidx.activity:activity-compose" + implementation platform("androidx.compose:compose-bom:2024.06.00") + implementation "androidx.compose.ui:ui" + implementation "androidx.compose.ui:ui-graphics" + implementation "androidx.compose.ui:ui-tooling-preview" + implementation "androidx.compose.material:material" + implementation "androidx.compose.material3:material3" + testImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.test.ext:junit:1.1.5" + androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" + androidTestImplementation platform("androidx.compose:compose-bom:2023.08.00") + androidTestImplementation "androidx.compose.ui:ui-test-junit4" + debugImplementation "androidx.compose.ui:ui-tooling" + debugImplementation "androidx.compose.ui:ui-test-manifest" + } + ``` + + + + + The second file requiring modifications is `android/build.gradle`. + + 1. Add the following buildscript block at the top of the file: + + + + + ```kotlin title="android/build.gradle.kts" + buildscript { + dependencies { + // Replace with the latest version. + classpath("com.android.tools.build:gradle:8.1.1") + } + repositories { + google() + mavenCentral() + } + } + ``` + + + + + ```groovy title="android/build.gradle" + buildscript { + dependencies { + // Replace with the latest version. + classpath 'com.android.tools.build:gradle:8.1.1' + } + repositories { + google() + mavenCentral() + } + } + ``` + + + + + The third file requiring modifications is + `android/app/src/main/AndroidManifest.xml`. + + 1. In the root application block, add the following `` declaration: + + ```xml title="android/app/src/main/AndroidManifest.xml" + + + + // START COPYING HERE + + // END COPYING HERE + + + … + + ``` + + The fourth and final code requiring modifications is + `android/app/src/main/kotlin/com/example/flutter_android_activity/MainActivity.kt`. + Here you'll write Kotlin code for your desired Android functionality. + + 1. Add the necessary imports at the top of the file + :::note + Your imports might vary if library versions have changed or + if you introduce different Compose classes when + you write your own Kotlin code. + Follow your IDE's hints for the correct imports you require. + ::: + +```kotlin + package com.example.android_launch_activity + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat +import io.flutter.embedding.android.FlutterActivity + ``` + + + 1. Add a second `Activity` to the bottom of the file, which you + referenced in the previous changes to `AndroidManifest.xml`: + + ```kotlin title="MainActivity.kt" +class SecondActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Column { + Text(text = "Second Activity") + // Note: This must match the shape of the data passed from your Dart code. + Text("" + getIntent()?.getExtras()?.getString("message")) + Button(onClick = { finish() }) { + Text("Exit") + } + } + } + } + } + } +} + ``` + + +## Launch Activity using `MethodChannels` + +### On the Dart side On the Dart side, create a method channel and invoke it from a specific user interaction, like tapping a button. @@ -105,7 +462,7 @@ There are 3 important values that must match across your Dart and Kotlin code: In this case, the data is a map with a single `"message"` key. -## On the Android side +### On the Android side You must make changes to 4 files in the generated Android app to ready it for launching fresh Compose activities. diff --git a/sites/docs/src/content/platform-integration/android/jnigen.md b/sites/docs/src/content/platform-integration/android/jnigen.md new file mode 100644 index 00000000000..90a8e982bff --- /dev/null +++ b/sites/docs/src/content/platform-integration/android/jnigen.md @@ -0,0 +1,221 @@ +--- +title: "Calling Android APIs using jnigen" +description: "Learn how to use auto-generated Dart bindings to call native Android APIs directly from your Flutter plugins." +--- + +## Overview + +In traditional Flutter development, +calling native Android platform APIs requires mapping asynchronous message channels +(using `MethodChannel` or `BasicMessageChannel`), +writing Kotlin or Java host-side listeners, +and manually serializing and deserializing types. + +With direct native interop using `jnigen` (Java Native Interface Generation), +this process is simplified: +1. **Configure**: Write a generation configuration script + pointing to the Android SDK or third-party classes you need. +2. **Generate**: Run the generation package + to analyze native classes and compile a type-safe Dart API bridge. +3. **Call directly**: Call Java/Kotlin code synchronously (where applicable) + and type-safely in your Dart code. + +![jnigen tooling flow chart](/assets/images/android/jnigen-flow.svg){:width="100%"} + +This tutorial guides you through building a Flutter plugin called `native_toast` +that interacts directly with Android's system `Toast` messages +without manually writing any Kotlin code. + +--- + +## Step 1: Create a plugin and add dependencies + +First, create a new Flutter plugin template focused on the Android platform. + +```sh +flutter create --template=plugin --platforms=android native_toast +cd native_toast +``` + +Now, add the mandatory runtime bindings package (`jni`), +the Flutter hooks integration wrapper (`jni_flutter`), +and the developer-facing generator tool (`jnigen`) as a dev dependency: + +```sh +flutter pub add jni jni_flutter +flutter pub add dev:jnigen +``` + +--- + +## Step 2: Create the generation config script + +Create a configuration script that details which native classes `jnigen` +needs to generate Dart models for. + +Specify `tool/jnigen.dart` at the root of your plugin directory: + +```sh +mkdir -p tool +touch tool/jnigen.dart +``` + +Add the following configuration code to `tool/jnigen.dart`: + +```dart +import 'dart:io'; +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/src/generated/android_os.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + // Automatically resolves your Android SDK path and evaluates the + // example app's build setup to acquire required platform dependencies. + androidSdkConfig: AndroidSdkConfig( + addGradleDeps: true, + androidExample: 'example/', + ), + classes: [ + 'android.widget.Toast', + 'android.os.Vibrator', + 'android.content.Context', + ], + ), + ); +} +``` +--- + +## Step 3: Run the code generator + +Run the code generation script to compile your bindings: + +```sh +dart run tool/jnigen.dart +``` + +Once execution completes, +`jnigen` creates a type-safe bridge inside `lib/src/generated/android_os.g.dart`. + +--- + +## Step 4: Implement the user-facing plugin API + +Now, build a clean, developer-facing API +that imports the generated interop bindings +and abstracts away the JNI details. + +Replace the contents of `lib/native_toast.dart` with the following implementation: + +```dart +import 'package:jni/jni.dart'; +import 'package:jni_flutter/jni_flutter.dart'; +import 'src/generated/android_os.g.dart' as native; + +class NativeToast { + // Extracts the live Android OS context handle and casts it to our generated class + final native.Context _context = androidApplicationContext.as(native.Context.type); + + void showToast(String message) { + // 1. Convert the Dart String into a native Java JString reference pointer + final jMessage = message.toJString(); + + // 2. Call the overloaded constructor generated by jnigen + final toast = native.Toast.makeText$1( + _context, + jMessage, + native.Toast.LENGTH_SHORT, + ); + + // 3. Trigger the standard Android System layout service + toast!.show(); + + // 4. Release the native JNI allocation pointer to prevent memory leaks + jMessage.release(); + } +} +``` + +### Understanding overloaded method mappings + +Java and Kotlin support method overloading (methods with the same name +that differ by parameter count or types). +Since Dart does not support method overloading, +`jnigen` appends a dollar suffix (such as `$1` or `$2`) +to disambiguate the generated Dart signatures in the order they are resolved. + +For example, `Toast.makeText` has two distinct signatures in Android: +* `Toast.makeText(Context context, int resId, int duration)` +* `Toast.makeText(Context context, CharSequence text, int duration)` + +In Dart, these are mapped respectively to: +* `Toast.makeText(...)` (Default / first signature) +* `Toast.makeText$1(...)` (Second signature, which accepts a string-mapped `CharSequence`) + +--- + +## Step 5: Test in the example app + +Verify the plugin within the automatically generated sandbox project under `example/`. + +Replace the contents of `example/lib/main.dart` with: + +```dart +import 'package:flutter/material.dart'; +import 'package:native_toast/native_toast.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Modern Native Toast Demo'), + backgroundColor: Colors.teal, + ), + body: Center( + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.teal), + onPressed: () { + // Instantiate our type-safe plugin wrapper class + final toastPlugin = NativeToast(); + + // Call the direct Android API wrapper + toastPlugin.showToast('Hello from Dart JNI Extension Types!'); + }, + child: const Text('Trigger Native Toast', style: TextStyle(color: Colors.white)), + ), + ), + ), + ); + } +} +``` + +Connect an Android emulator or device, +change into the example folder, +and run: + +```sh +cd example +flutter run +``` + +Once the application launches, +tap **Trigger Native Toast** to verify the system toast display: + +![JNI Toast Success Screenshot](/assets/images/android/jnigen-success.png){:width="300px" style="display: block; margin: 0 auto;"} diff --git a/sites/docs/src/content/platform-integration/android/request-permission.md b/sites/docs/src/content/platform-integration/android/request-permission.md new file mode 100644 index 00000000000..64956ef35ad --- /dev/null +++ b/sites/docs/src/content/platform-integration/android/request-permission.md @@ -0,0 +1,200 @@ +--- +title: "Request Android permissions" +description: "Request and manage Android permissions from jni code using a plugin" +--- + +## Overview + +This page details a means to use `jnigen` to wrap native Android code and package that functionality +as a plugin. It demonstrates requesting and querying Android permissions natively from Flutter. + +With this flow, one only has to edit a single file on the Android side. + +The code for [this plugin][] can be found here. + +## How It Works + +### `jnigen` Setup + +```dart + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/gen/android.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true, androidExample: 'example/'), + classes: [ + // provided by Android OS + 'android.app.Application', + 'androidx.activity.ComponentActivity', + 'androidx.fragment.app.FragmentActivity', + 'androidx.activity.result.ActivityResult', + 'androidx.core.app.ActivityCompat', + 'androidx.activity.result.ActivityResultCallback', + 'androidx.activity.result.ActivityResultLauncher', + 'androidx.activity.result.contract.ActivityResultContract', + 'android.content.Intent', + //'android.content.Context', + 'androidx.core.content.ContextCompat', + 'android.Manifest', + 'android.content.pm.PackageManager' + ], + ), + ); +} + +``` + +### Plugin implementation + +```dart +import 'package:flutter/foundation.dart'; + +import 'permissions_plugin_platform_interface.dart'; +import 'gen/android.g.dart'; +import 'package:jni/jni.dart'; + +class PermissionsPlugin { + Future getPlatformVersion() { + return PermissionsPluginPlatform.instance.getPlatformVersion(); + } + + // + bool checkPermission(JObject context, String permission) { + final jPermission = permission.toJString(); + try { + final result = ContextCompat.checkSelfPermission( + context, + jPermission, + ); + return result == PackageManager.PERMISSION_GRANTED; + } finally { + jPermission.release(); + } + } + + int checkAndRequestPermission( + JObject context, + String permission, + Function callback, + ) { + // Do I have permission? + if (ContextCompat.checkSelfPermission(context, permission.toJString()) == + PackageManager.PERMISSION_GRANTED) { + callback(); + } else if (ActivityCompat.shouldShowRequestPermissionRationale( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + permission.toJString(), + ) == + true) { + // Has the user denied the permission before? + // Give a reason why I need the permission + // and allow a re-request + print("I should ask for permission"); + // TODO Flow to show UI to reshow perms dialog + return -2; + } else { + // Ask for permission + ActivityCompat.requestPermissions( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + JArray.of(JString.type, [permission.toJString()]), + 0, + ); + } + return 0; + } +} + +``` + +### Using the plugin in an app + +Using this implementation of permissions means you only need to edit one bit of Android code to +add the possible permissions into the app's `AndroidManifest.xml` file. If the permission +does not exist in that file, `checkAndRequestPermission` will fail silently. + +```xml + + + + + + + +``` + +#### Initialize the plugin + +```dart +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _permissionsPlugin = PermissionsPlugin(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + // ... +} +``` + +Here is how the two implemented functions look on the Dart side. + +```dart +// Returns true or false if the permission has been granted +_permissionsPlugin.checkPermission( + Jni.androidApplicationContext, + "android.permission.CAMERA" +); + + +// Check for the permission and run execute a callback if allowed +_permissionsPlugin.checkAndRequestPermission( + Jni.androidApplicationContext, + "android.permission.CAMERA", + () { /* Code to run if the permission was granted */}, + ); + +``` + + + +```dart +@override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Plugin example app')), + body: Center( + child: Column( + children: [ + Text('Running on: $_platformVersion\n'), + FilledButton( + child: Text("Request Camera Permissions"), + onPressed: () { +_permissionsPlugin.checkAndRequestPermission( + Jni.androidApplicationContext, + "android.permission.CAMERA", + () {}, + ); + }, + ), + ], + ), + ), + ), + ); + } +} +``` + +[this plugin]: https://github.com/flutter/demos/tree/main/native_interop_demos/permissions_plugin diff --git a/sites/docs/src/content/platform-integration/index.md b/sites/docs/src/content/platform-integration/index.md index 712d0384a68..31712a00f4b 100644 --- a/sites/docs/src/content/platform-integration/index.md +++ b/sites/docs/src/content/platform-integration/index.md @@ -133,9 +133,9 @@ Learn how to add custom integrations with Android to your Flutter app.

Learn how to add the predictive back gesture to your app on Android.

- +
- Call JetPack APIs + Call Android APIs

Learn how the latest Android APIs in your app from Dart.

diff --git a/sites/docs/src/content/platform-integration/ios/ffigen.md b/sites/docs/src/content/platform-integration/ios/ffigen.md new file mode 100644 index 00000000000..4a882196d10 --- /dev/null +++ b/sites/docs/src/content/platform-integration/ios/ffigen.md @@ -0,0 +1,434 @@ +--- +title: "Calling iOS APIs using ffigen" +description: "Learn how to use auto-generated Dart bindings to call native Objective-C iOS APIs directly from your Flutter apps." +--- + +## Overview + +In traditional Flutter development, calling native iOS platform APIs requires mapping asynchronous message channels (using `MethodChannel`), writing Swift or Objective-C host-side listeners, and manually serializing and deserializing types. + +With direct native interop using `ffigen` (Foreign Function Interface Generation), this process is simplified: +1. **Configure**: Write a generation configuration script pointing to the iOS Frameworks or headers you need. +2. **Generate**: Run the generation package to analyze native Objective-C classes and compile a type-safe Dart API bridge. +3. **Call directly**: Call Objective-C code synchronously and type-safely in your Dart code. + +This tutorial guides you through building a Flutter app called `ios_calendar_demo` that interacts directly with iOS's system `EventKit` framework to fetch and create Calendar events without manually writing any Swift or Objective-C code. + +--- + +## Step 1: Create an app and add dependencies + +First, create a new Flutter app template focused on the iOS platform. + +```sh +flutter create --platforms=ios ios_calendar_demo +cd ios_calendar_demo +``` + +Now, add the mandatory runtime bindings package (`objective_c`), the `permission_handler` package to request Calendar access, and the developer-facing generator tool (`ffigen`) as a dev dependency: + +```sh +flutter pub add objective_c permission_handler +flutter pub add dev:ffigen +``` + +--- + +## Step 2: Create the generation config script + +Create a configuration script that details which native classes `ffigen` needs to generate Dart models for. + +Specify `tool/generate_code.dart` at the root of your app directory: + +```sh +mkdir -p tool +touch tool/generate_code.dart +``` + +Add the following configuration code to `tool/generate_code.dart` to target the `EventKit` framework: + +```dart +import 'package:ffigen/ffigen.dart'; + +void main() { + final config = Config( + output: Uri.file('lib/eventkit_bindings.dart'), + language: Language.objc, + name: 'EventKitWrapper', + description: 'Bindings for iOS EventKit framework', + headers: Headers( + entryPoints: [ + Uri.file( + '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/EventKit.framework/Headers/EventKit.h', + ), + ], + ), + objcInterfaces: ObjCInterfaces( + include: { + 'EKEventStore', + 'EKEvent', + 'EKCalendar', + 'NSArray', + 'NSDate', + }, + ), + ); + + final library = FfiGenerator(config).generate(); + library.generateFile(config.output.toFilePath()); +} +``` + +--- + +## Step 3: Generate the bindings + +Run the generator script. This will parse the `EventKit` headers and generate a type-safe Dart API at `lib/eventkit_bindings.dart` along with an Objective-C wrapper file. + +```sh +dart run tool/generate_code.dart +``` + +Because `ffigen` generates an Objective-C wrapper (`lib/eventkit_bindings.dart.m`) to safely map Objective-C blocks and initializers, you need to add it to your Xcode project. + +Update your `ios/Runner/Info.plist` to declare calendar usage: + +```xml +NSCalendarsFullAccessUsageDescription +We need calendar access to fetch and create events. +``` + +--- + +## Step 4: Write the Create Event Dialog + +To allow users to create events, we need a dialog. Create a new file `lib/create_event_dialog.dart` and add the following code. This code uses standard Flutter Date/Time pickers, and directly invokes the native `EKEventStore.saveEvent` API through our generated bindings! + +```dart +import 'package:flutter/material.dart'; +import 'package:objective_c/objective_c.dart'; + +import 'eventkit_bindings.dart'; + +class CreateEventDialog extends StatefulWidget { + final EKEventStore eventStore; + final VoidCallback onEventCreated; + + const CreateEventDialog({ + super.key, + required this.eventStore, + required this.onEventCreated, + }); + + @override + State createState() => _CreateEventDialogState(); +} + +class _CreateEventDialogState extends State { + final _titleController = TextEditingController(); + final _notesController = TextEditingController(); + DateTime _startDate = DateTime.now(); + DateTime _endDate = DateTime.now().add(const Duration(hours: 1)); + + Future _selectDateTime(BuildContext context, bool isStart) async { + final initialDate = isStart ? _startDate : _endDate; + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + if (pickedDate == null) return; + + if (!context.mounted) return; + + final pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(initialDate), + ); + if (pickedTime == null) return; + + final finalDateTime = DateTime( + pickedDate.year, + pickedDate.month, + pickedDate.day, + pickedTime.hour, + pickedTime.minute, + ); + + setState(() { + if (isStart) { + _startDate = finalDateTime; + if (_endDate.isBefore(_startDate)) { + _endDate = _startDate.add(const Duration(hours: 1)); + } + } else { + _endDate = finalDateTime; + } + }); + } + + void _saveEvent() { + final event = EKEvent.eventWithEventStore(widget.eventStore); + event.title = _titleController.text.toNSString(); + event.notes = _notesController.text.toNSString(); + event.startDate = _startDate.toNSDate(); + event.endDate = _endDate.toNSDate(); + + final defaultCalendar = widget.eventStore.defaultCalendarForNewEvents; + if (defaultCalendar != null) { + event.calendar = defaultCalendar; + final success = widget.eventStore.saveEvent( + event, + span: EKSpan.EKSpanThisEvent, + commit: true, + ); + if (success) { + widget.onEventCreated(); + } else { + debugPrint('Failed to save event'); + } + } else { + debugPrint('No default calendar found'); + } + + Navigator.of(context).pop(); + } + + @override + void dispose() { + _titleController.dispose(); + _notesController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Event'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Title'), + ), + TextField( + controller: _notesController, + decoration: const InputDecoration(labelText: 'Notes'), + maxLines: 3, + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Start Date'), + subtitle: Text('${_startDate.year}-${_startDate.month.toString().padLeft(2, '0')}-${_startDate.day.toString().padLeft(2, '0')} ${_startDate.hour.toString().padLeft(2, '0')}:${_startDate.minute.toString().padLeft(2, '0')}'), + onTap: () => _selectDateTime(context, true), + trailing: const Icon(Icons.calendar_today), + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('End Date'), + subtitle: Text('${_endDate.year}-${_endDate.month.toString().padLeft(2, '0')}-${_endDate.day.toString().padLeft(2, '0')} ${_endDate.hour.toString().padLeft(2, '0')}:${_endDate.minute.toString().padLeft(2, '0')}'), + onTap: () => _selectDateTime(context, false), + trailing: const Icon(Icons.calendar_today), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: _saveEvent, + child: const Text('Save'), + ), + ], + ); + } +} +``` + +--- + +## Step 5: Write the Main App + +Now, wire everything together in `lib/main.dart`. We'll request permissions on launch, and provide buttons to Retrieve and Create Events. + +```dart +import 'package:flutter/material.dart'; +import 'package:objective_c/objective_c.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'create_event_dialog.dart'; +import 'eventkit_bindings.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: CalendarHomePage()); + } +} + +class CalendarHomePage extends StatefulWidget { + const CalendarHomePage({super.key}); + + @override + State createState() => _CalendarHomePageState(); +} + +class _CalendarHomePageState extends State { + bool _hasCalendarPermission = false; + List> _events = []; + late final EKEventStore _eventStore; + + @override + void initState() { + super.initState(); + _eventStore = EKEventStore(); + _checkPermission(); + } + + Future _checkPermission() async { + final status = await Permission.calendarFullAccess.status; + setState(() { + _hasCalendarPermission = status.isGranted; + }); + } + + Future _requestPermission() async { + await Permission.calendarFullAccess + .onGrantedCallback(() { + debugPrint('Granted'); + setState(() { + _hasCalendarPermission = true; + }); + }) + .onDeniedCallback(() { + debugPrint('denied'); + }) + .request(); + } + + void _retrieveEvents() { + final startDate = NSDate.date(); + final endDate = NSDate.dateWithTimeIntervalSinceNow(30.0 * 24 * 60 * 60); + + final calendars = _eventStore.calendars; + final predicate = _eventStore.predicateForEventsWithStartDate( + startDate, + endDate: endDate, + calendars: calendars, + ); + + final NSArray eventsArray = _eventStore.eventsMatchingPredicate(predicate); + + final count = eventsArray.count; + List fetchedEvents = []; + for (int i = 0; i < count; i++) { + final obj = eventsArray.objectAtIndex(i); + final event = EKEvent.as(obj); + fetchedEvents.add(event); + } + + fetchedEvents.sort((a, b) { + return a.startDate.compare(b.startDate).value; + }); + + setState(() { + _events = fetchedEvents.map((e) { + final title = e.title.toDartString(); + final dt = e.startDate.toDateTime(); + return { + 'title': title, + 'date': '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}', + 'time': '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}', + }; + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('EventKit Native Interop Demo')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: _hasCalendarPermission ? null : _requestPermission, + child: Text( + _hasCalendarPermission + ? 'Calendar Access Granted' + : 'Request Calendar Permission', + ), + ), + const Divider(height: 32), + ElevatedButton( + onPressed: _hasCalendarPermission + ? () => showDialog( + context: context, + builder: (context) => CreateEventDialog( + eventStore: _eventStore, + onEventCreated: _retrieveEvents, + ), + ) + : null, + child: const Text('Create Event'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _hasCalendarPermission ? _retrieveEvents : null, + child: const Text('Retrieve Events'), + ), + const SizedBox(height: 24), + const Text( + 'Events', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Expanded( + child: _events.isEmpty + ? const Center(child: Text('No events to display.')) + : SingleChildScrollView( + child: DataTable( + columns: const [ + DataColumn(label: Text('Title')), + DataColumn(label: Text('Date')), + DataColumn(label: Text('Time')), + ], + rows: _events + .map( + (event) => DataRow( + cells: [ + DataCell(Text(event['title'] ?? '')), + DataCell(Text(event['date'] ?? '')), + DataCell(Text(event['time'] ?? '')), + ], + ), + ) + .toList(), + ), + ), + ), + ], + ), + ), + ); + } +} +``` + +## Conclusion + +You have successfully built a Flutter application that interacts directly with iOS APIs! By using `ffigen` and `objective_c`, you can skip writing manual message channels and focus on building high quality Dart code. diff --git a/sites/docs/src/content/platform-integration/native-code-options.md b/sites/docs/src/content/platform-integration/native-code-options.md new file mode 100644 index 00000000000..d926d63e953 --- /dev/null +++ b/sites/docs/src/content/platform-integration/native-code-options.md @@ -0,0 +1,84 @@ +--- +title: Writing custom platform-specific code +shortTitle: Platform-specific code +description: Learn how about the options to call platform specific code in your app. +--- + +## Overview + +Flutter and Dart provide multiple options for using platform-specific code +to access system APIs or leverage existing native SDKs. +These options range from low-level direct interop +requiring knowledge of Dart and the host language, +to high-level messaging paradigms +that abstract the details of the host platform. + +## Plugins + +We recommend checking [pub.dev](https://pub.dev) first +to see if a package or plugin already exists. +Using an existing plugin saves time, effort, and boilerplate. +If no plugin exists, +you can build a custom solution using one of the following approaches. + +## Custom solutions + +If a package does not support your desired feature on pub.dev, +or if you have custom integration requirements, +you can write platform-specific code. + +Dart and Flutter support two primary architectures for custom integrations. + +:::note +Neither solution below is inherently better or worse than existing plugins, +because all plugins use one of the following two options. +::: + +### Direct native interop (FFI or JNI) + +Direct native interop executes synchronously directly in memory. +This approach compiles the binding into your Dart executable +and executes native code on the UI thread. + +To create compilation bindings automatically, +use generator tools like [`ffigen`](https://pub.dev/packages/ffigen) +(for C, Objective-C, and Swift) +or [`jnigen`](https://pub.dev/packages/jnigen) (for Java and Kotlin on Android). + +For advanced or custom cases where automated generators do not fit, +you can write manual binding code using `dart:ffi`. + +### Method channels + +Message passing uses asynchronous communication +that serializes data across a platform bridge. + +We recommend using [`pigeon`](https://pub.dev/packages/pigeon) +to generate structured, type-safe channel code +and handle serialization automatically. + +If Pigeon does not support your use case, +you can write manual `MethodChannel` code +to serialize and pass data yourself. + +--- + +## Choose an approach + +Selecting an approach depends on your familiarity with the host languages, +comfort with low-level details (like memory management), +and the layout of the API you want to cover. + +### I need to access a few native-code functions + +Use **direct native interop** (`ffigen` or `jnigen`). +Creating a complete plugin wrapper for a few discrete functions +introduces unnecessary overhead. + +### I need to implement the same interface on iOS and Android + +Use **type-safe platform channels** with `pigeon`. + +### I need to re-implement a full native API in Dart + +Consider using **Pigeon** augmented by **`ffigen`/`jnigen`**. diff --git a/sites/docs/src/content/platform-integration/platform-channels.md b/sites/docs/src/content/platform-integration/platform-channels.md index 61f6c86b50c..277fbd53499 100644 --- a/sites/docs/src/content/platform-integration/platform-channels.md +++ b/sites/docs/src/content/platform-integration/platform-channels.md @@ -1,7 +1,7 @@ --- -title: Writing custom platform-specific code -shortTitle: Platform-specific code -description: Learn how to write custom platform-specific code in your app. +title: Using message passing to execute platform-specific code +shortTitle: Platform-channel code +description: Learn how to use platform channels in your app. --- diff --git a/sites/docs/src/data/sidenav/default.yml b/sites/docs/src/data/sidenav/default.yml index 26152d41fe1..066fb979905 100644 --- a/sites/docs/src/data/sidenav/default.yml +++ b/sites/docs/src/data/sidenav/default.yml @@ -369,9 +369,11 @@ permalink: /reference/supported-platforms - title: Build desktop apps with Flutter permalink: /platform-integration/desktop - - title: Write platform-specific code + - title: Write platform-specific code (new) + permalink: /platform-integration/native-code-options + - title: Communicate with the host OS (old pigeon/platform channels topic) permalink: /platform-integration/platform-channels - - title: Bind to native code + - title: Bind to native code using ffi (title change) permalink: /platform-integration/bind-native-code - title: Android permalink: /platform-integration/android @@ -384,10 +386,14 @@ permalink: /platform-integration/android/predictive-back - title: Host a native Android view permalink: /platform-integration/android/platform-views - - title: Calling JetPack APIs - permalink: /platform-integration/android/call-jetpack-apis - - title: Launch a Jetpack Compose activity + - title: Call native APIs (delete) + permalink: /platform-integration/android/call-native-apis + - title: Use jnigen to call Android code (new) + permalink: /platform-integration/android/jnigen + - title: Launch a Jetpack Compose activity (updated) permalink: /platform-integration/android/compose-activity + - title: Request Android permissions from Flutter (new) + permalink: /platform-integration/android/request-permission - title: Restore state on Android permalink: /platform-integration/android/restore-state-android - title: Target ChromeOS with Android @@ -403,6 +409,8 @@ permalink: /platform-integration/ios/ios-latest - title: Leverage Apple's system libraries permalink: /platform-integration/ios/apple-frameworks + - title: Use ffigen to use Apple APIs (new) + permalink: /platform-integration/ios/ffigen - title: Add a launch screen permalink: /platform-integration/ios/launch-screen - title: Add iOS App Clip support @@ -881,4 +889,3 @@ permalink: /reference/flutter-cli - title: API docs permalink: https://api.flutter.dev - diff --git a/sites/docs/web/assets/images/android/jnigen-flow.svg b/sites/docs/web/assets/images/android/jnigen-flow.svg new file mode 100644 index 00000000000..ba1f7ed3685 --- /dev/null +++ b/sites/docs/web/assets/images/android/jnigen-flow.svg @@ -0,0 +1,60 @@ + + + + + + Android SDK + or Native JAR/AAR + + + + + ApiSummarizer + + + + jnigen AST parser + Analyzes class layouts + + + + + Generate + + + + android_os.g.dart + Generated Dart Bindings + + + + + Type-safe Imports + + + + Flutter Plugin API + + + + + Calls + + + + Flutter App UI + diff --git a/sites/docs/web/assets/images/android/jnigen-success.png b/sites/docs/web/assets/images/android/jnigen-success.png new file mode 100644 index 00000000000..fb541a3f1b7 Binary files /dev/null and b/sites/docs/web/assets/images/android/jnigen-success.png differ