Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

12. Supporting the Development Lifecycle With Firebase
Written by Vid Palčar

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

You’ve gotten through the first 11 chapters, and you’ve finished your app — well done! You’ve set up the data layer, written the app’s business logic, spiced up the UI, and created custom components and packages. Just distribute it among the people, and your WonderWords app will be a hit.

But not so fast!

App development is an ongoing process that never stops. Once you finish the first version of your app, you must monitor its performance. Besides adding new features, you’ll have to release new versions of the app to improve users’ experience. This might be some UI and UX changes, adding a new feature or removing confusing ones, or just resolutions of the bugs that your QA team missed when testing the app.

Here, you might ask yourself how you can know what changes are required to make your app even better. Well, you have to monitor users’ engagement with the specific features of the app as well as analyze their interaction with the app. You might want to track the app’s crashes when users discover some side case you hadn’t thought about. Or, maybe you’ll have to run a few tests in your user group without necessarily releasing a new version of the app.

When dealing with these types of issues, Firebase can come in very handy. You’ve probably already heard a lot about Firebase. In this chapter, you’ll look at a few tools you might not be very familiar with, but are essential in almost any real-world app. Those tools are Firebase Analytics and Crashlytics.

Firebase Analytics lets you understand information about your app’s users, including:

  • Which features of your app they use the most or least.
  • How much time they spend on your app.
  • Where they come from.
  • Which devices they use.

By adding Firebase Crashlytics to your project, you may discover hidden issues in the app that you need to resolve immediately. Crashlytics does this by providing you with the record and stacktrace of an error or crash.

In this chapter, you’ll learn how to:

  • Add analytics on-screen view events.
  • Record crashes and non-fatal errors.

Throughout this chapter, you’ll work on the starter project from this chapter’s assets folder.

Firebase Analytics

Getting back to Chapter 1, “Setting up Your Environment”, you might remember adding Firebase to the WonderWords app. When you finally added all the necessary files to the project, you might’ve taken a sneak peek into the Firebase Analytics console. If so, you’ll remember that it offers a bunch of cool information about your audience. But in this section, you’ll focus primarily on capturing a screen_view event when users visit a specific screen in the app.

screen_view is one of the predefined events in Firebase Analytics, although it enables you to define custom events as well. A screen_view event occurs when the user visits a screen in your app.

But before continuing, you’ll look at some useful information that Firebase Analytics tracks for you automatically when you add it to your project. To check your behavior in the app, run WonderWords.

Note: If you’re having trouble running the app, you might have forgotten to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.

Go to Firebase console and navigate to Analytics ▸ Dashboard from the left-side menu. The Dashboard shows various eye-catching graphs and analyses recorded automatically by Firebase Analytics when users run your app. Here are a few that will be the most relevant for you:

Going through the selected information panels in the previous image, you can see:

  1. The first information panel, at the top-left, shows event counts for all users in descending order of their occurrence. The most interesting information for you in this section will be the screen_view event. By drilling down further into it, you can see which screens are most used by your users — but more about that later.
  2. The second graph represents the recent average engagement time.
  3. The third representation is a demographical view of user base distribution across countries.
  4. The fourth shows a count of users who’ve installed the app on a specific device model.

Besides the useful information highlighted here, the Dashboard also has a lot more, such as user activity over time, users by app versions, user retention, revenue statistics, etc.

Note: As soon as you add Firebase Analytics dependency in the project, it starts recording all this data automatically. This won’t be the case for a few types of information, such as the screen_view event, which you have to implement separately. Note that the data in your console will be different from what you may see in the image above.

Next, click screen_view on the Firebase Analytics page to see the user engagement per screen in the app. Scroll farther down and locate the User engagement card.

Compare the names listed in the TITLE column with the ones defined in the lib/routing_table.dart file. You can see that they match. All the titles are the names of MaterialPages in the Routes class. From the % TOTAL column, you can see that users use the quotes-list screen as much as they use all the other screens combined. You can also see the average time they spend on every single one of the pages. This small but meaningful information can help you determine which feature or screen your users spend the most time on. Using this analysis to enhance highly used features can be a great business strategy, especially when thinking about monetizing the app.

As you have a better overview of what Firebase Analytics offers, it’s time to jump into its implementation in the app.

Adding Firebase Analytics

Open lib/routing_table.dart at the root of the project:

MaterialPage(
  name: 'quotes-list'
  ...
)

MaterialPage(
  name: 'profile-menu'
  ...
)

Modifying ScreenViewObserver

WonderWords uses the Routemaster package as a navigation solution. The package offers RoutemasterDelegate with the observer’s attribute, which takes the list of RoutemasterObservers.

void _sendScreenView(PageRoute<dynamic> route) {
  // 1
  final String? screenName = route.settings.name;
  // 2
  if (screenName != null) {
    analyticsService.setCurrentScreen(screenName);
  }
}
@override
void didPush(Route route, Route? previousRoute) {
  super.didPush(route, previousRoute);
  if (route is PageRoute) {
    _sendScreenView(route);
  }
}

@override
void didPop(Route route, Route? previousRoute) {
  super.didPop(route, previousRoute);
  if (previousRoute is PageRoute && route is PageRoute) {
    _sendScreenView(previousRoute);
  }
}
ScreenViewObserver(
    analyticsService: _analyticsService,
  ),

Firebase Crashlytics

So far, you probably have a good understanding of why, in addition to users’ engagement, you also have to track your app’s crashes. So, you’ll start by getting straight to the point.

Enabling Firebase Crashlytics

Look at the Crashlytics tab in your Firebase console. Navigate to Release & Monitor ▸ Crashlytics in the menu on the left:

Setting up Firebase Crashlytics

Just like Firebase Analytics, Firebase Crashlytics is also a one-time setup. Add the required package in the monitoring package pubspec.yaml file located in packages/monitoring by replacing # TODO: Add crashlytics packages with the following code snippet:

firebase_crashlytics: ^2.8.4

Android-Specific Crashlytics Integration

Open android/build.gradle and add the following classpath under the dependencies group by replacing // TODO: add Firebase Crashlytics classpath with the following code:

classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
apply plugin: 'com.google.firebase.crashlytics'

iOS-Specific Crashlytics Integration

Use Xcode to open Runner.xcworkspace located in the root-level ios folder. Select Runner in the TARGETS section. Go to the Build Phases tab and add New Run Script Phase, as shown in the image below:

$PODS_ROOT/FirebaseCrashlytics/upload-symbols --build-phase --validate -ai <googleAppId>
$PODS_ROOT/FirebaseCrashlytics/upload-symbols --build-phase -ai <googleAppId>

Initializing a Flutter App With Firebase Crashlytics

Before accessing the Firebase Crashlytics instance in the app, you need to initialize Firebase core services in the Flutter app. You do this by invoking Firebase.initializeApp() before the runApp statement. Open lib/main.dart, and look at the current implementation of the main() function:

void main() async {
  // 1
  WidgetsFlutterBinding.ensureInitialized();
  // 2
  await initializeMonitoringPackage();

  // TODO: Perform explicit crash

  // TODO: Add Error reporting

  // the following line of code will be relevant for next chapter
  final remoteValueService = RemoteValueService();
  await remoteValueService.load();
  runApp(
    WonderWords(
      remoteValueService: remoteValueService,
    ),
  );
}

Finalizing Firebase Crashlytics Installation

Now, as you have that settled, go back to your Firebase console and navigate to the Crashlytics tab. You may notice that something has changed. The button Add SDK has changed to a loading indicator saying that an app has been detected, as shown in the following image:

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';

class ExplicitCrash {
  ExplicitCrash({
    @visibleForTesting FirebaseCrashlytics? crashlytics,
  }) : _crashlytics = crashlytics ?? FirebaseCrashlytics.instance;

  // 1
  final FirebaseCrashlytics _crashlytics;

  // 2
  crashTheApp() {
    _crashlytics.crash();
  }
}
final explicitCrash = ExplicitCrash();
explicitCrash.crashTheApp();

Analyzing Crashes

By navigating lower, you may see the list of issues recorded by Firebase Crashlytics:

Isolating Error-Catching Logic in a Single File

There are a few different types of errors. You’ll dive deeper into the specifics of different error types in just a second, but before that, you have to prepare a few things.

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';

/// Wrapper around [FirebaseCrashlytics].
class ErrorReportingService {
  ErrorReportingService({
    @visibleForTesting FirebaseCrashlytics? crashlytics,
  }) : _crashlytics = crashlytics ?? FirebaseCrashlytics.instance;

  // 1
  final FirebaseCrashlytics _crashlytics;

  // 2
  Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) {
    return _crashlytics.recordFlutterError(flutterErrorDetails);
  }

  // 3
  Future<void> recordError(
    dynamic exception,
    StackTrace? stack, {
    bool fatal = false,
  }) {
    return _crashlytics.recordError(
      exception,
      stack,
      fatal: fatal,
    );
  }
}
export 'src/error_reporting_service.dart';

Handling Errors in a Flutter App

As already mentioned, there are a few different types of errors. Most of the time, when resolving the different types, you won’t bother to distinguish between them. Nevertheless, for the sake of general knowledge, here are three types of errors that you may want to record:

void main() async {
  // 1
  // Has to be late so it doesn't instantiate before the
  // `initializeMonitoringPackage()` call.
  late final errorReportingService = ErrorReportingService();
  // 2
  runZonedGuarded<Future<void>>(
    () async {
      // 3
      WidgetsFlutterBinding.ensureInitialized();
      await initializeMonitoringPackage();

      final remoteValueService = RemoteValueService();
      await remoteValueService.load();
      // 4
      FlutterError.onError = errorReportingService.recordFlutterError;
      // 5
      Isolate.current.addErrorListener(
        RawReceivePort((pair) async {
          final List<dynamic> errorAndStacktrace = pair;
          await errorReportingService.recordError(
            errorAndStacktrace.first,
            errorAndStacktrace.last,
          );
        }).sendPort,
      );

      runApp(
        WonderWords(
          remoteValueService: remoteValueService,
        ),
      );
    },
    // 6
    (error, stack) => errorReportingService.recordError(
      error,
      stack,
      fatal: true,
    ),
  );
}

Handling a Flutter Framework Error

First, you have to intentionally produce the RenderFlex overflowed error. To achieve that, open packages/component_library/lib/src/count_indicator_icon_button.dart and scroll to the end of file. Locate // TODO: change the font size for invoking an error and replace small with xxLarge so that it matches the following code:

// TODO: change font size back to FontSize.small
fontSize: FontSize.xxLarge,

Key Points

  • Using Firebase services such as Analytics and Crashlytics is a must when supporting an app’s lifecycle.
  • Firebase Analytics offers valuable data on the structure of your audience as well as screen time, number of screen visits and custom event tracking.
  • With the help of Firebase Crashlytics, you can record all the errors you might miss during the development process.

Where to Go From Here

With this chapter, you’ve learned a very important lesson on how to track vital aspects of your app after finishing its initial development.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now