Вызов кода 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!

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


person Westy92    schedule 27.12.2018    source источник
comment
Вы когда-нибудь находили решение? У меня точно такая же проблема!   -  person Tobiaswk    schedule 21.01.2019
comment
Нет, я добавил награду; надеюсь, это поможет!   -  person Westy92    schedule 22.01.2019
comment
На самом деле канал вашей платформы или любой код дротика не будет выполняться, если ваше приложение не запущено и не работает. Или что вы можете сделать, это запустить код дротика как службу (проверьте плагин диспетчера аварийных сигналов). Затем бросьте намерение, которое будет обнаружено вашим классом обслуживания, который будет иметь фактический интерфейс канала платформы. Я постараюсь привести вам пример, если возможно.   -  person Hemanth Raj    schedule 24.01.2019
comment
Вы пробовали вызвать runFromBundle на своем FlutterNativeView? При этом я не уверен, поддерживается ли запуск кода дротика из виджета - если runFromBundle не помогает, возможно, стоит открыть ошибку в репозитории флаттера и спросить об этом там. И имейте в виду, что даже если это работает, многие плагины flutter и т. Д. Могут работать некорректно из-за ограниченного характера виджетов Android.   -  person rmtmckenzie    schedule 24.01.2019
comment
Добавьте результат в качестве параметра в метод channel.invoke и переопределите методы. Тогда вы сможете узнать, удачно это или нет, или нет.   -  person Harsha pulikollu    schedule 12.02.2019
comment
У меня такая же проблема. Также я попытался сделать вызов внутри Activity, который открывается с помощью PendingIntent. То же самое и здесь, канал не равен нулю, метод dart вызывается, но вызов внутри кода Dart никогда не выполняется.   -  person Timo Bähr    schedule 26.04.2019


Ответы (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 (по крайней мере, когда вызов будет успешным).

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

person bnxm    schedule 25.04.2020
comment
Я создал новый проект приложения Flutter на GitHub и интегрировал эти строки кода: github.com/timobaehr / flutter-demo-android-widget - person Timo Bähr; 05.09.2020

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

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

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

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

person SuperDeclarative    schedule 22.05.2019
comment
Ключевым моментом для меня было просто не вызывать setMethodCallHandler слишком рано. Как только я переместил этот вызов в функцию initState (), как показано Westy92, он начал работать для меня. - person AlanKley; 06.06.2019

Вам нужно передать 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;
        ...
    }
person mehrdad seyrafi    schedule 14.10.2019

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

person Liu Tom    schedule 12.09.2019

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

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

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

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

person Mahmud Basunia    schedule 02.07.2021