How do I track Flutter screens in Firebase analytics?
Asked Answered
J

5

62

I have a Flutter app and I'm testing Google Analytics for Firebase on Flutter.

I wanted to see the routes our users (well, me for now) are visiting. I followed the setup steps in firebase_analytics and I checked their example app, too. I enabled debugging for Analytics as described in the Debug View docs

Unfortunately, the only two kinds of screen views (firebase_screen_class) I receive in my Analytics Debug view are Flutter and MainActivity.

I'd expect to see /example-1, /example-2 and /welcome somewhere, but I don't.

This is the app I'm running in Flutter

class App extends StatelessWidget {
  final FirebaseAnalytics analytics = FirebaseAnalytics();
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: <String, WidgetBuilder>{
        '/example-1': (_) => Example1(),
        '/example-2': (_) => Example2(),
        '/welcome': (_) => Welcome(),
      },
      home: Welcome(),
      navigatorObservers: [FirebaseAnalyticsObserver(analytics: analytics)],
    );
  }
}
Joby answered 24/4, 2019 at 12:56 Comment(2)
Why you just dont put events on each separate screen and not make funnel ?Spoonbill
Add firebase Event in your Screen Constructor.?Indemnity
J
86

This exact use-case is in the documentation for Firebase Analytics under the Track Screenviews section.

Manually tracking screens is useful if your app does not use a separate UIViewController or Activity for each screen you may wish to track, such as in a game.

This is exactly the case with Flutter, as Flutter is taking care of the screen updates: most simple Flutter apps run one single FlutterActivity/FlutterAppDelegate and it takes care of rendering different screens on its own, so letting Firebase Analytics automatically track screens will not bring the desired effect.

As far as my past experience goes, the FirebaseAnalyticsObserver was not very helpful, however, I recommend you, too, check their docs again, they do imply that things should "just work". My best guess is that it didn't work well for me because I didn't use RouteSettings on any of my routes *.

In case FirebaseAnalyticsObserver won't work or apply for your app, the next approach worked quite well for me over the past months of development.

You can set the current screen with FirebaseAnalytics at any point, if you call the setCurrentScreen method with the screen name:

import 'package:firebase_analytics/firebase_analytics.dart';
// Somewhere in your widgets...
FirebaseAnalytics().setCurrentScreen(screenName: 'Example1');

As a first attempt I did this in the widget constructor, but that will not work well and miscount the events: if you pop or push routes, all widget constructors in the stack will be called, even though only the top route really qualifies as "the current screen".

To solve this, we need to use the RouteAware class and only set the current screen in case it's the top route: either our route is added to the stack or the previous top route was popped and we arrived onto the route.

RouteAware comes with boilerplate code and we don't want to repeat that boilerplate for all of our screens. Even for small apps, you have tens of different screens, so I created the RouteAwareAnalytics mixin:

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/widgets.dart';

// A Navigator observer that notifies RouteAwares of changes to state of their Route
final routeObserver = RouteObserver<PageRoute>();

mixin RouteAwareAnalytics<T extends StatefulWidget> on State<T>
    implements RouteAware {
  AnalyticsRoute get route;

  @override
  void didChangeDependencies() {
    routeObserver.subscribe(this, ModalRoute.of(context));
    super.didChangeDependencies();
  }

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

  @override
  void didPop() {}

  @override
  void didPopNext() {
    // Called when the top route has been popped off,
    // and the current route shows up.
    _setCurrentScreen(route);
  }

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

  @override
  void didPushNext() {}

  Future<void> _setCurrentScreen(AnalyticsRoute analyticsRoute) {
    print('Setting current screen to $analyticsRoute');
    return FirebaseAnalytics().setCurrentScreen(
      screenName: screenName(analyticsRoute),
      screenClassOverride: screenClass(analyticsRoute),
    );
  }
}

I created an enum to track the screens (and functions to turn the enum to screen names). I used the enums to be able to easily track all routes, refactor route names. Using these enums and functions, I can unit test all possible values and enforce consistent naming: no accidental spaces or special characters, no inconsistent capitalization. There could be other, better ways to determine screen class values, but I went with this approach.

enum AnalyticsRoute { example }

String screenClass(AnalyticsRoute route) {
  switch (route) {
    case AnalyticsRoute.example:
      return 'ExampleRoute';
  }
  throw ArgumentError.notNull('route');
}

String screenName(AnalyticsRoute route) {
  switch (route) {
    case AnalyticsRoute.example:
      return '/example';
  }
  throw ArgumentError.notNull('route');
}

Next step in the inital setup is to register the routeObserver as a navigatorObserver of your MaterialApp:

MaterialApp(
  // ...
  navigatorObservers: [
    routeObserver,
    // FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()),
  ],
);

Finally, we can add our first example route that's tracked. Add the with RouteAwareAnalytics to your states and override get route.

class ExampleRoute extends StatefulWidget {
  @override
  _ExampleRouteState createState() => _ExampleRouteState();
}

class _ExampleRouteState extends State<ExampleRoute> with RouteAwareAnalytics{
  @override
  Widget build(BuildContext context) => Text('Example');

  @override
  AnalyticsRoute get route => AnalyticsRoute.example;
}

Every time you add a new route, you can do so with little effort: first, add a new enum value, then the Dart compiler will guide you what to add next: add the screen name and class override values in their respective switch-case. Then, find your state that's building your route, add with RouteAwareAnalytics, and add the route getter.

* The reason why I didn't use RouteSettings is that I prefer Simon Lightfoot's approach with the typed arguments instead of the Object arguments the settings provide:

class ExampleRoute extends StatefulWidget {
  const ExampleRoute._({@required this.integer, Key key}) : super(key: key);
  // All types of members are supported, but I used int as example
  final int integer;
  static Route<void> route({@required int integer}) =>
      MaterialPageRoute(
        // I could add the settings here, though, it wouldn't enforce good types
        builder: (_) => ExampleRoute._(integer: integer),
      );
  // ...
}
Joby answered 24/4, 2019 at 13:22 Comment(10)
Advantages of DRY: I realized that I cannot build a funnel only with screen_view events, so I just updated my abstract class's setCurrentScreen with the extra analytics.logEvent(name: 'screen_view-$screenName');Joby
And you really see your real screen names in the Firebase console? I just see "screen_name" as events, no matter how i name the events with setCurrentScreen("whatever");Isoelectronic
Why not let AnalyticsScreen override StatefulWidget? This way, you can call the setCurrentScreen() in the AnalyticsScreen constructor, preventing you to forget this call for your screen.Lillylillywhite
@Lillylillywhite I updated my answer. Your recommendation would have been an improvement over my original answer. The issue is that, as it turned out, the constructor is not the right place to call the setCurrentScreen as it will count too many events. Currently, I am using RouteAware to correctly set the screen name.Joby
Is There any known way to achieve logging for ModalRoutes? I have a lot of bottom sheets in my app which are stateless widgets (that's why I don't want to use RouteAware, because it requires a stateful widget) and standard FirebaseAnalyticsObserver doesn't work because it is only observing PageRoutesVieira
The enum approach seems simple to work with already pre-defined screens. But how'd this work for tracking 'n' screens? One example would be a list view with 100 members where each entry pushes that particular member profile. Here I'd like to track which particular profiles were created. I'm thinking about adapting your enum+screenName+screenClass to a new RouteClass but I'm not sure if this would be the best approachCrystallite
any idea how to use mixin on stateless widgetsPropagation
@Vieira I'm using OpenContainer from the animations package extensively, which uses ModalRoutes. I changed the route observer type to ModalRoute, so it's final routeObserver = RouteObserver<ModalRoute>();, which works for me. Might work for you also since PageRoute extends ModalRoute?Scabble
can anyone tell me where exactly I can put each step , I'm a beginner and I'm still trying to figure out where specific code goes. the throw code is coming out as dead code. I skipped the Firebase().analytics step since it is wrong then I tried putting the first two parts outside of the classes, adding the materialApp inside a widget separately because I had Material not materialApp in one of my widgets. then placing the last two classes seperately but nothing changed. all I have is screen_view. can someone explain the first comment about the advantages of DRY. Thank you all :)Cordelia
you can actually use var screenName = ModalRoute.of(context)?.settings.name; to get route stringNanaam
C
40

Add a Navigation Observer

Add Firebase analytics navigation observer to your MatetialApp:

class MyApp extends StatelessWidget {
  FirebaseAnalytics analytics = FirebaseAnalytics();
...

MaterialApp(
  home: MyAppHome(),
  navigatorObservers: [
    FirebaseAnalyticsObserver(analytics: analytics), // <-- here
  ],
);

That's it! Your analytics should appear in the DebugView:

enter image description here

NOTE!

If it's the first time that you are integrating analytics in your app, it will take about a day for your analytics to appear in your dashboard.

See results right away

To see debug results right away, run the above command on your terminal, then check that they appear in the DebugView:

adb shell setprop debug.firebase.analytics.app [your_app_package_name]

Enjoy!

Churchwoman answered 10/11, 2020 at 17:1 Comment(3)
I don't see the actual screen name, just screen_name in your screenshot?Factional
If you press on one of the screen_view analytic you will get more information such as timestamp, token and screen nameChurchwoman
the screen_view event has multiple parameters, and info about an actual screen name located in the screen_view -> firebase_screen parameter. thank you, @ChurchwomanGreat
L
4

I experienced the issue for some time and was just able to make it work

The issue for me is that I'm not properly passing settings in MaterialPageRoute

return MaterialPageRoute(
      settings: RouteSettings(
        name: routeName,
      ),
      builder: (_) => viewToShow);
}

I follow the tutorial on FilledStack and was able to figure out my issue after seeing the sample code

Lobby answered 8/1, 2021 at 9:14 Comment(0)
D
3

If you are seeing "Flutter" in the firebase_screen_class parameter of the screen_view event, it means you have it configured properly.

You should find the values you are expecting in the firebase_screen parameter, instead of the firebase_screen_class.

It's also worth checking the firebase_previous_screen parameter to see what was the screen that was open before that one.

Daughter answered 28/5, 2020 at 18:13 Comment(0)
A
3

If you don't see your screen names that you set with setCurrentScreen make sure you set the dropdown field in the Pages & Screens page in Analytics console to Page title and screen name then you will see your flutter screens.

enter image description here

In our case we couldn't use a navigation observer but what finally did the trick was to use the VisibilityDetector of the package with the same name. In others we just called our own logScreen function at the beginning of the build function but only called the Firebase setCurrentScreen if the screen name has changed:

String _lastScreenName = '';

void logScreen(String screenName) {
  if (_lastScreenName == screenName) return;
  print('logScreen: $screenName');

  kFirebaseAnalytics?.setCurrentScreen(screenName: screenName);
  _lastScreenName = screenName;
}

(you could out this into a singleton if you like)

Akron answered 19/10, 2023 at 8:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.