Invoke Flutter (Dart) code from native Android home screen widget
Asked Answered
C

5

20

I added a native Android home screen widget to my Flutter application.

In my AppWidgetProvider implementation, I'd like to call dart code in my onUpdate() method using a platform channel.

Is this possible? If so, how can this be achieved?

My current Android (Java) code:

package com.westy92.checkiday;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.util.Log;

import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterNativeView;

public class HomeScreenWidget extends AppWidgetProvider {

    private static final String TAG = "HomeScreenWidget";
    private static final String CHANNEL = "com.westy92.checkiday/widget";

    private static FlutterNativeView backgroundFlutterView = null;
    private static MethodChannel channel = null;

    @Override
    public void onEnabled(Context context) {
        Log.i(TAG, "onEnabled!");
        backgroundFlutterView = new FlutterNativeView(context, true);
        channel = new MethodChannel(backgroundFlutterView, CHANNEL);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.i(TAG, "onUpdate!");
        if (channel != null) {
            Log.i(TAG, "channel not null, invoking dart method!");
            channel.invokeMethod("foo", "extraJunk");
            Log.i(TAG, "after invoke dart method!");
        }
    }
}

Dart code:

void main() {
  runApp(Checkiday());
}

class Checkiday extends StatefulWidget {
  @override
  _CheckidayState createState() => _CheckidayState();
}

class _CheckidayState extends State<Checkiday> {
  static const MethodChannel platform = MethodChannel('com.westy92.checkiday/widget');

  @override
  void initState() {
    super.initState();
    platform.setMethodCallHandler(nativeMethodCallHandler);
  }

  Future<dynamic> nativeMethodCallHandler(MethodCall methodCall) async {
    print('Native call!');
    switch (methodCall.method) {
      case 'foo':
        return 'some string';
      default:
      // todo - throw not implemented
    }
  }

  @override
  Widget build(BuildContext context) {
    // ...
  }
}

When I add the widget to my home screen, I see:

I/HomeScreenWidget(10999): onEnabled!
I/HomeScreenWidget(10999): onUpdate!
I/HomeScreenWidget(10999): channel not null, invoking dart method!
I/HomeScreenWidget(10999): after invoke dart method!

However, my dart code does not seem to be receiving the invocation.

Colure answered 27/12, 2018 at 6:0 Comment(6)
Did you ever find a solution? I'm having the exact same issue!Yttriferous
No. I added a bounty; hopefully that helps!Colure
Actually your platform channel or any dart code won't execute unless your application is up and running. Or what you can do is run dart code as service (check out alarm manager plugin). Then throw an intent that would be caught by your service class which would have actual platform channel interface. I'll try to get you an example if possible.Without
Have you tried calling runFromBundle on your FlutterNativeView? That being said, I'm not sure if running dart code is supported from a widget - if runFromBundle doesn't help, this might be worth opening a bug in the flutter repository and asking about it there. And please be aware that even if it does work, many flutter plugins etc might not work properly due to the constrained nature of android widgets.Nuno
Add result as a parameter to channel.invoke method and override the methods. Then you can know whether it's success or failure or not implemented.Extort
I have the same issue. Also I tried to make the call inside the Activity that is opened using PendingIntent. Same here, channel is not null, dart method is invoked, but call inside Dart code is never executed.Zoes
B
11

I also needed some native android widgets to communicate with my dart code and after some tinkering I managed to do this. In my opinion the documentation on how to do this is a bit sparse but with a bit of creativity I managed to get this to work. I haven't done enough testing to call this 100% production ready, but it seems to be working...

Dart setup

Go to main.dart and add the following top-level function:

void initializeAndroidWidgets() {
  if (Platform.isAndroid) {
    // Intialize flutter
    WidgetsFlutterBinding.ensureInitialized();

    const MethodChannel channel = MethodChannel('com.example.app/widget');

    final CallbackHandle callback = PluginUtilities.getCallbackHandle(onWidgetUpdate);
    final handle = callback.toRawHandle();

    channel.invokeMethod('initialize', handle);
  }
}

then call this function before running your app

void main() {
  initializeAndroidWidgets();
  runApp(MyApp());
}

this will ensure that we can get a callback handle on the native side for our entry point.

Now add an entry point like so:

void onWidgetUpdate() {
  // Intialize flutter
  WidgetsFlutterBinding.ensureInitialized();

  const MethodChannel channel = MethodChannel('com.example.app/widget');

  // If you use dependency injection you will need to inject
  // your objects before using them.

  channel.setMethodCallHandler(
    (call) async {
      final id = call.arguments;

      print('on Dart ${call.method}!');

      // Do your stuff here...
      final result = Random().nextDouble();

      return {
        // Pass back the id of the widget so we can
        // update it later
        'id': id,
        // Some data
        'value': result,
      };
    },
  );
}

This function will be the entry point for our widgets and gets called when our widgets onUpdate method is called. We can then pass back some data (for example after calling an api).

Android setup

The samples here are in Kotlin but should work with some minor adjustments also in Java.

Create a WidgetHelper class that will help us in storing and getting a handle to our entry point:

class WidgetHelper {
    companion object  {
        private const val WIDGET_PREFERENCES_KEY = "widget_preferences"
        private const val WIDGET_HANDLE_KEY = "handle"

        const val CHANNEL = "com.example.app/widget"
        const val NO_HANDLE = -1L

        fun setHandle(context: Context, handle: Long) {
            context.getSharedPreferences(
                WIDGET_PREFERENCES_KEY,
                Context.MODE_PRIVATE
            ).edit().apply {
                putLong(WIDGET_HANDLE_KEY, handle)
                apply()
            }
        }

        fun getRawHandle(context: Context): Long {
            return context.getSharedPreferences(
                WIDGET_PREFERENCES_KEY,
                Context.MODE_PRIVATE
            ).getLong(WIDGET_HANDLE_KEY, NO_HANDLE)
        }
    }
}

Replace your MainActivity with this:

class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)

        val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, WidgetHelper.CHANNEL)
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "initialize" -> {
                if (call.arguments == null) return
                WidgetHelper.setHandle(this, call.arguments as Long)
            }
        }
    }
}

This will simply ensure that we store the handle (the hash of the entry point) to SharedPreferences to be able to retrieve it later in the widget.

Now modify your AppWidgetProvider to look something similar to this:

class Foo : AppWidgetProvider(), MethodChannel.Result {

    private val TAG = this::class.java.simpleName

    companion object {
        private var channel: MethodChannel? = null;
    }

    private lateinit var context: Context

    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        this.context = context

        initializeFlutter()

        for (appWidgetId in appWidgetIds) {
            updateWidget("onUpdate ${Math.random()}", appWidgetId, context)
            // Pass over the id so we can update it later...
            channel?.invokeMethod("update", appWidgetId, this)
        }
    }

    private fun initializeFlutter() {
        if (channel == null) {
            FlutterMain.startInitialization(context)
            FlutterMain.ensureInitializationComplete(context, arrayOf())

            val handle = WidgetHelper.getRawHandle(context)
            if (handle == WidgetHelper.NO_HANDLE) {
                Log.w(TAG, "Couldn't update widget because there is no handle stored!")
                return
            }

            val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(handle)

            // Instantiate a FlutterEngine.
            val engine = FlutterEngine(context.applicationContext)
            val callback = DartExecutor.DartCallback(context.assets, loader.findAppBundlePath(), callbackInfo)
            engine.dartExecutor.executeDartCallback(callback)

            channel = MethodChannel(engine.dartExecutor.binaryMessenger, WidgetHelper.CHANNEL)
        }
    }

    override fun success(result: Any?) {
        Log.d(TAG, "success $result")

        val args = result as HashMap<*, *>
        val id = args["id"] as Int
        val value = args["value"] as Int

        updateWidget("onDart $value", id, context)
    }

    override fun notImplemented() {
        Log.d(TAG, "notImplemented")
    }

    override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
        Log.d(TAG, "onError $errorCode")
    }

    override fun onDisabled(context: Context?) {
        super.onDisabled(context)
        channel = null
    }
}

internal fun updateWidget(text: String, id: Int, context: Context) {
    val views = RemoteViews(context.packageName, R.layout.small_widget).apply {
        setTextViewText(R.id.appwidget_text, text)
    }

    val manager = AppWidgetManager.getInstance(context)
    manager.updateAppWidget(id, views)
}

The important thing here is initializeFlutter that will make sure we can get a handle to our entry point. In onUpdate we are then calling channel?.invokeMethod("update", appWidgetId, this) that will trigger the callback in our MethodChannel on the dart side defined earlier. Then we handle the result later in success (at least when the call is successful).

Hopefully this will give you a rough idea on how to achieve this...

Barthol answered 25/4, 2020 at 16:9 Comment(1)
I created a new Flutter app project on GitHub and integrated these code lines: github.com/timobaehr/flutter-demo-android-widgetZoes
I
2

First, please ensure that you are invoking FlutterMain.startInitialization() and then FlutterMain.ensureInitializationComplete() before attempting to execute any Dart code. These calls are necessary to bootstrap Flutter.

Second, can you try this same goal using the new experimental Android embedding?

Here is a guide for executing Dart code using the new embedding: https://github.com/flutter/flutter/wiki/Experimental:-Reuse-FlutterEngine-across-screens

If your code still doesn't work as expected with the new Android embedding then it should be easier to debug what the problem is. Please post back with success, or any new error information.

Icbm answered 22/5, 2019 at 19:54 Comment(1)
The key for me was simply not to call setMethodCallHandler too early. Once I moved that call into the initState() function as shown by Westy92, it started working for me.Menhir
D
1

You need to pass the getFlutterView() from the MainActivity instead of creating a new BackgroundFlutterView:

channel = new MethodChannel(MainActivity.This.getFlutterView(), CHANNEL);

"This" being like:

public class MainActivity extends FlutterActivity {
    public static MainActivity This;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        This = this;
        ...
    }
Docker answered 14/10, 2019 at 20:8 Comment(0)
I
0

maybe you can use invokeMethod(String method, @Nullable Object arguments, MethodChannel.Result callback) and use callback to get the fail reason.

Inexactitude answered 12/9, 2019 at 10:33 Comment(0)
S
0

FlutterMain is deprecated, use FlutterLoader.

For example (kotlin)

val loader = FlutterLoader()
loader?.startInitialization(context!!)
loader?.ensureInitializationComplete(context!!, arrayOf())

Another thing, when app is in background and you want to communicate with parent app, you need to initialize method channel again, initial initialization from onUpdate won't work then. In that case code in flutter part will be executed in separate isolate.

Shelba answered 2/7, 2021 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.