After one year of using Provider, I met a lot of resistance. During this period, I tried BLoC and MobX, but they were all unsatisfactory. One sample code was too much, and it was complicated to use, and one production code had to wait a long time. Doesn’t Flutter have a convenient package like the native Android Jetpack package? Then I started to try GetX and found it really fragrant, as the author said:
GetX is an ultra-lightweight and powerful solution to Flutter. It combines high-performance state management, intelligent dependency injection, and route management in a fast and practical way.
After I wrote a demo to explore the basics, I decided to write a to-do list app to practice Clean Architecture.
First of all, thank you for the Todo API of Hongyang. The first version is an online application developed by USING API. Later, without registration, it was added into the MOOR database and can be used offline. This part was changed in haste and will be improved in the next iteration.
Project dependencies and structure
"> < span style =" font-size: 14px; line-height: 20px; ^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.1Copy the code
The project network module encapsulates DIO. Since it is a request with cookie, cookie and localization are added, which is a relatively complete request module.
Moor is selected for the database. The letter of Room in Android is inverted like this, which can be as responsive as room and is very excellent.
The remaining third party packages are paging and sideslip controls, and a calendar package.
Structural reference for the overall projectgetx_pattern”, and according to their own habits to do modification.
Start with GetX
Using 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(),
));
}
Copy the code
After routing
To use the full routing functionality, the MaterialApp needs to be replaced with GetMaterialApp, with the Builder added to address the need to hide the keyboard in the click space, which is also common in the native.
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(),
),
];
}
Copy the code
Accustomed to using named routes, a routing table is defined. Binding is one of my favorite features of GetX — dependency injection, just like native Hilt, keeps code structure free of intrusion layers. And if you use streams or timers, they will automatically turn off, so developers don’t have to worry. The Binding class is a decouple dependency injection class that is used during routing. You know the scope of the injection, and you know where and how to handle the injected objects.
The login
The API is an open API for playing Android, and you need to use the API and repository to log in.
class LoginPageBinding implements Bindings { @override void dependencies() { Get.lazyPut(() => LoginApi()); Get.lazyPut(() => LoginRepository()); Get.lazyPut<LoginController>( () => LoginController(), ); }}Copy the code
Find directly when used:
final LoginRepository repository = Get.find<LoginRepository>();
Copy the code
Get. Put () is the most common way to inject dependencies, which is directly into memory. You can find injected objects anywhere, which providers don’t have.
GetX also provides another method, get.lazyPut, that lazily loads a dependency so that it is instantiated only when it is used. This is useful for computationally expensive classes, or if you want to instantiate several classes in one place (such as in the Bindings class) but don’t know if they will be used, lazy loading is the right choice, much like Kotlin’s lazy.
The password display function has not been added.
The welcome page will inject global dependencies, and then determine whether to log in, corresponding to different navigation:
@override void onReady() async { super.onReady(); await GloabConfig.init(); await DenpendencyInjection.init(); LoginProvider loginProvider = Get.find<LoginProvider>(); print(loginProvider); If (loginprovider.islogin ()) {get.offnamed (routes.task); } else { Get.offNamed(Routes.LOGIN); }}}Copy the code
The Task list
At the bottom of the page to achieve the navigation and embedded FloatingActionButtonLocation, no task will pop up when using guide. Click the plus sign to add tasks. Because the API is paginated, it is paginated as well.
@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); }, ), ], ), ), ); }}Copy the code
You can click the task item to enter details and sideslip, and there are two sideslip menus, edit and delete, corresponding to different functions. The circular checkbox can complete the task, and there will be a deletion line when the task title and time are completed.
GetView is a encapsulated StatelessWidget with a get method inside that conveniently retrieves the injected Controller, eliminating the need to get it.
Add and edit
The corresponding title is required, the description can be empty, the time is current by default, and the priority is medium by default.
Selecting the date will pop up the calendar to you, using partial refresh to improve performance,update([updateDateId])
The function takes an ID and only the corresponding ID is refreshedGetBuilder
And GetX is not affectedInheritedWidget
Can be referenced anywhere that has not been reclaimedController
, so you can edit the page and make the list page refresh at the same time.
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]); }}Copy the code
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()); }}}Copy the code
In view of the
The month view uses the Table_calendar package, which is powerful enough to customize the calendar view. The default display is two weeks. Click on the month to expand the month view for four weeks. Tasks can be filtered by date. Here the task can be clicked into details and clicked checkBox to change status.
TableCalendar(
onDaySelected: (DateTime day, _, __) {
controller.selectedDate(day);
},
calendarController: controller.calendarController,
startingDayOfWeek: StartingDayOfWeek.monday,
initialCalendarFormat: CalendarFormat.week,
calendarStyle: CalendarStyle(
selectedColor: Theme.of(context).accentColor,
),
)
Copy the code
After changing the status, you can also get the Controller of the list page to update the list page:
modifyTaskStatus(Task task) async {
try {
TaskController taskController = Get.find<TaskController>();
await taskController.modifyTaskStatus(task);
} catch (e) {}
update();
}
Copy the code
Personal center
The personal center is a static page with a screenshot of the GetX demo I wrote at the bottom. Click to enlarge and do it in the iteration.
There’s welfare in here, a beautiful two-dollar cutie.
Extension function
Two extension functions are written in the utils folder to extend date formatting and getx-based global load boxes.
extension DateExtension on DateTime { String format() { return formatDate(this, [ yyyy, '-', mm, '-', dd, ]); }}Copy the code
extension GetExtension on GetInterface { dismiss() { if (Get.isDialogOpen) { Get.back(); } } loading() { if (Get.isDialogOpen) { Get.back(); } Get.dialog(LoadingDialog()); }}Copy the code
It’s easy to use, but don’t forget to import the extension function class:
DateTime. Format ();Copy the code
Get.loading(); . Get.dismiss();Copy the code
GetService
Services like SharedPreferences, Database, and classes that need to be initialized asynchronously are a good place to inject them:
TaskDao init() { TaskDatabase database = TaskDatabase(); return TaskDao(database); }}Copy the code
class AppSpController extends GetxService { Future<SharedPreferences> init() async { return await SharedPreferences.getInstance(); }}Copy the code
If it is synchronized, inject it synchronously:
// Get. Put (TaskDaoController().init());Copy the code
Injected by an asynchronous method:
// shared_preferences
await Get.putAsync(() => AppSpController().init());
Copy the code
The use of database MOOR
We all know the convenience Android brings to development through Room. Moor is room on Flutter.
Moor uses Dart’s source code generator to generate code that allows us to manipulate the database with functional calls. This is why you need the moor_generator dependency as well as build_runner.
One of the advantages of moor is that we can fully manipulate the database using Dart without having to write database statements. This also applies to defining SQL tables. Create a class that represents 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 ().zipzipgex)(); 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); }); }Copy the code
The moor also provides DAOs. It is a good habit to put operations in the Dao class:
@UseDao(tables: [Tasks]) class TaskDao extends DatabaseAccessor<TaskDatabase> with _$TaskDaoMixin { TaskDao(TaskDatabase db) : super(db); <List<Task>> get getAllTasks => select(tasks).get(); Future<List<Task>> getTasks(int limit, {int offset}) {return (select(tasks).. limit(limit, offset: offset)).get(); 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(); }}Copy the code
conclusion
From routing management to dependency injection, to state management, to Service, this application applies everything and easily decouples code. Plus SAO Powder UI, is a good novice learning project.
Todo:
- Show the password
- Log out
- Split network requests and local storage
- Personal center view
- internationalization
- Switch the theme
- Modify the icon
.
Source code portal