Last active
March 9, 2024 17:41
-
-
Save CoderNamedHendrick/039e0d363979c77bcc20ea6c30367167 to your computer and use it in GitHub Desktop.
Flutter to native communication via declarative UI toolkits article codesnippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
android { | |
namespace "com.example.mobile_declarative_ui" | |
compileSdkVersion 34 | |
ndkVersion flutter.ndkVersion | |
compileOptions { | |
sourceCompatibility JavaVersion.VERSION_1_8 | |
targetCompatibility JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = '1.8' | |
} | |
buildFeatures { | |
compose true | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion compose_version | |
kotlinCompilerVersion kotlin_version | |
} | |
sourceSets { | |
main.java.srcDirs += 'src/main/kotlin' | |
} | |
defaultConfig { | |
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | |
applicationId "com.example.mobile_declarative_ui" | |
// You can update the following values to match your application needs. | |
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. | |
minSdkVersion 21 | |
targetSdkVersion 34 | |
versionCode flutterVersionCode.toInteger() | |
versionName flutterVersionName | |
} | |
buildTypes { | |
release { | |
// TODO: Add your own signing config for the release build. | |
// Signing with the debug keys for now, so `flutter run --release` works. | |
signingConfig signingConfigs.debug | |
} | |
} | |
packagingOptions { | |
resources { | |
excludes += '/META-INF/{AL2.0,LGPL2.1}' | |
} | |
} | |
} | |
flutter { | |
source '../..' | |
} | |
dependencies { | |
implementation("androidx.activity:activity-compose:1.7.2") | |
implementation("androidx.compose.ui:ui") | |
implementation("androidx.compose.ui:ui-graphics") | |
implementation("androidx.compose.ui:ui-tooling-preview") | |
implementation("androidx.compose.foundation:foundation") | |
implementation("androidx.compose.foundation:foundation-layout") | |
implementation("androidx.compose.material:material") | |
implementation("androidx.compose.runtime:runtime-livedata") | |
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' | |
implementation platform('androidx.compose:compose-bom:2023.03.00') | |
implementation 'androidx.compose.material3:material3' | |
implementation platform('androidx.compose:compose-bom:2023.03.00') | |
androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') | |
androidTestImplementation 'androidx.compose.ui:ui-test-junit4' | |
androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') | |
debugImplementation 'androidx.compose.ui:ui-tooling' | |
debugImplementation 'androidx.compose.ui:ui-test-manifest' | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import SwiftUI | |
import Flutter | |
@UIApplicationMain | |
@objc class AppDelegate: FlutterAppDelegate { | |
private let channelName = "com.hendrick.navigateChannel" | |
private let navigateFunctionName = "flutterNavigate" | |
override func application( | |
_ application: UIApplication, | |
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | |
) -> Bool { | |
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController | |
GeneratedPluginRegistrant.register(with: self) | |
let navigationController = DelegateViewController(rootViewController: controller) | |
navigationController.isNavigationBarHidden = true | |
window?.rootViewController = navigationController | |
window?.makeKeyAndVisible() | |
let methodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: controller.binaryMessenger) | |
methodChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in | |
navigationController.result = result | |
if call.method == self.navigateFunctionName { | |
let swiftUIViewController = UIHostingController(rootView: SwiftUIView(navigationController: navigationController, delegate: navigationController)) | |
navigationController.pushViewController(swiftUIViewController, animated: true) | |
} | |
}) | |
return super.application(application, didFinishLaunchingWithOptions:launchOptions) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import SwiftUI | |
import Flutter | |
typealias FlutterResult = (Result<String, Error>) -> Void | |
@UIApplicationMain | |
@objc class AppDelegate: FlutterAppDelegate, NativeMobileHostApi { | |
private var navigationController: DelegateViewController? = nil | |
override func application( | |
_ application: UIApplication, | |
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | |
) -> Bool { | |
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController | |
GeneratedPluginRegistrant.register(with: self) | |
navigationController = DelegateViewController(rootViewController: controller) | |
navigationController?.isNavigationBarHidden = true | |
window?.rootViewController = navigationController | |
window?.makeKeyAndVisible() | |
NativeMobileHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: self) | |
return super.application(application, didFinishLaunchingWithOptions:launchOptions) | |
} | |
func getNativeUiResult(completion: @escaping (Result<String, Error>) -> Void) { | |
navigationController?.result = completion | |
let swiftUiViewController = UIHostingController(rootView: SwiftUIView(navigationController: self.navigationController, delegate: self.navigationController)) | |
navigationController?.pushViewController(swiftUiViewController, animated: true) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.mobile_declarative_ui | |
import android.content.Intent | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.text.BasicTextField | |
import androidx.compose.foundation.text.KeyboardOptions | |
import androidx.compose.material3.Button | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.text.input.ImeAction | |
import androidx.compose.ui.text.input.KeyboardType | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import com.example.mobile_declarative_ui.ui.theme.AndroidTheme | |
class ComposeActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
AndroidTheme { | |
// A surface container using the 'background' color from the theme | |
Surface( | |
modifier = Modifier.fillMaxSize(), | |
color = MaterialTheme.colorScheme.background | |
) { | |
ComposeBody { | |
} | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
fun ComposeBody(modifier: Modifier = Modifier, onClick: (value: String) -> Unit) { | |
var input1 by remember { | |
mutableStateOf("") | |
} | |
var input2 by remember { | |
mutableStateOf("") | |
} | |
Column( | |
modifier = modifier.padding(horizontal = 12.dp), | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally, | |
) { | |
Greeting("Enter two numbers whose sum we're going to send to flutter") | |
Spacer(modifier = modifier.height(15.dp)) | |
BasicTextField( | |
value = input1, | |
modifier = modifier | |
.fillMaxWidth() | |
.background(color = MaterialTheme.colorScheme.primaryContainer) | |
.padding(6.dp), | |
singleLine = true, | |
keyboardOptions = KeyboardOptions( | |
keyboardType = KeyboardType.Number, | |
imeAction = ImeAction.Done, | |
), | |
textStyle = MaterialTheme.typography.bodyLarge, | |
onValueChange = { | |
input1 = it | |
}, | |
) | |
Spacer(modifier = modifier.height(10.dp)) | |
BasicTextField( | |
value = input2, | |
modifier = modifier | |
.fillMaxWidth() | |
.background(color = MaterialTheme.colorScheme.primaryContainer) | |
.padding(6.dp), | |
singleLine = true, | |
keyboardOptions = KeyboardOptions( | |
keyboardType = KeyboardType.Number, | |
imeAction = ImeAction.Done, | |
), | |
textStyle = MaterialTheme.typography.bodyLarge, | |
onValueChange = { | |
input2 = it | |
}, | |
) | |
Spacer(modifier = modifier.height(20.dp)) | |
Text(text = "Sum of inputs: ${sum(input1, input2)}") | |
Button(onClick = { | |
onClick("${sum(input1, input2)}") | |
}) { | |
Text( | |
text = "Send sum to flutter", | |
fontSize = 14.sp, | |
) | |
} | |
} | |
} | |
fun sum(input1: String, input2: String): Int { | |
val val1 = (input1.toIntOrNull() ?: 0).toInt() | |
val val2 = (input2.toIntOrNull() ?: 0).toInt() | |
return val1 + val2 | |
} | |
@Composable | |
fun Greeting(name: String, modifier: Modifier = Modifier) { | |
Text( | |
text = "$name!", | |
modifier = modifier | |
) | |
} | |
@Preview(showBackground = true) | |
@Composable | |
fun GreetingPreview() { | |
AndroidTheme { | |
ComposeBody {} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ComposeActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
AndroidTheme { | |
// A surface container using the 'background' color from the theme | |
Surface( | |
modifier = Modifier.fillMaxSize(), | |
color = MaterialTheme.colorScheme.background | |
) { | |
ComposeBody { | |
val replyIntent = Intent() | |
replyIntent.putExtra(REPLY_MESSAGE, it) | |
setResult(RESULT_OK, replyIntent) | |
finish() | |
} | |
} | |
} | |
} | |
} | |
companion object { | |
const val REPLY_MESSAGE: String = "reply_message" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Flutter | |
class DelegateViewController : UINavigationController, DelegateProtocol { | |
var result : FlutterResult? | |
func popViewController(string: String) { | |
result?(string) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol DelegateProtocol: AnyObject { | |
func popViewController(string: String) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Flutter | |
class DelegateViewController : UINavigationController, DelegateProtocol { | |
var result : FlutterResult? | |
func popViewController(string: String) { | |
result?(.success(string)) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.mobile_declarative_ui | |
import android.content.Intent | |
import io.flutter.embedding.android.FlutterActivity | |
import io.flutter.embedding.engine.FlutterEngine | |
import io.flutter.plugin.common.MethodChannel | |
class MainActivity : FlutterActivity() { | |
private val eventChannel = "com.hendrick.navigateChannel" | |
private val navigateFunctionName = "flutterNavigate" | |
private var methodChannelResult: MethodChannel.Result? = null | |
private val composeActivityRequestCode: Int = 4 | |
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | |
super.configureFlutterEngine(flutterEngine) | |
MethodChannel( | |
flutterEngine.dartExecutor.binaryMessenger, | |
eventChannel | |
).setMethodCallHandler { call, result -> | |
methodChannelResult = result | |
if (call.method == navigateFunctionName) { | |
val intent = Intent(this, ComposeActivity::class.java) | |
startActivityForResult(intent, composeActivityRequestCode) | |
} | |
} | |
} | |
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | |
super.onActivityResult(requestCode, resultCode, data) | |
if (requestCode == composeActivityRequestCode) { | |
if (resultCode == RESULT_OK) { | |
val value = data?.getStringExtra(ComposeActivity.REPLY_MESSAGE) | |
methodChannelResult?.success(value) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
useMaterial3: true, | |
), | |
home: const MyHomePage(title: 'Flutter Demo Home Page'), | |
); | |
} | |
} | |
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 textFromNative = ''; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
backgroundColor: Theme.of(context).colorScheme.inversePrimary, | |
title: Text(widget.title), | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Text.rich( | |
TextSpan( | |
text: 'Text from the other side::', | |
style: Theme.of(context).textTheme.bodyLarge, | |
children: [ | |
TextSpan( | |
text: textFromNative, | |
style: Theme.of(context).textTheme.headlineSmall, | |
) | |
], | |
), | |
), | |
const SizedBox(height: 20), | |
MaterialButton( | |
onPressed: () {}, | |
child: const Text('Call native code'), | |
) | |
], | |
), | |
), | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.mobile_declarative_ui | |
import io.flutter.embedding.android.FlutterActivity | |
import io.flutter.embedding.engine.FlutterEngine | |
import io.flutter.plugin.common.MethodChannel | |
class MainActivity : FlutterActivity() { | |
private val eventChannel = "com.hendrick.navigateChannel" | |
private val navigateFunctionName = "flutterNavigate" | |
private var methodChannelResult: MethodChannel.Result? = null | |
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | |
super.configureFlutterEngine(flutterEngine) | |
MethodChannel( | |
flutterEngine.dartExecutor.binaryMessenger, | |
eventChannel | |
).setMethodCallHandler { call, result -> | |
methodChannelResult = result | |
if (call.method == navigateFunctionName) { | |
methodChannelResult?.success("Hello from the android side") | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.mobile_declarative_ui | |
import FlutterError | |
import NativeMobileHostApi | |
import android.content.Intent | |
import io.flutter.embedding.android.FlutterActivity | |
import io.flutter.embedding.engine.FlutterEngine | |
import io.flutter.plugin.common.MethodChannel | |
typealias FlutterResultCallback = (Result<String>) -> Unit | |
class MainActivity : FlutterActivity(), NativeMobileHostApi { | |
private var nativeUiResultCallback: FlutterResultCallback? = null | |
private val composeActivityRequestCode: Int = 4 | |
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | |
super.configureFlutterEngine(flutterEngine) | |
NativeMobileHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this) | |
} | |
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | |
super.onActivityResult(requestCode, resultCode, data) | |
if (nativeUiResultCallback == null) return@onActivityResult | |
if (requestCode == composeActivityRequestCode && resultCode == RESULT_OK) { | |
val value = data?.getStringExtra(ComposeActivity.REPLY_MESSAGE) | |
if (value == null) { | |
nativeUiResultCallback?.invoke( | |
Result.failure(FlutterError("code", "message", "details")) | |
) | |
return@onActivityResult | |
} | |
nativeUiResultCallback?.invoke(Result.success(value)) | |
} | |
} | |
override fun getNativeUiResult(callback: (Result<String>) -> Unit) { | |
nativeUiResultCallback = callback | |
val intent = Intent(this, ComposeActivity::class.java) | |
startActivityForResult(intent, composeActivityRequestCode) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
buildscript { | |
ext { | |
kotlin_version = '1.8.10' | |
compose_version = '1.4.2' | |
} | |
repositories { | |
google() | |
mavenCentral() | |
} | |
dependencies { | |
classpath 'com.android.tools.build:gradle:7.4.2' | |
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/services.dart'; | |
const nativeChannelName = 'com.hendrick.navigateChannel'; | |
const methodChannel = MethodChannel(nativeChannelName); | |
const navigateFunctionName = 'flutterNavigate'; | |
Future<String> navigateToNative() async { | |
try { | |
var data = await methodChannel.invokeMethod(navigateFunctionName); | |
return data; | |
} on PlatformException catch (e) { | |
return 'Failed to invoke: ${e.message}'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:pigeon/pigeon.dart'; | |
@ConfigurePigeon(PigeonOptions( | |
dartOut: 'lib/native_api/native_mobile_ui.g.dart', | |
kotlinOut: | |
'android/app/src/main/kotlin/com/example/mobile_declarative_ui/NativeMobileUi.g.kt', | |
javaOut: 'android/app/src/main/java/io/flutter/plugins/NativeMobileUi.java', | |
swiftOut: 'ios/Runner/NativeMobileUi.g.swift', | |
dartPackageName: 'native_mobile_ui', | |
)) | |
@HostApi() | |
abstract class NativeMobileHostApi { | |
@async | |
String getNativeUiResult(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'native_mobile_ui.g.dart'; | |
final _api = NativeMobileHostApi(); | |
Future<String> getNativeUiResult() async { | |
try { | |
return await _api.getNativeUiResult(); | |
} catch (e) { | |
return 'Failed to retrieve result'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct SwiftUIView: View { | |
@State private var input1: String = ""; | |
@State private var input2: String = ""; | |
weak var navigationController: UINavigationController? | |
weak var delegate: DelegateProtocol? | |
var body: some View { | |
VStack(alignment: .center) { | |
Text("Enter two numbers whose sum we're going to send to flutter") | |
.frame(alignment: .center) | |
Spacer().frame(height: 12) | |
TextField("Input 1", text: $input1) | |
.padding(.horizontal, 12) | |
.textFieldStyle(RoundedBorderTextFieldStyle()) | |
.keyboardType(.numberPad) | |
Spacer().frame(height: 12) | |
TextField("Input 2", text: $input2) | |
.padding(.horizontal, 12) | |
.textFieldStyle(RoundedBorderTextFieldStyle()) | |
.keyboardType(.numberPad) | |
Spacer().frame(height: 20) | |
Text("Sum of inputs: \(calculateInputs())") | |
Button(action: { | |
let message: String = "\(calculateInputs())"; | |
delegate?.popViewController(string: message) | |
navigationController?.popViewController(animated: true) | |
}) { | |
Text("Send sum to flutter") | |
} | |
.padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) | |
.background(Color.blue) | |
.foregroundColor(.white) | |
.cornerRadius(12) | |
}.padding(12) | |
.onTapGesture { | |
self.hideKeyboard() | |
} | |
} | |
func calculateInputs() -> Int { | |
return (Int(input1) ?? 0) + (Int(input2) ?? 0) | |
} | |
} | |
extension View { | |
func hideKeyboard() { | |
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) | |
} | |
} | |
struct SwiftUIView_Previews: PreviewProvider { | |
static var previews: some View { | |
SwiftUIView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment