从原生 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) 代码的主要内容,如果未能解决你的问题,请参考以下文章