Skip to content

Instantly share code, notes, and snippets.

@CoderNamedHendrick
Last active March 9, 2024 17:41
Show Gist options
  • Save CoderNamedHendrick/039e0d363979c77bcc20ea6c30367167 to your computer and use it in GitHub Desktop.
Save CoderNamedHendrick/039e0d363979c77bcc20ea6c30367167 to your computer and use it in GitHub Desktop.
Flutter to native communication via declarative UI toolkits article codesnippets
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'
}
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)
}
}
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)
}
}
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 {}
}
}
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"
}
}
import Foundation
import Flutter
class DelegateViewController : UINavigationController, DelegateProtocol {
var result : FlutterResult?
func popViewController(string: String) {
result?(string)
}
}
protocol DelegateProtocol: AnyObject {
func popViewController(string: String)
}
import Foundation
import Flutter
class DelegateViewController : UINavigationController, DelegateProtocol {
var result : FlutterResult?
func popViewController(string: String) {
result?(.success(string))
}
}
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)
}
}
}
}
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'),
)
],
),
),
);
}
}
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")
}
}
}
}
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)
}
}
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"
}
}
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}';
}
}
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();
}
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';
}
}
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