chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板

Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sunny_mochi/core/observability/talker_setup.dart';
import 'package:sunny_mochi/features/auth/presentation/notifiers/auth_status_provider.dart';
import 'routes.dart';
part 'app_router.g.dart';
// 不需要登录即可访问的路径
const _publicPaths = {'/login'};
@Riverpod(keepAlive: true)
GoRouter appRouter(Ref ref) {
final auth = ref.read(authStatusProvider);
return GoRouter(
debugLogDiagnostics: true,
initialLocation: '/home',
refreshListenable: auth.listenable,
redirect: (BuildContext context, GoRouterState state) {
final loggedIn = auth.value;
final path = state.matchedLocation;
appTalker.verbose('[Router] path=$path loggedIn=$loggedIn');
if (loggedIn == null) return null; // bootstrap 未完成,保持当前
if (!loggedIn && !_publicPaths.contains(path)) return '/login';
if (loggedIn && path == '/login') return '/home';
return null;
},
routes: $appRoutes,
);
}
+51
View File
@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_router.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(appRouter)
final appRouterProvider = AppRouterProvider._();
final class AppRouterProvider
extends $FunctionalProvider<GoRouter, GoRouter, GoRouter>
with $Provider<GoRouter> {
AppRouterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appRouterProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appRouterHash();
@$internal
@override
$ProviderElement<GoRouter> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
GoRouter create(Ref ref) {
return appRouter(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GoRouter value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GoRouter>(value),
);
}
}
String _$appRouterHash() => r'6f5b7a9f1595fbf5343fdaf160df95a775d77b57';
+89
View File
@@ -0,0 +1,89 @@
import 'package:go_router/go_router.dart';
import 'package:sunny_mochi/core/widgets/shell_scaffold.dart';
import 'package:flutter/material.dart';
import 'package:sunny_mochi/features/auth/presentation/pages/login_page.dart';
import 'package:sunny_mochi/features/dev_panel/presentation/pages/dev_panel_page.dart';
import 'package:sunny_mochi/features/error_report/presentation/error_report_page.dart';
part 'routes.g.dart';
// ── StatefulShellRoute(底部导航 2 Tab)─────────────────────────────────────
@TypedStatefulShellRoute<ShellRouteData>(
branches: [
TypedStatefulShellBranch<HomeBranch>(
routes: [TypedGoRoute<HomeRoute>(path: '/home')],
),
TypedStatefulShellBranch<MineBranch>(
routes: [TypedGoRoute<MineRoute>(path: '/mine')],
),
],
)
class ShellRouteData extends StatefulShellRouteData {
const ShellRouteData();
@override
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell shell,
) =>
ShellScaffold(shell: shell);
}
class HomeBranch extends StatefulShellBranchData {
const HomeBranch();
}
class MineBranch extends StatefulShellBranchData {
const MineBranch();
}
// ── Shell 叶子路由(TODO: 业务项目替换为真实页面)───────────────────────────
class HomeRoute extends GoRouteData with $HomeRoute {
const HomeRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const Scaffold(
body: Center(child: Text('Home — TODO: Replace with HomePage')),
);
}
class MineRoute extends GoRouteData with $MineRoute {
const MineRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const Scaffold(
body: Center(child: Text('Mine — TODO: Replace with MinePage')),
);
}
// ── 独立路由(Shell 之外)────────────────────────────────────────────────────
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData with $LoginRoute {
const LoginRoute();
@override
Widget build(BuildContext context, GoRouterState state) =>
const LoginPage();
}
@TypedGoRoute<DevPanelRoute>(path: '/dev')
class DevPanelRoute extends GoRouteData with $DevPanelRoute {
const DevPanelRoute();
@override
Widget build(BuildContext context, GoRouterState state) =>
const DevPanelPage();
}
@TypedGoRoute<ErrorReportRoute>(path: '/error-report')
class ErrorReportRoute extends GoRouteData with $ErrorReportRoute {
const ErrorReportRoute();
@override
Widget build(BuildContext context, GoRouterState state) =>
const ErrorReportPage();
}
+147
View File
@@ -0,0 +1,147 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'routes.dart';
// **************************************************************************
// GoRouterGenerator
// **************************************************************************
List<RouteBase> get $appRoutes => [
$shellRouteData,
$loginRoute,
$devPanelRoute,
$errorReportRoute,
];
RouteBase get $shellRouteData => StatefulShellRouteData.$route(
factory: $ShellRouteDataExtension._fromState,
branches: [
StatefulShellBranchData.$branch(
routes: [
GoRouteData.$route(path: '/home', factory: $HomeRoute._fromState),
],
),
StatefulShellBranchData.$branch(
routes: [
GoRouteData.$route(path: '/mine', factory: $MineRoute._fromState),
],
),
],
);
extension $ShellRouteDataExtension on ShellRouteData {
static ShellRouteData _fromState(GoRouterState state) =>
const ShellRouteData();
}
mixin $HomeRoute on GoRouteData {
static HomeRoute _fromState(GoRouterState state) => const HomeRoute();
@override
String get location => GoRouteData.$location('/home');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin $MineRoute on GoRouteData {
static MineRoute _fromState(GoRouterState state) => const MineRoute();
@override
String get location => GoRouteData.$location('/mine');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $loginRoute =>
GoRouteData.$route(path: '/login', factory: $LoginRoute._fromState);
mixin $LoginRoute on GoRouteData {
static LoginRoute _fromState(GoRouterState state) => const LoginRoute();
@override
String get location => GoRouteData.$location('/login');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $devPanelRoute =>
GoRouteData.$route(path: '/dev', factory: $DevPanelRoute._fromState);
mixin $DevPanelRoute on GoRouteData {
static DevPanelRoute _fromState(GoRouterState state) => const DevPanelRoute();
@override
String get location => GoRouteData.$location('/dev');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $errorReportRoute => GoRouteData.$route(
path: '/error-report',
factory: $ErrorReportRoute._fromState,
);
mixin $ErrorReportRoute on GoRouteData {
static ErrorReportRoute _fromState(GoRouterState state) =>
const ErrorReportRoute();
@override
String get location => GoRouteData.$location('/error-report');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}