【Flutter】Riverpodを利用しAppLifecycleStateとネットワーク接続(connectivity_plus)を検出

2024/01/03に公開

1. はじめに

Flutterにはアプリケーションの実行状態を示すAppLifecycleStateがあります。
Riverpodを利用しつつ、このAppLifecycleStateを入手したいと考えました。

公式ドキュメントを見ると、以下のような説明があります。

The current application state can be obtained from SchedulerBinding.instance.lifecycleState, and changes to the state can be observed by creating an AppLifecycleListener, or by using a WidgetsBindingObserver by overriding the WidgetsBindingObserver.didChangeAppLifecycleState method.

あとは、こちらのRemiさんのtweetを参考にします。
https://x.com/remi_rousselet/status/1486675682491092997?s=20

それから(これはある意味ついでなのですが)、connectivity_plusを利用してネットワーク接続も検出するようにしておきます。

2. pubspec.yaml

Riverpodはv3を利用しました。

name: app_lifecycle
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.2.3 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  hooks_riverpod: ^3.0.0-dev.3
  flutter_hooks: ^0.20.4
  riverpod_annotation: ^3.0.0-dev.3
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1
  connectivity_plus: ^5.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  riverpod_generator: ^3.0.0-dev.11
  build_runner: ^2.4.7
  custom_lint: ^0.5.7
  riverpod_lint: ^3.0.0-dev.4
  freezed: ^2.4.6
  json_serializable: ^6.7.1

flutter:
  uses-material-design: true

3. AppLifecycleStateとネットワーク接続を検出するProvider

以下のように実装しました。Widgetからは、appLifecycleNotifierProviderをwatchします。

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'app_lifecycle_notifier.freezed.dart';
part 'app_lifecycle_notifier.g.dart';


FutureOr<AppLifecycle?> appLifecycleNotifier(AppLifecycleNotifierRef ref) {
  final connectivity = ref.watch(connectivityProvider).valueOrNull;
  if (connectivity == null) {
    ref.state = const AsyncLoading<AppLifecycle?>().copyWithPrevious(ref.state);
    return Future.value(ref.state.valueOrNull);
  }

  final appLifecycle = ref.watch(appLifecycleProvider);
  return AppLifecycle(appLifecycleState: appLifecycle, connectivityResult: connectivity);
}


AppLifecycleState appLifecycle(AppLifecycleRef ref) {
  final appLifecycleObserver =
      _AppLifecycleObserver((appLifecycleState) => ref.state = appLifecycleState);

  final widgetsBinding = WidgetsBinding.instance..addObserver(appLifecycleObserver);
  ref.onDispose(() => widgetsBinding.removeObserver(appLifecycleObserver));

  return AppLifecycleState.resumed;
}

class _AppLifecycleObserver extends WidgetsBindingObserver {
  final ValueChanged<AppLifecycleState> _didChangeAppLifecycle;

  _AppLifecycleObserver(this._didChangeAppLifecycle);

  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    _didChangeAppLifecycle(state);
    super.didChangeAppLifecycleState(state);
  }
}


Stream<ConnectivityResult> connectivity(ConnectivityRef ref) =>
    Connectivity().onConnectivityChanged;


class AppLifecycle with _$AppLifecycle {
  const factory AppLifecycle({
    required AppLifecycleState appLifecycleState,
    required ConnectivityResult connectivityResult,
  }) = _AppLifecycle;
}

appLifecycleProvider_AppLifecycleObserverは、Remiさんのtweetを参考にしています。
_AppLifecycleObserverへコールバックを渡しています。didChangeAppLifecycleStateAppLifecycleStateが渡されてきますので、そこでコールバックを呼び出してappLifecycleProviderのstateを更新しています。

あとは、Widgetからref.watch(appLifecycleNotifierProvider)でfreezedのインスタンスAppLifecycleを入手できます。
以下はmain.dartのサンプルです。アプリを表示した状態で機内モードにしたり、アプリをバックグラウンドに移行したり、スコープが当たっていない状態(inactive)にしたり諸々確認してみましたが機能しているようでした。

import 'package:app_lifecycle/app_lifecycle_notifier.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AppLifecycle(),
    );
  }
}

class AppLifecycle extends ConsumerWidget {
  const AppLifecycle({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: switch (ref.watch(appLifecycleNotifierProvider)) {
          AsyncData(:final value) => value == null
              ? const SizedBox()
              : Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('appLifecycleState=${value.appLifecycleState}'),
                    Text('connectivityResult=${value.connectivityResult}'),
                  ],
                ),
          AsyncError(:final error, :final stackTrace) =>
            Text('error=$error, stackTrace=$stackTrace'),
          _ => const CircularProgressIndicator(),
        },
      ),
    );
  }
}

4. おわりに

私の場合、AppLifecycleStateを入手したいというモチベーションはFirestore絡みでした。
Firestoreのsnapshotを利用しているとき、アプリがバックグラウンドへ移行したのであればsnapshotをキャンセルしたかったためです。そうしないと、アプリが表示されていないのにドキュメントの更新に伴う通信を受けてしまいますからね。

この実装でAppLifecycleState(と、ついでにネットワーク接続も)検出できるようになりましたので、Firestore周りに適用していこうと思います。

5. 追記

hooksが使えるのであれば、useAppLifecycleStateを使っても良いですね。

6. さらに追記

Androidの場合、AppLifecycleState.pausedが検出できないという問題に気づきました。

https://github.com/flutter/flutter/issues/114756

flutter_hooksにも以下のissueを起票してみました。

https://github.com/rrousselGit/flutter_hooks/issues/409

image

Remiさんから上記のコメントがありましたので、useOnAppLifecycleStateChangeを利用することにしました。

    useOnAppLifecycleStateChange((_, current) {
      switch (current) {
        case AppLifecycleState.resumed || AppLifecycleState.inactive:
          FirebaseFirestore.instance.enableNetwork();
          break;
        default:
          FirebaseFirestore.instance.disableNetwork();
      }
    });
GitHubで編集を提案

Discussion