I did a plugin in native code(Kotlin) and tried to use that in a Flutter application. I used a method channel for Flutter to native code communication and an event channel for native code to Flutter. Method channel communication is working properly, but event channel isn't.
FLUTTER:
// will execute this in initState
EventChannel _eventChannel = EventChannel("STREAM_CHANNEL");
_eventChannel.receiveBroadcastStream().map((event) => {
// some actions
})
// will execute this by button click (happens after widget got built)
result =
await MethodChannel("METHOD_CHANNEL").invokeMethod("functionToInvoke", {
// payload
})
KOTLIN:
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
startEventChannel(flutterEngine)
startMethodChannel(flutterEngine.dartExecutor.binaryMessenger)
}
fun startMethodChannel(#NonNull flutterEngine: FlutterEngine) {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
"METHOD_CHANNEL").setMethodCallHandler {
call, result -> when(call.method) {
"functionToInvoke" -> {
// some action
}
}
}
// method not called inspite of calling from configureFlutterEngine
fun startEventChannel(messenger: BinaryMessenger) {
eventChannel = EventChannel(messenger, "STREAM_CHANNEL");
eventChannel.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, eventSink: EventChannel.EventSink)
{
eventSink.success("success")
}
}
)
}
The startEventChannel method is not even called but startMethodChannel has been called and methods are registered properly.
There is no issue with the channel names (checked both sides).
Am I missing anything?
Problems:
Stream not listened to:
The stream returned by _eventChannel.receiveBroadcastStream() is not being listened to. You're only mapping the stream but this callback won't be triggered until you get data in the stream.
And from your Kotlin EventChannel code, data is not added into the stream until you listen to the event channel.
Argument Mismatch:
The startEventChannel method requires an argument of type BinaryMessenger but you're sending it flutterEngine which is of type FlutterEngine.
Solution:
Add a listener to the stream:
Call .listen on the mapped stream from _eventChannel.receiveBroadcastStream() like shown below:
_eventChannel.receiveBroadcastStream().map((event) {}).listen((event) {
// some actions
});
Pass the right argument to startEventChannel:
Pass the BinaryMessenger to the startEventChannel instead of the FlutterEngine.
startEventChannel(flutterEngine.dartExecutor.binaryMessenger)
Related
I'm working on a pet project where I'm trying to create a hybrid app using a WebView. The web platform that I run in the WebView sends events to the WebView/App through a #JavascriptInterface object. I can also command the web navigation by running a set of javascript functions against the web platform via the WebView using the evaluateJavascript(String, (String) -> Unit) function.
What I'm trying to achieve right now is that these commands that I execute through the evaluateJavascript(String, (String) -> Unit) function run sequentially. I might execute these commands from many different places at the same time, so I want them to run, wait for the callback from the evaluateJavascript() function to get called, and then execute the next command in the queue.
This is what I have in my custom WebView class:
val scriptQueue = mutableListOf<String>()
fun queueEvaluateJavascript(script: String) {
if (webViewIsLoading) {
scriptQueue.add(script)
} else {
scriptQueue.add(script)
runScriptQueue()
}
}
fun runScriptQueue() {
for (script in scriptQueue) {
evaluateJavascript(script, { })
}
scriptQueue.clear()
}
As you can see this is a super basic approach, and I don't really account for the evaluateJavascript() callback. Ideally, I'd like to find a way to flat map each of this evaluateJavascript() calls so we execute one after another, but waiting for the callback to go through.
With RxJava I think I'd create an Observable and then have the evaluateJavascript() callback trigger the subscriber's onNext(). Since, I'm using Kotlin Coroutines I wanted to do something with Coroutines, so I can queue these evaulateJavascript() calls. But I'm not 100% sure what would be the equivalent here.
That would be a nice problem to approach with coroutines.
The usual way to convert callback based APIs to suspend functions is the following:
suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
You can then use that in combination maybe with a Channel (to serve as a queue) and a coroutine that processes this channel:
class MyWebView(context: Context) : WebView(context) {
private val jsQueue = Channel<String>(BUFFERED)
fun startJsProcessingLoopIn(scope: CoroutineScope) {
scope.launch {
for (script in jsQueue) {
evaluateJs(script)
}
}
}
// you could also make this function non-suspend if necessary by calling
// sendBlocking (or trySend depending on coroutines version)
suspend fun queueEvaluateJavascript(script: String) {
jsQueue.send(script)
}
private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
}
Alternatively you can create your own coroutine scope and make sure to tie it with some sort of lifecycle of your webview (I'm not familiar with WebView so I'll let you judge which kind of method is correct):
class MyWebView2(context: Context) : WebView(context) {
// you can even further customize the exact thread pool used here
// by providing a particular dispatcher
private val jsProcessingScope = CoroutineScope(CoroutineName("js-processing"))
private val jsQueue = Channel<String>(BUFFERED)
// this starts the loop right away but you can also put this in a method
// to start it at a more appropriate moment
init {
jsProcessingScope.launch {
for (script in jsQueue) {
evaluateJs(script)
}
}
}
// you could also make this function non-suspend if necessary by calling
// sendBlocking (or trySend depending on coroutines version)
suspend fun queueEvaluateJavascript(script: String) {
jsQueue.send(script)
}
private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
fun someCloseOrDisposeCallback() {
jsProcessingScope.cancel()
}
}
I am building a Flutter Application, and for one of the API's I am using, it does not have Flutter support, only Android and iOS. My solution to this was to use Platform Channels, but how would I pass an Image as an argument?
To explain a little further, I am picking an image from the gallery with ImagePicker().getImage in the dart file, and I want to send the image selected to the method in the Kotlin file that will do something with the image on its end and return a string.
After looking at the docs, I was able to make a channel like this:
static const platform = const MethodChannel('app.dev/channel');
final string result = await platform.invokeMethod('returnStringfromImage');
And in the Kotlin file:
private val CHANNEL = "app.dev/channel"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// Note: this method is invoked on the main thread.
call, result ->
if (call.method == "returnStringfromImage") {
val return = returnStringfromImage(call.arguments)
}
else {
result.notImplemented()
}
}
}
How would I send the image over, and pass it as an argument for the returnStringfromImage() method? Thank you!
You need to convert the picked image to any of these supported types, most probably as bytes (Uint8list) to send over to the platform side.
ImagePicker would return a File, so You can call readAsBytes on it to get Uint8list. Then you can pass it via arguments of invokeMethod.
I have created a custom MethodChannel "com.example.app/widget" that updates a home screen widget on Android after receiving a Firebase Cloud Message. It runs fine when it is called while the app is in the foreground, but I would also like to call it when a Firebase Cloud Message is received while the app is closed or in the background.
When the app is in the background, it gives me a MissingPluginException error, like below:
E/flutter (28540): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: MissingPluginException(No implementation found for method updateFromFlutter on channel com.example.app/widget)
E/flutter (28540): #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:157:7)
... and so on. There are a lot of other threads about MissingPluginException errors that deal with adding a plugin to the registry, but I haven't been able to find any that address custom MethodChannels that are not part of another plugin. Is it possible to add my custom MethodChannel to the registry or do something similar that will result in the Dart code being able to call a method from that channel while in the background?
I have tried using workmanager and android_alarm_manager and they seem to run fine themselves, but they still can't get past this block with my custom channel.
My MainActivity.kt has the method details in it:
class MainActivity: FlutterActivity(), MethodChannel.MethodCallHandler {
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/widget")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"updateFromFlutter" -> {
val views = RemoteViews(context.packageName, R.layout.appwidget).apply {
setTextViewText(R.id.text, call.argument("text"))
}
val manager = AppWidgetManager.getInstance(this)
manager.updateAppWidget(call.argument("idNumber"), views)
}
}
}
}
Then in main.dart I call:
Future<void> updateAndroidWidget(String text) async {
WidgetsFlutterBinding.ensureInitialized();
const MethodChannel platform = MethodChannel('com.example.app/widget');
try {
platform.invokeMethod("updateFromFlutter", {
"text": text,
"idNumber": savedPreferences.androidWidgetID
});
} catch (e) {
print("failed: $e");
}
}
I already have Flutter Android Embedding V2 (Flutter Version >= 1.12).
Any help is greatly appreciated.
I am trying to create a Android app which launches flutter inside it. I have learnt that we can pass data to flutter via MethodChannels like this:
MethodChannel(
FlutterEngineCache.getInstance().get(NEARBY_PLACES_ENGINE_KEY)?.dartExecutor?.binaryMessenger,
"APP_CHANNEL"
).setMethodCallHandler { call, result ->
if (call.method == "getGreetings") {
val coordinates = "Hello"
result.success(coordinates)
}
}
startActivity(FlutterActivity
.withCachedEngine(NEARBY_PLACES_ENGINE_KEY)
.build(this))
This is possible when I have access to FlutterEngine instance (In this case with help of FlutterEngineCache). But how do we get the BinaryMessenger of a engine created with FlutterActivity.withNewEngine() ? Please help. TIA!
Okay so I found the way to do it.
Created a custom FlutterActivity class and added that in the manifest instead of the one from io.flutter.embedding.android.FlutterActivity. The custom FlutterActivity extends from FlutterActivity, and overrides some methods to be able to access the newEngine.
class CustomFlutterActivity : FlutterActivity() {
companion object {
var methodChannelInvoker: (MethodCall, Result) -> Unit = { _, _ -> }
fun withCachedEngine(cachedEngineId: String): CachedEngineIntentBuilder {
return CachedEngineIntentBuilder(CompassFlutterActivity::class.java, cachedEngineId)
}
fun withNewEngine(): NewEngineIntentBuilder {
return NewEngineIntentBuilder(CompassFlutterActivity::class.java)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "APP_CHANNEL")
.setMethodCallHandler { call, result ->
methodChannelInvoker(call, result)
}
}
}
and at the launch site,
CustomFlutterActivity.methodChannelInvoker = { call, result ->
if (call.method == "getGreetings") {
val greetings = "Hello there!"
result.success(coordinates)
}
}
startActivity(CustomFlutterActivity
.withNewEngine()
.initialRoute("/custom_route")
.build(this))
This way, when the new engine launches, you get callback at configureFlutterEngine with new engine as parameter, we can simply access dartExecutor at that point.
Oh yes, and don't forget to add the CustomFlutterActivity in the manifest instead of FlutterActivity.
I have successfully integrated flutter module in my native android application by following steps here .
The process of caching flutter engine I have already done in Application class. I am launching my flutter screen with this from android fragment.
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
Now I want to pass my auth token to flutter module for making api calls.
I am following the process from here and created method channel in dart code but I do not know where to create the method channel in the native side.
If I am creating it in project/moduleName/.android/app/src/main/java/com/package/host/MainActivity.java
It is giving exception Unhandled Exception: MissingPluginException(No implementation found for method
Also note that this folder is placed in .gitignore by default when I created this flutter module in Android studio.
I already has a look at older tutiorals but this caching of flutter engine option is not there in them.
Please tell where am I doing wrong ?
You need to have a reference for the Flutter Engine and then use it to create the Method Channel. Take in account that the activity/fragment that launch the Flutter activity is in charge to manage the Method Channel.
private const val FLUTTER_ENGINE_ID = "flutter_engine"
private const val CHANNEL = "myApp.flutter.dev/auth"
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var flutterEngine: FlutterEngine
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupFlutterEngine()
setupMethodChannel()
setSupportActionBar(findViewById(R.id.toolbar))
fab.setOnClickListener {
launchFlutterModule()
}
}
private fun setupFlutterEngine() {
createAndConfigureFlutterEngine()
FlutterEngineCache
.getInstance()
.put(FLUTTER_ENGINE_ID, flutterEngine)
}
private fun createAndConfigureFlutterEngine() {
flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
}
private fun setupMethodChannel() {
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
// All your implementation for auth token
}
}
private fun launchFlutterModule() {
startActivity(getFlutterIntent())
}
private fun getFlutterIntent(): Intent {
return FlutterActivity
.withCachedEngine(FLUTTER_ENGINE_ID)
.backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
.build(this)
}
}
You can make a Wrapper where you can hold the method channel code and flutter engine setup, and this wrapper is init in your Application class or maybe init and injected with Dagger or Hilt where is needed.
You can pass simple data as request parameters.
Android code:
FlutterActivity
.withNewEngine()
.initialRoute("/MyPage?username=my_user&age=30&gender=male")
.build(MainActivity.this)
Flutter code, I use GetX library to get the parameters
Map<String, String?> parameters = Get.parameters;
print("parameters: $parameters");
Output:
parameters: {username: my_user, age: 30, gender: male}
First create flutter platform client with channel
set methodCallHamdler passing same channel
Refer this official doc-
https://flutter.dev/docs/development/platform-integration/platform-channels