flutter_startup_metrics 0.1.3 copy "flutter_startup_metrics: ^0.1.3" to clipboard
flutter_startup_metrics: ^0.1.3 copied to clipboard

Measure Flutter app startup time and cold start in production: time to initial display (TTID), time to full display (TTFD), and a phase breakdown. Works in release builds.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_startup_metrics/flutter_startup_metrics.dart';

/// Release builds on iOS do not route Dart's print to the system log, so the
/// example forwards its report to a native logger to make on-device runs
/// observable. Your app would send the report to your metrics backend instead.
const _deviceLog = MethodChannel('example/device_log');

Future<void> _report(Object message) async {
  debugPrint('STARTUP_METRICS $message');
  try {
    await _deviceLog.invokeMethod<void>('log', 'STARTUP_METRICS $message');
  } on MissingPluginException {
    // Android and the simulator read debugPrint directly.
  }
}

void main() {
  // First statement: the Dart-entry anchor is taken here, so anything above it
  // is attributed to the previous phase instead.
  FlutterStartupMetrics.start();
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(home: Launcher());
}

/// The case that makes time-to-full-display awkward in real apps: which screen
/// the user lands on is not known until an async check has run, so there is no
/// single place to report from. Every branch that can be a first screen needs
/// the call, and missing one means those launches silently report no TTFD.
class Launcher extends StatefulWidget {
  const Launcher({super.key});

  @override
  State<Launcher> createState() => _LauncherState();
}

class _LauncherState extends State<Launcher> {
  Widget? _screen;

  @override
  void initState() {
    super.initState();
    _resolveFirstScreen();
  }

  Future<void> _resolveFirstScreen() async {
    // Stands in for reading a token, checking onboarding state, or resolving a
    // deep link — work that happens after the first frame has already painted.
    final signedIn = await _fakeAuthCheck();
    if (!mounted) return;
    setState(() => _screen = signedIn ? const Dashboard() : const SignIn());
  }

  Future<bool> _fakeAuthCheck() async {
    await Future<void>.delayed(const Duration(milliseconds: 250));
    return true;
  }

  @override
  Widget build(BuildContext context) =>
      _screen ??
      const Scaffold(body: Center(child: CircularProgressIndicator()));
}

/// A first screen with its own async work. Reports once the content is real.
class Dashboard extends StatefulWidget {
  const Dashboard({super.key});

  @override
  State<Dashboard> createState() => _DashboardState();
}

class _DashboardState extends State<Dashboard> {
  String? _data;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    await Future<void>.delayed(const Duration(milliseconds: 400));
    if (!mounted) return;
    setState(() => _data = 'Dashboard content');

    // The call site: after the state that makes the screen useful. Not on a
    // timer, not in build(), and not in main() — at main() nothing is displayed
    // yet, so reporting there would make TTFD meaningless.
    FlutterStartupMetrics.reportFullyDisplayed();

    // Where you would forward the numbers to whatever you already run. Done
    // here rather than in build(), which Flutter may call many times.
    await _report(await FlutterStartupMetrics.fullDisplay);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Startup metrics')),
    body:
        _data == null
            ? const Center(child: CircularProgressIndicator())
            : const StartupSummary(),
  );
}

/// The other possible first screen. It has nothing to wait for, so it reports as
/// soon as it is built — but it still has to report, or launches that land here
/// would have no TTFD at all.
class SignIn extends StatefulWidget {
  const SignIn({super.key});

  @override
  State<SignIn> createState() => _SignInState();
}

class _SignInState extends State<SignIn> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      FlutterStartupMetrics.reportFullyDisplayed();
    });
  }

  @override
  Widget build(BuildContext context) =>
      const Scaffold(body: Center(child: Text('Sign in')));
}

/// Reads the report and renders whichever outcome the launch produced.
class StartupSummary extends StatelessWidget {
  const StartupSummary({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<StartupReport>(
      // fullDisplay resolves once reportFullyDisplayed() lands, or after the
      // timeout. Use initialDisplay instead if you only want TTID.
      future: FlutterStartupMetrics.fullDisplay,
      builder: (context, snapshot) {
        final report = snapshot.data;
        if (report == null) {
          return const Center(child: CircularProgressIndicator());
        }
        return switch (report) {
          StartupExcluded(:final reason) => Center(
            child: Text('Launch not measured: ${reason.name}'),
          ),
          StartupMeasurement() => _MeasurementView(report),
        };
      },
    );
  }
}

class _MeasurementView extends StatelessWidget {
  const _MeasurementView(this.report);

  final StartupMeasurement report;

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Text(
          'Time to initial display: '
          '${report.timeToInitialDisplay.inMilliseconds} ms',
          style: Theme.of(context).textTheme.titleMedium,
        ),
        if (report.timeToFullDisplay case final ttfd?)
          Text('Time to full display: ${ttfd.inMilliseconds} ms'),
        Text('Launch type: ${report.launchType.name}'),
        const Divider(height: 32),
        for (final entry in report.phases.toMap().entries)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 4),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(entry.key),
                Text('${entry.value.inMicroseconds / 1000} ms'),
              ],
            ),
          ),
      ],
    );
  }
}
3
likes
150
points
201
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Measure Flutter app startup time and cold start in production: time to initial display (TTID), time to full display (TTFD), and a phase breakdown. Works in release builds.

Repository (GitHub)
View/report issues
Contributing

Topics

#startup #performance #monitoring #observability #metrics

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on flutter_startup_metrics

Packages that implement flutter_startup_metrics