flu-cli-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/chunk-FOMWV2YP.js +378 -0
- package/dist/chunk-SW6YDKXI.js +112 -0
- package/dist/factory-6DDXZYQP.js +6 -0
- package/dist/index.cjs +4668 -0
- package/dist/index.d.cts +644 -0
- package/dist/index.d.ts +644 -0
- package/dist/index.js +4037 -0
- package/dist/upgrade_snippets-XFR7Q444.js +8 -0
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +52 -0
- package/templates/README.md +129 -0
- package/templates/core_files/base/base_list_page.dart.template +225 -0
- package/templates/core_files/base/base_list_viewmodel.dart.template +164 -0
- package/templates/core_files/base/base_page.dart.template +252 -0
- package/templates/core_files/base/base_viewmodel.dart.template +68 -0
- package/templates/core_files/base/index.dart.template +5 -0
- package/templates/core_files/config/app_config.dart.template +142 -0
- package/templates/core_files/config/app_initializer.dart.template +74 -0
- package/templates/core_files/config/index.dart.template +3 -0
- package/templates/core_files/index.dart.template +8 -0
- package/templates/core_files/network/README.md +378 -0
- package/templates/core_files/network/app_error_code.dart.template +49 -0
- package/templates/core_files/network/app_http.dart.template +306 -0
- package/templates/core_files/network/app_response.dart.template +81 -0
- package/templates/core_files/network/index.dart.template +12 -0
- package/templates/core_files/network/interceptors/app_response_interceptor.dart.template +44 -0
- package/templates/core_files/network/interceptors/auth_interceptor.dart.template +30 -0
- package/templates/core_files/network/interceptors/error_interceptor.dart.template +48 -0
- package/templates/core_files/network/interceptors/index.dart.template +6 -0
- package/templates/core_files/network/interceptors/log_interceptor.dart.template +97 -0
- package/templates/core_files/network/interceptors/network_error_interceptor.dart.template +58 -0
- package/templates/core_files/network/interceptors/retry_interceptor.dart.template +69 -0
- package/templates/core_files/network/response_adapter.dart.template +69 -0
- package/templates/core_files/router/app_routes.dart.template +32 -0
- package/templates/core_files/router/index.dart.template +3 -0
- package/templates/core_files/router/navigator_util_getx.dart.template +131 -0
- package/templates/core_files/router/navigator_util_material.dart.template +191 -0
- package/templates/core_files/storage/index.dart.template +3 -0
- package/templates/core_files/storage/storage_keys.dart.template +34 -0
- package/templates/core_files/storage/storage_util.dart.template +102 -0
- package/templates/core_files/theme/app_theme.dart.template +37 -0
- package/templates/core_files/theme/index.dart.template +3 -0
- package/templates/core_files/theme/status_views_theme.dart.template +40 -0
- package/templates/core_files/utils/index.dart.template +2 -0
- package/templates/core_files/utils/loading_util.dart.template +55 -0
- package/templates/core_files/utils/toast_util.dart.template +128 -0
- package/templates/examples/eg_list_page.dart.template +340 -0
- package/templates/examples/eg_list_viewmodel.dart.template +31 -0
- package/templates/examples/eg_service.dart.template +78 -0
- package/templates/examples/mock_data.dart.template +50388 -0
- package/templates/examples/tu_chong_model.dart.template +633 -0
- package/templates/request_helper.dart.template +59 -0
- package/templates/snippets/flu-cli.code-snippets +268 -0
- package/templates/snippets/flu-cli.code-snippets.backup +268 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import 'base_viewmodel.dart';
|
|
2
|
+
|
|
3
|
+
/// 列表基础 ViewModel(轻量版)
|
|
4
|
+
///
|
|
5
|
+
/// - 提供分页/刷新/加载更多的最小实现
|
|
6
|
+
abstract class BaseListViewModel<T> extends BaseViewModel {
|
|
7
|
+
final List<T> items = <T>[];
|
|
8
|
+
int page = 1;
|
|
9
|
+
int pageSize = 20;
|
|
10
|
+
bool hasMore = true;
|
|
11
|
+
bool isLoadingMore = false;
|
|
12
|
+
bool loadMoreFailed = false;
|
|
13
|
+
Object? loadMoreError;
|
|
14
|
+
|
|
15
|
+
/// 子类必须实现:拉取一页数据(函数级注释)
|
|
16
|
+
Future<List<T>> fetchPage({required int page, required int pageSize});
|
|
17
|
+
|
|
18
|
+
/// 初始化加载(函数级注释)
|
|
19
|
+
@override
|
|
20
|
+
Future<void> onInit() async {
|
|
21
|
+
super.onInit();
|
|
22
|
+
await loadData();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// 首屏加载(函数级注释)
|
|
26
|
+
Future<void> loadData() async {
|
|
27
|
+
setState(ViewState.loading);
|
|
28
|
+
page = 1;
|
|
29
|
+
hasMore = true;
|
|
30
|
+
try {
|
|
31
|
+
final list = await fetchPage(page: page, pageSize: pageSize);
|
|
32
|
+
if (isDisposed) return; // 异步完成后检查是否已释放
|
|
33
|
+
|
|
34
|
+
items
|
|
35
|
+
..clear()
|
|
36
|
+
..addAll(list);
|
|
37
|
+
_configHasMore(list);
|
|
38
|
+
setState(ViewState.success);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (isDisposed) return; // 捕获异常后也需检查
|
|
41
|
+
setState(ViewState.error, error: e.toString());
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// 下拉刷新(函数级注释)
|
|
46
|
+
@override
|
|
47
|
+
Future<void> refreshData() async {
|
|
48
|
+
if (isRefreshing || isDisposed) return; // 添加释放检查
|
|
49
|
+
setRefreshing(true);
|
|
50
|
+
final old = state;
|
|
51
|
+
try {
|
|
52
|
+
page = 1;
|
|
53
|
+
hasMore = true;
|
|
54
|
+
final list = await fetchPage(page: page, pageSize: pageSize);
|
|
55
|
+
if (isDisposed) return; // 异步完成后检查是否已释放
|
|
56
|
+
|
|
57
|
+
items
|
|
58
|
+
..clear()
|
|
59
|
+
..addAll(list);
|
|
60
|
+
_configHasMore(list);
|
|
61
|
+
setState(ViewState.success);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
if (isDisposed) return; // 捕获异常后也需检查
|
|
64
|
+
// 刷新失败回退原状态
|
|
65
|
+
setState(old);
|
|
66
|
+
} finally {
|
|
67
|
+
setRefreshing(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// 加载更多(函数级注释)
|
|
72
|
+
Future<void> loadMore() async {
|
|
73
|
+
if (!hasMore || isLoadingMore || isDisposed) return; // 添加释放检查
|
|
74
|
+
isLoadingMore = true;
|
|
75
|
+
loadMoreFailed = false;
|
|
76
|
+
loadMoreError = null;
|
|
77
|
+
try {
|
|
78
|
+
final next = page + 1;
|
|
79
|
+
final list = await fetchPage(page: next, pageSize: pageSize);
|
|
80
|
+
if (isDisposed) return; // 异步完成后检查是否已释放
|
|
81
|
+
|
|
82
|
+
if (list.isNotEmpty) {
|
|
83
|
+
items.addAll(list);
|
|
84
|
+
page = next;
|
|
85
|
+
}
|
|
86
|
+
_configHasMore(list);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (isDisposed) return; // 捕获异常后也需检查
|
|
89
|
+
loadMoreFailed = true;
|
|
90
|
+
loadMoreError = e;
|
|
91
|
+
} finally {
|
|
92
|
+
isLoadingMore = false;
|
|
93
|
+
notifyDataChange();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// 重置分页(函数级注释)
|
|
98
|
+
void resetPaging({int initialPage = 1, int pageSize = 20}) {
|
|
99
|
+
page = initialPage;
|
|
100
|
+
this.pageSize = pageSize;
|
|
101
|
+
hasMore = true;
|
|
102
|
+
items.clear();
|
|
103
|
+
loadMoreFailed = false;
|
|
104
|
+
loadMoreError = null;
|
|
105
|
+
setState(ViewState.idle);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// 计算是否还有更多(函数级注释)
|
|
109
|
+
void _configHasMore(List<T> data) {
|
|
110
|
+
hasMore = data.length >= pageSize;
|
|
111
|
+
loadMoreFailed = false;
|
|
112
|
+
loadMoreError = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// 清空列表
|
|
116
|
+
void clearItems() {
|
|
117
|
+
items.clear();
|
|
118
|
+
page = 1;
|
|
119
|
+
hasMore = true;
|
|
120
|
+
notifyDataChange();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// 数据操作:添加(函数级注释)
|
|
124
|
+
void addItem(T item) {
|
|
125
|
+
items.add(item);
|
|
126
|
+
notifyDataChange();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// 数据操作:移除(函数级注释)
|
|
130
|
+
void removeItem(T item) {
|
|
131
|
+
items.remove(item);
|
|
132
|
+
notifyDataChange();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// 数据操作:更新(函数级注释)
|
|
136
|
+
void updateItem(int index, T item) {
|
|
137
|
+
if (index >= 0 && index < items.length) {
|
|
138
|
+
items[index] = item;
|
|
139
|
+
notifyDataChange();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// 批量操作:批量添加
|
|
144
|
+
void addItems(List<T> newItems) {
|
|
145
|
+
if (newItems.isEmpty) return;
|
|
146
|
+
items.addAll(newItems);
|
|
147
|
+
notifyDataChange();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// 批量操作:批量移除
|
|
151
|
+
void removeItems(List<T> targets) {
|
|
152
|
+
if (targets.isEmpty) return;
|
|
153
|
+
items.removeWhere((item) => targets.contains(item));
|
|
154
|
+
notifyDataChange();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// 批量操作:替换整个列表
|
|
158
|
+
void replaceItems(List<T> newItems) {
|
|
159
|
+
items
|
|
160
|
+
..clear()
|
|
161
|
+
..addAll(newItems);
|
|
162
|
+
notifyDataChange();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
|
|
3
|
+
import '../theme/status_views_theme.dart';
|
|
4
|
+
import 'base_viewmodel.dart';
|
|
5
|
+
|
|
6
|
+
/// Page 基类
|
|
7
|
+
///
|
|
8
|
+
/// 提供统一的页面结构和状态处理
|
|
9
|
+
///
|
|
10
|
+
/// 示例:
|
|
11
|
+
/// ```dart
|
|
12
|
+
/// class HomePage extends BasePage<HomeViewModel> {
|
|
13
|
+
/// const HomePage({super.key});
|
|
14
|
+
///
|
|
15
|
+
/// @override
|
|
16
|
+
/// State<HomePage> createState() => _HomePageState();
|
|
17
|
+
/// }
|
|
18
|
+
///
|
|
19
|
+
/// class _HomePageState extends BasePageState<HomeViewModel, HomePage> {
|
|
20
|
+
/// @override
|
|
21
|
+
/// HomeViewModel createViewModel() => HomeViewModel();
|
|
22
|
+
///
|
|
23
|
+
/// @override
|
|
24
|
+
/// String get title => '首页';
|
|
25
|
+
///
|
|
26
|
+
/// @override
|
|
27
|
+
/// Widget buildContent(BuildContext context) {
|
|
28
|
+
/// return Center(
|
|
29
|
+
/// child: Text('Counter: ${viewModel.counter}'),
|
|
30
|
+
/// );
|
|
31
|
+
/// }
|
|
32
|
+
/// }
|
|
33
|
+
/// ```
|
|
34
|
+
abstract class BasePage<VM extends BaseViewModel> extends StatefulWidget {
|
|
35
|
+
const BasePage({super.key});
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
State<BasePage<VM>> createState();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Page State 基类
|
|
42
|
+
abstract class BasePageState<VM extends BaseViewModel, T extends BasePage<VM>>
|
|
43
|
+
extends State<T> {
|
|
44
|
+
/// ViewModel 实例
|
|
45
|
+
late final VM viewModel;
|
|
46
|
+
|
|
47
|
+
// ==================== 子类需要实现的方法 ====================
|
|
48
|
+
|
|
49
|
+
/// 创建 ViewModel
|
|
50
|
+
VM createViewModel();
|
|
51
|
+
|
|
52
|
+
/// 构建内容区域
|
|
53
|
+
Widget buildContent(BuildContext context);
|
|
54
|
+
|
|
55
|
+
// ==================== 可选重写的 UI 配置 ====================
|
|
56
|
+
|
|
57
|
+
/// 是否显示 AppBar(默认 true)
|
|
58
|
+
bool get showAppBar => true;
|
|
59
|
+
|
|
60
|
+
/// AppBar 标题(默认空串)
|
|
61
|
+
String get title => '';
|
|
62
|
+
|
|
63
|
+
/// AppBar 操作按钮(默认 null)
|
|
64
|
+
List<Widget>? get actions => null;
|
|
65
|
+
|
|
66
|
+
/// 是否显示返回按钮(默认自动判断)
|
|
67
|
+
bool get showBackButton => true;
|
|
68
|
+
|
|
69
|
+
/// 背景颜色(默认 null)
|
|
70
|
+
Color? get backgroundColor => null;
|
|
71
|
+
|
|
72
|
+
/// 是否启用 SafeArea(默认 true)
|
|
73
|
+
bool get useSafeArea => true;
|
|
74
|
+
|
|
75
|
+
/// SafeArea 上边框(默认 true)
|
|
76
|
+
bool get safeAreaTop => true;
|
|
77
|
+
|
|
78
|
+
/// SafeArea 下边框(默认 true)
|
|
79
|
+
bool get safeAreaBottom => true;
|
|
80
|
+
|
|
81
|
+
// ==================== 生命周期 ====================
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
void initState() {
|
|
85
|
+
super.initState();
|
|
86
|
+
viewModel = createViewModel();
|
|
87
|
+
_setupListener();
|
|
88
|
+
viewModel.onInit();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// 设置监听器
|
|
92
|
+
void _setupListener() {
|
|
93
|
+
// 所有状态管理器都使用统一的 addListener 方式
|
|
94
|
+
viewModel.addListener(_onViewModelChanged);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
void dispose() {
|
|
99
|
+
viewModel.removeListener(_onViewModelChanged);
|
|
100
|
+
viewModel.dispose();
|
|
101
|
+
super.dispose();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// ViewModel 变化回调
|
|
105
|
+
void _onViewModelChanged() {
|
|
106
|
+
if (mounted) {
|
|
107
|
+
setState(() {});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ==================== UI 构建 ====================
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
Widget build(BuildContext context) {
|
|
115
|
+
Widget body = _buildBody();
|
|
116
|
+
body = buildSafeArea(child: body);
|
|
117
|
+
body = buildScaffold(body: body);
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// 构建 SafeArea
|
|
122
|
+
Widget buildSafeArea({required Widget child}) {
|
|
123
|
+
return useSafeArea
|
|
124
|
+
? SafeArea(
|
|
125
|
+
top: safeAreaTop,
|
|
126
|
+
bottom: safeAreaBottom,
|
|
127
|
+
child: child,
|
|
128
|
+
)
|
|
129
|
+
: child;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// 构建 AppBar
|
|
133
|
+
PreferredSizeWidget? buildAppBar() {
|
|
134
|
+
return AppBar(
|
|
135
|
+
title: Text(title),
|
|
136
|
+
actions: actions,
|
|
137
|
+
automaticallyImplyLeading: showBackButton,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// 构建 Scaffold
|
|
142
|
+
Widget buildScaffold({required Widget body}) {
|
|
143
|
+
return Scaffold(
|
|
144
|
+
appBar: showAppBar ? buildAppBar() : null,
|
|
145
|
+
backgroundColor: backgroundColor,
|
|
146
|
+
body: body,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// 构建 Body
|
|
151
|
+
Widget _buildBody() {
|
|
152
|
+
final theme = Theme.of(context).extension<StatusViewsTheme>();
|
|
153
|
+
|
|
154
|
+
if (viewModel.isLoading) {
|
|
155
|
+
final global = theme?.loadingBuilder?.call(context);
|
|
156
|
+
return global ?? buildLoading();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (viewModel.isError) {
|
|
160
|
+
final global = theme?.errorBuilder?.call(context, viewModel.errorMessage);
|
|
161
|
+
return global ?? buildError();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isEmptyContent) {
|
|
165
|
+
final global = theme?.emptyBuilder?.call(context);
|
|
166
|
+
return global ?? buildEmpty();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return buildContent(context);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ==================== 可重写的 UI 组件 ====================
|
|
173
|
+
|
|
174
|
+
/// 构建 Loading 视图
|
|
175
|
+
Widget buildLoading() {
|
|
176
|
+
return const Center(
|
|
177
|
+
child: CircularProgressIndicator(),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// 构建错误视图
|
|
182
|
+
Widget buildError() {
|
|
183
|
+
return Center(
|
|
184
|
+
child: Column(
|
|
185
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
186
|
+
children: [
|
|
187
|
+
const Icon(
|
|
188
|
+
Icons.error_outline,
|
|
189
|
+
size: 64,
|
|
190
|
+
color: Colors.red,
|
|
191
|
+
),
|
|
192
|
+
const SizedBox(height: 16),
|
|
193
|
+
Text(
|
|
194
|
+
viewModel.errorMessage ?? '加载失败',
|
|
195
|
+
style: const TextStyle(fontSize: 16),
|
|
196
|
+
),
|
|
197
|
+
const SizedBox(height: 24),
|
|
198
|
+
ElevatedButton.icon(
|
|
199
|
+
onPressed: viewModel.refreshData,
|
|
200
|
+
icon: const Icon(Icons.refresh),
|
|
201
|
+
label: const Text('重试'),
|
|
202
|
+
),
|
|
203
|
+
],
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// 构建空视图
|
|
209
|
+
Widget buildEmpty() {
|
|
210
|
+
return const Center(
|
|
211
|
+
child: Text('暂无数据'),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// 页面是否为空(默认 false,子类可重写)
|
|
216
|
+
bool get isEmptyContent => false;
|
|
217
|
+
|
|
218
|
+
/// 显示 SnackBar
|
|
219
|
+
void showSnackBar(String message, {Duration? duration}) {
|
|
220
|
+
if (!mounted) return;
|
|
221
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
222
|
+
SnackBar(
|
|
223
|
+
content: Text(message),
|
|
224
|
+
duration: duration ?? const Duration(seconds: 2),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// 显示 Loading Dialog
|
|
230
|
+
void showLoadingDialog({String? message}) {
|
|
231
|
+
showDialog(
|
|
232
|
+
context: context,
|
|
233
|
+
barrierDismissible: false,
|
|
234
|
+
builder: (context) => AlertDialog(
|
|
235
|
+
content: Row(
|
|
236
|
+
children: [
|
|
237
|
+
const CircularProgressIndicator(),
|
|
238
|
+
const SizedBox(width: 16),
|
|
239
|
+
Text(message ?? '加载中...'),
|
|
240
|
+
],
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// 隐藏 Loading Dialog
|
|
247
|
+
void hideLoadingDialog() {
|
|
248
|
+
if (mounted) {
|
|
249
|
+
Navigator.of(context).pop();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
|
|
3
|
+
enum ViewState { idle, loading, success, error }
|
|
4
|
+
|
|
5
|
+
class BaseViewModel extends ChangeNotifier {
|
|
6
|
+
ViewState _state = ViewState.idle;
|
|
7
|
+
String? _errorMessage;
|
|
8
|
+
bool _isRefreshing = false;
|
|
9
|
+
bool _disposed = false;
|
|
10
|
+
|
|
11
|
+
// ==================== Getters ====================
|
|
12
|
+
// 当前视图状态
|
|
13
|
+
ViewState get state => _state;
|
|
14
|
+
// 错误信息
|
|
15
|
+
String? get errorMessage => _errorMessage;
|
|
16
|
+
// 是否正在加载
|
|
17
|
+
bool get isLoading => _state == ViewState.loading;
|
|
18
|
+
// 是否加载失败
|
|
19
|
+
bool get isError => _state == ViewState.error;
|
|
20
|
+
// 是否加载成功
|
|
21
|
+
bool get isSuccess => _state == ViewState.success;
|
|
22
|
+
// 是否空闲状态
|
|
23
|
+
bool get isIdle => _state == ViewState.idle;
|
|
24
|
+
// 是否正在刷新
|
|
25
|
+
bool get isRefreshing => _isRefreshing;
|
|
26
|
+
// 是否已释放
|
|
27
|
+
bool get isDisposed => _disposed;
|
|
28
|
+
|
|
29
|
+
// ==================== 状态设置 ====================
|
|
30
|
+
|
|
31
|
+
/// 设置视图状态
|
|
32
|
+
void setState(ViewState state, {String? error}) {
|
|
33
|
+
if (_disposed) return;
|
|
34
|
+
_state = state;
|
|
35
|
+
_errorMessage = error;
|
|
36
|
+
notifyListeners();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// 设置刷新状态
|
|
40
|
+
void setRefreshing(bool refreshing) {
|
|
41
|
+
if (_disposed) return;
|
|
42
|
+
_isRefreshing = refreshing;
|
|
43
|
+
notifyListeners();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// 通知数据变化(仅用于局部刷新)
|
|
47
|
+
void notifyDataChange() {
|
|
48
|
+
if (_disposed) return;
|
|
49
|
+
notifyListeners();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ==================== 生命周期 ====================
|
|
53
|
+
/// 初始化钩子
|
|
54
|
+
Future<void> onInit() async {
|
|
55
|
+
// 子类可以重写此方法进行初始化
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// 刷新数据(钩子)
|
|
59
|
+
Future<void> refreshData() async {
|
|
60
|
+
// 子类可以重写此方法实现刷新逻辑
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
void dispose() {
|
|
65
|
+
_disposed = true;
|
|
66
|
+
super.dispose();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
|
|
3
|
+
import '../storage/storage_keys.dart';
|
|
4
|
+
import '../storage/storage_util.dart';
|
|
5
|
+
|
|
6
|
+
/// 应用常量
|
|
7
|
+
class AppConstants {
|
|
8
|
+
AppConstants._();
|
|
9
|
+
|
|
10
|
+
static const String appName = '火叶';
|
|
11
|
+
static const String appVersion = '1.0.0';
|
|
12
|
+
|
|
13
|
+
/// 免责声明
|
|
14
|
+
static const String disclaimer =
|
|
15
|
+
'本应用仅供学习交流使用,API 数据由第三方提供,本应用不保证数据的准确性、完整性及安全性。如有侵权,请联系删除。';
|
|
16
|
+
|
|
17
|
+
/// 国内可直接访问的 API 地址 (示例使用 图虫API)
|
|
18
|
+
static const String apiBaseUrl = 'https://api.tuchong.com';
|
|
19
|
+
|
|
20
|
+
static const int apiTimeout = 30000;
|
|
21
|
+
static const int pageSize = 20;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// 应用配置
|
|
25
|
+
///
|
|
26
|
+
/// 统一管理应用配置和生命周期状态
|
|
27
|
+
///
|
|
28
|
+
/// 使用示例:
|
|
29
|
+
/// ```dart
|
|
30
|
+
/// void main() async {
|
|
31
|
+
/// WidgetsFlutterBinding.ensureInitialized();
|
|
32
|
+
/// await AppConfig.init();
|
|
33
|
+
/// runApp(const App());
|
|
34
|
+
/// }
|
|
35
|
+
/// ```
|
|
36
|
+
class AppConfig {
|
|
37
|
+
static AppConfig? _instance;
|
|
38
|
+
static AppConfig get I => _instance!;
|
|
39
|
+
|
|
40
|
+
// ==================== 配置 ====================
|
|
41
|
+
|
|
42
|
+
/// API 基础地址
|
|
43
|
+
final String apiBaseUrl;
|
|
44
|
+
|
|
45
|
+
/// 连接超时时间
|
|
46
|
+
final Duration connectTimeout;
|
|
47
|
+
|
|
48
|
+
/// 接收超时时间
|
|
49
|
+
final Duration receiveTimeout;
|
|
50
|
+
|
|
51
|
+
/// 是否启用日志
|
|
52
|
+
final bool enableLog;
|
|
53
|
+
|
|
54
|
+
/// 是否使用 Mock 数据 (用于网络示例演示)
|
|
55
|
+
final bool useMockData;
|
|
56
|
+
|
|
57
|
+
// ==================== 生命周期状态 ====================
|
|
58
|
+
|
|
59
|
+
/// 是否首次启动
|
|
60
|
+
final bool isFirstLaunch;
|
|
61
|
+
|
|
62
|
+
/// 是否更新后首次启动
|
|
63
|
+
final bool isFirstLaunchAfterUpdate;
|
|
64
|
+
|
|
65
|
+
/// 当前版本号
|
|
66
|
+
final String currentVersion;
|
|
67
|
+
|
|
68
|
+
/// 上一个版本号
|
|
69
|
+
final String previousVersion;
|
|
70
|
+
|
|
71
|
+
AppConfig._({
|
|
72
|
+
required this.apiBaseUrl,
|
|
73
|
+
required this.connectTimeout,
|
|
74
|
+
required this.receiveTimeout,
|
|
75
|
+
required this.enableLog,
|
|
76
|
+
required this.useMockData,
|
|
77
|
+
required this.isFirstLaunch,
|
|
78
|
+
required this.isFirstLaunchAfterUpdate,
|
|
79
|
+
required this.currentVersion,
|
|
80
|
+
required this.previousVersion,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/// 初始化应用配置
|
|
84
|
+
///
|
|
85
|
+
/// 必须在应用启动时调用(runApp 之前)
|
|
86
|
+
static Future<void> init({
|
|
87
|
+
String? apiBaseUrl,
|
|
88
|
+
Duration connectTimeout =
|
|
89
|
+
const Duration(milliseconds: AppConstants.apiTimeout),
|
|
90
|
+
Duration receiveTimeout =
|
|
91
|
+
const Duration(milliseconds: AppConstants.apiTimeout),
|
|
92
|
+
bool? enableLog,
|
|
93
|
+
bool useMockData = false,
|
|
94
|
+
}) async {
|
|
95
|
+
// 1. 初始化存储
|
|
96
|
+
await StorageUtil.init();
|
|
97
|
+
|
|
98
|
+
// 2. 获取版本信息
|
|
99
|
+
const currentVersion = AppConstants.appVersion;
|
|
100
|
+
final previousVersion = StorageUtil.getString(StorageKeys.appVersion) ?? '';
|
|
101
|
+
|
|
102
|
+
// 3. 判断启动状态
|
|
103
|
+
final isFirstLaunch = !StorageUtil.containsKey(StorageKeys.isFirstLaunch);
|
|
104
|
+
final isFirstLaunchAfterUpdate =
|
|
105
|
+
previousVersion.isNotEmpty && previousVersion != currentVersion;
|
|
106
|
+
|
|
107
|
+
// 4. 更新存储
|
|
108
|
+
if (isFirstLaunch) {
|
|
109
|
+
await StorageUtil.setBool(StorageKeys.isFirstLaunch, false);
|
|
110
|
+
}
|
|
111
|
+
await StorageUtil.setString(StorageKeys.appVersion, currentVersion);
|
|
112
|
+
|
|
113
|
+
// 5. 创建实例
|
|
114
|
+
_instance = AppConfig._(
|
|
115
|
+
apiBaseUrl: apiBaseUrl ?? AppConstants.apiBaseUrl,
|
|
116
|
+
connectTimeout: connectTimeout,
|
|
117
|
+
receiveTimeout: receiveTimeout,
|
|
118
|
+
enableLog: enableLog ?? kDebugMode,
|
|
119
|
+
useMockData: useMockData,
|
|
120
|
+
isFirstLaunch: isFirstLaunch,
|
|
121
|
+
isFirstLaunchAfterUpdate: isFirstLaunchAfterUpdate,
|
|
122
|
+
currentVersion: currentVersion,
|
|
123
|
+
previousVersion: previousVersion,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
_printInitLog();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static void _printInitLog() {
|
|
130
|
+
if (!_instance!.enableLog) return;
|
|
131
|
+
debugPrint('┌─────────────────────────────────────');
|
|
132
|
+
debugPrint('│ AppConfig 初始化完成');
|
|
133
|
+
debugPrint('│ API: ${_instance!.apiBaseUrl}');
|
|
134
|
+
debugPrint('│ 版本: ${_instance!.currentVersion}');
|
|
135
|
+
debugPrint('│ 首次启动: ${_instance!.isFirstLaunch}');
|
|
136
|
+
debugPrint('│ 更新后首次: ${_instance!.isFirstLaunchAfterUpdate}');
|
|
137
|
+
debugPrint('└─────────────────────────────────────');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// 是否已初始化
|
|
141
|
+
static bool get isInitialized => _instance != null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
{{#if_network}}
|
|
4
|
+
import '../network/app_http.dart';
|
|
5
|
+
import '../network/interceptors/index.dart';
|
|
6
|
+
import '../network/response_adapter.dart';
|
|
7
|
+
{{/if_network}}
|
|
8
|
+
import 'app_config.dart';
|
|
9
|
+
/// 应用初始化器
|
|
10
|
+
///
|
|
11
|
+
/// 统一管理应用启动前的所有初始化逻辑
|
|
12
|
+
///
|
|
13
|
+
/// 使用示例:
|
|
14
|
+
/// ```dart
|
|
15
|
+
/// void main() async {
|
|
16
|
+
/// WidgetsFlutterBinding.ensureInitialized();
|
|
17
|
+
/// await AppInitializer.init();
|
|
18
|
+
/// runApp(const App());
|
|
19
|
+
/// }
|
|
20
|
+
/// ```
|
|
21
|
+
class AppInitializer {
|
|
22
|
+
AppInitializer._();
|
|
23
|
+
|
|
24
|
+
/// 初始化应用
|
|
25
|
+
static Future<void> init({
|
|
26
|
+
String? apiBaseUrl,
|
|
27
|
+
bool? enableLog,
|
|
28
|
+
bool useMockData = false,
|
|
29
|
+
}) async {
|
|
30
|
+
// 1. 初始化应用配置
|
|
31
|
+
await AppConfig.init(
|
|
32
|
+
apiBaseUrl: apiBaseUrl,
|
|
33
|
+
enableLog: enableLog,
|
|
34
|
+
useMockData: useMockData,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
{{#if_network}}
|
|
38
|
+
// 2. 初始化网络层
|
|
39
|
+
_initNetwork();
|
|
40
|
+
{{/if_network}}
|
|
41
|
+
|
|
42
|
+
// 3. 其他初始化 (埋点、推送等) - 可根据需要添加
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
{{#if_network}}
|
|
46
|
+
/// 初始化网络层配置
|
|
47
|
+
static void _initNetwork() {
|
|
48
|
+
AppHttp.init(
|
|
49
|
+
// 使用图虫 API 适配器 {result, message, feedList}
|
|
50
|
+
adapter: TuChongAdapter(),
|
|
51
|
+
|
|
52
|
+
// 自动显示错误弹窗 (默认 true)
|
|
53
|
+
autoShowError: true,
|
|
54
|
+
|
|
55
|
+
interceptors: [
|
|
56
|
+
// Token 自动注入
|
|
57
|
+
AuthInterceptor(
|
|
58
|
+
getToken: () {
|
|
59
|
+
// 从本地存储获取 token
|
|
60
|
+
// return StorageUtil.getString('token') ?? '';
|
|
61
|
+
return '';
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
// 请求重试
|
|
65
|
+
RetryInterceptor(
|
|
66
|
+
dio: AppHttp().dio,
|
|
67
|
+
maxRetries: 3,
|
|
68
|
+
retryDelay: const Duration(seconds: 1),
|
|
69
|
+
),
|
|
70
|
+
],
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
{{/if_network}}
|
|
74
|
+
}
|