Flutter 封装:最佳实践 —— 数据库 Isar 极简封装

一、需求来源

最近工作中需要快速获取及更新大量数据(千条级别)缓存,还要考虑弱网情况,经过调查,初步确定使用 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) {

github

相关推荐
吃杠碰小鸡33 分钟前
commitlint校验git提交信息
前端
虾球xz1 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇1 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐2 小时前
前端图像处理(一)
前端
程序猿阿伟2 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒2 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪2 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背2 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript