Mahdi-Malv
Mahdi-Malv

Reputation: 19200

Run flutter code when android application class starts

I'm making a plugin for Flutter to handle fcm messages using an android native library.

As we know when a message is received by FCM, it starts the app (It's application class) and runs the codes within Application#onCreate block, so we can run native code when app starts by fcm in the background.

My question is, is it possible to run flutter code at that time when application starts?
For instance if the message was received:

Application class:

public class Application extends FlutterApplication {

  @Override
  public void onCreate() {
    super.onCreate();
    // Start flutter engine
    // Invoke a dart code in the Plugin using methodChannel or etc.
  }
}

Upvotes: 13

Views: 8483

Answers (5)

Ben Butterworth
Ben Butterworth

Reputation: 28482

I did it a different, simpler way compared to Mahdi's answer. I avoided defining an additional entrypoint/ callback, using PluginUtilities, callback handles, saving handles in SharedPreferences, passing messages with handles between dart and platform, or implementing a FlutterApplication.

I was working on a flutter plugin (so you don't have to worry about this if you use my library for push notifications 😂), so I implement FlutterPlugin. If I want to do background processing and the Flutter app isn't running, I just launch the Flutter app without an Activity or View. This is only necessary on Android, since the FlutterEngine/ main dart function runs already runs when a background message is received in an iOS app. The benefit is that this is the same behaviour as iOS: a Flutter app is always running when the app is launched, even if there is no app shown to the user.

I launch the application by using:

flutterEngine = new FlutterEngine(context, null);
DartExecutor executor = flutterEngine.getDartExecutor();
backgroundMethodChannel = new MethodChannel(executor, "com.example.package.background");
backgroundMethodChannel.setMethodCallHandler(this);
// Get and launch the users app isolate manually:
executor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());

I did this to implement background push notification handling in a library, ably_flutter. It seems to work well. The FlutterEngine/ application is launched only when the application is not already running. I do this by keeping track of the activity (using ActivityAware):

    if (isApplicationInForeground) {
      // Send message to Dart side app already running
      Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
      onMessageReceivedIntent.putExtras(intent.getExtras());
      LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
    } else if (AblyFlutterPlugin.isActivityRunning) {
      // Flutter is already running, just send a background message to it.
      Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
      onMessageReceivedIntent.putExtras(intent.getExtras());
      LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
    } else {
      // No existing Flutter Activity is running, create a FlutterEngine and pass it the RemoteMessage
      new PushBackgroundIsolateRunner(context, asyncCompletionHandlerPendingResult, message);
    }

Then, I just use a separate MethodChannel to pass the messages back to the dart side. There's more to this parallel processing (like telling the Java side that the App is running/ ready. Search for call.method.equals(pushSetOnBackgroundMessage) in the codebase.). You can see more about the implementation PushBackgroundIsolateRunner.java at ably_flutter. I also used goAsync inside the broadcast receiver to extend the execution time from 10s to 30s, to be consistent with iOS 30s wall clock time.

Upvotes: 2

Mahdi-Malv
Mahdi-Malv

Reputation: 19200

Short answer, Yes

You can call a Dart method in background using it's handle key.

1. Register your plugin in the background

Implement a custom application class (override FlutterApplication)

public class MyApp extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {

    @Override
    public void registerWith(io.flutter.plugin.common.PluginRegistry registry) {
        // For apps using FlutterEmbedding v1
        GeneratedPluginRegistrant.registerWith(registry);
       // App with V2 will initialize plugins automatically, you might need to register your own however
    }
}

Remember to register the class in the AndroidManifest by adding android:name=".MyApp" to <application> attributes.

What is embedding v2?

2. Create a setup function as top level function in your flutter code

/// Define this TopLevel or static
void _setup() async {
  MethodChannel backgroundChannel = const MethodChannel('flutter_background');
  // Setup Flutter state needed for MethodChannels.
  WidgetsFlutterBinding.ensureInitialized();

  // This is where the magic happens and we handle background events from the
  // native portion of the plugin.
  backgroundChannel.setMethodCallHandler((MethodCall call) async {
    if (call.method == 'handleBackgroundMessage') {
      final CallbackHandle handle =
          CallbackHandle.fromRawHandle(call.arguments['handle']);
      final Function handlerFunction =
          PluginUtilities.getCallbackFromHandle(handle);
      try {
        var dataArg = call.arguments['message'];
        if (dataArg == null) {
          print('Data received from callback is null');
          return;
        }
        await handlerFunction(dataArg);
      } catch (e) {
        print('Unable to handle incoming background message.\n$e');
      }
    }
    return Future.value();
  });

3. Create a top level callback that will get the background message and calls it

_bgFunction(dynamic message) {
    // Message received in background
    // Remember, this will be a different isolate. So, no widgets
}

4. Get the handle key of the background function and setup and send it to native via MethodChannel

// dart:ui needed
CallbackHandle setup PluginUtilities.getCallbackHandle(_setup);
CallbackHandle handle PluginUtilities.getCallbackHandle(_bgFunction);

_channel.invokeMethod<bool>(
  'handleFunction',
  <String, dynamic>{
    'handle': handle.toRawHandle(),
    'setup': setup.toRawHandle()
  },
);

5. Save them into SharedPref in the native side

public void onMethodCall(MethodCall call, MethodChannel.Result result) {
  String methodName = call.method
  if (methodName == "handleFunction") {
     long handle = call.argument("handle");
     long setup = call.argument("setup");
     // save them
  }
}

6. When background is awaken, start a background isolate

FlutterMain.ensureInitializationComplete(context, null)
val appBundlePath = FlutterMain.findAppBundlePath()
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(setupHandleYouHadSaved)

FlutterNativeView backgroundFlutterView = FlutterNativeView(context, true)

val args = FlutterRunArguments()
args.bundlePath = appBundlePath
args.entrypoint = flutterCallback.callbackName
args.libraryPath = flutterCallback.callbackLibraryPath

backgroundFlutterView?.runFromBundle(args)

// Initialize your registrant in the app class
pluginRegistrantCallback?.registerWith(backgroundFlutterView?.pluginRegistry)

7. When your plugin is registered, create a background channel and pass it to

val backgroundChannel = MethodChannel(messenger, "pushe_flutter_background")

8. Call the setup method that would call and give the message to you callback

private fun sendBackgroundMessageToExecute(context: Context, message: String) {
    if (backgroundChannel == null) {
        return
    }

    val args: MutableMap<String, Any?> = HashMap()
    if (backgroundMessageHandle == null) {
        backgroundMessageHandle = getMessageHandle(context)
    }
    args["handle"] = backgroundMessageHandle
    args["message"] = message
    // The created background channel at step 7
    backgroundChannel?.invokeMethod("handleBackgroundMessage", args, null)
}

The sendBackgroundMessageToExecute will execute the dart _setup function and pass the message and callback handle. In the step 2, callback will be called.

Note: There are still certain corner cases you may want to consider (for instance thread waiting and ...). Checkout the samples and see the source code.

There are several projects which support background execution when app is started in the background.

FirebaseMessaging

Pushe

WorkManager

Upvotes: 10

timr
timr

Reputation: 6962

If you mean you want to run some arbitrary Dart code in the background you can use the this plugin we created which really facilitates the use of background work.
You can register a background job that should be executed at a given point in time and it will call back in to your Dart code where you can run some code in the background.

//Provide a top level function or static function.
//This function will be called by Android and will return the value you provided when you registered the task.
//See below
void callbackDispatcher() {
  Workmanager.defaultCallbackDispatcher((echoValue) {
    print("Native echoed: $echoValue");
    return Future.value(true);
  });
}

Workmanager.initialize(callbackDispatcher)

Then you can schedule them.

Workmanager.registerOneOffTask(
    "1", 
    "simpleTask"
);

The String simpleTask will be returned in the callbackDispatcher function once it starts running in the background. This allows for you to schedule multiple background jobs and identify them by this id.

Upvotes: 0

user11608547
user11608547

Reputation:

According to my knowledge we have to call a class GeneratedPluginRegistrant.registerWith(this); at oncreate method where flutter code has to run.

Upvotes: 0

athor
athor

Reputation: 6928

You can use a headless Runner to run dart code from an Application class (or service, broadcast receiver etc).

There's a good in depth article on how to implement this: https://medium.com/flutter/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124

Upvotes: 0

Related Questions