在使用了 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 開始開發(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 列表
主頁實現(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