一、需求来源
最近工作中需要快速获取及更新大量数据(千条级别)缓存,还要考虑弱网情况,经过调查,初步确定使用 isar 数据库。好处是
- 1、社区活跃。
- 2、文档非常完善。
- 3、现有代码改动少。
用 isar 实现了一个事项选择列表和学生选择列表,效果相同如下:
二、使用示例
1、Provider 使用示例
ini
/// DBGenericController<DBTodo> 示例
class TodoListPage extends StatefulWidget {
TodoListPage({
super.key,
this.title,
});
final String? title;
@override
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> with DBDialogMxin {
final titleController = TextEditingController();
bool isAllChoic = false;
final provider = Get.put(DBGenericController<DBTodo>());
@override
Widget build(BuildContext context) {
final automaticallyImplyLeading = Get.currentRoute.toLowerCase() == "/$widget".toLowerCase();
return Scaffold(
backgroundColor: Colors.black12,
appBar: AppBar(
title: Text("$widget"),
automaticallyImplyLeading: automaticallyImplyLeading,
actions: [
IconButton(
onPressed: onAddItemRandom,
icon: Icon(Icons.add)
),
],
),
body: GetBuilder<DBGenericController<DBTodo>>(
builder: (value) {
final checkedItems = value.entitys.where((e) => e.isFinished == true).toList();
isAllChoic = value.entitys.firstWhereOrNull((e) => e.isFinished == false) == null;
final checkIcon = isAllChoic ? Icons.check_box : Icons.check_box_outline_blank;
final checkDesc = "已选择 ${checkedItems.length}/${value.entitys.length}";
Widget content = NPlaceholder(
onTap: (){
provider.update();
},
);
if (value.entitys.isNotEmpty) {
content = buildRefresh(
onRefresh: (){
provider.update();
},
child: ListView.builder(
padding: EdgeInsets.all(10),
itemCount: value.entitys.length,
itemBuilder: (context, index) {
final model = value.entitys.reversed.toList()[index];
onToggle(){
model.isFinished = !model.isFinished;
provider.put(model);
}
return InkWell(
onTap: onToggle,
child: TodoItem(
model: model,
onToggle: onToggle,
onEdit: (){
titleController.text = model.title;
presentDialog(
controller: titleController,
onSure: (val){
model.title = val;
provider.put(model);
}
);
},
onDelete: () {
provider.delete(model.id);
},
),
);
}
),
);
}
return Column(
children: [
Expanded(
child: content,
),
NChoicBottomBar(
checkIcon: checkIcon,
checkDesc: checkDesc,
onCheck: () async {
ddlog("isAllChoic TextButton: $isAllChoic");
for (var i = 0; i < value.entitys.length; i++) {
final e = value.entitys[i];
e.isFinished = !isAllChoic;
}
provider.putAll(value.entitys);
},
onAdd: onAddItemRandom,
onDelete: () async {
final choicItems = value.entitys.where((e) => e.isFinished).map((e) => e.id).toList();
await provider.deleteAll(choicItems);
},
),
],
);
},
),
);
}
Widget buildRefresh({
EasyRefreshController? controller,
FutureOr Function()? onRefresh,
FutureOr Function()? onLoad,
required Widget? child,
}) {
return EasyRefresh(
// refreshOnStart: true,
canRefreshAfterNoMore: true,
canLoadAfterNoMore: true,
controller: controller,
onRefresh: onRefresh,
onLoad: onLoad,
child: child,
);
}
onAddItemRandom() {
titleController.text = "项目${IntExt.random(max: 999)}";
addTodoItem(title: titleController.text);
}
addTodoItem({required String title}) {
if (title.isEmpty) {
return;
}
var todo = DBTodo(
title: title,
isFinished: false,
createdDate: DateTime.now().toIso8601String(),
);
provider.put(todo);
}
}
2、Getx 使用示例
ini
/// DBGenericProvider<DBTodo> 示例
class TodoListPageOne extends StatefulWidget {
TodoListPageOne({
super.key,
this.title
});
final String? title;
@override
State<TodoListPageOne> createState() => _TodoListPageOneState();
}
class _TodoListPageOneState extends State<TodoListPageOne> with DBDialogMxin {
final titleController = TextEditingController();
bool isAllChoic = false;
DBGenericProvider<DBTodo> get provider => Provider.of<DBGenericProvider<DBTodo>>(context, listen: false);
@override
Widget build(BuildContext context) {
final automaticallyImplyLeading = Get.currentRoute.toLowerCase() == "/$widget".toLowerCase();
return Scaffold(
backgroundColor: Colors.black12,
appBar: AppBar(
title: Text("$widget"),
automaticallyImplyLeading: automaticallyImplyLeading,
actions: [
IconButton(
onPressed: onAddItemRandom,
icon: Icon(Icons.add)
),
],
),
body: Consumer<DBGenericProvider<DBTodo>>(
builder: (context, value, child) {
final checkedItems = value.entitys.where((e) => e.isFinished == true).toList();
isAllChoic = value.entitys.firstWhereOrNull((e) => e.isFinished == false) == null;
final checkIcon = isAllChoic ? Icons.check_box : Icons.check_box_outline_blank;
final checkDesc = "已选择 ${checkedItems.length}/${value.entitys.length}";
Widget content = NPlaceholder(
onTap: (){
provider.update();
},
);
if (value.entitys.isNotEmpty) {
content = buildRefresh(
onRefresh: (){
provider.update();
},
child: ListView.builder(
padding: EdgeInsets.all(10),
itemCount: value.entitys.length,
itemBuilder: (context, index) {
final model = value.entitys.reversed.toList()[index];
onToggle(){
model.isFinished = !model.isFinished;
provider.put(model);
}
return InkWell(
onTap: onToggle,
child: TodoItem(
model: model,
onToggle: onToggle,
onEdit: (){
titleController.text = model.title;
presentDialog(
controller: titleController,
onSure: (val){
model.title = val;
provider.put(model);
}
);
},
onDelete: () {
provider.delete(model.id);
},
),
);
}
),
);
}
return Column(
children: [
Expanded(
child: content,
),
NChoicBottomBar(
checkIcon: checkIcon,
checkDesc: checkDesc,
onCheck: () async {
ddlog("isAllChoic TextButton: $isAllChoic");
for (var i = 0; i < value.entitys.length; i++) {
final e = value.entitys[i];
e.isFinished = !isAllChoic;
}
provider.putAll(value.entitys);
},
onAdd: onAddItemRandom,
onDelete: () async {
final choicItems = value.entitys.where((e) => e.isFinished).map((e) => e.id).toList();
await provider.deleteAll(choicItems);
},
),
],
);
},
),
);
}
Widget buildRefresh({
EasyRefreshController? controller,
FutureOr Function()? onRefresh,
FutureOr Function()? onLoad,
required Widget? child,
}) {
return EasyRefresh(
// refreshOnStart: true,
canRefreshAfterNoMore: true,
canLoadAfterNoMore: true,
controller: controller,
onRefresh: onRefresh,
onLoad: onLoad,
child: child,
);
}
onAddItemRandom() {
titleController.text = "项目${IntExt.random(max: 999)}";
addTodoItem(title: titleController.text);
}
addTodoItem({required String title}) {
if (title.isEmpty) {
return;
}
var todo = DBTodo(
title: title,
isFinished: false,
createdDate: DateTime.now().toIso8601String(),
);
provider.put(todo);
}
}
三、源码
1、DBManager数据库管理类 - isar 封装,
swift
/// 数据库管理类
class DBManager {
DBManager._(){
init();
}
static final DBManager _instance = DBManager._();
factory DBManager() => _instance;
static DBManager get instance => _instance;
late Isar isar;
Future<void> init() async {
isar = await openDB(schemas: [
DBTodoSchema,
DBStudentSchema,
]);
}
Future<Isar> openDB({required List<CollectionSchema<dynamic>> schemas,}) async {
final dir = await getApplicationDocumentsDirectory();
final result = await Isar.open(
schemas,
directory: dir.path,
inspector: true,
);
return result;
}
}
2、Provider 特定模型的数据提供类
scss
class DBGenericProvider<E> extends ChangeNotifier {
DBGenericProvider() {
init();
}
final isar = DBManager().isar;
final List<E> _entitys = <E>[];
List<E> get entitys => _entitys;
Future<void> init() async {
isar.txn(() async {
await update();
});
}
/// 查
Future<void> update() async {
final items = await isar.collection<E>().where().findAll();
_entitys.clear();
_entitys.addAll(items);
notifyListeners();
}
/// 增/改
Future<void> putAll(List<E> list) async {
await isar.writeTxn(() async {
await isar.collection<E>().putAll(list);
await update();
});
}
/// 增/改
Future<void> put(E e) async {
await putAll([e]);
}
/// 删
Future<void> deleteAll(List<Id> ids) async {
await isar.writeTxn(() async {
final count = await isar.collection<E>().deleteAll(ids);
debugPrint('$this deleted $count');
await update();
});
}
/// 删
Future<void> delete(Id id) async {
await deleteAll([id]);
}
}
3、Getx特定模型的数据提供类 - GetxController 为例
scss
class DBGenericController<E> extends GetxController {
DBGenericController() {
init();
}
final isar = DBManager().isar;
final List<E> _entitys = <E>[];
List<E> get entitys => _entitys;
Future<void> init() async {
isar.txn(() async {
await update();
});
}
/// 查
@override
Future<void> update([List<Object>? ids, bool condition = true]) async {
if (!Get.isRegistered<DBGenericController<E>>()) {
return;
}
final items = await isar.collection<E>().where().findAll();
_entitys.clear();
_entitys.addAll(items);
super.update(ids, condition);
}
/// 增/改
Future<void> putAll(List<E> list) async {
await isar.writeTxn(() async {
await isar.collection<E>().putAll(list);
await update();
});
}
/// 增/改
Future<void> put(E e) async {
await putAll([e]);
}
/// 删
Future<void> deleteAll(List<Id> ids) async {
await isar.writeTxn(() async {
final count = await isar.collection<E>().deleteAll(ids);
debugPrint('$this deleted $count');
await update();
});
}
/// 删
Future<void> delete(Id id) async {
await deleteAll([id]);
}
}
四、总结
1、使用 DBGenericProvider 需要在 main 函数提前声明
scss
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => DBGenericProvider<DBTodo>()),
ChangeNotifierProvider(create: (context) => DBGenericProvider<DBStudent>()),
ChangeNotifierProvider(create: (context) => DBGenericProvider<DBOrder>()),
],
child: MyApp(),
),
);
}
2、封装思路是通过泛型声明模型实体
- provider 仅需要一个 DBGenericProvider 数据提供类即可,无论多少种模型;
- GetxController 仅需要一个 DBGenericController 数据提供类即可,无论多少种模型;
极大的减少相似代码;
3、此种封装方法好处是仅需要在开始设置一下泛型类型,后续方法都不需要再次声明泛型类型,减少出错的可能
Getx - GetxController
ini
DBGenericProvider<DBTodo> get provider => Provider.of<DBGenericProvider<DBTodo>>(context, listen: false);
swift
Consumer<DBGenericProvider<DBTodo>>(
builder: (context, value, child) {
provider - ChangeNotifier:
less
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => DBGenericProvider<DBTodo>()),
],
child: MyApp(),
),
);
}
ini
final provider = Get.put(DBGenericController<DBTodo>());
swift
GetBuilder<DBGenericController<DBTodo>>(
builder: (value) {