全栈工程师?
嗯.....目前并不是!

小熊爸爸Flutter学习笔记——GetX如何实现底部菜单切换

小熊爸爸阅读(53)

GetX实现底部菜单切换

学习了Flutter两个月,看过无数大神的视频于文章,发现在状态管理与框架方面GetX的优势在于代码量少,书写逻辑利于我这样的小白用户理解。
目前在自己完成一个独立的项目,笔记将详细记录学习过程中遇到的问题与解决方法。

依赖

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.0

  #GetX
  get: ^3.24.0

  #底部菜单
  custom_navigation_bar: ^0.6.0

底部菜单交互使用了custom_navigation_bar,因为主要是理解GetX实现切换的逻辑,所以用已有的组件实现比较快。

controller

import 'package:get/get.dart';

class ApplicationController extends GetxController {
  ///当前Tap页码
  var selectedIndex = 0.obs;

  // 显示页面对应页码
  void onBottomNavigationBar(int index) {
    selectedIndex.value = index;
  }
}

view

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:newspmbear_app/page/application/application_controller.dart';
import 'package:newspmbear_app/page/find/find_view.dart';
import 'package:newspmbear_app/page/home/home_view.dart';
import 'package:custom_navigation_bar/custom_navigation_bar.dart';
import 'package:newspmbear_app/page/my/my_view.dart';
import 'package:newspmbear_app/page/video/video_view.dart';

class ApplicationPage extends StatelessWidget {
  final ApplicationController applicationController =
      Get.put(ApplicationController());

  //底部菜单
  Widget _buildBottomNavigationBar() {
    return Obx(
      () => CustomNavigationBar(
        iconSize: 28.0,
        selectedColor: Color(0xff040307),
        strokeColor: Color(0x30040307),
        unSelectedColor: Color(0xffacacac),
        backgroundColor: Colors.white,
        items: [
          CustomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text("首页"),
          ),
          CustomNavigationBarItem(
            icon: Icon(Icons.shopping_cart),
            title: Text("视频"),
          ),
          CustomNavigationBarItem(
            icon: Icon(Icons.lightbulb_outline),
            title: Text("发现"),
          ),
          CustomNavigationBarItem(
            icon: Icon(Icons.account_circle),
            title: Text("我的"),
          ),
        ],
        //获取当前按钮数值
        currentIndex: applicationController.selectedIndex.value,
        //切换当前页面
        onTap: applicationController.onBottomNavigationBar,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    ScreenUtil.init(context,
        designSize: Size(375, 812), allowFontScaling: true);
    List<Widget> _getPageView = [
      HomePage(),
      VideoPage(),
      FindPage(),
      MyPage(),
    ];
    return Scaffold(
      // 显示对应的页面
      body: Obx(() =>
          _getPageView.elementAt(applicationController.selectedIndex.value)),
      bottomNavigationBar: _buildBottomNavigationBar(),
    );
  }
}

GetX- 简化flutter状态管理器

小熊爸爸阅读(47)

文档地址
如何定义状态和响应式状态
简化路由操作
参考在语雀上整理的代码demo

0. 参考文档示例,项目组织机构


| - lib
  | - pages
    | - home
      | - home_view.dart
      | - home_controller.dart
      | - home_binding.dart

在这里插入图片描述

1. 定义响应式数据

  • 虽然文档上给了3种方式参考文档,但我这边测试的第二种泛型的写法一直报红,有搞通的留言一个
  • 以下语法是最简单的了

1.1 定义x响应式数据(也就是上边目录的controller文件)

home_controller.dart

import 'package:get/get.dart';
class HomeController extends GetxController {
  var num = 0.obs;
  //var num = RxInt(0), 此语法也可以,也是具有响应式的
  void increment() => num++;
  void decrement() => num--;
}

home_binding.dart

import 'package:get/get.dart';
// 引入你的控制器
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}

home_view.dart 将试图和数据关联

import 'package:get/get.dart';

class HomeView extends GetView {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Hello WOrld!")),
      body: Column(
        children: [
          // 使用Obx() 让视图更新
          Obx(() => Text("${controller.num}")),
          FlatButton(
            onPressed: () {
              controller.increment();
            },
            child: Text("add"),
          ),
          FlatButton(
            onPressed: () {
              controller.decrement();
            },
            child: Text("decrement"),
          ),
        ],
      ),
    );
  }
}
跑起来计数器

main.dart

main(List args) {
  runApp(GetMaterialApp(
    enableLog: true,
    initialRoute: "/home",
    getPages: [
      GetPage(name: "/home", page: () => HomeView(), binding: HomeBinding())
    ],
  ));
}

注释一波:

控制器(ctroller): 定义初始数据源 ,需要继承 `GetxController` 类,
`Bindings` ,用于将控制器推入getx中,可以理解为要注册控制器
使用 `GetView<T>` 语法将 `controller 和 binding` 进行关联 ,内部可以直接 使用 `controller的实例调用数据和方法`
使用 `Obx(()=> widget)` 语法,返回你要变更的组件, 让数据在视图上更新,并且是局部刷新的,

案例-登录跳转,传参,接收


目录
在这里插入图片描述
login_controller.dart

class LoginController extends GetxController {
  var userName = RxString("");
  var pwd = RxString("");
  void saveUserName(uName) => this.userName.value = uName;
  void savePwd(pwd) => this.pwd.value = pwd;
}

login_binding.dart

import 'package:get/get.dart';
import 'login_controller.dart';
class LoginBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => LoginController());
  }
}

login_view.dart

import 'package:app_demo/login/login_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class LoginView extends GetView {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: Size.fromHeight(0),
        child: AppBar(elevation: 0, backgroundColor: Colors.red),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            inputWidget(
              label: "用户名",
              hintText: "用户名",
              cb: (val) => controller.saveUserName(val),
            ),
            inputWidget(
              label: "密码",
              hintText: "密码",
              obscureText: true,
              cb: (val) => controller.savePwd(val),
            ),
            loginButton()
          ],
        ),
      ),
    );
  }

  // 输入框
  Widget inputWidget(
      {String label, String hintText, Function cb, bool obscureText = false}) {
    return TextField(
      obscureText: obscureText,
      decoration: InputDecoration(
        labelText: label + ':',
        icon: Icon(Icons.people),
        hintText: "请输入" + hintText,
        hintStyle: TextStyle(color: Colors.green),
      ),
      onChanged: (val) => cb(val),
    );
  }

  // 登录
  Widget loginButton() => Container(
        width: double.infinity,
        height: 45,
        margin: const EdgeInsets.only(top: 20),
        color: Colors.blue,
        child: FlatButton(
          onPressed: () {
            // 延迟两秒跳转 传参的
            Future.delayed(Duration(seconds: 2), () {
              Get.toNamed(
                  "/home?name=${controller.userName}&pwd=${controller.pwd}");
            });
          },
          child: Text(
            "登录",
            style: TextStyle(color: Colors.white),
          ),
        ),
      );
}

main.dart(自己导包)

import 'package:flutter/material.dart';
import 'package:get/get.dart';
main(List args) {
  runApp(GetMaterialApp(
    enableLog: true,
    initialRoute: "/login",
    debugShowCheckedModeBanner: false,
    getPages: [
      GetPage(name: "/login", page: () => LoginView(), binding: LoginBinding()),
      GetPage(name: "/home", page: () => HomeView(), binding: HomeBinding()),
    ],
  ));
}

在home_view.dart 接收参数

import 'package:app_demo/home/home_controller.dart';
import 'package:app_demo/login/login_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class HomeView extends GetView {
  final LoginController _loginController = Get.find();

  @override
  Widget build(BuildContext context) {
    var name = Get.parameters['name'];
    var pwd = Get.parameters['pwd'];

    return Scaffold(
      appBar: AppBar(title: Text("Hello WOrld!")),
      body: Column(
        children: [
          Obx(() => Text("${controller.num}")),
          Text('接收用户参数:$name'),
          Text('接收密码参数:$pwd'),
          Text('通过控制器获取用户名:${_loginController.userName}'),
          Text('通过控制器获取密码:${_loginController.pwd}'),
          FlatButton(
            onPressed: () {
              controller.increment();
            },
            child: Text("add"),
          ),
          FlatButton(
            onPressed: () {
              controller.decrement();
            },
            child: Text("decrement"),
          ),
        ],
      ),
    );
  }
}

输入
在这里插入图片描述
接收
在这里插入图片描述

GetX项目级实战

小熊爸爸阅读(53)

在使用了 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,又按照自己的习惯做了修改。
file

从 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。

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

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

  @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 列表

file

主页实现了底部导航和嵌入式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,这样连获取的步骤都能省略。

增加和编辑

file

file

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

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

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

月份视图

file

月份视图用了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();
  }

个人中心

file

个人中心是一个静态页面,最下面展示了我写的 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();
<span class="copy-code-btn">复制代码</span>
      Get.loading();
            。。。。。。
      Get.dismiss();

GetService

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

  TaskDao init() {
    TaskDatabase database = TaskDatabase();
    return TaskDao(database);
  }
}
<span class="copy-code-btn">复制代码</span>
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:

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

。。。
file

file

源码传送门

Flutter Getx响应式状态管原理分析

小熊爸爸阅读(53)

前言

最近新写一个项目,要用到状态管理。 而之前选择BLOC作为状态管理, 效果还是很不错的. UI和逻辑 分离,可是回过头看项目会发现中各种状态类、事件类的模板代码太多。于是想看看现在社区里有什么新的状态管理方案, 找呀找 于是今天我们的主角登场了GetX 初看它的API发现足够简单,也符合直觉,剧作者说性能还好。

既然实现的简单,当然要看它的实现原理啦

  • GetBuilder 手动管理状态
  • GetX 响应式状态管理

GetBuilder 手动管理

先看看一个简单的例子 建立Controller和View类,Controller里面主动调用update() 去刷新View层里面的UI。

class Controller extends GetxController {
  int counter = 0;
  void increment() {
    counter++;
    update(); // 当调用增量时,使用update()来更新用户界面上的计数器变量。
  }
}

GetBuilder<Controller>(
  init: Controller(), 
  builder: (_) => Text(
    '${_.counter}',
  ),

核心思想

  1. 定义一个继承GetxController的类: 逻辑- Controller
  2. 定义一个GetBuilder类:界面- *View 层
  3. controlelr 里面调用update() 刷新UI

View、Controller是如何建立关系的

分析 GetBuilder 的源码

  1. 它是一个StatefulWidget
  2. initState()寻找定义的Controller. 这里有一些寻找策略,跟我们这没关系忽略掉
  3. subscribeToController() Cool找到了! 准备开始建立关系 给Controller通过调用addListner()的方法增加一个监听者, 并绑定一个函数getUpdate
  4. getUpdate 来源于GetStateUpdaterMixin 功能很简单: 当前widget存活就执行setState()
// get-3.22.2/lib/get_state_manager/src/simple/get_state.dart 
class GetBuilder<T extends GetxController> extends StatefulWidget {
  final GetControllerBuilder<T> builder;
  final T init;
  ...

    @override
  _GetBuilderState<T> createState() => _GetBuilderState<T>();

}

class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>> with GetStateUpdaterMixin {
  T controller;

  @override
  void initState() {
    // 寻找Controller
    ...
    // 订阅
    _subscribeToController();
  }

  void _subscribeToController() {
    remove?.call();
    remove = (widget.id == null)
        ? controller?.addListener(getUpdate)
        : controller?.addListenerId(widget.id, getUpdate);
  }

}
mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
  void getUpdate() {
    if (mounted) setState(() {});
  }
}

分析 GetxController 的源码

  1. GetxController 实现了Listenable可监听对象的协议
  2. addListener时记录到一个List对象里, 并取名为updater
  3. 调用update() -> refresh() 迭代所有监听者并调用.
  class GetxController extends DisposableInterface with ListNotifier {
     void update([List<String> ids, bool condition = true]) {
     // 移除无用的代码
     ...
     refresh();
     ...
     }
 }

 class ListNotifier implements Listenable {
     List<GetStateUpdate> _updaters = <GetStateUpdate>[];

     @protected
     void refresh() {
     for (var element in _updaters) {
         element();
     }
     }

     @override
     Disposer addListener(GetStateUpdate listener) {
     _updaters.add(listener);
     return () => _updaters.remove(listener);
     }

}
  1. GetBuilder是一个状态类,它建立了和Controller的关系并在initState()时增加当前View作为监听者.
  2. Controller里维护了一个updater的列表记录每个监听者,在update()的时候会通知刷新所有View的状态。

总结

  • 优点: API足够简单清晰,占用很少内存资源。
  • 不足: 还是需要我们去手动update(), 有没有响应式的接口? 能让我们直接调用,当然有,请往下面看。

GetX 响应式自动管理

同样需要我们声明Controller和View类,此时最直观的感受就是Controller里面 不需要手动update() 太棒了!!!

class Controller extends GetXcontroller {
  final count1 = 0.obs;
  final count2 = 0.obs;
  int get sum => count1.value + count2.value;
}

GetX<Controller>(
  builder: (controller) {
    print("count 1 rebuild");
    return Text('${controller.count1.value}');
  },
),

核心思想

  1. 定义一个Controller类: 逻辑 - Controller

    Controller里包含多个以 .obs 的形式表示的变量,我们把这个变量称作为响应式变量

  2. 定义一个GetBuilder类:界面 - View
  3. 只要响应式变量发生改变,就会自动通知View刷新界面, 哇太神奇了 🙂

分析GetX的源码

  1. GetX 是一个StatefulWidget
    class GetX<T extends DisposableInterface> extends StatefulWidget {
    final GetXControllerBuilder<T> builder;
    const GetX({
    this.builder,
    });
    @override
    GetXState<T> createState() => GetXState<T>();
    }
  2. GetState类初始化的时候创建一个 _observer = RxNotifier() RxNotifier()里包含一个Stream的流
    1. initState() _observer 开始增加一个监听者刷新View
      class GetXState<T extends DisposableInterface> extends State<GetX<T>> {
      GetXState() {
      _observer = RxNotifier();
      }
      RxInterface _observer;
      StreamSubscription subs;
      @override
      void initState() {
      // 寻找controller
      // ... 
      subs = _observer.listen((data) => setState(() {}));
      super.initState();
      }
      }
    2. build() 这里很有意思. 把_observer作为RxInterface.proxy 的临时属性,在使用builder的后恢复原有的属性, 需要注意的是builder(controller)函数里一定要包含obs.value, 要不然会提示异常。
      Widget get notifyChildren {
      final observer = RxInterface.proxy;
      RxInterface.proxy = _observer;
      final result = widget.builder(controller);
      if (!_observer.canUpdate) {
      throw """
      [Get] the improper use of a GetX has been detected. 
      You should only use GetX or Obx for the specific widget that will be updated.
      If you are seeing this error, you probably did not insert any observable variables into GetX/Obx 
      or insert them outside the scope that GetX considers suitable for an update 
      (example: GetX => HeavyWidget => variableObservable).
      If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
      """;
      }
      RxInterface.proxy = observer;
      return result;
      }
      @override
      Widget build(BuildContext context) => notifyChildren;
    3. 执行builder(controller) 包含调用obs.value的属性. 开始订阅当前属性的变化, 当发生改变事就自动触发setState() 这是GetX的核心的地方;
      mixin RxObjectMixin on NotifyManager {
      // controller.count.value
      T get value {
      if (RxInterface.proxy != null) {
      RxInterface.proxy.addListener(subject);
      }
      return _value;
      }
      }
      mixin NotifyManager {
      GetStream subject = GetStream();
      final _subscriptions = [];
      void addListener(GetStream rxGetx) {
      if (_subscriptions.contains(rxGetx.listen)) {
      return;
      }
      final subs = rxGetx.listen((data) {
      subject.add(data);
      });
      _subscriptions.add(subs);
      }
      }

总结

GetX 初始化的时候会创建一个Stream,它来监听当前View的变化,当UI中响加载 obs.value 时,就会把 View刷新 挂载到这个obs属性上。 只要属性变化就会通知View刷新页面。

GetBuilder vs GetX

方式 自动update 内存占用
GetBuilder 非常少
GetX 一般

一个GetX的demo

小熊爸爸阅读(48)


关于Flutter的多个状态管理就不多说了,我自己也写过类似的状态管理。Flutter的状态管理原理都很简单,基本都是把数据放到上一个节点,或者搞成全局的类似EventBus然后手动控制删除。这次的GetX也一样,但是这次的GetX封装更强,功能更强,我用wanandroid写了个demo,可以方便的学习GetX常用功能。


Demo功能

这个例子的功能很简单,就一个首页,做好首页的流程其他功能都大同小异

第三方lib

  • flutter_easyrefresh: ^1.2.7
  • dio: ^3.0.10
  • get: ^3.17.0

开发流程

创建一个base_lib,用来做基础类库

  • 创建DioCreator,用来操作联网
  • 创建CommonApiService,将我们要用到的所有的网络请求放到这里
  • 创建PagingListControl,这个类我自认为我封装的比较巧妙,因为所有的app都会有列表展示的需求,我封装了一下GetxController,简单来说就是我把获取首页,上拉加载,下拉刷新,重试按钮抽象了一下,该类有一个抽象方法loadPagingData,该方法有两个参数int pageIndex,bool reset,子类根据这两个参数返回对应的分页数据PagingData,然后该类通过分页数据PagingData来判断当前页面应该是什么显示状态
  • 创建PagingData,自定义的分页数据封装类

结束,主要就这几个类

在lib包下面新建app相关包

这部分不重要,按照自己的开发习惯即可

  • network,将网络相关类放到此包
  • page,将所有页面放入此包
  • util,将所有工具类放入此包
  • constant,将所有静态变量放入此包
  • widget,将公共Widget放入此包

开始开发app

  1. 创建ApiService,这个类封装DioCreator,里面的方法是所有和业务相关的联网接口
  2. 创建AppNet,这个类保存和接口相关的Url
  3. 创建自定义的网络日志打印类LogInterceptor
  4. 创建StatusCheckInterceptor,检查业务接口是否报错
  5. 创建HomeChangeControl,继承自PagingListControl,重写loadPagingData方法将首页数据返回给父类
  6. 编写main.dart,创建App要显示的页面

核心代码讲解

main.dart

void main() {
  ApiService.init(
    AppNet.BASE_URL,
    interceptors: [
      LogInterceptor(),
      StatusCheckInterceptor(),
    ],
  );
  runApp(
    App(),
  );
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialBinding: BindingsBuilder(() {
        Get.lazyPut<HomeChangeControl>(() => HomeChangeControl(), fenix: true);
        Get.lazyPut<AppChangeControl>(() => AppChangeControl(), fenix: true);
      }),
      title: 'WanAndroid',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AppPage(),
    );
  }
}

class AppPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('首页'),
      ),
      body: Home(),
    );
  }
}

首先调用ApiService.init初始化联网类,添加日志拦截器和返回状态拦截器,然后通过runApp方法启动名为App的Widget,在这个Widget的build方法里我们返回GetMaterialApp,为什么返回GetMaterialApp,这个在GetX的文档里说了,如下

GetMaterialApp会创建路由,注入它们,注入翻译,注入你需要的一切路由导航。如果你只用Get来进行状态管理或依赖管理,就没有必要使用GetMaterialApp。GetMaterialApp对于路由、snackbar、国际化、bottomSheet、对话框以及与路由相关的高级apis和没有上下文(context)的情况下是必要的。

然后我们在GetMaterialAppinitialBinding方法里通过Get.lazyPut方法传入我们自定义的GetxController,顾名思义,Get.lazyPut传入的GetxController只有在使用的时候才会实例化,其中AppChangeControl()我们可以用来保存全局的一些配置,在HomeChangeControl()里我们保存首页数据,具体的这些GetX的参数请看文档。

然后我们看Home.dartpaging_list.dart这两个文件

Home.dart

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PagingListWidget<Datas, HomeChangeControl>(
      createItem: (BuildContext context, int index, Datas data) {
        return Container(
          padding: const EdgeInsets.all(10),
          child: Text(data.title),
        );
      },
    );
  }
}

paging_list.dart

typedef ItemBuilder<T> = Widget Function(BuildContext context, int index, T data);
typedef RetryLoad = void Function(BuildContext context);

///继承该类的Widget就有了上拉加载下拉刷新的功能
class PagingListWidget<LIST_ITEM, C extends PagingListControl<LIST_ITEM>> extends StatelessWidget {
  final ItemBuilder<LIST_ITEM> _createItem;

  //是否可以下拉刷新
  final bool _refreshEnable;

  //是否可以上拉加载
  final bool _loadMoreEnable;

  PagingListWidget({
    bool refreshEnable: true,
    bool loadMoreEnable: true,
    @required ItemBuilder<LIST_ITEM> createItem,
  })  : this._createItem = createItem,
        this._refreshEnable = refreshEnable,
        this._loadMoreEnable = loadMoreEnable;

  @override
  Widget build(BuildContext context) {
    return GetBuilder<C>(
      builder: (control) {
        return MultiStateList(
          easyRefreshKey: control.easyRefreshKey,
          headerKey: control.headerKey,
          footerKey: control.footerKey,
          listState: control.listState,
          retry: control.onRetry,
          onRefresh: _refreshEnable
              ? () async {
                  await control.onRefresh();
                }
              : null,
          loadMore: _loadMoreEnable
              ? () async {
                  await control.loadMore();
                }
              : null,
          child: ListView.separated(
            separatorBuilder: (BuildContext context, int index) {
              return Divider(height: 0, color: Colors.grey);
            },
            itemCount: control.dataList?.length ?? 0,
            itemBuilder: (BuildContext context, int index) {
              Widget child = _createItem(context, index, control.dataList[index]);
              return Column(
                children: [
                  Row(
                    children: [
                      Text("$index"),
                      Expanded(child: child),
                    ],
                  ),
                  Divider(height: 0, color: Colors.grey),
                ],
              );
            },
          ),
        );
      },
    );
  }
}

enum ListState {
  //第一次显示时的loading状态
  LOADING,
  //显示内容
  CONTENT,
  //显示空数据
  EMPTY,
  //显示错误信息
  ERROR
}

/// 多状态列表,具有下拉刷新,上拉加载,无数据时显示无数据控件,发生错误时显示错误信息这4种功能
/// 临时封装了一个第三方具有下拉刷新,上拉加载功能的列表,后期替换为自定义的
class MultiStateList extends StatelessWidget {
  static const TEXT_TIP = Color(0xFF666666); //提示文字的颜色
  //第一次显示时的widget
  final Widget _init;

  //页面无数据时显示的widget
  final Widget _empty;

  //页面发生错误时显示的widget
  final Widget _error;

  //点击无数据或者错误信息的重试按钮时回调的事件
  final RetryLoad _retry;
  final OnRefresh _onRefresh;
  final LoadMore _loadMore;

  final GlobalKey<EasyRefreshState> _easyRefreshKey;
  final GlobalKey<RefreshHeaderState> _headerKey;
  final GlobalKey<RefreshFooterState> _footerKey;

  //列表
  final Widget _child;

  //列表状态
  final ListState _listState;

  MultiStateList(
      {Key key,
      GlobalKey<EasyRefreshState> easyRefreshKey,
      GlobalKey<RefreshHeaderState> headerKey,
      GlobalKey<RefreshFooterState> footerKey,
      Widget init,
      Widget empty,
      Widget error,
      ListState listState,
      OnRefresh onRefresh,
      LoadMore loadMore,
      @required RetryLoad retry,
      @required Widget child})
      : assert(retry != null),
        assert(child != null),
        _init = init,
        _empty = empty,
        _error = error,
        _listState = listState ?? ListState.LOADING,
        _retry = retry,
        _onRefresh = onRefresh,
        _loadMore = loadMore,
        _easyRefreshKey = easyRefreshKey,
        _headerKey = headerKey,
        _footerKey = footerKey,
        _child = child,
        super(key: key);

  Widget getDefaultInitView() {
    return Center(
      child: Container(
        width: 50,
        height: 50,
        child: CircularProgressIndicator(),
      ),
    );
  }

  Widget getDefaultEmpty(BuildContext context) {
    return Center(
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () {
            _retry(context);
          },
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Image(
                image: AssetImage("assets/images/empty.png", package: "base_lib"),
                width: 80,
                height: 80,
              ),
              Text("暂无数据"),
              Text(
                "点击重试",
                style: TextStyle(
                  color: TEXT_TIP,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget getDefaultError(BuildContext context) {
    return Center(
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () {
            _retry(context);
          },
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Image(
                image: AssetImage("assets/images/error.png", package: "base_lib"),
                width: 100,
                height: 80,
              ),
              Text("加载失败"),
              Text(
                "点击重试",
                style: TextStyle(
                  color: TEXT_TIP,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    switch (_listState) {
      case ListState.LOADING:
        return _init ?? getDefaultInitView();
      case ListState.EMPTY:
        return _empty ?? getDefaultEmpty(context);
      case ListState.ERROR:
        return _error ?? getDefaultError(context);
      case ListState.CONTENT:
        return EasyRefresh(
          key: _easyRefreshKey,
          behavior: ScrollOverBehavior(),
          refreshHeader: ClassicsHeader(
            key: _headerKey,
            refreshText: '下拉刷新',
            refreshReadyText: '释放刷新',
            refreshingText: '正在刷新...',
            refreshedText: '刷新结束',
            moreInfo: '更新于 %T',
            bgColor: Colors.transparent,
            textColor: Colors.black87,
            moreInfoColor: Colors.black54,
            showMore: true,
          ),
          refreshFooter: ClassicsFooter(
            key: _footerKey,
            loadText: '上拉加载',
            loadReadyText: '释放加载',
            loadingText: '正在加载...',
            loadedText: '加载结束',
            noMoreText: '没有更多数据',
            moreInfo: '更新于 %T',
            bgColor: Colors.transparent,
            textColor: Colors.black87,
            moreInfoColor: Colors.black54,
            showMore: true,
          ),
          child: _child,
          onRefresh: _onRefresh,
          loadMore: _loadMore,
        );
      default:
        return null;
    }
  }
}

Home这个Widget的build方法里,我们返回了一个PagingListWidget,这个是我自己封装的一个公共列表类,可以下拉刷新和上拉加载,这个不详细解释,在这里把它当成一个普通列表Widget即可。重点是PagingListWidgetbuild方法里我们返回了一个GetBuilder,它是在GetX里做状态管理的,关于GetX的状态管理有4种,这些都可以在GetX的文档里看到,在文章的最后我也会将我学习到的总结一下,这里记住它是做状态管理的即可,到这里,这篇文章已经结束了,我们通过GetBuilder拿到App这个Widget里GetMaterialAppinitialBinding参数配置的HomeChangeControl,然后通过这个HomeChangeControl去取数据即可,而HomeChangeControl继承自PagingListControl,在PagingListControl这个类的onInit()方法在调用的时候会去调用loadFirstPage()方法获取首页数据,而PagingListControl类里有一个listState属性,该属性默认值为ListState.LOADING,也就是说第一次进入页面的时候列表页默认是Loading状态,在获取到数据后PagingListControl会调用update()方法,然后刷新列表显示数据。


文章结束,这个GetX的demo也就写完了,核心内容就是如上所述。有一些和框架没什么关系的细节我没有说,比如如何封装的PagingListControlPagingListWidget,这些有兴趣可以去看我的源码。连接我都放到下面了。

GetX的中文官方文档

wanAndroid的demo(注意切换到getX分支)


然后我发一下我学习GetX总结的一些东西,仅供参考

状态管理(4个Widget):
Obx=>
GetX<? extends GetxController>与? extends GetxController+T.obs/Rx<T>/Rx...
GetBuilder<? extends GetxController>与? extends GetxController+普通变量+update()
MixinBuilder,既可以通过改变".obs "变量进行响应式改变,也可以通过update()进行手动更新。然而,在4个widget中,他是消耗资源最多的一个
注意:
上面的1和2在被观察的值改变后widiget可以自动更新,而3需要手动调用update()方法,但是update可以更精细的更新指定了id的GetBuilder
效率排行:Obx>GetX>GetBuilder

依赖管理:
注册:
Get.put<S>(),可以设置是否永久保存,是否设置唯一tag用来区分相同类型的类等。只有调用此方法的widget被销毁后put的实例才会销毁
Get.lazyPut,懒加载,可以设置保存的类在被第一次调用时执行某方法,设置唯一tag,还有类似于“永久”的设置。在find该实例的widget销毁后就会销毁lazyPut保存的实例,如果fenix为false并且"smartManagement "不是 "keepFactory",那么再次进入find该实例的widget报错说找不到;如果如果fenix为true或者"smartManagement "是 "keepFactory",则会再次调用懒加载方法创建实例。
Get.putAsync,注册异步实例
Get.create
获取:
final controller = Get.find<Controller>();
// 或者
Controller controller = Get.find();
删除:
Get.delete<Controller>(); //通常你不需要这样做,因为GetX已经删除了未使用的控制器。

智能管理:
如果你想改变GetX控制类的销毁方式,你可以用SmartManagement类设置不同的行为。
使用:
SmartManagement.full:这是默认的。
SmartManagement.onlyBuilders:只有在init:中启动的控制器或用Get.lazyPut()加载到Binding中的控制器才会被销毁
SmartManagement.keepFactory:它被设计成在没有Bindings的情况下使用,或者在GetMaterialApp的初始Binding中链接一个Binding。
注释:
如果你使用多个Bindings,不要使用SmartManagement.keepFactory。它被设计成在没有Bindings的情况下使用,或者在GetMaterialApp的初始Binding中链接一个Binding。
使用Bindings是完全可选的,你也可以在使用给定控制器的类上使用Get.put()和Get.find()。 然而,如果你使用Services或任何其他抽象,我建议使用Bindings来更好地组织。

Flutter状态管理终极方案GetX第三篇-依赖注入

小熊爸爸阅读(54)

为什么要使用依赖注入

依赖注入是什么

本来接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。

依赖注入的目的

依赖注入是为了将依赖组件的配置和使用分离开,以降低使用者与依赖之间的耦合度。

依赖注入的好处

实现依赖项注入可为您带来以下优势:

  • 重用代码 更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。
  • 易于重构 依赖项的创建分离,可以在创建对象时或编译时进行检查、修改,一处修改,使用处不需修改。
  • 易于测试 类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。

举个例子

老王的玛莎拉蒂需要换个v8引擎,他是自己拼装个引擎呢还是去改装店买一个呢? 如果自己拼装个,引擎的构造更新了,他需要学习改进自己的技术,买新零件,而直接买一个成品,就是依赖注入。

class Car(private val engineParts: String,val enginePiston: String) {

    fun start() {
        val engine= Engine(engineParts,enginePiston)
        engine.start()
    }
}

class Engine(private val engineParts: String,val enginePiston: String){
}

上面代码中的 Engine 类如果构造方法变动了,也需要去 Car 类里更改。而使用依赖注入就不需要改动 Car 类。

手动实现依赖注入通常有两种,构造函数传入和字段传入。 构造方法:

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

字段传入:

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

上面虽然实现了依赖注入,但是增加了样板代码,如果注入实例多,也很麻烦。Android 上有 DaggerHilt 实现自动注入, GetX 也给我们提供了 Binding 类实现。

使用依赖注入

Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到 Controller 或者需要依赖的类,不需要提供上下文,不需要在 inheritedWidget 的子节点。

注入依赖:

Get.put<PutController>(PutController());

获取依赖:

Get.find<PutController>();

就是这么简单。

Get.put()

这是个立即注入内存的注入方法。调用后已经注入到内存中。

Get.put<S>(
  // 必备:要注入的类。
  // 注: S 意味着它可以是任何类型的类。
  S dependency

  // 可选:想要注入多个相同类型的类时,可以用这个方法。
  // 比如有两个购物车实例,就需要使用标签区分不同的实例。
  // 必须是唯一的字符串。
  String tag,

  // 可选:默认情况下,get会在实例不再使用后进行销毁
  // (例如:一个已经销毁的视图的Controller)
  // 如果需要这个实例在整个应用生命周期中都存在,就像一个sharedPreferences的实例。
  // 默认值为false
  bool permanent = false,

  // 可选:允许你在测试中使用一个抽象类后,用另一个抽象类代替它,然后再进行测试。
  // 默认为false
  bool overrideAbstract = false,

  // 可选:允许你使用函数而不是依赖(dependency)本身来创建依赖。
  // 这个不常用
  InstanceBuilderCallback<S> builder,
)

permanent是代表是否不销毁。通常Get.put()的实例的生命周期和 put 所在的 Widget 生命周期绑定,如果在全局 (main 方法里)put,那么这个实例就一直存在。如果在一个 Widget 里 put ,那么这个那么这个 Widget 从内存中删除,这个实例也会被销毁。注意,这里是删除,并不是dispose,具体看上一篇最后的部分。

Get.lazyPut

懒加载一个依赖,只有在使用时才会被实例化。适用于不确定是否会被使用的依赖或者计算高昂的依赖。类似 Kotlin 的 Lazy 代理。

  Get.lazyPut<LazyController>(() => LazyController());

LazyController 在这时候并不会被创建,而是等到你使用的时候才会被 initialized,也就是执行下面这句话的时候才 initialized

Get.find<LazyController>();

在使用后,使用时的 Wdiget 的生命周期结束,也就是这个 Widgetdispose,这个实例就会被销毁。

如果在一个 Widget 里 find,然后退出这个 widget,此时这个实例也被销毁,再进入另一个路由的 Widget,再次 find,GetX会打印错误信息,提醒没有 put 。及时全局注入,也一样。可以理解为, Get.lazyPut 注入的实例的生命周期是和在Get.find时的上下文所绑定。

如果想每次 find 获取到不同的实例,可以借助fenix参数。

Get.lazyPut<S>(
  // 必须:当你的类第一次被调用时,将被执行的方法。
  InstanceBuilderCallback builder,

  // 可选:和Get.put()一样,当你想让同一个类有多个不同的实例时,就会用到它。
  // 必须是唯一的
  String tag,

  // 可选:下次使用时是否重建,
  // 当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例,
  // 就像 bindings api 中的 SmartManagement.keepFactory 一样。
  // 默认值为false
  bool fenix = false

)

Get.putAsync

注入一个异步创建的实例。比如SharedPreferences

  Get.putAsync<SharedPreferences>(() async {
    final sp = await SharedPreferences.getInstance();
    return sp;
  });

作用域参考Get.put

Get.create

这个方法可以创建很多实例。很少用到。可以当做Get.put

Bindings类

上面实现了依赖注入和使用,但是和前面讲的手动注入一样,为了生命周期和使用的 Widget 绑定,需要在 Widget 里注入和使用,并没有完全解耦。要实现自动注入,我们就需要这个类。

这个包最大的区别之一,也许就是可以将路由、状态管理器和依赖管理器完全集成。 当一个路由从Stack中移除时,所有与它相关的控制器、变量和对象的实例都会从内存中移除。如果你使用的是流或定时器,它们会自动关闭,我们不必担心这些。Bindings 类是一个解耦依赖注入的类,同时 Binding 路由到状态管理器和依赖管理器。 这使得 GetX 可以知道当使用某个控制器时,哪个页面正在显示,并知道在哪里以及如何销毁它。 此外,Bindings 类将允许我们利用 SmartManager 配置控制。

  • 创建一个类并实现Binding
class InjectSimpleBinding implements Bindings {}

因为Bindings是抽象方法,所以要ide会提示要实现dependencies。在里面注入我们需要的实例:

class InjectSimpleBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<Api>(() => Api());
    Get.lazyPut<InjectSimpleController>(() => InjectSimpleController());
  }
}
  • 通知路由,我们要使用该 Binding 来建立路由管理器、依赖关系和状态之间的连接。

这里有两种方式,如果使用的是命名路由表:

    GetPage(
      name: Routes.INJECT,
      page: () => InjectSimplePage(),
      binding:InjectSimpleBinding(),
    ),

如果是直接跳转:

Get.to(InjectSimplePage(), binding: InjectSimpleBinding());

现在,我们不必再担心应用程序的内存管理,Get将为我们做这件事。

上面我们注入依赖解耦了,但是获取还是略显不方便,GetX 也为我们考虑到了。GetView完美的搭配 Bindings。

class InjectSimplePage extends GetView<InjectSimpleController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MyPage')),
      body: Center(
        child: Obx(() => Text(controller.obj.toString())),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.getAge();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

这里完全没有Get.find,但是可以直接使用controller,因为GetView里封装好了:

abstract class GetView<T> extends StatelessWidget {
  const GetView({Key key}) : super(key: key);

  final String tag = null;

  T get controller => GetInstance().find<T>(tag: tag);

  @override
  Widget build(BuildContext context);
}

不在需要 StatelessWidget 和 StatefulWidget。这也是开发最常用的模式,推荐大家使用。

当然,也许有时候觉得每次声明一个 Bingings 类也很麻烦,那么可以使用 BindingsBuilder ,这样就可以简单地使用一个函数来实例化任何想要注入的东西。

  GetPage(
    name: '/details',
    page: () => DetailsView(),
    binding: BindingsBuilder(() => {
      Get.lazyPut<DetailsController>(() => DetailsController());
    }),

就是这么简单,Bingings 都不需要创建。两种方式都可以,大家根据自己的编码习惯选择最适合的风格。

Bindings的工作原理

Bindings 会创建过渡性工厂,在点击进入另一个页面的那一刻,这些工厂就会被创建,一旦路由过渡动画发生,就会被销毁。 工厂占用的内存很少,它们并不持有实例,而是一个具有我们想要的那个类的 形状的函数。 这在内存上的成本很低,但由于这个库的目的是用最少的资源获得最大的性能,所以Get连工厂都默认删除。

智能管理

GetX 默认情况下会将未使用的控制器从内存中移除。 但是如果想改变GetX控制类的销毁方式怎么办呢,可以用SmartManagement 类设置不同的行为。

如何改变

如果想改变这个配置(通常不需要),就用这个方法。

void main () {
  runApp(
    GetMaterialApp(
      smartManagement: SmartManagement.onlyBuilders //这里
      home: Home(),
    )
  )
}
  • SmartManagement.full

这是默认的。销毁那些没有被使用的、没有被设置为永久的类。在大多数情况下,我们都使用这个,不需要更改。

  • SmartManagement.onlyBuilders

使用该选项,只有在init:中启动的控制器或用Get.lazyPut()加载到Binding中的控制器才会被销毁。

如果使用Get.put()或Get.putAsync()或任何其他方法,SmartManagement 没有权限也就是不能移除这个依赖。
  • SmartManagement.keepFactory

就像SmartManagement.full一样,当它不再被使用时,它将删除它的依赖关系,但它将保留它们的工厂,这意味着如果再次需要该实例,它将重新创建该依赖关系。

Flutter状态管理终极方案GetX第二篇——状态管理

小熊爸爸阅读(52)

GetX状态管理

说状态管理到底在说些什么

Flutter 应用是声明式的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态。当 Flutter 应用的状态发生改变时(例如,用户在设置界面中点击了一个开关选项)改变了状态,这将会触发用户界面的重绘。

一个应用的状态就是当这个应用运行时存在于内存中的所有内容。当然许多状态,例如纹理、动画状态等,框架本身会替我管理,所以对于状态更合适的定义是“当任何时候你需要重建你的用户界面时你所需要的数据”,我们需要自己管理的状态可以分为两种概念类型:短时 (ephemeral) 状态和应用 (app) 状态。

短时状态

短时状态是可以完全包含在一个独立 widget 中的状态,也成为局部状态。

  • 一个 PageView 组件中的当前页面
  • 一个复杂动画中当前进度
  • 一个 BottomNavigationBar 中当前被选中的 tab
应用状态

如果在应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。

  • 用户选项
  • 登录信息
  • 一个社交应用中的通知
  • 一个电商应用中的购物车
  • 一个新闻应用中的文章已读/未读状态

为什么选择 GetX 做状态管理?

开发者一直致力于业务逻辑分离的概念,Flutter 也有利用 BLoc 、Provider 衍生的 MVC、MVVM 等架构模式,但是这几种方案的状态管理均使用了上下文(context),需要上下文来寻找InheritedWidget,这种解决方案限制了状态管理必须在父子代的 widget 树中,业务逻辑也会对 View 产生较强依赖。

而 GetX 因为不需要上下文,突破了InheritedWidget的限制,我们可以在全局和模块间共享状态,这正是 BLoc 、Provider 等框架的短板。

另外 GetX 控制器也是有生命周期的,例如当我们需要业务层进行 APIREST 时,我们可以不依赖于界面中的任何东西。可以使用onInit来启动http调用,当数据到达赋值给变量后,利用 GetX 响应式的特性,使用该变量的 Widgets 将在界面中自动更新。这样在 UI层只需要写界面,除了用户事件(比如点击按钮)之外,不需要向业务逻辑层发送任何东西。


GetX 的状态管理

Get具有两个不同的状态管理器: 简单状态管理器(称为GetBuilder)。 响应式状态管理器(称为GetX)。 除此之外还有一个最为简单的响应式状态管理器:Obx;

简单状态管理器

使用 StatefulWidget 意味着我们在非必要地存储整个屏幕的状态,StatefulWidget 类是一个比StatelessWidget 大的类,它将分配更多的内存,这在一个或两个类之间可能不会产生很大的区别,但是我们的应用会拥有上百个 Widget。

GetBuilder 状态管理器非常轻巧且易于使用。我们不再需要StatefulWidget。除非需要使用诸如 TickerProviderStateMixin 之类的 mixin,否则完全不需要将 StatefulWidget 与 GetX 一起使用。

我们可以直接从 GetBuilder 调用 StatefulWidget 的所有方法。例如,如果需要调用initState()或dispose()方法,则可以直接调用它们。

GetBuilder<Controller>(
  initState: (_) => Controller.to.fetchApi(),
  dispose: (_) => Controller.to.closeStreams(),
  builder: (s) => Text('${s.username}'),
),
响应式状态管理器

当我们需要对正在更新的内容进行更多控制时,GetX()在合适不过了。 GetBuilder 是一个简单的状态更新程序(类似于setState()),只需几行代码即可完成。重点是使CPU影响最小,并且仅实现一个目的:状态重建,并花费尽可能少的资源。 响应式状态管理器里面的一切都是由 Stream 数据流操作的。可以使用rxDart并可以订阅数据的改变。

var name = '新垣结衣';

把一个变量变得可观察,每次改变,使用它的小部件都会更新:

var name = '新垣结衣'.obs;

就这么简单,这个变量已经是响应式了。每次改变,下面的小部件就会更新。

Obx (() => Text (controller.name));

上面的代码在后台做了什么?创造了 String 类型的 Stream,分配了初始值 新垣结衣,.obs把字符串变成了 RxString ,当其中的字符串改变,就会通知到使用它的小部件重建。

简单使用

对于以前使用过 ChangeNotifier 的同学来说,可以把GetxController当做ChangeNotifier,我们使用计数器示例来演示一下基本使用:


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimpleController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">GetxController</span> </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;
  <span class="hljs-built_in">int</span> <span class="hljs-keyword">get</span> counter => _counter;

  <span class="hljs-keyword">void</span> increment() {
    _counter++;
    update();
  }
}

这是一个控制器,有 UI 需要的数据counter和用户点击一次加1的方法。

在 UI 层一个展示的文本和一个按钮:

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimplePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'SimplePage--build'</span>);
    <span class="hljs-keyword">return</span> GetBuilder<SimpleController>(
        init: SimpleController(),
        builder: (controller) {
          <span class="hljs-keyword">return</span> Scaffold(
            appBar: AppBar(title: Text(<span class="hljs-string">'Simple'</span>)),
            body: Center(
              child: Text(controller.counter.toString()),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                controller.increment();
              },
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

使用了GetBuilder这个 Widget 包裹了页面,在 init初始化SimpleController,然后每次点击,都会更新builder对应的 Widget ,GetxController通过update()更新GetBuilder

这看起来和别状态管理框架并无不同,有时我们只想重新 build 需要变化的部分,遵循最小原则,那么我们改下GetBuilder的位置,只包裹 Text:

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('SimplePage--build');
    return Scaffold(
      appBar: AppBar(title: Text('Simple')),
      body: Center(
        child: GetBuilder<SimpleController>(
            init: SimpleController(),
            builder: (controller) {
              return Text(controller.counter.toString());
            }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

因为 controlle作用域问题,此时按钮里面的 controller会找不到,GetX强大的一点的就表现出来了,按钮和文本并不在父子组件,并且和GetBuilder不在一个作用域,但是我们依然能正确得到:

  onPressed: () {
          Get.find<SimpleController>().increment();
          <span class="hljs-comment">// controller..increment();</span>
        },

GetxController也有生命周期的:

class SimpleController extends GetxController {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    update();
  }

  @override
  void onInit() {
    super.onInit();
    print('SimpleController--onInit');
  }

  @override
  void onReady() {
    super.onReady();
    print('SimpleController--onReady');
  }

  @override
  void onClose() {
    super.onClose();
    print('SimpleController--onClose');
  }
}

之前在这里打印了一句:

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('SimplePage--build');
    return Scaffold(
    。。。

再次打开这个页面,控制台输出:

flutter: SimplePage--build
flutter: SimpleController--onInit
[GETX] <span class="hljs-string">SimpleController</span> has been initialized
flutter: SimpleController--onReady
SimplePage-build->SimpleController-onInit->SimpleController-onReady

返回:

[GETX] CLOSE TO ROUTE /SimplePage
flutter: SimpleController--onClose
[GETX] <span class="hljs-string">SimpleController</span> onClose() called
[GETX] <span class="hljs-string">SimpleController</span> deleted from memory
[GETX] Instance <span class="hljs-string">SimpleController</span> already removed.

可以看到SimpleController已经被删除。

局部更新

可以多种状态分别更新,我们不需要为每个状态创建一个类。

再添加一个变量:

  int _counter = 0;
  int get counter => _counter;

  String _name = Lili;
  String get firstName => _name;

    void increment() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update(['counter']);
  }

  void changeName() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update(['name']);
  }

两个方法都改变这两个变量,但是注意update(['counter']里添加了id数组,这样就职更新这个 id 对应的GetBuilder:

    GetBuilder<SimpleAdvancedController>(
            id: 'counter',
            builder: (ctl) => Text(ctl.counter.toString()),
          ),
          SizedBox(
            height: 50,
          ),
          GetBuilder<SimpleAdvancedController>(
            id: 'name',
            builder: (ctl) => Text(ctl.firstName),
          ),

响应式更新

我们都用过 StreamController ,然后以流的方式发送数据。在 GetX 可以实现同样的功能,并且实现起来只有几个单词,不需要为每个观察的对象创建一个 StreamController ,也不需要创建 StreamBuilder。

下面写个计算器的例子:

 final count1 = 0.obs;
 final count2 = 0.obs;

.obs就实现了一个被观察者,他们不再是 int 类型,而是 RxInt类型。对应的小部件也不再是GetBuilder了,而是下面两种:

           GetX<SumController>(
                  builder: (_) {
                    print(count1 rebuild);
                    return Text(
                      '${_.count1}',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    );
                  },
                ),
               Obx(() => Text(
                      '${Get.find<SumController>().count2}',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    )),

因为是响应式,不再需要update,每次更改值,都自动刷新。但是更神奇的是,他们的运算和也是响应式的:

  int get sum => count1.value + count2.value;

只要更新count1或者count2使用sum的小部件也会更改:

    Obx(() => Text(
                      '${Get.find<SumController>().sum}',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    )),

非常简单的使用方式,不是吗?除了使用.obs还有2种方法把变量变成可观察的:

  1. 第一种是使用 Rx{Type}。
// 建议使用初始值,但不是强制性的
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});
  1. 第二种是使用 Rx,规定泛型 Rx。
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0)
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});

// 自定义类 - 可以是任何类
final user = Rx<User>();

将一个对象转变成可观察的,也有2种方法:

  1. 可以将我们的类值转换为 obs
class RxUser {
  final name = Camila.obs;
  final age = 18.obs;
}
  1. 或者可以将整个类转换为一个可观察的类。
class User {
  User({String name, int age});
  var name;
  var age;
}

//实例化时。
final user = User(name: Camila, age: 18).obs;

注意,转化为可观察的变量后,它的类型不再是原生类型,所以取值不能用变量本身,而是.value

当然 GetX 也提供了 api 简化对 int、List 的操作。此外,Get还提供了精细的状态控制。我们可以根据特定的条件对一个事件进行条件控制(比如将一个对象添加到List中):

// 第一个参数:条件,必须返回true或false。
// 第二个参数:如果条件为真,则为新的值。
list.addIf(item < limit, item);

响应式编程虽好,可不要贪杯。因为响应式对 RAM 的消耗比较大,因为他们的实现都是流,如果创建一个有80个对象的List,每个对象都有几个流,打开dart inspect,查看一个StreamBuilder的消耗量,我们就会明白这不是一个好的方法。而 GetBuilder在 RAM 中是非常高效的,几乎没有比他更高效的方法。所以这些使用方式在使用过程中要斟酌。

Workers

响应式不只这些好处,还有一个 Workers ,将协助我们在事件发生时触发特定的回调,也就是 RxJava 的一些操作符;


  @override
  onInit() {
    super.onInit();

    /// 每次更改都会回调
    ever(count1, (_) => print($_ has been changed));

    /// 第一次更改回调
    once(count1, (_) => print($_ was changed once));

    /// 更改后3秒回调
    debounce(count1, (_) => print(debouce$_), time: Duration(seconds: 3));

    ///3秒内更新回调一次
    interval(count1, (_) => print(interval $_), time: Duration(seconds: 3));
  }

我们可以利用 Workers ,去实现写一堆对代码才能实现的功能。比如防抖函数,在搜索的时候使用,节流函数,在点击事件的时候使用。

跨路由

上面演示过在同一个页面兄弟组件跨组件使用,这里实现下不同页面跨组件,首先在CrossOnePage里 put 一个 Controller:

class CrossOnePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    CrossOneController controller = Get.put(CrossOneController());
...
}}

然后去到CrossTwoPage,打印下签名页面 put 的控制器:

  CheetahButton('打印CrossOneController的age', () {
            print(Get.find<CrossOneController>().age);
          }),

正常输出。

那么CrossOneController的生命周期多久呢?如果像第一个页面一样是在build里 put 的,那么当前页面退出就销毁了。如果是成员变量,那么当前页面的引用销毁才会销毁:

class CrossTwoPage extends StatelessWidget {
  final CrossTwoSecondController controller = Get.put(CrossTwoSecondController());
  @override
  Widget build(BuildContext context) {
    Get.put(CrossTwoController());
    return Scaffold(
      appBar: AppBar(title: Text('CrossTwoPage')),
      body: Container(
          child: Column(
        children: [
          CheetahButton('打印CrossTwoController', () {
            print(Get.find<CrossTwoController>());
          }),
          CheetahButton('CrossTwoSecondController', () {
            print(Get.find<CrossTwoSecondController>());
          }),
          CheetahButton('打印CrossOneController的age', () {
            print(Get.find<CrossOneController>().age);
          }),
        ],
      )),
    );
  }
}

CrossTwoSecondController是成员变量,CrossTwoController是在build的时候 put 进去的,现在打印2个控制器,都能打印出来:

[GETX] CrossTwoSecondController has been initialized
[GETX] GOING TO ROUTE /CrossTwoPage
[GETX] CrossTwoController has been initialized
I/flutter (16952): Instance of 'CrossTwoController'
I/flutter (16952): Instance of 'CrossTwoSecondController'

现在返回第一个页面,GetX 已经给我们打印了:

GETX] CLOSE TO ROUTE /CrossTwoPage
[GETX] CrossTwoController onClose() called
[GETX] CrossTwoController deleted from memory

然后我们在第一个页面点击按钮,分别打印页面2的2个控制器:


════════ Exception caught by gesture ═══════════════════════════════════════════
CrossTwoController not found. You need to call Get.put(CrossTwoController()) or Get.lazyPut(()=>CrossTwoController())
════════════════════════════════════════════════════════════════════════════════
I/flutter (16952): Instance of 'CrossTwoSecondController'

build里 put 的控制器已经销毁为 null 了,另一个依然存在,那是不是这种不会销毁呢?因为第一个页面的路由依然持有第二个页面,第二个页面的实例还在内存中,所以控制器作为成员变量依然存在,退出第一个页面,自然就销毁了:

[GETX] CLOSE TO ROUTE /CrossOnePage
[GETX] CrossOneController onClose() called
[GETX] CrossOneController deleted from memory
[GETX] CrossTwoSecondController onClose() called
[GETX] CrossTwoSecondController deleted from memory

flutter plugin not installed this adds flutter specific functionalityflutter教程flutter教程

Flutter状态管理终极方案GetX第一篇——路由

小熊爸爸阅读(81)

undefined
undefined
undefined
undefined
代码示例

前言

GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。

为什么是 GetX,而不是 BLoC、MobX、Provider?

BLoC 非常安全和高效,但是对于初学者来说非常复杂,即使学会,样板代码也很多。

MobX 比 BLoC 更容易,而且是响应式的,但是需要使用一个代码生成器,需要等很久,这降低了生产力。

GetX我喜欢的地方:

  • 轻量。模块单独编译,没用到的功能不会编译进我们的代码。
  • 语法简洁。个人非常喜欢,显而易见且实用,比如路由摆脱了 context 的依赖,Get.to(SomePage())就能导航到新路由。
  • 性能。Provider、BLoC 等只能在父子组件保存状态,同层级模块状态管理需要全局处理,存活在整个应用生命周期。而 GetX 可以随时添加控制器和删除控制器,并且会自动释放使用完的控制器。
  • 依赖注入。提供依赖注入功能,代码层级可以完全分离,甚至依赖注入的代码也是分离的。
  • 丰富的api。许多复杂的操作,使用 GetX 就会有简单的实现。

有的同学看过我写的Flutter状态管理provider的使用和封装,讲解了 Provider 的使用,其实在使用过程中发现了许多痛点,最致命的是 Provider 使用InheritedWidget 来传递相同的监听器,这意味着对其 ChangeNotifier 类的任何访问都必须在父子widget树内。非父子组件的状态管理问题,需要借助别的手段(eventbus,全局,单例),十分痛苦,在改用GetX后,越来越舒服了。


路由

普通路由导航

打开到新的页面:

Get.to(NextScreen());

对应原生路由:

    Navigator.push(context, MaterialPageRoute<<span class="hljs-keyword">void</span>>(
      builder: (BuildContext context) {
        <span class="hljs-keyword">return</span> NextScreen();
      },
    ));

返回:

Get.back();

对应原生路由:

    Navigator.pop(context);

打开新页面,并且用新页面替换旧页面(删除旧页面):

Get.off(NextScreen());

对应原生路由:

 Navigator.pushReplacement(context, MaterialPageRoute<<span class="hljs-keyword">void</span>>(
      builder: (BuildContext context) {
        <span class="hljs-keyword">return</span> NextScreen();
      },
    ));

打开新页面并删除之前的所有路由:

Get.offAll(NextScreen());

对应原生路由:

   Navigator.pushAndRemoveUntil(
      context,
      MaterialPageRoute<<span class="hljs-keyword">void</span>>(
        builder: (BuildContext context) {
          <span class="hljs-keyword">return</span> NextScreen();
        },
      ),
      (Route<<span class="hljs-built_in">dynamic</span>> route) => <span class="hljs-keyword">false</span>,
    );

导航到新页面,在返回时接收返回数据:

<span class="hljs-keyword">var</span> data = <span class="hljs-keyword">await</span> Get.to(NextScreen());

对应原生路由:

    <span class="hljs-keyword">var</span> data = <span class="hljs-keyword">await</span>  Navigator.push(context, MaterialPageRoute<<span class="hljs-keyword">void</span>>(
      builder: (BuildContext context) {
        <span class="hljs-keyword">return</span> NextScreen();
      },
    ));

带返回值返回前一个路由,配合上面使用:

Get.back(result: <span class="hljs-string">'success'</span>);

对应原生路由:

 Navigator.pop(context, <span class="hljs-string">'success'</span>);

别名路由导航

  1. 声明别名:
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Routes</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> Initial = <span class="hljs-string">'/'</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> NextScreen = <span class="hljs-string">'/NextScreen'</span>;

}
  1. 注册路由表:
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppPages</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> pages = [
    GetPage(
      name: Routes.Initial,
      page: () => HomePage(),
    ),
    GetPage(
      name: Routes.NextScreen,
      page: () => NextScreen(),
    ),
  ];
}
  1. 替换MaterialApp为GetMaterialApp:
void main() {
  runApp(GetMaterialApp(
    debugShowCheckedModeBanner: false,
    initialRoute: '/',
    theme: appThemeData,
    defaultTransition: Transition.fade,
    getPages: AppPages.pages,
    home: HomePage(),
  ));
}

使用

导航到下一个页面:

Get.toNamed(Routes.NextScreen);

导航到下一个页面并删除前一个页面:

Get.offNamed(Routes.NextScreen);

导航到下一个页面并删除以前所有的页面:

Get.offAllNamed(Routes.NextScreen);

发送数据到别名路由:

Get在这里接受任何东西,无论是一个字符串,一个Map,一个List,甚至一个类的实例。

Get.toNamed(Routes.NextScreen, arguments: '新垣结衣');

获取参数:

String name=Get.arguments;

动态网页链接:

像web一样携带参数,适合前端开发的风格。

Get.offAllNamed(/NextScreen?device=phone&id=354&name=Enzo);

获取参数:

int id = Get.parameters['id'];
// out: 354
String name=Get.parameters['name'];

还可以这样定义路由别名:

       GetPage(
        name: '/profile/:user',
        page: () => UserProfile(),
      ),

导航:

Get.toNamed(/profile/34954);

在第二个页面上,通过参数获取数据

print(Get.parameters['user']);
// out: 34954

中间件

在跳转前做些事情,比如判断是否登录,可以使用routingCallback来实现:

GetMaterialApp(
  routingCallback: (routing) {
    if(routing.current == '/second'){
     // 如果登录。。。
    }
  }
)

小部件导航

SnackBars

弹出:

Get.snackbar('Hi', 'i am a modern snackbar');

对应原生写法:

final snackBar = SnackBar(
  content: Text('Hi!'),
  action: SnackBarAction(
    label: 'I am a old and ugly snackbar',
    onPressed: (){}
  ),
);

//用Flutter创建一个简单的SnackBar,你必须获得Scaffold的context,或者你必须使用一个GlobalKey附加到你的Scaffold上。

Scaffold.of(context).showSnackBar(snackBar);

Dialogs

打开一个默认的Dialog:

Get.defaultDialog(
  onConfirm: () => print(Ok),
  middleText: Dialog made in 3 lines of code
);

打开自定义的Dialog:

Get.dialog(YourDialogWidget());

BottomSheets

Get.bottomSheet类似于showModalBottomSheet,但不需要context:

Get.bottomSheet(
  Container(
    child: Wrap(
      children: <Widget>[
        ListTile(
          leading: Icon(Icons.music_note),
          title: Text('Music'),
          onTap: () => {}
        ),
        ListTile(
          leading: Icon(Icons.videocam),
          title: Text('Video'),
          onTap: () => {},
        ),
      ],
    ),
  )
);

大前端WP主题 更专业 更方便

联系我们联系我们