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...
PendingIntent
. Same here, channel is not null, dart method is invoked, but call inside Dart code is never executed. – Zoes