Dev-hwang / flutter_foreground_task

This plugin is used to implement a foreground service on the Android platform.
https://pub.dev/packages/flutter_foreground_task
MIT License
140 stars 105 forks source link

Keep dart code running when the user swipes the app from recent apps. #235

Closed Elvis5566 closed 3 weeks ago

Elvis5566 commented 1 month ago

When the user clears apps from recent apps, the MainActivity get destroyed, which causes the dart code stop working. We could set android:stopWithTask="false" to keep the foreground service running, then listen ActivityAware.onDetachedFromActivity and executeDartCallback again.

Do you think it's a good idea to add this feature?

Dev-hwang commented 1 month ago

Are you saying that the TaskHandler doesn't work when clear the app from recent apps list?

Elvis5566 commented 1 month ago

Yes! That's what I meant, and for android specifically.

Dev-hwang commented 1 month ago

Please update to the latest version and check.

Elvis5566 commented 1 month ago

It doesn't work. Swipe the app from recent app list will NOT kill the app when the service isSticky, thus, won't trigger service restart. This feature has nothing to do with REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission. Version 6.1.2, the version I used, is already able to restart the app if the user ignore battery optimizations. I've tried this commit and it works. https://github.com/Dev-hwang/flutter_foreground_task/compare/master...Elvis5566:flutter_foreground_task:master

PS: my device is android 10.

Dev-hwang commented 1 month ago

@Elvis5566

that's weird.. Even if the Activity is destroyed, the TaskHandler will not be terminated while the service is alive.

When you swipe the app, is TaskHandler onDestroy called? or is it just dart code that stop working?

Elvis5566 commented 1 month ago

TaskHandler.onDestroy did not be called. When I swiped the app, I got a log I/ViewRootImpl@ceadc95[MainActivity](26753): dispatchDetachedFromWindow, and dart code stop working. What I did is not restarting the service; it just executes dart code again.

Dev-hwang commented 1 month ago

@Elvis5566

Does the example stop the same way?

Elvis5566 commented 1 month ago

Yes, actually, I cannot use my app to test the latest version due to dependency issue, so I created a sample minimal app using the example code to test version 7.4.3. When I swipe the app, the dart code just stop working.

Elvis5566 commented 1 month ago

It can be verified quite easily. Run a timer to print log every second, then swipe the app, you can see the timer stop working.

Dev-hwang commented 1 month ago

@Elvis5566

(tested debug/profile mode)
Flutter 3.22.2 / Dart 3.4.3
Flutter 3.10.0 / Dart 3.0.0

Real Device
- Galaxy S10 (Android 10)
- Galaxy Note 10 (Android 12)
- Galaxy Fold 4 (Android 14)
- LG Q92 (Android 10)

Virtual Device (with Google Play)
- Android 14
- Android 12
- Android 10

it worked normally when tested with the above device.

i don't know if the Flutter version is affected or if the problem is with the device manufacturer...

what is your Flutter/Dart version and device manufacturer?

Dev-hwang commented 1 month ago

check if the same problem occurs on the virtual device.

Elvis5566 commented 1 month ago

interesting... My device is samsung note 9 / android 10 / flutter 3.16.9 / dart 3.2.6 I've tried simulator on android 14, still doesn't work. I don't have more real devices atm. https://github.com/Elvis5566/foreground_task_trial Here is the app I used for testing; Can you verify whether the timer keeps working on your devices when you swipe the app? It's so weird...

Dev-hwang commented 1 month ago

@Elvis5566

Flutter has main isolate and background isolate. The main isolate is called the UI isolate and is created when the app starts and destroyed when it exits. No reference variables or state are shared between two isolates and cannot be accessed.

MyTimer or static instance is created in main isolate. Therefore, when the app terminates, the static instance is removed.

TashHandler is created in background isolate. You can think of this as another process or another app.

If you want to share data between different isolates, you can use the method below:

1. Use sendPort and sendData

This plugin supports two-way communication between TaskHandler and UI. The data trying to send must be a type provided by Flutter. like int, double, bool, String, Map. If you want to send a custom object, send it in String format using jsonEncode and jsonDecode. JSON and serialization >> https://docs.flutter.dev/data-and-backend/serialization/json

// send (TaskHandler -> UI)
@override
void onStart(DateTime timestamp, SendPort? sendPort) async {
  sendPort?.send(Object); // this
}

// receive
void _onReceiveData(dynamic data) {
  if (data is int) {
    print('count: $data');
  } else if (data is DateTime) {
    print('timestamp: ${data.toString()}');
  }
}
// send (UI -> TaskHandler)
void _sendData() {
  final Random random = Random();
  final int data = random.nextInt(100);
  FlutterForegroundTask.sendData(data); // this
}

// receive
@override
void onReceiveData(Object data) {
  print('onReceiveData: $data');
}

2. Use shared_preferences

There are some functions for storing and managing data that are only used in this plugin.

void function() async {
  await FlutterForegroundTask.getData(key: String);
  await FlutterForegroundTask.getAllData();
  await FlutterForegroundTask.saveData(key: String, value: Object);
  await FlutterForegroundTask.removeData(key: String);
  await FlutterForegroundTask.clearAllData();
}
Elvis5566 commented 1 month ago

@Dev-hwang

Thanks for the explanation. I see what you mean, but technically, I am not trying to share data between isolates or trying to run tasks in a background isolation. I am building a tracking app. If I use this approach, I will have to move quite a lot business logic, such as listening location update, saving db model and tracking logic to background isolation, then using messages to communicate between UI and background isolation. I could, but this approach will introduce a lot of additional burden, which is just not practical for my circumstances. What I need is to ensure a piece of my code can be run in the background. So basically, My workaround to this problem is

  1. When the UI isolation get destroyed, the background isolation will take over the task.
  2. When the user launches the app again, I'll stop the background task and let UI isolation take over from there.

PS: I hate this isolation mechanism 🤣

WingCH commented 1 month ago

I think you should always put your task on the foreground service whether the app is launched or in the background, and if you want to update the ui just use sendPort and sendData.

It's simpler this way.

Elvis5566 commented 1 month ago

@WingCH

If the 'Task' is just like a function, you run it in the background. Then, when it finishes, it returns the result via sendPort. That makes sense and indeed is simpler. But if the Task is like a service, which returns multiple data every second, such as location, duration, speed, etc.. And you have multiple command to control the service, ex: Start, Pause, Resume, Stop, etc... In addition to the overhead of handling these communications, that is a lot of unnecessary data serialization/deserialization. That's why I don't see it is simper.