feat(useragent): settled on routing architecture

This commit is contained in:
hdbg
2026-03-15 11:23:45 +01:00
parent 62c4bc5ade
commit 16d5b9a233
16 changed files with 994 additions and 863 deletions

View File

@@ -37,6 +37,18 @@
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "auto_route",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/auto_route-11.1.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "auto_route_generator",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/auto_route_generator-10.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "biometric_signature",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/biometric_signature-10.2.0",
@@ -213,7 +225,7 @@
},
{
"name": "flutter_adaptive_scaffold",
"rootUri": "file:///Users/kaska/.pub-cache/git/flutter_adaptive_scaffold-4852b5041d8d9ed6cacb4dc4c6723086f0612e37/",
"rootUri": "file:///Users/kaska/.pub-cache/git/flutter_adaptive_scaffold-b2e3615901a7ab837cb7fc35efbfcf8b55f27638/",
"packageUri": "lib/",
"languageVersion": "3.10"
},
@@ -355,6 +367,12 @@
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "hotreloader",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/hotreloader-4.3.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "http",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/http-1.6.0",
@@ -415,6 +433,12 @@
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lean_builder",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lean_builder-0.1.6",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lints-6.1.0",
@@ -813,9 +837,9 @@
},
{
"name": "watcher",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/watcher-1.2.1",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/watcher-1.1.4",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.1"
},
{
"name": "web",
@@ -853,6 +877,12 @@
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "xxh3",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/xxh3-1.2.0",
"packageUri": "lib/",
"languageVersion": "2.16"
},
{
"name": "yaml",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/yaml-3.1.3",

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +0,0 @@
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:sizer/sizer.dart';
const transitionDuration = Duration(milliseconds: 800);
class AdaptiveBuilders {
WidgetBuilder? buildSmall;
WidgetBuilder? buildMediumLarge;
WidgetBuilder? buildLarge;
WidgetBuilder? buildExtraLarge;
WidgetBuilder? build;
AdaptiveBuilders({
this.buildSmall,
this.buildMediumLarge,
this.buildLarge,
this.buildExtraLarge,
this.build,
});
}
class Destination {
final String label;
final String? tooltip;
final Icon icon;
final Icon? selectedIcon;
final AdaptiveBuilders main;
final AdaptiveBuilders? secondary;
Destination({
required this.label,
required this.icon,
this.selectedIcon,
required this.main,
this.secondary,
this.tooltip,
});
}
class Switcher extends StatelessWidget {
final Widget? child;
const Switcher({super.key, this.child});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(
milliseconds: transitionDuration.inMilliseconds ~/ 100,
),
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
child: child,
);
}
}
WidgetBuilder? patchAnimated(WidgetBuilder? input) {
if (input == null) return null;
return (context) => Switcher(child: input(context));
}
class HomeRouter extends HookWidget {
final List<Destination> destinations;
HomeRouter({super.key, required this.destinations})
: assert(destinations.isNotEmpty);
@override
Widget build(BuildContext context) {
final selectedIndex = useState(0);
final destination = useMemoized(() => destinations[selectedIndex.value], [
selectedIndex.value,
]);
final dispatcher = useMemoized(() => destination.main, [
selectedIndex.value,
]);
final secondaryDispatcher = useMemoized(() => destination.secondary, [
selectedIndex.value,
]);
return AdaptiveScaffold(
destinations: destinations
.map(
(destination) => NavigationDestination(
label: destination.label,
icon: destination.icon,
selectedIcon: destination.selectedIcon,
tooltip: destination.tooltip,
),
)
.toList(),
selectedIndex: selectedIndex.value,
onSelectedIndexChange: (index) => selectedIndex.value = index,
useDrawer: true,
smallBody: patchAnimated(dispatcher.buildSmall),
body: patchAnimated(dispatcher.build),
mediumLargeBody: patchAnimated(dispatcher.buildMediumLarge),
largeBody: patchAnimated(dispatcher.buildLarge),
extraLargeBody: patchAnimated(dispatcher.buildExtraLarge),
smallSecondaryBody: patchAnimated(secondaryDispatcher?.buildSmall),
secondaryBody: patchAnimated(secondaryDispatcher?.build),
mediumLargeSecondaryBody: patchAnimated(
secondaryDispatcher?.buildMediumLarge,
),
largeSecondaryBody: patchAnimated(secondaryDispatcher?.buildLarge),
extraLargeSecondaryBody: patchAnimated(
secondaryDispatcher?.buildExtraLarge,
),
);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:arbiter/features/adaptive_switcher.dart';
import 'package:arbiter/screens/about.dart';
import 'package:arbiter/screens/calc.dart';
import 'package:flutter/material.dart';
final _destinations = [
Destination(
label: "About",
icon: Icon(Icons.info_outline),
main: AdaptiveBuilders(build: (_) => About()),
),
Destination(
label: "Calc",
icon: Icon(Icons.calculate),
main: AdaptiveBuilders(build: (_) => CalcScreen()),
),
];
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomeRouter(destinations: _destinations);
}
}

View File

@@ -3,9 +3,12 @@ import 'package:flutter/material.dart' hide Router;
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MaterialApp(
home: ProviderScope(child: Scaffold(body: Router())),
ProviderScope(
child: MaterialApp.router(
routerConfig: Router().config(),
),
),
);
}

View File

@@ -1,4 +1,3 @@
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:arbiter/features/pk_manager.dart';
@@ -11,22 +10,22 @@ KeyManager keyManager(Ref ref) {
return SimpleEd25519Manager();
}
@riverpod
@Riverpod(keepAlive: true)
class Key extends _$Key {
@override
Future<KeyHandle?> build() async {
final manager = SimpleEd25519Manager();
final manager = ref.watch(keyManagerProvider);
final keyHandle = await manager.get();
return keyHandle;
}
Future<void> create() async {
final manager = ref.watch(keyManagerProvider);
if (state.value != null) {
return;
}
state = await AsyncValue.guard(() async {
final manager = SimpleEd25519Manager();
final newKeyHandle = await manager.create();
return newKeyHandle;
});
@@ -34,7 +33,7 @@ class Key extends _$Key {
}
class KeyBootstrapper implements StageFactory {
final MutationTarget ref;
final ProviderContainer ref;
KeyBootstrapper({required this.ref});
@@ -43,14 +42,14 @@ class KeyBootstrapper implements StageFactory {
@override
Future<bool> get isAlreadyCompleted async {
final key = await ref.container.read(keyProvider.future);
final key = await ref.read(keyProvider.future);
return key != null;
}
@override
Future<void> start(StageController controller) async {
controller.setIndefiniteProgress();
final key = ref.container.read(keyProvider.notifier);
final key = ref.read(keyProvider.notifier);
await key.create();
}

View File

@@ -60,7 +60,7 @@ final class KeyProvider extends $AsyncNotifierProvider<Key, KeyHandle?> {
argument: null,
retry: null,
name: r'keyProvider',
isAutoDispose: true,
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@@ -73,7 +73,7 @@ final class KeyProvider extends $AsyncNotifierProvider<Key, KeyHandle?> {
Key create() => Key();
}
String _$keyHash() => r'6d66204174c4d2d5c76e27f3a8de8f9a9c88a3e0';
String _$keyHash() => r'37b209825067adadbb75fe0b4ce936ea1c201dc8';
abstract class _$Key extends $AsyncNotifier<KeyHandle?> {
FutureOr<KeyHandle?> build();

View File

@@ -1,32 +1,24 @@
import 'dart:async';
import 'package:arbiter/screens/dashboard/about.dart';
import 'package:arbiter/screens/dashboard/calc.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:arbiter/features/bootstrap.dart';
import 'package:arbiter/home.dart';
import 'package:flutter/src/widgets/async.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'router.gr.dart';
final _bootstapCompleter = Completer<void>();
class Router extends HookConsumerWidget {
@AutoRouterConfig(generateForDir: ['lib/screens'])
class Router extends RootStackRouter {
@override
Widget build(BuildContext context, WidgetRef ref) {
final bootstrapper = useMemoized(
() => Bootstrap(completer: _bootstapCompleter),
);
final bootstrapFuture = useFuture(_bootstapCompleter.future);
List<AutoRoute> get routes => [
AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true),
switch (bootstrapFuture.connectionState) {
case ConnectionState.none ||
ConnectionState.waiting ||
ConnectionState.active:
return bootstrapper;
case ConnectionState.done:
break;
}
return Home();
}
AutoRoute(
page: DashboardRouter.page,
path: '/dashboard',
children: [
AutoRoute(page: AboutRoute.page, path: 'about'),
AutoRoute(page: CalcRoute.page, path: 'calc'),
],
),
];
}

View File

@@ -0,0 +1,80 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:arbiter/screens/bootstrap.dart' as _i2;
import 'package:arbiter/screens/dashboard.dart' as _i4;
import 'package:arbiter/screens/dashboard/about.dart' as _i1;
import 'package:arbiter/screens/dashboard/calc.dart' as _i3;
import 'package:auto_route/auto_route.dart' as _i5;
/// generated route for
/// [_i1.AboutScreen]
class AboutRoute extends _i5.PageRouteInfo<void> {
const AboutRoute({List<_i5.PageRouteInfo>? children})
: super(AboutRoute.name, initialChildren: children);
static const String name = 'AboutRoute';
static _i5.PageInfo page = _i5.PageInfo(
name,
builder: (data) {
return _i1.AboutScreen();
},
);
}
/// generated route for
/// [_i2.Bootstrap]
class Bootstrap extends _i5.PageRouteInfo<void> {
const Bootstrap({List<_i5.PageRouteInfo>? children})
: super(Bootstrap.name, initialChildren: children);
static const String name = 'Bootstrap';
static _i5.PageInfo page = _i5.PageInfo(
name,
builder: (data) {
return const _i2.Bootstrap();
},
);
}
/// generated route for
/// [_i3.CalcScreen]
class CalcRoute extends _i5.PageRouteInfo<void> {
const CalcRoute({List<_i5.PageRouteInfo>? children})
: super(CalcRoute.name, initialChildren: children);
static const String name = 'CalcRoute';
static _i5.PageInfo page = _i5.PageInfo(
name,
builder: (data) {
return const _i3.CalcScreen();
},
);
}
/// generated route for
/// [_i4.DashboardRouter]
class DashboardRouter extends _i5.PageRouteInfo<void> {
const DashboardRouter({List<_i5.PageRouteInfo>? children})
: super(DashboardRouter.name, initialChildren: children);
static const String name = 'DashboardRouter';
static _i5.PageInfo page = _i5.PageInfo(
name,
builder: (data) {
return const _i4.DashboardRouter();
},
);
}

View File

@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mtcore/markettakers.dart';
class About extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AboutScreen(decription: "Arbiter is bla bla bla");
}
}

View File

@@ -1,19 +1,31 @@
import 'dart:async';
import 'package:arbiter/providers/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:arbiter/router.gr.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mtcore/markettakers.dart';
@RoutePage()
class Bootstrap extends HookConsumerWidget {
final Completer<void> completer;
const Bootstrap({required this.completer});
const Bootstrap({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final container = ProviderScope.containerOf(context);
final container = ProviderScope.containerOf( context);
final completer = useMemoized(() {
final completer = Completer<void>();
completer.future.then((_) async {
if (context.mounted) {
final router = AutoRouter.of(context);
router.replace(const DashboardRouter());
}
});
return completer;
}, []);
final stages = useMemoized(() {
return [KeyBootstrapper(ref: container)];
}, []);
@@ -21,6 +33,7 @@ class Bootstrap extends HookConsumerWidget {
() => Bootstrapper(stages: stages, onCompleted: completer),
[stages],
);
return bootstrapper;
}
}

View File

@@ -0,0 +1,42 @@
import 'package:arbiter/router.gr.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
const breakpoints = MaterialAdaptiveBreakpoints();
final routes = [AboutRoute(), CalcRoute()];
@RoutePage()
class DashboardRouter extends StatelessWidget {
const DashboardRouter({super.key});
@override
Widget build(BuildContext context) {
return AutoTabsRouter(
routes: routes,
transitionBuilder: (context, child, animation) => FadeTransition(
opacity: animation,
// the passed child is technically our animated selected-tab page
child: child,
),
builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context);
final currentActive = tabsRouter.activeIndex;
return AdaptiveScaffold(
destinations: [
NavigationDestination(icon: Icon(Icons.book), label: "About"),
NavigationDestination(icon: Icon(Icons.calculate), label: "Calc"),
],
body: (ctx) => child,
onSelectedIndexChange: (index) {
tabsRouter.navigate(routes[index]);
},
selectedIndex: currentActive,
transitionDuration: Duration(milliseconds: 800),
internalAnimations: true,
);
},
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:mtcore/markettakers.dart' as mt;
@RoutePage()
class AboutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return mt.AboutScreen(decription: "Arbiter is bla bla bla");
}
}

View File

@@ -1,5 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@RoutePage()
class CalcScreen extends StatefulWidget {
const CalcScreen({super.key});

View File

@@ -49,6 +49,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_route:
dependency: "direct main"
description:
name: auto_route
sha256: e9acfeb3df33d188fce4ad0239ef4238f333b7aa4d95ec52af3c2b9360dcd969
url: "https://pub.dev"
source: hosted
version: "11.1.0"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: "04300eaf5821962aae8b5cd94f67013fd2fd326dc3be212d3ec1ae7470f09834"
url: "https://pub.dev"
source: hosted
version: "10.4.0"
biometric_signature:
dependency: "direct main"
description:
@@ -283,10 +299,10 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "4852b5041d8d9ed6cacb4dc4c6723086f0612e37"
resolved-ref: b2e3615901a7ab837cb7fc35efbfcf8b55f27638
url: "https://github.com/hdbg/flutter_adaptive_scaffold.git"
source: git
version: "0.3.3+1"
version: "1.0.0+1"
flutter_bloc:
dependency: transitive
description:
@@ -465,6 +481,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.3.0"
http:
dependency: transitive
description:
@@ -545,6 +569,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lean_builder:
dependency: transitive
description:
name: lean_builder
sha256: "6af3cfbf34400eb14b89fe20111e5981e7083362f00ea10b9ed2a6e833250d76"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
lints:
dependency: transitive
description:
@@ -1074,10 +1106,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.1.4"
web:
dependency: transitive
description:
@@ -1126,6 +1158,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml:
dependency: transitive
description:

View File

@@ -27,6 +27,7 @@ dependencies:
riverpod_annotation: ^4.0.0
grpc: ^5.1.0
flutter_hooks: ^0.21.3+1
auto_route: ^11.1.0
dev_dependencies:
flutter_test:
@@ -35,6 +36,7 @@ dev_dependencies:
flutter_lints: ^6.0.0
riverpod_generator: ^4.0.0+1
build_runner: ^2.12.2
auto_route_generator: ^10.4.0
flutter:
uses-material-design: true