60 votes

Comment exécuter du code en arrière-plan, même lorsque l'écran est éteint ?

J'ai une application simple de minuterie dans Flutter, qui affiche un compte à rebours avec le nombre de secondes restantes. J'ai :

new Timer.periodic(new Duration(seconds: 1), _decrementCounter);

Il semble fonctionner correctement jusqu'à ce que l'écran de mon téléphone s'éteigne (même si je passe à une autre application) et se mette en veille. Ensuite, la minuterie se met en pause. Existe-t-il un moyen recommandé de créer un service qui fonctionne en arrière-plan même lorsque l'écran est éteint ?

2 votes

La vraie question est peut-être la suivante : est-il possible d'exécuter du code en arrière-plan (par exemple, des minuteries) pour une application Flutter lorsque l'activité est détruite ? Dans mon cas, le minuteur continuerait à fonctionner même si j'éteins l'écran (voir réponse ci-dessous).

0 votes

Vous ne pouvez pas faire cela complètement du côté client, je pense que vous devez lancer une minuterie sur le serveur et la synchroniser avec le front-end, quelque chose comme un flux de données de sorte que lorsque le téléphone se met en veille et revient à l'application, il devrait commencer à partir de la minuterie actuelle sur le serveur.

0 votes

Vous pouvez également consulter stackoverflow.com/a/59057145/6668797 pour d'autres moyens

43voto

Eric Seidel Points 1407

La réponse à la question de savoir comment mettre en œuvre votre cas spécifique de minuterie n'a rien à voir avec le code d'arrière-plan. En général, l'exécution de code en arrière-plan est déconseillée sur les systèmes d'exploitation mobiles.

Par exemple, la documentation iOS traite du code d'arrière-plan de manière plus détaillée ici : https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html

Au lieu de cela, les systèmes d'exploitation mobiles fournissent des apis (comme des apis de minuterie/alarme/notification) pour rappeler votre application après un temps spécifique. Par exemple, sur iOS, vous pouvez demander que votre application soit notifiée/réveillée à un moment précis dans le futur via UINotificationRequest : https://developer.apple.com/reference/usernotifications/unnotificationrequest Cela leur permet de tuer/suspendre votre application afin de réaliser de meilleures économies d'énergie et d'avoir à la place un seul service système partagé très efficace pour le suivi de ces notifications/alarmes/gestion de la sécurité, etc.

Flutter ne fournit pas actuellement de wrappers autour de ces services d'OS prêts à l'emploi, mais il est facile d'écrire les vôtres en utilisant notre modèle de services de plateforme : flutter.io/platform-services

Nous travaillons sur un système de publication/partage d'intégrations de services comme celle-ci, de sorte qu'une fois qu'une personne a écrit cette intégration (pour, par exemple, programmer une exécution future de votre application), tout le monde peut en bénéficier.

Séparément, la question plus générale "est-il possible d'exécuter du code Dart en arrière-plan" (sans avoir un FlutterView actif à l'écran), est "pas encore". Nous avons un bug dans le dossier : https://github.com/flutter/flutter/issues/3671

Le cas d'utilisation de ce type d'exécution de code en arrière-plan est le suivant : votre application reçoit une notification et souhaite la traiter à l'aide d'un code Dart sans faire appel à votre application. Si vous avez d'autres cas d'utilisation du code en arrière-plan que vous aimeriez nous faire connaître, les commentaires sont les bienvenus sur ce bug !

27voto

raju-bitter Points 3789

Réponse courte : non, ce n'est pas possible, bien que j'ai observé un comportement différent pour l'écran se mettant en veille. Le code suivant vous aidera à comprendre les différents états d'une application Flutter sur Android, testée avec ces versions de Flutter et Flutter Engine :

  • Révision du cadre b339c71523 (il y a 6 heures), 2017-02-04 00:51:32
  • Révision du moteur cd34b0ef39

Créez une nouvelle application Flutter, et remplacez le contenu du fichier lib/main.dart avec ce code :

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => new _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher>
    with WidgetsBindingObserver {
  AppLifecycleState _lastLifecyleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void onDeactivate() {
    super.deactivate();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print("LifecycleWatcherState#didChangeAppLifecycleState state=${state.toString()}");
    setState(() {
      _lastLifecyleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecyleState == null)
      return new Text('This widget has not observed any lifecycle changes.');
    return new Text(
        'The most recent lifecycle state this widget observed was: $_lastLifecyleState.');
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter App Lifecycle'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _timerCounter = 0;
  // ignore: unused_field only created once
  Timer _timer;

  _MyHomePageState() {
    print("_MyHomePageState#constructor, creating new Timer.periodic");
    _timer = new Timer.periodic(
        new Duration(milliseconds: 3000), _incrementTimerCounter);
  }

  void _incrementTimerCounter(Timer t) {
    print("_timerCounter is $_timerCounter");
    setState(() {
      _timerCounter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(config.title),
      ),
      body: new Block(
        children: [
          new Text(
            'Timer called $_timerCounter time${ _timerCounter == 1 ? '' : 's' }.',
          ),
          new LifecycleWatcher(),
        ],
      ),
    );
  }
}

Au lancement de l'application, la valeur de _timerCounter est incrémentée toutes les 3s. Un champ de texte situé sous le compteur indique toute AppLifecycleState pour l'application Flutter, vous verrez la sortie correspondante dans le journal de débogage de Flutter, par exemple :

[raju@eagle:~/flutter/helloworld]$ flutter run
Launching lib/main.dart on SM N920S in debug mode...
Building APK in debug mode (android-arm)...         6440ms
Installing build/app.apk...                         6496ms
I/flutter (28196): _MyHomePageState#constructor, creating new Timer.periodic
Syncing files to device...
I/flutter (28196): _timerCounter is 0

  To hot reload your app on the fly, press "r" or F5. To restart the app entirely, press "R".
The Observatory debugger and profiler is available at: http://127.0.0.1:8108/
For a more detailed help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.
I/flutter (28196): _timerCounter is 1
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 2
I/flutter (28196): _timerCounter is 3
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.resumed
I/flutter (28196): _timerCounter is 4
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 5
I/flutter (28196): _timerCounter is 6
I/flutter (28196): _timerCounter is 7
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.resumed
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 8
I/flutter (28196): _MyHomePageState#constructor, creating new Timer.periodic
I/flutter (28196): _timerCounter is 0
I/flutter (28196): _timerCounter is 1

Pour la sortie du journal ci-dessus, voici les étapes que j'ai suivies :

  1. Lancer l'application avec flutter run
  2. Passage à une autre application (_timerCounter valeur 1)
  3. Retour à l'application Flutter (valeur 3 du _timerCounter)
  4. Appui sur le bouton d'alimentation, l'écran s'est éteint (_timerCounter valeur 4)
  5. Téléphone déverrouillé, reprise de l'application Flutter (_timerCounter valeur 7)
  6. Appuyez sur la touche retour du téléphone (la valeur de _timerCounter n'a pas changé). C'est à ce moment que l'activité FlutterActivity est détruite et que la VM Dart est également isolée.
  7. Reprise de l'application Flutter (la valeur de _timerCounter est à nouveau 0)

Passage d'une application à l'autre, en appuyant sur le bouton d'alimentation ou le bouton arrière
Lorsque vous passez à une autre application ou que vous appuyez sur le bouton d'alimentation pour éteindre l'écran, le minuteur continue de fonctionner. Mais si vous appuyez sur le bouton retour alors que l'application Flutter a le focus, l'activité est détruite, et avec elle l'isolat Dart. Vous pouvez tester cela en vous connectant à l'application Observatoire de Dart lorsque vous passez d'une application à l'autre, ou lorsque vous tournez l'écran. L'Observatoire montre une application Flutter active, Isolate, en cours d'exécution. Mais lorsque vous appuyez sur le bouton retour, l'Observatoire n'affiche pas Isolate en cours d'exécution. Ce comportement a été confirmé sur un Galaxy Note 5 fonctionnant sous Android 6.x, et un Nexus 4 fonctionnant sous Android 4.4.x.

Cycle de vie des applications Flutter et cycle de vie Android Pour la couche de widgets de Flutter, seul l'élément interrompu et a repris Les états sont exposés. Destroy est géré par Activité Android pour une application Android Flutter :

/**
 * @see android.app.Activity#onDestroy()
 */
@Override
protected void onDestroy() {
    if (flutterView != null) {
        flutterView.destroy();
    }
    super.onDestroy();
}

Comme la VM Dart d'une application Flutter est exécutée dans l'activité, la VM sera arrêtée chaque fois que l'activité sera détruite.

Logique du code du moteur Flutter
Cela ne répond pas directement à votre question, mais vous donnera des informations de base plus détaillées sur la façon dont le moteur Flutter gère les changements d'état pour Android.
En examinant le code du moteur de Flutter, il est évident que la boucle d'animation est mise en pause lorsque la fonction FlutterActivity reçoit l'Android Activité#en#pause événement. Lorsque l'application passe en interrompu l'état, selon le commentaire de la source ici il se passe ce qui suit :

" L'application n'est pas actuellement visible par l'utilisateur. Lorsque l'application est dans cet état, le moteur n'appelle pas le rappel [onBeginFrame]."

D'après mes tests, la minuterie continue de fonctionner même si le rendu de l'interface utilisateur est mis en pause, ce qui est logique. Il serait bon d'envoyer un événement dans la couche de widgets en utilisant la fonction WidgetsBindingObserver lorsque l'activité est détruite, de sorte que les développeurs peuvent s'assurer de stocker l'état de l'application Flutter jusqu'à ce que l'activité soit reprise.

14voto

Jehad Nasser Points 313

J'ai rencontré le même problème et ma solution pour ce cas spécifique (compte à rebours) était d'utiliser la même logique que celle utilisée dans certaines applications natives Android/ios, c'est-à-dire.. :

  1. Lorsque l'application est mise en pause (envoi en arrière-plan), je stocke l'objet DateTime qui se termine. de fin.
  2. Lorsque l'application reprend (au premier plan à nouveau), je recalcule la durée entre l'heure actuelle de l'appareil (Datetime.now()) et l'objet de fin stocké objet Datetime. Duration remainingTime = _endingTime.difference(dateTimeNow);
  3. Mettez à jour la valeur du compte à rebours avec la nouvelle durée.

REMARQUE : la valeur de la date de fin a été enregistrée. dans un singleton , I n'ai pas utilisé Préférences partagées pas besoin dans mon cas mais c'est une option acceptable option acceptable au cas où vous en auriez besoin.

en détail :

J'ai créé ce gestionnaire pour définir et obtenir le temps restant :

class TimerHandler {
  DateTime _endingTime;

  TimerHandler._privateConstructor();
  TimerHandler();

  static final TimerHandler _instance = new TimerHandler();
  static TimerHandler get instance => _instance;

  int get remainingSeconds {
    final DateTime dateTimeNow = new DateTime.now();
    Duration remainingTime = _endingTime.difference(dateTimeNow);
    // Return in seconds
    return remainingTime.inSeconds;
  }

  void setEndingTime(int durationToEnd) {
    final DateTime dateTimeNow = new DateTime.now();

    // Ending time is the current time plus the remaining duration.
    this._endingTime = dateTimeNow.add(
      Duration(
        seconds: durationToEnd,
      ),
    );

  }
}
final timerHandler = TimerHandler.instance;

puis à l'intérieur de l'écran de minuterie, j'ai observé le cycle de vie de l'application ;

  • donc une fois que j'ai envoyé en arrière-plan (en pause), je vais économiser le temps de fin,
  • et une fois qu'elle est de nouveau au premier plan (reprise), je démarre le minuteur avec la nouvelle durée restante. nouveau temps restant (au lieu de démarrer directement avec la nouvelle durée), vous pouvez vérifier si l'état a été mis en pause ou démarré avant d'être envoyé en arrière-plan, si vous en avez besoin).

NOTES :

1- Je ne vérifie pas l'état de la minuterie avant de définir la nouvelle durée restante, car la logique dont j'ai besoin dans mon application est de pousser le endingTime au cas où l'utilisateur a mis en pause le timer, au lieu de réduire la timerDuration, Tout dépend du cas d'utilisation.

2- Mon timer vit dans un bloc( TimerBloc).

class _TimerScreenState extends State<TimerScreen> {
  int remainingDuration;
//...

  @override
  void initState() {
    super.initState();

    SystemChannels.lifecycle.setMessageHandler((msg) {

      if (msg == AppLifecycleState.paused.toString() ) {
        // On AppLifecycleState: paused
        remainingDuration = BlocProvider.of<TimerBloc>(context).currentState.duration ?? 0;
        timerHandler.setEndingTime(remainingDuration);
        setState((){});
      }

      if (msg == AppLifecycleState.resumed.toString() ) {
        // On AppLifecycleState: resumed
        BlocProvider.of<TimerBloc>(context).dispatch(
          Start(
            duration: timerHandler.remainingSeconds,
          ),
        );
        setState((){});
      }
      return;
    });
  }

//....
}

si quelque chose n'est pas clair, laissez un commentaire.

0 votes

Je me demande ce qu'il advient des données dans le modèle singleton lorsque l'application passe en arrière-plan.

6voto

tim Points 2842

Vous pourriez utiliser le gestionnaire du travail de flutter plugin.
C'est mieux que ce qui est mentionné ci-dessus AlarmManager puisque ce n'est plus recommandé pour Android.
Le plugin permet également de toujours iOS exécution en arrière-plan

Ce plugin vous permet d'enregistrer un travail d'arrière-plan et d'obtenir un rappel dans Dart lorsqu'il se produit, afin de pouvoir effectuer une action personnalisée.

void callbackDispatcher() {
  Workmanager.executeTask((backgroundTask) {
    switch(backgroundTask) {
      case Workmanager.iOSBackgroundTask:
      case "firebaseTask":
        print("You are now in a background Isolate");
        print("Do some work with Firebase");
        Firebase.doSomethingHere();
        break;
    }
    return Future.value(true);
  });
}

void main() {
  Workmanager.initialize(callbackDispatcher);
  Workmanager.registerPeriodicTask(
    "1",
    "firebaseTask",
    frequency: Duration(days: 1),
    constraints: WorkManagerConstraintConfig(networkType: NetworkType.connected),
  );
  runApp(MyApp());
}

2 votes

Le processus périodique en arrière-plan ne semble pas fonctionner sur iOS

0 votes

Comment puis-je vérifier l'autorisation de localisation en utilisant WorkManager, car il semble que cela ne fonctionne pas lorsque la localisation n'est pas activée dans Android.

1voto

dodgy_coder Points 2778

Vous pouvez utiliser le android_alarm_manager plugin flutter qui vous permet d'exécuter du code Dart en arrière-plan lorsqu'une alarme se déclenche.

Une autre façon, avec plus de contrôle, serait d'écrire un programme natif Service Android (en utilisant Java ou Kotlin) pour votre application qui communique avec le front-end de Flutter par le biais du stockage des périphériques ou des préférences partagées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X