Files
buzz/mobile/test/shared/widgets/ios_navigation_bar_test.dart
klopez4212 96e56c5b33 Add participant avatars to mobile conversation headers (#9369)
## Summary

Add participant portraits to mobile conversation headers, with up to
four portraits in a compact cluster. DM and Solo headers show
counterparts; channel headers use the member roster. Names stay on one
line, and tapping either the portrait or title opens the existing
settings.

- iOS: float portraits above the glass title capsule; lift standalone
animated artwork and tuck the title underneath.
- Android: place the cluster inside a left-aligned title pill, 16 dp
after Back, with smaller agent portraits and a 48 dp tap target.
- Remove header presence/member-count subtitles. In confirmed two-member
conversations (DM, Solo, or channel), hide redundant incoming author
avatars and give text the freed 48 dp while retaining the previous total
width. Apply this to thread replies and align their footers; messages
with avatars and outgoing messages retain their production width caps.

Keep sender attribution visible while DM metadata and current membership
disagree. Forum loading, error, and post-list states reserve the full
conversation header height. The floating title follows the app-selected
appearance, including when it differs from iOS. Conversation headers
remain name-only, including temporary conversations, as requested;
retention accessibility text is preserved.

## Validation

- Formatting/static analysis and full mobile suite: 4,695 passed, 71
skipped; auxiliary push and crypto suites passed.
- Full native iOS suite passed, including title positioning/reset and
settings activation.
- Installed previews on physical iPhone Air and Pixel; Android
avatar/title settings taps exercised.
- Final diff reviewed for correctness, accessibility, scope, and
public-source boundaries; no blocking findings.

The width follow-up is covered by rendered conversation/thread tests.
The width follow-up is installed and launched on iPhone Air; the Pixel
preview remains on the preceding header build. The review fixes have not
been installed on physical devices.

## Snapshots

Current Pixel preview, cropped to the header to omit conversation
history. These captures show a single agent and a four-portrait cluster.
The current iOS preview is installed, but a fresh physical-device
screenshot was blocked by device authentication; no outdated iOS capture
is included.

### Android — single agent

![Android single-agent
header](https://github.com/user-attachments/assets/9ecb45b7-a2e0-4699-8889-ab7934fd1759)

### Android — grouped agents

![Android grouped-agent
header](https://github.com/user-attachments/assets/f4435720-0d10-4b4e-98c2-5494dc9a83c4)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-11 15:28:39 -08:00

768 lines
25 KiB
Dart

import 'package:buzz/shared/widgets/dm_header_portrait.dart';
import 'dart:async';
import 'package:buzz/shared/relay/media_image.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
import 'package:buzz/shared/widgets/frosted_scaffold.dart';
import 'package:buzz/shared/widgets/ios_navigation_bar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
testWidgets(
'animated DM portrait opens the same settings as the native title',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
var settingsOpened = 0;
await tester.pumpWidget(
_testApp(
home: MediaQuery(
data: const MediaQueryData(
viewPadding: EdgeInsets.only(top: 62),
disableAnimations: true,
),
child: SizedBox(
width: 393,
height: 166,
child: IosNavigationBar(
title: 'Agent',
onTitlePressed: () => settingsOpened++,
subtitle: 'Online',
titleAvatars: const [
IosNavigationAction(
label: 'Agent',
avatarInitial: 'A',
avatarIsAgent: true,
imageUrl:
'https://dwwgwmfqqjotj.cloudfront.net/avatars/20260925T154625166Z/poster/gloopies/gloopies-13.png',
),
],
),
),
),
),
);
final params =
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams!
as Map;
expect((params['titleAvatars'] as List).length, 1);
expect(params['titleHasAnimatedPortrait'], isTrue);
final portrait = find.byType(DmHeaderPortrait);
expect(tester.getSize(portrait), const Size(64, 64));
expect(
tester.getTopLeft(portrait).dy -
tester.getTopLeft(find.byType(UiKitView)).dy,
58,
);
await tester.tap(
find.byKey(const ValueKey('navigation-header-avatar-settings')),
);
expect(settingsOpened, 1);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
testWidgets(
'native toolbar receives stable safe inset inside a translated route',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(
_testApp(
home: MediaQuery(
data: const MediaQueryData(viewPadding: EdgeInsets.only(top: 62)),
child: Transform.translate(
offset: const Offset(0, 600),
child: const SizedBox(
height: 120,
child: IosNavigationBar(title: 'Settings'),
),
),
),
),
);
expect(
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams,
containsPair('safeTop', 62.0),
);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
testWidgets(
'native readiness waits for configuration and layout acknowledgement',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('buzz/ios_navigation_bar/844');
final applied = Completer<void>();
final readiness = <bool>[];
final calls = <String>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
calls.add(call.method);
if (call.method == 'prepareForReveal') await applied.future;
return null;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
await tester.pumpWidget(
_testApp(
home: SizedBox(
height: 100,
child: IosNavigationBar(
title: 'Bravo',
onReadyChanged: readiness.add,
),
),
),
);
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
844,
);
await tester.pumpAndSettle();
expect(calls, contains('prepareForReveal'));
expect(readiness.last, false);
applied.complete();
await tester.pumpAndSettle();
await tester.pump();
await tester.pump();
expect(readiness.last, true);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
testWidgets('native landing ignores pending profile artwork', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
final pending = Completer<http.Response>();
var requested = false;
final client = MockClient((_) {
requested = true;
return pending.future;
});
addTearDown(client.close);
const channel = MethodChannel('buzz/ios_navigation_bar/846');
final readiness = <bool>[];
final calls = <String>[];
final createdChannels = <MethodChannel>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform_views,
(call) async {
if (call.method == 'create') {
final id = (call.arguments as Map)['id'];
final native = MethodChannel('${IosNavigationBar.viewType}/$id');
createdChannels.add(native);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
native,
(call) async {
calls.add(call.method);
return null;
},
);
}
return null;
},
);
addTearDown(() {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform_views,
null,
);
for (final native in createdChannels) {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
native,
null,
);
}
});
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
calls.add(call.method);
return null;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
await tester.pumpWidget(
ProviderScope(
overrides: [mediaHttpClientProvider.overrideWithValue(client)],
child: _testApp(
home: IosNavigationBar(
title: 'Bravo',
leading: IosNavigationAction(
label: 'Community settings',
avatarInitial: 'B',
onAvatarBoundsChanged: (_) {},
),
actions: const [
IosNavigationAction(
label: 'Profile',
avatarInitial: 'P',
imageUrl: 'https://slow.example.com/profile.png',
),
],
onReadyChanged: readiness.add,
),
),
),
);
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
846,
);
await tester.pumpAndSettle();
Map leading() =>
(tester.widget<UiKitView>(find.byType(UiKitView)).creationParams
as Map)['leading']
as Map;
await tester.runAsync(() async {
for (
var attempt = 0;
attempt < 100 && leading()['imageData'] == null;
attempt++
) {
await Future<void>.delayed(const Duration(milliseconds: 10));
await tester.pump();
}
});
await tester.pumpAndSettle();
expect(requested, isTrue);
expect(pending.isCompleted, isFalse);
expect(calls, contains('prepareForReveal'));
expect(readiness.last, isTrue);
expect(leading()['imageData'], isNotNull);
pending.complete(http.Response('offline', 503));
await tester.pumpAndSettle();
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
testWidgets(
'native avatar reports global transition bounds and retains a hidden slot',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('${IosNavigationBar.viewType}/842');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(_) async => null,
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
Rect? measured;
await tester.pumpWidget(
_testApp(
home: Padding(
padding: const EdgeInsets.only(top: 30),
child: IosNavigationBar(
title: 'Alpha',
leading: IosNavigationAction(
label: 'Community settings',
avatarInitial: 'A',
avatarHidden: true,
onAvatarBoundsChanged: (bounds) => measured = bounds,
onPressed: () {},
),
),
),
),
);
final view = tester.widget<UiKitView>(find.byType(UiKitView));
final leading = (view.creationParams as Map)['leading'] as Map;
expect(leading['tracksAvatarBounds'], isTrue);
expect(leading['avatarHidden'], isTrue);
view.onPlatformViewCreated!(842);
await tester.pump();
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(
const MethodCall('avatarBounds', {
'id': 'leading',
'x': 16.0,
'y': 4.0,
'width': 36.0,
'height': 36.0,
}),
),
(_) {},
);
expect(measured, const Rect.fromLTWH(16, 34, 36, 36));
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
for (final reverse in [false, true]) {
testWidgets('initial material tracks content above reverse=$reverse', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
final controller = ScrollController();
addTearDown(controller.dispose);
ValueListenable<double>? offset;
Widget page(int count) => _testApp(
home: FrostedScaffold(
appBar: const FrostedAppBar(title: Text('Conversation')),
body: Builder(
builder: (context) {
offset = IosNavigationScrollScope.maybeOf(context);
return ListView(
controller: controller,
reverse: reverse,
children: [
for (var i = 0; i < count; i++)
SizedBox(height: 60, child: Text('Message $i')),
],
);
},
),
),
);
await tester.pumpWidget(page(40));
await tester.pumpAndSettle();
expect(offset!.value, reverse ? greaterThan(0) : 0);
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(offset!.value, reverse ? 0 : greaterThan(0));
await tester.pumpWidget(page(1));
await tester.pumpAndSettle();
expect(offset!.value, 0);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
}
testWidgets('native material follows live theme surface changes', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('buzz/ios_navigation_bar/845');
final configurations = <Map<Object?, Object?>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
if (call.method == 'configure') configurations.add(call.arguments as Map);
return null;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
Widget app(Color surface) => ProviderScope(
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: surface,
).copyWith(surface: surface),
),
home: const Scaffold(
body: IosNavigationBar(title: 'general', subtitle: '3 members'),
),
),
);
await tester.pumpWidget(app(const Color(0xFFFFEEDD)));
final view = tester.widget<UiKitView>(find.byType(UiKitView));
expect(view.creationParams, containsPair('background', 0xFFFFEEDD));
view.onPlatformViewCreated!(845);
await tester.pump();
await tester.pumpWidget(app(const Color(0xFF223344)));
await tester.pumpAndSettle();
expect(configurations.last['background'], 0xFF223344);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
testWidgets('larger native metrics keep a deeply scrolled title collapsed', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('buzz/ios_navigation_bar/843');
final calls = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
calls.add(call);
return null;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
await tester.pumpWidget(
_testApp(
home: FrostedScaffold(
nativePinnedBody: true,
appBar: const FrostedAppBar(
title: Text('Search'),
nativeLargeTitle: true,
),
body: ListView(
children: [
for (var i = 0; i < 40; i++)
SizedBox(height: 60, child: Text('Row $i')),
],
),
),
),
);
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
843,
);
await tester.pump();
await tester.drag(find.byType(ListView), const Offset(0, -250));
await tester.pumpAndSettle();
expect(
calls.where((call) => call.method == 'scroll').last.arguments,
greaterThan(52),
);
final bodyTop = tester.getTopLeft(find.byType(ListView)).dy;
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(
const MethodCall('metrics', {
'compactHeight': 44.0,
'expandedHeight': 124.0,
}),
),
(_) {},
);
await tester.pump();
await tester.pump();
expect(
calls.where((call) => call.method == 'scroll').last.arguments,
greaterThan(80),
);
expect(tester.getSize(find.byType(UiKitView)).height, 44);
expect(tester.getTopLeft(find.byType(ListView)).dy, bodyTop);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
testWidgets(
'iOS routes use UIKit and route native menus to current callbacks',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('${IosNavigationBar.viewType}/42');
final calls = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
calls.add(call);
return null;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
var selected = 0;
var callbackVersion = 1;
Widget page() {
final version = callbackVersion;
return _testApp(
theme: AppTheme.light(),
home: FrostedScaffold(
appBar: FrostedAppBar(
nativeTitle: 'Activity',
nativeLargeTitle: true,
nativeActions: [
IosNavigationAction(
label: 'Filter',
symbol: 'line.3.horizontal.decrease',
children: [
IosNavigationAction(
label: 'Mentions',
onPressed: () => selected = version,
),
],
),
],
),
body: ListView(
children: [
for (var i = 0; i < 40; i++)
SizedBox(height: 60, child: Text('Item $i')),
],
),
),
);
}
await tester.pumpWidget(page());
final view = tester.widget<UiKitView>(find.byType(UiKitView));
expect(view.viewType, IosNavigationBar.viewType);
expect(view.creationParams, containsPair('largeTitle', true));
view.onPlatformViewCreated!(42);
await tester.pump();
callbackVersion = 2;
await tester.pumpWidget(page());
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(const MethodCall('action', '0.0')),
(_) {},
);
expect(selected, 2);
await tester.drag(find.byType(ListView), const Offset(0, -180));
await tester.pumpAndSettle();
expect(
calls.where((call) => call.method == 'scroll').last.arguments,
greaterThan(52),
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
testWidgets('native back button respects the Flutter route stack', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
final navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
_testApp(
navigatorKey: navigator,
home: const Scaffold(body: Text('Root')),
),
);
navigator.currentState!.push(
MaterialPageRoute<void>(
builder: (_) => const FrostedScaffold(
appBar: FrostedAppBar(title: Text('Theme')),
body: SizedBox.expand(),
),
),
);
await tester.pumpAndSettle();
final native = tester.widget<IosNavigationBar>(
find.byType(IosNavigationBar),
);
expect(native.title, 'Theme');
expect(native.onBack, isNotNull);
native.onBack!();
await tester.pumpAndSettle();
expect(find.text('Root'), findsOneWidget);
expect(find.byType(UiKitView), findsNothing);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('UIKit measurements resize the bar and page spacing together', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
const channel = MethodChannel('${IosNavigationBar.viewType}/91');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(_) async => null,
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
await tester.pumpWidget(
_testApp(
home: Builder(
builder: (context) => FrostedScaffold(
appBar: const FrostedAppBar(
title: Text('Home'),
nativeLargeTitle: true,
),
body: ListView(
children: [
SizedBox(
key: const ValueKey('body-inset'),
height: frostedAppBarHeight(context, nativeLargeTitle: true),
),
],
),
),
),
),
);
final view = tester.widget<UiKitView>(find.byType(UiKitView));
view.onPlatformViewCreated!(91);
await tester.pump();
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(
const MethodCall('metrics', {
'compactHeight': 56.0,
'expandedHeight': 112.0,
}),
),
(_) {},
);
await tester.pump();
expect(tester.getSize(find.byType(UiKitView)).height, 112);
expect(
tester.getSize(find.byKey(const ValueKey('body-inset'))).height,
112,
);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
testWidgets('loading avatars use initials instead of navigation symbols', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(
_testApp(
home: const IosNavigationBar(
title: 'Home',
leading: IosNavigationAction(
label: 'Community',
symbol: 'building.2',
avatarInitial: 'K',
avatarIdentity: 'community-one',
),
actions: [
IosNavigationAction(
label: 'Profile',
symbol: 'person.crop.circle',
avatarInitial: 'A',
avatarIdentity: 'profile-one',
),
],
),
),
);
final params =
tester.widget<UiKitView>(find.byType(UiKitView)).creationParams! as Map;
expect((params['leading'] as Map)['symbol'], isNull);
expect((params['leading'] as Map)['avatarInitial'], 'K');
final profile = (params['actions'] as List).single as Map;
expect(profile['symbol'], isNull);
expect(profile['avatarInitial'], 'A');
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
});
testWidgets(
'retains decoded avatars on theme refresh but isolates identities',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
final messenger = tester.binding.defaultBinaryMessenger;
final channels = <MethodChannel>[];
messenger.setMockMethodCallHandler(SystemChannels.platform_views, (
call,
) async {
if (call.method == 'create') {
final id = (call.arguments as Map)['id'];
final channel = MethodChannel('${IosNavigationBar.viewType}/$id');
channels.add(channel);
messenger.setMockMethodCallHandler(channel, (_) async => null);
}
return null;
});
addTearDown(() {
messenger.setMockMethodCallHandler(SystemChannels.platform_views, null);
for (final channel in channels) {
messenger.setMockMethodCallHandler(channel, null);
}
});
Widget page(String identity, ThemeData theme) => _testApp(
theme: theme,
home: IosNavigationBar(
title: 'Home',
leading: IosNavigationAction(
label: 'Profile',
avatarInitial: 'K',
avatarIdentity: identity,
),
),
);
Map avatar() =>
(tester.widget<UiKitView>(find.byType(UiKitView)).creationParams!
as Map)['leading']
as Map;
await tester.pumpWidget(page('one', AppTheme.light()));
await tester.runAsync(() async {
for (
var attempt = 0;
attempt < 100 && avatar()['imageData'] == null;
attempt++
) {
await Future<void>.delayed(const Duration(milliseconds: 10));
await tester.pump();
}
});
final image = avatar()['imageData'];
expect(image, isNotNull);
await tester.pumpWidget(page('one', AppTheme.dark()));
expect(avatar()['imageData'], image);
await tester.pumpWidget(page('two', AppTheme.dark()));
expect(avatar()['imageData'], isNull);
expect(avatar()['symbol'], isNull);
await tester.pumpWidget(const SizedBox());
debugDefaultTargetPlatformOverride = null;
},
);
testWidgets('Android retains its Flutter header', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(
_testApp(
theme: AppTheme.light(),
home: const FrostedScaffold(
appBar: FrostedAppBar(title: Text('Settings')),
body: SizedBox.expand(),
),
),
);
expect(find.byType(UiKitView), findsNothing);
expect(find.text('Settings'), findsOneWidget);
debugDefaultTargetPlatformOverride = null;
});
}
Widget _testApp({
ThemeData? theme,
required Widget home,
GlobalKey<NavigatorState>? navigatorKey,
}) => ProviderScope(
child: MaterialApp(
theme: theme,
home: home,
navigatorKey: navigatorKey,
builder: (context, child) => IosNavigationMetricsHost(child: child!),
),
);