FlutterのCasual Game ToolKitを使ってみる! その3

ブログ
FlutterのCasual Game ToolKitを使ってみる! その2
FlutterのCasual Game ToolKitを使ってみましょう。

お疲れ様です。前回の投稿でようやくCasual Game ToolKitのソースコードの中身に入っていけましたね。

今回はmain.dartをもう少しいじってから、画面をもう1つ追加して画面遷移させてみましょう。

samples/game_template at main · flutter/samples
AcollectionofFlutterexamplesanddemos.Contributetoflutter/samplesdevelopmentbycreatinganaccountonGitHub.

MultiProvider

main.dartを早速いじりましょう。ソースコードを載っけておきます。

main.dart

void main() {
  runApp(const MyApp(
    counterPersistence: LocalCounterPersistence(),
));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.counterPersistence});
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) {
          var counterState = CounterState(counterPersistence);
          counterState.getFirstCounter();
          return counterState;
        },
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

前回までの投稿で、以下の変更を加えたんでした。

  • ChangeNotifierクラスを継承したCounterStateクラスを準備して、Providerパッケージによる状態管理を行うようにする
  • 状態管理を行っているCounterStateの1階層下にshared_persistenceの継承クラスを配置して、状態が端末と紐づくようにする

ただ、現時点だと状態はCounterStateしか管理できないです。CounterStateにアプリが保持する状態を全て持たせるわけにもいきません。

そこで使用するのが、MultiProviderというわけです。

実際のソースコードを見た方が早いと思うので、Casual Game ToolKit内のmain.dartを抜粋します。

@override
  Widget build(BuildContext context) {
    return AppLifecycleObserver(
      child: MultiProvider( <-ここ!
        providers: [
          ChangeNotifierProvider(
            create: (context) {
              var progress = PlayerProgress(playerProgressPersistence);
              progress.getLatestFromStore();
              return progress;
            },
          ),
          Provider<GamesServicesController?>.value(
              value: gamesServicesController),
          Provider<AdsController?>.value(value: adsController),
          ChangeNotifierProvider<InAppPurchaseController?>.value(
              value: inAppPurchaseController),
          Provider<SettingsController>(
            lazy: false,
            create: (context) => SettingsController(
              persistence: settingsPersistence,
            )..loadStateFromPersistence(),
          ),
          ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>,
              AudioController>(
            lazy: false,
            create: (context) => AudioController()..initialize(),
            update: (context, settings, lifecycleNotifier, audio) {
              if (audio == null) throw ArgumentError.notNull();
              audio.attachSettings(settings);
              audio.attachLifecycleNotifier(lifecycleNotifier);
              return audio;
            },
            dispose: (context, audio) => audio.dispose(),
          ),
          Provider(
            create: (context) => Palette(),
          ),
        ],
        child: Builder(builder: (context) {
          final palette = context.watch<Palette>();

          return MaterialApp.router(
            title: 'Flutter Demo',
            theme: ThemeData.from(
              colorScheme: ColorScheme.fromSeed(
                seedColor: palette.darkPen,
                background: palette.backgroundMain,
              ),
              textTheme: TextTheme(
                bodyMedium: TextStyle(
                  color: palette.ink,
                ),
              ),
              useMaterial3: true,
            ),
            routeInformationProvider: _router.routeInformationProvider,
            routeInformationParser: _router.routeInformationParser,
            routerDelegate: _router.routerDelegate,
            scaffoldMessengerKey: scaffoldMessengerKey,
          );
        }),
      ),
    );
  }

上記のように書き換えることで、MultiProviderで複数のProvider(=複数の状態管理)を持たせることができるようになるわけです。

AppLifeCycleState

ところで、以下の部分が少し気になりませんかね?

return AppLifecycleObserver( <-ここ!
      child: MultiProvider( 
        providers: [
          ChangeNotifierProvider(
            create: (context) {

AppLifecycleObserverで更にWrapしているようなのですが、このAppLifecycleObserverって何なのでしょうか。。。

これ自体はCasual Game ToolKitが独自に実装しているようなのですが、役割がイメージ湧かないですね。

なので、一旦中身を見てみることにしましょう。

app_lifecycle.dart


class AppLifecycleObserver extends StatefulWidget {
  final Widget child;

  const AppLifecycleObserver({required this.child, super.key});

  @override
  State<AppLifecycleObserver> createState() => _AppLifecycleObserverState();
}

class _AppLifecycleObserverState extends State<AppLifecycleObserver>
    with WidgetsBindingObserver {
  static final _log = Logger('AppLifecycleObserver');

  final ValueNotifier<AppLifecycleState> lifecycleListenable =
      ValueNotifier(AppLifecycleState.inactive);

  @override
  Widget build(BuildContext context) {
    // Using InheritedProvider because we don't want to use Consumer
    // or context.watch or anything like that to listen to this. We want
    // to manually add listeners. We're interested in the _events_ of lifecycle
    // state changes, and not so much in the state itself. (For example,
    // we want to stop sound when the app goes into the background, and
    // restart sound again when the app goes back into focus. We're not
    // rebuilding any widgets.)
    //
    // Provider, by default, throws when one
    // is trying to provide a Listenable (such as ValueNotifier) without using
    // something like ValueListenableProvider. InheritedProvider is more
    // low-level and doesn't have this problem.
    return InheritedProvider<ValueNotifier<AppLifecycleState>>.value(
      value: lifecycleListenable,
      child: widget.child,
    );
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    _log.info(() => 'didChangeAppLifecycleState: $state');
    lifecycleListenable.value = state;
  }

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

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _log.info('Subscribed to app lifecycle updates');
  }
}

このbuildメソッドに注目していただきたいのですが、

@override
  Widget build(BuildContext context) {
    return InheritedProvider<ValueNotifier<AppLifecycleState>>.value(
      value: lifecycleListenable,
      child: widget.child,
    );
  }

ここのchildで各Provider(正確にいうと各ProviderをWrapしたMultiProvider)を渡していることを考えると、これは各Providerを更にProviderでWrapしているものと言えそうです。

で、ソースコードを見てみるとその時に渡しているvalueはAppLifeCycleState(正確にいうとValueNotifier=下位のウィジェットで参照・変更される値)なわけです。

ではこのAppLifeCycleStateって何でしょう?

一番わかりやすいなー、と思った記事を載っけておきます。

Flutterアプリのライフサイクル - Qiita
はじめにFlutterアプリのライフサイクルについてまとめています。Flutterのライフサイクルというと、アプリ(AppLifecycleState) ← 今回の内容画面(Statefu…

こちらの記事を要約すると、didChangeAppLifecycleStateを呼び出しすることでアプリの現在の状態を管理できるようにするもののようです。

これを加味すると、下位のウィジェットでアプリの現在の状態をwatchしておいて例えばアプリをバックグラウンドにしたり、電源をオフにしたりといったアプリの状態変更を検知して何か操作するもののようです。

いずれにせよ、ここら辺は実際に使う段になったらまた改めて説明しましょう。

以上の点を踏まえて、main.dartを書き換えてみましょう。

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp(
    counterPersistence: LocalCounterPersistence(),
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.counterPersistence});

  final CounterPersistence counterPersistence;
  @override
  Widget build(BuildContext context) {
    return AppLifecycleObserver( <-追加
      child: MultiProvider( <-追加
        providers: [
          ChangeNotifierProvider(create: (context) {
            var counterState = CounterState(counterPersistence);
            counterState.getFirstCounter();
            return counterState;
          }),
        ],
        child: Builder(builder: (context) { <-追加
          return MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData.from(
              colorScheme: ColorScheme.fromSeed(
                  seedColor: Colors.deepPurple
              ),
              useMaterial3: true,
            ),
          );
        }),
      ),
    );
  }
}

main.dartを少しいじってみました。

まとめ

GoRouteパッケージの導入までやろうとしましたが、実際説明してみると手前の説明で思いの外紙面を使ってしまいました。。

次回、GoRouteパッケージを使用して画面追加と画面の遷移処理を実装してみようと思います。

コメント