从原生 Android 主屏幕小部件调用 Flutter (Dart) 代码

Posted

技术标签:

【中文标题】从原生 Android 主屏幕小部件调用 Flutter (Dart) 代码【英文标题】:Invoke Flutter (Dart) code from native Android home screen widget 【发布时间】:2019-05-25 05:00:48 【问题描述】:

我向我的 Flutter 应用程序添加了一个原生 android 主屏幕小部件。

在我的AppWidgetProvider 实现中,我想使用平台通道在我的onUpdate() 方法中调用飞镖代码。

这可能吗?如果可以,如何实现?

我当前的 Android (Java) 代码:

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!");
        
    

飞镖代码:

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) 
    // ...
  

当我将小部件添加到主屏幕时,我看到:

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

但是,我的 dart 代码似乎没有收到调用。

【问题讨论】:

您找到解决方案了吗?我遇到了完全相同的问题! 没有。我加了一个赏金;希望对您有所帮助! 实际上,除非您的应用程序启动并运行,否则您的平台通道或任何 dart 代码都不会执行。或者您可以做的是将飞镖代码作为服务运行(查看警报管理器插件)。然后抛出一个意图,该意图将被您的服务类捕获,该服务类将具有实际的平台通道接口。如果可能的话,我会尽量给你举个例子。 你试过在 FlutterNativeView 上调用 runFromBundle 吗?话虽如此,我不确定小部件是否支持运行飞镖代码 - 如果 runFromBundle 没有帮助,这可能值得在颤振存储库中打开一个错误并在那里询问它。请注意,即使它确实有效,由于 android 小部件的受限性质,许多颤振插件等可能无法正常工作。 将结果作为参数添加到 channel.invoke 方法并覆盖这些方法。然后你就可以知道它是成功还是失败。 【参考方案1】:

我还需要一些原生的 android 小部件来与我的 dart 代码进行通信,经过一些修补后,我设法做到了。在我看来,关于如何做到这一点的文档有点稀少,但我凭借一点创造力设法让它发挥作用。我还没有做足够的测试来称这个 100% 生产就绪,但它似乎正在工作......

飞镖设置

转到main.dart并添加以下***函数:

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);
  

然后在运行您的应用程序之前调用此函数

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

这将确保我们可以在本机端为我们的入口点获取回调句柄。

现在像这样添加一个入口点:

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,
      ;
    ,
  );

这个函数将是我们的小部件的入口点,并在我们的小部件onUpdate 方法被调用时被调用。然后我们可以传回一些数据(例如在调用 api 之后)。

Android 设置

这里的示例在 Kotlin 中,但在 Java 中也应该进行一些小的调整。

创建一个WidgetHelper 类,它将帮助我们存储和获取入口点的句柄:

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)
        
    

用这个替换你的MainActivity

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)
            
        
    

这将确保我们将句柄(入口点的哈希)存储到SharedPreferences,以便稍后在小部件中检索它。

现在修改您的 AppWidgetProvider 使其看起来与此类似:

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)

这里重要的是initializeFlutter,它将确保我们能够获得入口点的句柄。在onUpdate 中,我们调用channel?.invokeMethod("update", appWidgetId, this),这将触发前面定义的飞镖端MethodChannel 中的回调。然后我们稍后在success 处理结果(至少在调用成功时)。

希望这能让您大致了解如何实现这一目标...

【讨论】:

我在 GitHub 上新建了一个 Flutter 应用项目,并集成了这些代码行:github.com/timobaehr/flutter-demo-android-widget【参考方案2】:

首先,在尝试执行任何 Dart 代码之前,请确保您正在调用 FlutterMain.startInitialization(),然后是 FlutterMain.ensureInitializationComplete()。这些调用是引导 Flutter 所必需的。

其次,您可以使用新的实验性 Android 嵌入来尝试同样的目标吗?

以下是使用新嵌入执行 Dart 代码的指南: https://github.com/flutter/flutter/wiki/Experimental:-Reuse-FlutterEngine-across-screens

如果您的代码在新的 Android 嵌入中仍然无法按预期工作,那么应该更容易调试问题所在。请回复成功,或任何新的错误信息。

【讨论】:

对我来说关键是不要太早调用 setMethodCallHandler。一旦我将该调用移到 Westy92 所示的 initState() 函数中,它就开始为我工作了。【参考方案3】:

您需要从 MainActivity 传递 getFlutterView() 而不是创建新的 BackgroundFlutterView:

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

“这个”就像:

public class MainActivity extends FlutterActivity 
    public static MainActivity This;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        This = this;
        ...
    

【讨论】:

【参考方案4】:

也许您可以使用invokeMethod(String method, @Nullable Object arguments, MethodChannel.Result callback) 并使用回调来获取失败原因。

【讨论】:

【参考方案5】:

FlutterMain 已弃用,请使用 FlutterLoader

例如(科特林)

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

另外,当app在后台,你想和父app通信时,你需要再次初始化方法通道,从onUpdate初始化初始化将不起作用。在这种情况下,颤振部分的代码将在单独的隔离中执行。

【讨论】:

以上是关于从原生 Android 主屏幕小部件调用 Flutter (Dart) 代码的主要内容,如果未能解决你的问题,请参考以下文章

如何从 android 主屏幕小部件启动活动

如何从我的应用程序将小部件添加到 Android 主屏幕?

Android主屏幕小部件未更新

Android 主屏幕小部件 textsize 动态

在颤动中,我可以使用Android或iOS小部件,就像反应原生一样

Check Widget放置在Android屏幕上