用GetX寫了一個待辦事項

在使用了 Provider 一年后哮洽,遇到了很多阻力,期間嘗試過 BLoC 弦聂、MobX 鸟辅,均不如意,一個樣本代碼太多莺葫,使用復雜匪凉,一個生產(chǎn)代碼要等很久。難道 Flutter 就沒有諸如原生 Android 的 jetpack 套裝一樣方便的套件嗎捺檬?后來開始嘗試 GetX再层,才發(fā)現(xiàn)真香,正如作者所說:

GetX是Flutter的超輕便且強大的解決方案堡纬。它以快速實用的方式結(jié)合了高性能狀態(tài)管理聂受,智能依賴性注入和路由管理。

我寫了一個demo探索過了基本使用方式之后烤镐,又決定寫一個 待辦清單app 實踐一下 Clean Architecture 蛋济。

首先感謝下鴻洋大佬的 todo api,第一版是利用 api 開發(fā)的一個在線應用炮叶,后來在不注冊的情況下碗旅,加入 moor 數(shù)據(jù)庫,可以離線使用镜悉。這一部分改的倉促祟辟,下一期迭代會改進。

項目依賴和結(jié)構(gòu)

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

項目網(wǎng)絡模塊封裝了 dio侣肄,因為是 帶 cookie 的 請求川尖,所以加入了 cookie 和本地化,算是一個比較完善的請求模塊。

數(shù)據(jù)庫選用了 moor 叮喳,Android 中 room 的字母倒過來就是這個被芳,和 room 一樣可以響應式,十分優(yōu)秀馍悟。

剩下的第三方包就是分頁和側(cè)滑控件畔濒,還有一個日歷包。

整體項目的結(jié)構(gòu)參考getx_pattern锣咒,又按照自己的習慣做了修改侵状。

getx_pattern

從 GetX 開始開發(fā)

使用 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 一樣戏蔑,讓代碼結(jié)構(gòu)無侵分層蹋凝。并且如果使用的是流或計時器,它們將自動關(guān)閉总棵,開發(fā)者根據(jù)不用擔心鳍寂。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()是最常見的注入依賴的方法,它是直接注入到內(nèi)存里尚胞。你可以在任何地方找到注入的對象硬霍,這是 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

主頁實現(xiàn)了底部導航和嵌入式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可以點擊進入詳情和側(cè)滑戈毒,有兩個側(cè)滑菜單,編輯和刪除横堡,對應不同的功能埋市,圓形的checkbox可以完成任務,任務標題和時間在完成時會有刪除線命贴。

GetView 就是封裝的StatelessWidget,內(nèi)部有一個 get方法便捷的獲取注入的controller道宅,這樣連獲取的步驟都能省略。

增加和編輯

編輯
添加

對應的標題是必須項套么,描述可以為空,時間是默認當前碳蛋,優(yōu)先級有高低中三個胚泌,默認是中。

選擇日期會彈出日歷你肃弟,采用局部刷新玷室,提高性能,update([updateDateId])函數(shù)的參數(shù)是一個 id笤受,只會刷新對應 id 的 GetBuilder,并且 GetX 不受 InheritedWidget的限制穷缤,所以可以在任意地方引用未被內(nèi)存回收的 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更改狀態(tài)咐蝇。

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

這里更改狀態(tài)后涯鲁,同樣可以拿到列表頁的Controller去更新列表頁:

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

個人中心

個人中心

個人中心是一個靜態(tài)頁面,最下面展示了我寫的 GetX 的 demo 截圖。點擊放大的功能放在迭代里做吧抹腿。

這里藏有福利岛请,一個漂亮的二次元萌妹子。

擴展函數(shù)

utils文件夾下寫了兩個擴展函數(shù)幢踏,擴展了日期格式化和基于 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());
  }
}

使用也很簡單,但不要忘了要導入擴展函數(shù)類:

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();
  }
}

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

    // 數(shù)據(jù)庫
    Get.put(TaskDaoController().init());

異步的用異步方法注入:

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

數(shù)據(jù)庫 moor 的使用

Android 通過 room 給開發(fā)帶來的便利,用過的都知道桌肴。moor 就是 Flutter 上的 room皇筛。

Moor 使用 Dart 的源代碼生成器生成代碼,我們可以用函數(shù)式的調(diào)用操作數(shù)據(jù)庫坠七。這也是需要 moor_generator 依賴項以及 build_runner 的原因水醋。

moor 優(yōu)點之一是我們可以完全使用 Dart 操作數(shù)據(jù)庫,而不必寫數(shù)據(jù)庫語句彪置。這也適用于定義SQL表拄踪。創(chuàng)建一個表示 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);
  });
}

數(shù)據(jù)庫操作寫在這里也可以拳魁,但是會顯得臃腫惶桐,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查詢來限制返回的結(jié)果數(shù)量
  ///offset偏移量
  Future<List<Task>> getTasks(int limit, {int offset}) {
    return (select(tasks)..limit(limit, offset: offset)).get();
  }

  ///imit查詢來限制返回的結(jié)果數(shù)量
  ///offset偏移量
  Future<List<Task>> getTasksWithDateStr(String dateStr) {
    return (select(tasks)..where((e) => e.dateStr.equals(dateStr))).get();
  }

  /// 獲取單個數(shù)據(jù)
  /// 沒必要用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);
  }

  /// 表中數(shù)據(jù)改變,會發(fā)生一個流
  Stream<List<Task>> watchEntriesInCategory() {
    return select(tasks).watch();
  }
}

總結(jié)

從路由管理到依賴注入潘懊,再到狀態(tài)管理耀盗,還有 Service ,這個應用都應用到了卦尊,并輕松的實現(xiàn)了代碼解耦十拣。再加上騷粉的 UI ,是不錯新手學習項目宋梧。

todo:

  • 顯示密碼
  • 退出登錄
  • 拆分網(wǎng)絡請求和本地存儲
  • 個人中心大圖瀏覽
  • 國際化
  • 切換主題
  • 修改圖標
    盈匾。。裙椭。


    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

源碼傳送門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市署浩,隨后出現(xiàn)的幾起案子揉燃,更是在濱河造成了極大的恐慌,老刑警劉巖筋栋,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炊汤,死亡現(xiàn)場離奇詭異,居然都是意外死亡弊攘,警方通過查閱死者的電腦和手機抢腐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來襟交,“玉大人迈倍,你說我怎么就攤上這事〉酚颍” “怎么了啼染?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長焕梅。 經(jīng)常有香客問我迹鹅,道長,這世上最難降的妖魔是什么贞言? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任斜棚,我火速辦了婚禮,結(jié)果婚禮上蜗字,老公的妹妹穿的比我還像新娘打肝。我一直安慰自己脂新,他們只是感情好挪捕,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著争便,像睡著了一般级零。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滞乙,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天奏纪,我揣著相機與錄音,去河邊找鬼斩启。 笑死序调,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的兔簇。 我是一名探鬼主播发绢,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼硬耍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了边酒?” 一聲冷哼從身側(cè)響起经柴,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎墩朦,沒想到半個月后坯认,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡氓涣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年牛哺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片春哨。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡荆隘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赴背,到底是詐尸還是另有隱情椰拒,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布凰荚,位于F島的核電站燃观,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏便瑟。R本人自食惡果不足惜缆毁,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望到涂。 院中可真熱鬧脊框,春花似錦、人聲如沸践啄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屿讽。三九已至昭灵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伐谈,已是汗流浹背烂完。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诵棵,地道東北人抠蚣。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像履澳,于是被迫代替她去往敵國和親嘶窄。 傳聞我的和親對象是個殘疾皇子缓屠,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容