Вызвать код Flutter (Dart) из родного виджета домашнего экрана Android

Я добавил собственный виджет домашнего экрана Android в свое приложение Flutter.

В моем 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!

Тем не менее, мой код дротика, кажется, не получает вызов.

5 ответов

Мне также потребовались нативные виджеты Android для связи с моим кодом дротика, и после некоторой работы мне удалось это сделать. На мой взгляд, документации о том, как это сделать, немного, но, проявив немного творчества, мне удалось заставить это работать. Я не провел достаточно тестов, чтобы назвать это 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)
            // You could also use a hard coded value to save you from all
            // the hassle with SharedPreferences, but alas when running your
            // app in release mode this would fail.
            val entryPointFunctionName = callbackInfo.callbackName

            // Instantiate a FlutterEngine.
            val engine = FlutterEngine(context.applicationContext)
            val entryPoint = DartEntrypoint(FlutterMain.findAppBundlePath(), entryPointFunctionName)
            engine.dartExecutor.executeDartEntrypoint(entryPoint)

            // Register Plugins when in background. When there 
            // is already an engine running, this will be ignored (although there will be some
            // warnings in the log).
            GeneratedPluginRegistrant.registerWith(engine)

            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 (по крайней мере, при успешном звонке).

Надеюсь, это даст вам общее представление о том, как этого добиться...

Во-первых, пожалуйста, убедитесь, что вы вызываете FlutterMain.startInitialization() а потом FlutterMain.ensureInitializationComplete() прежде чем пытаться выполнить любой код Dart. Эти звонки необходимы для начальной загрузки Flutter.

Во-вторых, можете ли вы попробовать эту же цель с помощью нового экспериментального встраивания Android?

Вот руководство по выполнению кода Dart с использованием нового встраивания: https://github.com/flutter/flutter/wiki/Experimental:-Reuse-FlutterEngine-across-screens

Если ваш код все еще не работает должным образом с новым встраиванием Android, тогда должно быть легче отладить, в чем проблема. Пожалуйста, отправьте обратно с успехом, или любую новую информацию об ошибке.

Вам нужно передать getFlutterView() из MainActivity вместо создания нового 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;
        ...
    }

Может ты сможешь использовать invokeMethod(String method, @Nullable Object arguments, MethodChannel.Result callback) и используйте обратный вызов, чтобы узнать причину сбоя.

FlutterMain устарело, используйте FlutterLoader.

Например (котлин)

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

Другое дело, когда приложение находится в фоновом режиме, и вы хотите общаться с родительским приложением, вам нужно снова инициализировать канал метода, начальная инициализация из onUpdateтогда не сработает. В этом случае код в флаттерной части будет выполняться в отдельном изоляторе.

Другие вопросы по тегам