Flutter hooks で画面遷移のイベント変化を custom hooks でいい感じに検知する

Flutter で画面遷移の検知をするのには routeObserver.subscribe(...) したり with RouteAware する必要があって、結構めんどい。(参考: 【Flutter】RouteAwareで遷移を検知する方法 | 417.Run())

理想としては、 final appLifecycleState = useAppLifecycleState(); みたいに、 final routeAware = useRouteAwareEvent(); という形で custom hooks 形式で利用したい。

試行錯誤した結果、final routeAware = useRouteAwareEvent(ref.watch(routeObserverProvider)); で利用できるようになった。 useAppLifecycleState を参考に実装した。

実装

依存

以下に依存している。

  • go_router
  • go_router_builder
  • flutter_riverpod
  • riverpod_annotation
  • flutter_hooks
  • riverpod_generator

実装内容

まず RouteObserver をつくる。

@Riverpod(keepAlive: true)
RouteObserver routeObserver(RouteObserverRef ref) => RouteObserver();

GoRouter でそれを利用する。

@Riverpod(keepAlive: true)
GoRouter makeRouter(MakeRouterRef ref) => GoRouter(
      initialLocation: '/',
      routes: $appRoutes,
      observers: [
        FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
        ref.watch(routeObserverProvider),
      ],
    );

custom hooks を実装する。

// RouteAware は abstract class なのでコールバックするだけの実装をする
class RouteAwareEvent extends RouteAware {
  VoidCallback? onDidPopNext;
  VoidCallback? onDidPop;
  VoidCallback? onDidPushNext;
  VoidCallback? onDidPush;

  @override
  void didPopNext() => onDidPopNext?.call();
  @override
  void didPop() => onDidPop?.call();
  @override
  void didPushNext() => onDidPushNext?.call();
  @override
  void didPush() => onDidPush?.call();
}

// hooks の返り値
enum RouteAwareType {
  didPush,
  didPop,
  didPopNext,
  didPushNext,
}

RouteAwareType? useRouteAwareEvent(RouteObserver routeObserver) {
  return use(_RouteAwareHook(routeObserver));
}

class _RouteAwareHook extends Hook<RouteAwareType?> {
  const _RouteAwareHook(this.routeObserver) : super();
  final RouteObserver routeObserver;

  @override
  __RouteAwareState createState() => __RouteAwareState();
}

class __RouteAwareState extends HookState<RouteAwareType?, _RouteAwareHook> {
  RouteAwareType? _state;
  final aware = RouteAwareEvent();

  @override
  void initHook() {
    super.initHook();
    aware.onDidPop = () => _state = RouteAwareType.didPop;
    aware.onDidPopNext = () => _state = RouteAwareType.didPopNext;
    aware.onDidPush = () => _state = RouteAwareType.didPush;
    aware.onDidPushNext = () => _state = RouteAwareType.didPushNext;
  }

  var firstBuild = true;
  @override
  RouteAwareType? build(BuildContext context) {
    // initHook で context を触るとエラーになるので、ここで初期化する
    // また、ここでは useEffect が使えないので、firstBuild を利用し初回のみ subscribe する
    if (firstBuild) {
      firstBuild = false;
      hook.routeObserver.subscribe(aware, ModalRoute.of(context)!);
    }
    return _state;
  }

  @override
  void dispose() {
    super.dispose();
    hook.routeObserver.unsubscribe(aware);
  }
}

Widget ではこのように利用できる。

class HogePage extends HookConsumerWidget {
  HogePage({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    final routeAware = useRouteAwareEvent(ref.watch(routeObserverProvider));
    useEffect(() {
      if (routeAware == RouteAwareType.didPop) {
        print('did pop');
      }
      return null;
    }, [routeAware]);

    return Scaffold();
  }
}

おわり。