Open8

Flutter Firebase Analytics を observer に登録してログを送りたい。 bottom tab navigation を使ってる時もどうするか。

Tatsuya YamamotoTatsuya Yamamoto

navigationObservers に登録すればログが取れる

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static FirebaseAnalytics analytics = FirebaseAnalytics.instance;
  static FirebaseAnalyticsObserver observer =
      FirebaseAnalyticsObserver(analytics: analytics);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Analytics Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      navigatorObservers: <NavigatorObserver>[observer],
      home: MyHomePage(
        title: 'Firebase Analytics Demo',
        analytics: analytics,
        observer: observer,
      ),
    );
  }
}

firebase_analytics | Flutter Package

Tatsuya YamamotoTatsuya Yamamoto

bottomNavigationBar を使った実装で参考になるコード

flutterfire/tabs_page.dart at master · firebase/flutterfire

bottomNavigationBar を以下でラップしたらいい感じに使える。
後で didPush とかを DI する形に変更する。


class _RouteAwareWidget extends StatefulWidget {
  const _RouteAwareWidget({
    Key? key,
    required this.child,
    required this.routeObserver,
  }) : super(key: key);

  final Widget child;
  final RouteObserver<ModalRoute<dynamic>> routeObserver;

  
  State<_RouteAwareWidget> createState() => _RouteAwareWidgetState();
}

class _RouteAwareWidgetState extends State<_RouteAwareWidget> with RouteAware {
  
  void didChangeDependencies() {
    super.didChangeDependencies();
    widget.routeObserver
        .subscribe(this, ModalRoute.of(context)! as PageRoute<dynamic>);
  }

  
  void dispose() {
    widget.routeObserver.unsubscribe(this);
    super.dispose();
  }

  
  void didPush() {
    debugPrint('😈didPush');
  }

  
  void didPop() {
    debugPrint('😈didPop');
  }

  
  void didPushNext() {
    debugPrint('😈didPushNext');
  }

  
  void didPopNext() {
    debugPrint('😈didPopNext');
  }

  
  Widget build(BuildContext context) => widget.child;
}

RouteAware class - widgets library - Dart API は初見だった。

  • didPush は表示された時に呼ばれる。(Navigator.push → bottomNavigationBar)
  • didPushNext は次の画面を表示した時に呼ばれる。(bottomNavigationBar → Navigator.push)
  • didPopNext は push した後に pop で戻ってきた時に呼ばれる。(bottomNavigationBar → Navigator.pushNavigator.pop → bottomNavigationBar)
  • didPushNext は新しい route に push された時に呼ばれるみたい。検証してない。
/// An interface for objects that are aware of their current [Route].
///
/// This is used with [RouteObserver] to make a widget aware of changes to the
/// [Navigator]'s session history.
abstract class RouteAware {
  /// Called when the top route has been popped off, and the current route
  /// shows up.
  void didPopNext() { }

  /// Called when the current route has been pushed.
  void didPush() { }

  /// Called when the current route has been popped off.
  void didPop() { }

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  void didPushNext() { }
}
Tatsuya YamamotoTatsuya Yamamoto

全然上手くいかんなと思ったら Navigator をネストしてたからだ。

・タブ内遷移のように、WidgetsApp内のNavigatorによるnavigationとは別の内部のnavigationを実現したい時にNavigatorをネストする。
・ネストしたNavigatorの子孫で画面遷移する場合は、どのNavigatorを使って遷移するかを意識する必要がある。
・NavigatorObserverはNavigatorごとに指定する必要がある。1つのobserverのインスタンスは1つのNavigatorしか監視できない。

Navigatorをネストしてタブ内遷移を実現する|木藤紘介|note

Tatsuya YamamotoTatsuya Yamamoto

observer も riverpod で管理すれば mockit 差し込めるな。
既存テストでエラー出てたから mockit で差し込もう

Tatsuya YamamotoTatsuya Yamamoto

こんな感じかな。
これを navigator の observer に登録する。

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final analyticsHelperProvider = Provider.autoDispose<AnalyticsHelper>((ref) {
  return FirebaseAnalyticsHelper(
    ref.watch(analyticsObserverProvider),
  );
});

final analyticsObserverProvider =
    Provider.autoDispose<RouteObserver<ModalRoute<dynamic>>>((ref) {
  return FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance);
});

abstract class AnalyticsHelper {
  AnalyticsHelper(this.observer);
  final RouteObserver<ModalRoute<dynamic>> observer;
  Future<void> logAppOpen();
  Future<void> logLogin();
  Future<void> logSignUp();
  Future<void> logShowAdvertisement({required String advertisementName});
  Future<void> logClickAdvertisement({required String advertisementName});
}

class FirebaseAnalyticsHelper implements AnalyticsHelper {
  FirebaseAnalyticsHelper(this.observer);

  final analytics = FirebaseAnalytics.instance;

  
  final RouteObserver<ModalRoute<dynamic>> observer;

  
  Future<void> logAppOpen() async {
    await analytics.logAppOpen();
  }

  
  Future<void> logLogin() async {
    await analytics.logLogin();
  }

  
  Future<void> logSignUp() async {
    await analytics.logSignUp(signUpMethod: 'email');
  }

  
  Future<void> logShowAdvertisement({required String advertisementName}) async {
    await analytics.logAdImpression(
      adUnitName: advertisementName,
    );
  }

  
  Future<void> logClickAdvertisement({
    required String advertisementName,
  }) async {
    await analytics.logEvent(
      name: 'ad_clicked', //  `ad_click` だと Event name is reserved というエラーが出た
      parameters: {
        'ad_unit_name': advertisementName,
      },
    );
  }
}

Tatsuya YamamotoTatsuya Yamamoto

DI する形にした。使うメソッドだけ残した。

class RouteAwareWidget extends StatefulWidget {
  const RouteAwareWidget({
    Key? key,
    required this.child,
    required this.routeObserver,
    this.didPush,
    this.didPopNext,
  }) : super(key: key);

  final Widget child;
  final RouteObserver<ModalRoute<dynamic>> routeObserver;
  final VoidCallback? didPush;
  final VoidCallback? didPopNext;

  
  State<RouteAwareWidget> createState() => _RouteAwareWidgetState();
}

class _RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
  
  void didChangeDependencies() {
    super.didChangeDependencies();
    widget.routeObserver
        .subscribe(this, ModalRoute.of(context)! as PageRoute<dynamic>);
  }

  
  void dispose() {
    widget.routeObserver.unsubscribe(this);
    super.dispose();
  }

  
  void didPush() {
    widget.didPush?.call();
  }

  
  void didPopNext() {
    widget.didPopNext?.call();
  }

  
  Widget build(BuildContext context) => widget.child;
}