数据层设计
- 数据源抽象:
- 定义统一的数据源接口,如
BaseDataSource
,包含异步获取分页数据的方法,例如 Future<List<T>> fetchPage(int pageIndex, int pageSize)
,其中 T
是泛型,代表不同类型的数据。
- 针对不同类型数据,实现具体的数据源类,如
TypeADataSource
、TypeBDataSource
等,继承自 BaseDataSource
并实现其方法。这些数据源类负责与后端 API 交互获取数据,或者从本地缓存读取数据。
- 本地缓存:
- 使用如
shared_preferences
或 sqflite
进行简单数据或复杂关系型数据的缓存。
- 例如,对于简单的分页列表数据,可使用
shared_preferences
缓存 JSON 格式的数据;对于存在关联关系的复杂数据,使用 sqflite
数据库进行存储。
- 缓存时,为每个类型数据设置对应的缓存键值,便于后续读取和更新。同时记录缓存的版本号,用于判断数据是否过期。
- 数据一致性处理:
- 在数据更新时,无论是从网络获取新数据还是本地数据发生变化,都要同时更新缓存和相关联数据。
- 例如,若类型 A 数据更新后包含对类型 B 数据的引用变化,不仅要更新类型 A 数据的缓存,还要检查并更新类型 B 数据的缓存,确保数据一致性。
业务逻辑层设计
- 数据仓库:
- 创建数据仓库类,如
DataRepository
,它聚合多个数据源,提供统一的数据访问入口。
- 数据仓库类内部维护不同类型数据的缓存状态,并根据缓存和网络情况决定是从缓存读取还是从网络获取数据。例如:
class DataRepository {
final BaseDataSource typeADataSource;
final BaseDataSource typeBDataSource;
DataRepository({
required this.typeADataSource,
required this.typeBDataSource,
});
Future<List<TypeA>> getTypeAPage(int pageIndex, int pageSize) async {
// 检查缓存
final cachedData = await _getCachedTypeA(pageIndex, pageSize);
if (cachedData!= null) {
return cachedData;
}
// 从网络获取
final newData = await typeADataSource.fetchPage(pageIndex, pageSize);
// 更新缓存
await _cacheTypeA(newData, pageIndex, pageSize);
return newData;
}
// 其他类似方法
}
- 关联数据处理:
- 当获取一种类型数据时,如果需要关联其他类型数据,在业务逻辑层进行处理。
- 例如,类型 A 数据中包含类型 B 的 ID,在获取类型 A 数据后,根据这些 ID 调用
DataRepository
中获取类型 B 数据的方法,然后将关联数据组装好提供给界面层。
- 异步任务管理:
- 使用
Future
和 async/await
处理异步操作,确保代码的可读性和顺序执行性。
- 对于可能并发执行的多个异步任务,如同时获取多种类型数据的分页,可以使用
Future.wait
来等待所有任务完成,并处理可能出现的错误。例如:
Future<void> fetchAllData() async {
try {
await Future.wait([
dataRepository.getTypeAPage(0, 10),
dataRepository.getTypeBPage(0, 10),
]);
} catch (e) {
// 处理错误
}
}
界面层设计
- 状态管理:
- 使用状态管理库,如
provider
或 mobx
来管理界面状态。
- 创建状态类,例如
PageDataState
,包含不同类型数据的列表、加载状态(如加载中、加载完成、加载失败)等属性。
- 在界面中通过状态管理机制监听状态变化,当数据加载完成时更新 UI 显示。
- 分页加载实现:
- 在列表组件中,如
ListView
或 GridView
,使用 ScrollController
监听滚动事件。
- 当滚动到列表底部一定距离时,触发加载下一页数据的操作,调用业务逻辑层的获取下一页数据方法,并更新状态管理中的数据列表。
- 例如:
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final ScrollController _scrollController = ScrollController();
late PageDataState _pageDataState;
@override
void initState() {
super.initState();
_pageDataState = Provider.of<PageDataState>(context, listen: false);
_scrollController.addListener(_scrollListener);
_fetchInitialData();
}
void _fetchInitialData() async {
await _pageDataState.fetchTypeAPage(0, 10);
await _pageDataState.fetchTypeBPage(0, 10);
}
void _scrollListener() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_pageDataState.fetchNextTypeAPage();
_pageDataState.fetchNextTypeBPage();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemCount: _pageDataState.typeAList.length,
itemBuilder: (context, index) {
// 构建列表项
},
),
);
}
}
并发问题和数据冲突处理
- 并发问题处理:
- 使用
Isolate
进行多线程处理,但要注意 Flutter 中 UI 操作必须在主线程进行。对于一些耗时的计算任务,如数据预处理等,可以放在 Isolate
中执行。
- 利用
Semaphore
来控制并发访问资源的数量。例如,若 API 有并发请求限制,可以使用 Semaphore
来确保同时请求的数量不超过限制。
- 数据冲突处理:
- 在缓存更新时,使用乐观锁或悲观锁机制。例如,悲观锁可以在读取缓存数据时锁定数据,直到更新完成后再解锁,防止其他操作在更新过程中读取到不一致的数据。
- 对于网络数据和本地缓存数据的冲突,在从网络获取数据后,与本地缓存数据进行对比,根据业务规则决定是使用网络数据覆盖本地数据,还是保留本地数据并提示用户有新数据可更新。例如,如果网络数据版本号高于本地缓存数据版本号,则更新本地缓存。