用GetX写了一个待办事项

作者: A_si | 来源:发表于2020-12-13 03:16 被阅读0次

    在使用了 Provider 一年后,遇到了很多阻力,期间尝试过 BLoC 、MobX ,均不如意,一个样本代码太多,使用复杂,一个生产代码要等很久。难道 Flutter 就没有诸如原生 Android 的 jetpack 套装一样方便的套件吗?后来开始尝试 GetX,才发现真香,正如作者所说:

    GetX是Flutter的超轻便且强大的解决方案。它以快速实用的方式结合了高性能状态管理,智能依赖性注入和路由管理。

    我写了一个demo探索过了基本使用方式之后,又决定写一个 待办清单app 实践一下 Clean Architecture 。

    首先感谢下鸿洋大佬的 todo api,第一版是利用 api 开发的一个在线应用,后来在不注册的情况下,加入 moor 数据库,可以离线使用。这一部分改的仓促,下一期迭代会改进。

    项目依赖和结构

    dependencies:
      flutter:
        sdk: flutter
      cookie_jar: ^1.0.1
      cupertino_icons: ^1.0.0
      date_format: ^1.0.9
      dio: ^3.0.10
      dio_cookie_manager: ^1.0.0
      dio_http_cache: ^0.2.11
      flutter_slidable: ^0.5.7
      get: ^3.21.2
      google_fonts: ^1.1.1
      moor: ^3.4.0
      path: ^1.7.0
      path_provider: ^1.6.24
      pull_to_refresh: ^1.6.3
      shared_preferences: ^0.5.12+4
      table_calendar: ^2.3.1
    

    项目网络模块封装了 dio,因为是 带 cookie 的 请求,所以加入了 cookie 和本地化,算是一个比较完善的请求模块。

    数据库选用了 moor ,Android 中 room 的字母倒过来就是这个,和 room 一样可以响应式,十分优秀。

    剩下的第三方包就是分页和侧滑控件,还有一个日历包。

    整体项目的结构参考getx_pattern,又按照自己的习惯做了修改。

    getx_pattern

    从 GetX 开始开发

    使用 GetX

    void main() async {
      runApp(GetMaterialApp(
        debugShowCheckedModeBanner: false,
        initialRoute: '/',
        builder: (context, child) => Scaffold(
          // Global GestureDetector that will dismiss the keyboard
          body: GestureDetector(
            onTap: () {
              hideKeyboard(context);
            },
            child: child,
          ),
        ),
        theme: appThemeData,
        defaultTransition: Transition.fade,
        getPages: AppPages.pages,
        initialBinding: SplashBinding(),
        home: SplashPage(),
      ));
    }
    

    命名路由

    要使用完整的路由功能,需要把 MaterialApp 替换为 GetMaterialApp ,中间加入的builder 是为了解决点击空白处隐藏键盘的需求,这个在原生也很常见。

      static final pages = [
        GetPage(
          name: Routes.LOGIN,
          page: () => LoginPage(),
          binding: LoginPageBinding(),
        ),
        GetPage(
          name: Routes.SPLASH,
          page: () => SplashPage(),
          binding: SplashBinding(),
        ),
        GetPage(
          name: Routes.SIGN_UP,
          page: () => SignUpPage(),
          binding: SiginUpBinding(),
        ),
        GetPage(
          name: Routes.TASK,
          page: () => TaskPage(),
          binding: TaskBinding(),
        ),
        GetPage(
          name: Routes.TASK_ADD,
          page: () => AddTaskPage(),
          binding: AddTaskBinding(),
        ),
        GetPage(
          name: Routes.TASK_DETAILS,
          page: () => TaskDetailsPage(),
        ),
        GetPage(
          name: Routes.TASK_EDIT,
          page: () => EditTaskPage(),
          binding: EditTaskBinding(),
        ),
        GetPage(
          name: Routes.TASK_MOTHLY,
          page: () => MonthlyPage(),
          binding: MonthlyBinding(),
        ),
        GetPage(
          name: Routes.PROFILE,
          page: () => ProfilePage(),
        ),
      ];
    }
    

    习惯了使用命名路由,所以定义了路由表。binding是 GetX 中我特别喜欢的功能——依赖注入,就像原生的 Hilt 一样,让代码结构无侵分层。并且如果使用的是流或计时器,它们将自动关闭,开发者根据不用担心。Binding 类是一个解耦依赖注入的类,在路由的时候使用。就可以知道注入的作用域,以及知道在何处以及如何处置注入的对象。

    登录

    api 是玩安卓的开放 api,登录要使用 api 和 repository,所以依赖注入的形式注入:

    class LoginPageBinding implements Bindings {
      @override
      void dependencies() {
        Get.lazyPut(() => LoginApi());
        Get.lazyPut(() => LoginRepository());
        Get.lazyPut<LoginController>(
          () => LoginController(),
        );
      }
    }
    

    在使用的时候直接 find

      final LoginRepository repository = Get.find<LoginRepository>();
    

    Get.put()是最常见的注入依赖的方法,它是直接注入到内存里。你可以在任何地方找到注入的对象,这是 Provider 所没有的功能。

    仅有put还不够,GetX 还提供另外一个方法,Get.lazyPut可以懒加载一个依赖,这样它只有在使用时才会被实例化。这对于计算代价高的类来说非常有用,或者如果你想在一个地方实例化几个类(比如在 Bindings 类中),但是不知道会不会使用到,那懒加载是正确的选择,是不是很像 kotlin 的 lazy。

    显示密码的功能暂时未加。


    登录

    在欢迎页会注入全局的依赖,然后判断是否登录,对应不同的导航:

      @override
      void onReady() async {
        super.onReady();
        await GloabConfig.init();
        await DenpendencyInjection.init();
        LoginProvider loginProvider = Get.find<LoginProvider>();
        print(loginProvider);
        // 如果未登录就登录
        // 如果已登录就去task页面
        if (loginProvider.isLogin()) {
          Get.offNamed(Routes.TASK);
        } else {
          Get.offNamed(Routes.LOGIN);
        }
      }
    }
    

    Task 列表

    task

    主页实现了底部导航和嵌入式FloatingActionButtonLocation,没有任务的时候会弹出使用引导。点击加号可以添加任务。因为 api 是分页的,所以也做了分页处理。

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('My Task')),
          body: Body(),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              Get.toNamed(Routes.TASK_ADD);
            },
            child: Icon(Icons.add),
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
          bottomNavigationBar: BottomAppBar(
            shape: CircularNotchedRectangle(),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                IconButton(
                  icon: Icon(Icons.calendar_today_sharp),
                  onPressed: () {
                    Get.toNamed(Routes.TASK_MOTHLY);
                  },
                ),
                IconButton(
                  icon: Icon(Icons.settings),
                  onPressed: () {
                    Get.toNamed(Routes.PROFILE);
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    

    任务 item可以点击进入详情和侧滑,有两个侧滑菜单,编辑和删除,对应不同的功能,圆形的checkbox可以完成任务,任务标题和时间在完成时会有删除线。

    GetView 就是封装的StatelessWidget,内部有一个 get方法便捷的获取注入的controller,这样连获取的步骤都能省略。

    增加和编辑

    编辑 添加

    对应的标题是必须项,描述可以为空,时间是默认当前,优先级有高低中三个,默认是中。

    选择日期会弹出日历你,采用局部刷新,提高性能,update([updateDateId])函数的参数是一个 id,只会刷新对应 id 的 GetBuilder,并且 GetX 不受 InheritedWidget的限制,所以可以在任意地方引用未被内存回收的 Controller,所以可以在编辑页面,让列表页也同时刷新。

    日历
      void handleDatePicker() async {
        final datePick = await showDatePicker(
            context: Get.context,
            firstDate: DateTime(2000),
            initialDate: _dateTime,
            lastDate: DateTime(2100));
        if (datePick != null && datePick != _dateTime) {
          _dateTime = datePick;
          task.dateStr = _dateTime.format();
          dateTimeController.text = task.dateStr;
          update([updateDateId]);
        }
      }
    
      void submit() async {
        if (formKey.currentState.validate()) {
          formKey.currentState.save();
          try {
            Get.loading();
            await _taskRepository.updateTask(task);
            Get.dismiss();
            // 刷新列表页
            Get.find<TaskController>().update();
            // controller.updateTask(task);
            Get.back();
          } catch (e) {
            print(e);
            Get.dismiss();
            Get.snackbar('Error', e.toString());
          }
        }
      }
    

    月份视图

    月份视图

    月份视图用了table_calendar包,这个包功能强大,可以定制日历视图。默认显示两周,点击月份展开四周的月份视图。可以按日期筛选出任务。这里的任务可以点击进入详情和点击checkbox更改状态。

    TableCalendar(
              onDaySelected: (DateTime day, _, __) {
                controller.selectedDate(day);
              },
              calendarController: controller.calendarController,
              startingDayOfWeek: StartingDayOfWeek.monday,
              initialCalendarFormat: CalendarFormat.week,
              calendarStyle: CalendarStyle(
                selectedColor: Theme.of(context).accentColor,
              ),
            )
    

    这里更改状态后,同样可以拿到列表页的Controller去更新列表页:

    modifyTaskStatus(Task task) async {
        try {
          TaskController taskController = Get.find<TaskController>();
          await taskController.modifyTaskStatus(task);
        } catch (e) {}
        update();
      }
    

    个人中心

    个人中心

    个人中心是一个静态页面,最下面展示了我写的 GetX 的 demo 截图。点击放大的功能放在迭代里做吧。

    这里藏有福利,一个漂亮的二次元萌妹子。

    扩展函数

    utils文件夹下写了两个扩展函数,扩展了日期格式化和基于 GetX 的全局加载框。

    extension DateExtension on DateTime {
      String format() {
        return formatDate(this, [
          yyyy,
          '-',
          mm,
          '-',
          dd,
        ]);
      }
    }
    
    
    extension GetExtension on GetInterface {
      dismiss() {
        if (Get.isDialogOpen) {
          Get.back();
        }
      }
    
      loading() {
        if (Get.isDialogOpen) {
          Get.back();
        }
        Get.dialog(LoadingDialog());
      }
    }
    

    使用也很简单,但不要忘了要导入扩展函数类:

    dateTime.format();
    
          Get.loading();
                。。。。。。
          Get.dismiss();
    

    GetService

    GetService 我的理解是类似服务,比如 SharedPreferences、Database,还有需要异步初始化的类,放在这里注入非常合适:

      TaskDao init() {
        TaskDatabase database = TaskDatabase();
        return TaskDao(database);
      }
    }
    
    class AppSpController extends GetxService {
      Future<SharedPreferences> init() async {
        return await SharedPreferences.getInstance();
      }
    }
    
    

    同步的就用同步方法注入:

        // 数据库
        Get.put(TaskDaoController().init());
    

    异步的用异步方法注入:

        // shared_preferences
        await Get.putAsync(() => AppSpController().init());
    

    数据库 moor 的使用

    Android 通过 room 给开发带来的便利,用过的都知道。moor 就是 Flutter 上的 room。

    Moor 使用 Dart 的源代码生成器生成代码,我们可以用函数式的调用操作数据库。这也是需要 moor_generator 依赖项以及 build_runner 的原因。

    moor 优点之一是我们可以完全使用 Dart 操作数据库,而不必写数据库语句。这也适用于定义SQL表。创建一个表示 table 的类即可。

    class Tasks extends Table {
      // 可空类型
      IntColumn get completeDate => integer().nullable()();
      TextColumn get completeDateStr => text().nullable()();
      TextColumn get content => text().nullable()();
    
      // 为空自动生成默认值
      IntColumn get date =>
          integer().clientDefault(() => DateTime.now().millisecondsSinceEpoch)();
    
      // 为空自动生成默认值
      TextColumn get dateStr =>
          text().nullable().clientDefault(() => DateTime.now().format())();
    
      // 主键
      IntColumn get id => integer().nullable().autoIncrement()();
    
      // 为空自动生成默认值
      IntColumn get priority => integer().nullable().withDefault(Constant(0))();
    
      // 为空自动生成默认值
      IntColumn get status => integer().nullable().withDefault(Constant(0))();
    
      TextColumn get title => text()();
    
      IntColumn get type => integer().withDefault(Constant(0))();
    
      IntColumn get userId => integer().nullable()();
    }
    
    @UseMoor(tables: [Tasks], daos: [TaskDao])
    class TaskDatabase extends _$TaskDatabase {
      // we tell the database where to store the data with this constructor
      TaskDatabase() : super(_openConnection());
    
      // you should bump this number whenever you change or add a table definition. Migrations
      // are covered later in this readme.
      @override
      int get schemaVersion => 1;
    }
    
    LazyDatabase _openConnection() {
      // the LazyDatabase util lets us find the right location for the file async.
      return LazyDatabase(() async {
        // put the database file, called db.sqlite here, into the documents folder
        // for your app.
        final dbFolder = await getApplicationDocumentsDirectory();
        final file = File(join(dbFolder.path, 'db.sqlite'));
        return VmDatabase(file);
      });
    }
    

    数据库操作写在这里也可以,但是会显得臃肿,moor 还提供 Dao ,把操作放在 Dao 类是个好习惯:

    
    @UseDao(tables: [Tasks])
    class TaskDao extends DatabaseAccessor<TaskDatabase> with _$TaskDaoMixin {
      TaskDao(TaskDatabase db) : super(db);
    
      /// 获取全部
      Future<List<Task>> get getAllTasks => select(tasks).get();
    
      ///imit查询来限制返回的结果数量
      ///offset偏移量
      Future<List<Task>> getTasks(int limit, {int offset}) {
        return (select(tasks)..limit(limit, offset: offset)).get();
      }
    
      ///imit查询来限制返回的结果数量
      ///offset偏移量
      Future<List<Task>> getTasksWithDateStr(String dateStr) {
        return (select(tasks)..where((e) => e.dateStr.equals(dateStr))).get();
      }
    
      /// 获取单个数据
      /// 没必要用list
      Future<Task> getTaskById(int id) {
        return (select(tasks)..where((t) => t.id.equals(id))).getSingle();
      }
    
      Future<bool> updateTask(Task entry) {
        TasksCompanion();
    
        return update(tasks).replace(entry);
      }
    
      Future<int> createOrUpdateUser(String title,
          {String content, String date, int type = 0, int priority = 0}) {
        return into(tasks).insertOnConflictUpdate(TasksCompanion(
          title: Value(title),
          content: Value(content),
          dateStr: Value(date),
          type: Value(type),
          priority: Value(priority),
        ));
      }
    
      Future<Task> createTask(TasksCompanion task) async {
        var id = await into(tasks).insertOnConflictUpdate(task);
        return getTaskById(id);
      }
    
      /// 批量插入
      Future<void> insertMultipleTasks(List<Task> entries) async {
        await batch((batch) {
          batch.insertAll(tasks, entries);
        });
      }
    
      Future<int> deleteTaskById(int id) {
        return (delete(tasks)..where((t) => t.id.equals(id))).go();
      }
    
      Future<int> deleteTask(Task entry) {
        return delete(tasks).delete(entry);
      }
    
      Future<Task> modifyStatusByid(int id, int status) async {
        // into(tasks).up
        Task task = await getTaskById(id);
        task.copyWith(
          status: status,
        );
        await updateTask(task);
        return task;
      }
    
      Future<bool> modifyTask(Task task) {
        return update(tasks).replace(task);
      }
    
      /// 表中数据改变,会发生一个流
      Stream<List<Task>> watchEntriesInCategory() {
        return select(tasks).watch();
      }
    }
    

    总结

    从路由管理到依赖注入,再到状态管理,还有 Service ,这个应用都应用到了,并轻松的实现了代码解耦。再加上骚粉的 UI ,是不错新手学习项目。

    todo:

    • 显示密码
    • 退出登录
    • 拆分网络请求和本地存储
    • 个人中心大图浏览
    • 国际化
    • 切换主题
    • 修改图标
      。。。


      Simulator Screen Shot - iPhone 11 - 2020-12-12 at 15.37.53.png
    Simulator Screen Shot - iPhone 11 - 2020-12-12 at 15.39.56.png

    源码传送门

    相关文章

      网友评论

        本文标题:用GetX写了一个待办事项

        本文链接:https://www.haomeiwen.com/subject/fyrugktx.html