一个方便好用的 Flutter 框架 — GetX 的介绍和使用

1. 介绍

Flutter 是一个强大的跨平台移动应用开发框架,它易于学习并构建漂亮的用户界面。 但是,如果你是一个经验丰富的 C#/Java 程序员(就是我~哈哈),也许你会有点不适应,因为 Flutter 是一个响应式编程框架,响应式编程是一种基于异步事件处理和数据流的声明式编程方式。 简单地说就是你要改变一个界面上的元素,并不是像传统语言写法那样,按着顺序写,如:

txtLabel.value = "";

void onClick() {
    txtLabel.value = "你点击了按钮";
}

以上示例是传统语言的常见用法,点击一下就改变了 label 的值,但在 Flutter 中可不是直接这样写就能改变界面元素的,而是通过改变变量的值,然后使用状态管理重建界面元素,如上面的例子中,要改变界面 AppBar 的标题内容:

String _label = "欢迎"; //先定义一个标签变量

Scaffold(
    appBar: AppBar(
    title: Text(_label),
),

按传统语言逻辑,会直接改变 AppBartitle 属性, 但这个在 Flutter 是做不到的,因为你根本获取不了这个对象,就算你获取到也更改了,但界面也不会被刷新的,这时就要用到状态管理,要在点击事件中直接改变 _label 变量的值,然后再反过来刷新界面,而要做到刷新界面就要使用 setState 方法,如下:

void _onClick() {
    setState(() {
      _label = "你好,代码部落!";
    });
}

这就是响应式编程语言的特点,但它的刷新只是局部的,就是你哪里改变就只刷新哪里,这也能提高整体的效率,不用每次都重载整个页面。

所以,如果你想在 Flutter 中更新 UI,就需要更新状态来刷新布局,编程逻辑与 C# 或 Java 会有所不同,但幸运的是有很多状态管理框架可以帮助做到这一点,如 GetX、MobX、BLoC、Redux、Provider 等,不过这些框架中除了 GetX 之外,其他都不那么容易理解和使用。

2. 为什么是 GetX

GetX 专注于性能和最小资源消耗。 GetX 的关键性能优势之一在于其开销非常小。 通过最大限度地减少不必要的重新渲染和重建小部件,GetX 显著地减轻了应用程序的计算负担,从而加快了渲染速度并提高了整体性能。

此外,GetX 利用了高效依赖注入的力量。 其轻量级依赖注入机制可以在不影响性能的情况下创建和管理依赖项。 通过有效管理依赖关系,GetX 有助于消除不必要的对象实例化并确保高效的内存利用。

同时 GetX 简单易用,很容易理解,使用过程中比较接近于传统语言的逻辑。接下来就让我们看看如何使用它吧

3. 基本运用

3.1 安装

添加以下依赖到 pubspec.yaml 文件:

dependencies:
    get: 4.6.6

然后在你的 dart 文件里导入相关的包

import 'package:get/get.dart';

3.2 基本架构

GetX 使用的是 MVC 的架构,即可以明确区分了模型、视图和控制器,这也是我喜欢的架构之一。这样做好处是可以很好地将界面布局与后台逻辑分开,因此你可以在控制器里写主要的逻辑代码,然后在视图中调用。如果你学习过 ASP.NET MVC ,对此就绝对不会陌生了。

如以下的简单示例(我之后会另有文章详细说明)

//controller 类继承自 GetxController
class HomeController extends GetxController {
    var title = '';
}

//view 视图类,主要负责界面布局
class HomePage extends GetView<HomeController> {
 @override
  Widget build(BuildContext context) {
    return MainLayout(
      appBar: AppBar(
        title: Text(controller.title),
        centerTitle: true,
      ),
    )
  }
}

可以看到,要在视图调用控制器变量,只需直接使用 controller.变量 即可

3.3 状态管理

GetX 的使用很简单,例如,你可直接定义一个变量,然后在其默认值后而添加 .obs,此时这就变成了一个 GetX 变量, 如:

var isEnabled = false.obs;

...

//改变其值
isEnabled.value = true;

之后在界面布局中使用 Obx(() => ) 以监测其改动

return Scaffold(
      appBar: AppBar(
        title: const Text('Test'),
        centerTitle: true,
      ),
      body: Stack(
        children: [
          Obx(
            () => controller.isEnabled.value 
                ? ListView(
                    children: <Widget>[
                     ...
                    ],
                  )
                : const SizedBox.shrink(),
          ),
        ],
      ), 
    );
  }

以上的例子中,只要 isEnabled 的值被更新为 true 后(不需要使用 setState ),那么在界面布局中就显示出 ListView 来,否则就什么也不显示,这是不是很简单? 🙂

3.4 路由管理

导航在一个应用中是非常重要的, GetX 可以帮助你轻松地管理它,以下是其一些用法:

//导航到下一页
Get.to(NextPage());

//根据路由名称进行导航
Get.tonamed('/next');

//返回到前一页,或者关闭如 snackbar, dialog 之类
Get.back();

//进入到下一页,但不提供返回上一页的按钮,一般可用在启动页或者登入页
Get.off(NextPage());

//进入下一页同时取消所有之前的路由,通常可用在购物车、投票或者测试等页面
Get.offAll(NextPage());

Ok, 这些只是一部分,你可以到官方网站查看更多详细的用法,而接下来我将继续告诉你其他的使用技巧 🙂

4. 其他的用法

对于 GetX 的基本使用,其实官方已有很好的说明,在这里我就不再一一述说了,而我想介绍的是在实际项目中的一些使用技巧和问题

4.1 在 bottomNavigationBar 中使用

在大部分时间里,我们只需在应用启动时,在 main.dart 里绑定相关的依赖,但如果你在使用 bottomNavigationBar 的话就要注意了,你同时还要在导航的主页面绑定相关的依赖,如下面的例子

如下导航文件结构, dashboard 是整个框架主页面, home 是第一个 tab,然后里面有2个产品页面,每个产品页面又再有一个明细页面,而 settings 就是第二个 tab:

dashboard (bottomNavigationBar 的主页面)
    -- home  
        -- product1
            -- product1_detail
        -- product2
            -- product1_detail
    -- settings

因此,我们也要在 dashboard 里绑定相关依赖

class DashboardBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<DashboardController>(
      () => DashboardController(),
    );
    Get.lazyPut<HomeController>(
      () => HomeController(),
    );
    Get.lazyPut<Product1Controller>(
      () => Product1Controller(),
    );
    Get.lazyPut<Product2Controller>(
      () => Product2Controller(),
    );
  }
}

这时如果你想从产品1导航其明细页面,即按以下顺序:

home ==> product1  ==>  product1_detail

你就必须要在进入明细页面前再次绑定它,如下是产品页面点击后将要点到明细页面的代码:

return InkWell(
onTap: () {
  Get.lazyPut<Product1DetailController>(
    () => Product1DetailController(),
  );
  Get.to(() => const Product1DetailPage());
},

4.2 使用服务来做通用功能函数

GetX 的服务可以很方便地让你创建全局的通用功能函数,你可以在控制器里去处理 onInit(), onReady(), onClose() 这几个事件,同时你也可以在任何地方使用 Get.find() 获取到所要的服务。如下例子,创建一个存储器的服务以处理 SharedPreferences 相关逻辑

//创建一个服务并继承自 GetxService
class LocalStorageService extends GetxService {
Future<T?> getValue<T>(
    LocalStorageKeys key, [
    T Function(Map<String, dynamic>)? fromJson,
  ]) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    switch (T) {
      case int:
        return prefs.getInt(key.name) as T?;
      case double:
        return prefs.getDouble(key.name) as T?;
      case String:
        return prefs.getString(key.name) as T?;
      case bool:
        return prefs.getBool(key.name) as T?;
      default:
        assert(fromJson != null, 'fromJson must be provided for Object values');
        if (fromJson != null) {
          final stringObject = prefs.getString(key.name);
          if (stringObject == null) return null;
          final jsonObject = jsonDecode(stringObject) as Map<String, dynamic>;
          return fromJson(jsonObject);
        }
    }
    return null;
  }

  void setValue<T>(LocalStorageKeys key, T value) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    switch (T) {
      case int:
        prefs.setInt(key.name, value as int);
        break;
      case double:
        prefs.setDouble(key.name, value as double);
        break;
      case String:
        prefs.setString(key.name, value as String);
        break;
      case bool:
        prefs.setBool(key.name, value as bool);
        break;
      default:
        assert(
          value is Map<String, dynamic>,
          'value must be int, double, String, bool or Map<String, dynamic>',
        );
        final stringObject = jsonEncode(value);
        prefs.setString(key.name, stringObject);
    }
  }
}

然后在 main.dart 初始化服务

Get.put(LocalStorageService());

之后你就可以在任何地方获取和使用它了

//获取服务 
var localStorage = Get.find<LocalStorageService>();

...

//保存值
localStorage.setValue<String>('currentLanguage', 'en');

//获取值
var currentLanguage =
      await localStorage.getValue<String>('currentLanguage');

4.3 使用对话框

GetX 还支持使用弹出对话框,你可以轻松地使用默认的对话框:

Get.defaultDialog(
    title: '标题',
    titleStyle: const TextStyle(color: Colors.red),
    middleText: '信息');

这只是普通的对话框,如果你想使用自定义样式,你可以使用以下方式实现

Get.dialog(
  Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Padding(
        padding: const EdgeInsets.symmetric(horizontal: 40),
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(
              Radius.circular(20),
            ),
          ),
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: Material(
              child: 

              //这里放置你的其他页面布局代码

            ),
          ),
        ),
      ),
    ],
  ),
);

正如你所看到的那样,你可以放任何小部件到 Get.dialog() 里,所以其实这已不只是对话框了,你甚至可以当作一个弹出页面来使用哦 🙂

当然,你也可以将自定义的对话框做成一个服务,然后就可以随时调用了:

enum DialogType {
  info,
  success,
  error,
  warning,
}


class DialogService extends GetxService {
  showAlert(
    String title,
    String message, {
    DialogType dialogType = DialogType.info,
    Function? callback,
  }) {
    IconData iconData = Icons.info;
    Color iconColor = Colors.blueGrey;
    if (dialogType == DialogType.error) {
      iconData = Icons.error;
      iconColor = Colors.red;
    } else if (dialogType == DialogType.warning) {
      iconData = Icons.warning;
      iconColor = Colors.yellow;
    } else if (dialogType == DialogType.success) {
      iconData = Icons.done_rounded;
      iconColor = Colors.green;
    }

    Get.dialog(
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 40),
            child: Container(
              decoration: const BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.all(
                  Radius.circular(20),
                ),
              ),
              child: Padding(
                padding: const EdgeInsets.all(20.0),
                child: Material(
                  child: Column(
                    children: [
                      const SizedBox(height: 10),
                      Icon(
                        iconData,
                        color: iconColor,
                        size: 50,
                      ),
                      const SizedBox(height: 10),
                      Text(
                        title,
                        style: const TextStyle(
                            fontSize: 24, fontWeight: FontWeight.bold),
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 20),
                      Text(
                        message,
                        style: const TextStyle(fontSize: 18),
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 50),
                      //Buttons
                      Row(
                        children: [
                          Expanded(
                            child: ElevatedButton(
                              style: ElevatedButton.styleFrom(
                                foregroundColor: const Color(0xFFFFFFFF),
                                backgroundColor: Colors.blue,
                                minimumSize: const Size(0, 45),
                                shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(8),
                                ),
                              ),
                              onPressed: () {
                                callback != null ? callback() : null;
                              },
                              child: Text(
                                style: const TextStyle(fontSize: 18),
                                LabelKeys.ok.tr,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

现在你可以在任何地方使用它

final dialog = Get.find<DialogService>();


//显示普通对话框
dialog.showAlert(
  '标题',
  '这是一个普通的信息对话框'
);

//显示错误对话框
dialog.showAlert(
  '错误',
  '这是一个错误警告',
  dialogType: DialogType.error
);

5. 高级 API 用法

GetX 里还有很多高级 API 可以使用,如下面这些

// 获取当前页面中的参数,通常是前一页传递过来的路由里的参数
Get.arguments

// 前一页路由的名字
Get.previousRoute

// 获取原始路由, 如 rawRoute.isFirst()
Get.rawRoute

// 访问路由的 API
Get.routing

// 判断 snackbar 是否打开
Get.isSnackbarOpen

// 判断对话框是否打开
Get.isDialogOpen

// 判断 bottomsheet 是否打开
Get.isBottomSheetOpen

// 判断当前使用的是哪个平台
GetPlatform.isAndroid
GetPlatform.isIOS
GetPlatform.isMacOS
GetPlatform.isWindows
GetPlatform.isLinux
GetPlatform.isFuchsia

// 判断设备型号
GetPlatform.isMobile
GetPlatform.isDesktop
GetPlatform.isWeb


// 相当于: MediaQuery.of(context).size.height,
// 不可修改的
Get.height
Get.width

// 当前导航的上下文
Get.context

// 对于 snackbar/dialog/bottomsheet 的上下文,全局可用
Get.contextOverlay

...

你可以在这里找到更详细的说明。

6. 总结

GetX 功能强大,其节省了我很多时间,而且很容易理解,它还有一个非常活跃和乐于助人的社区,如果有任何问题基本上都可以在他们的社区中找到解决方案,而且还可以安装 VSCode 扩展以帮助构建 GetX 项目。 如果你对 GetX 有其他想法的话,也欢迎给我留言哦 🙂

代码部落

免费订阅以得到最新文章发布的通知

请放心,这个绝对不会是垃圾邮件
而且您随时也可以取消的

版权声明:
作者:winson
链接:https://www.coderblog.cc/2024/05/how-getx-easy-to-use/
来源:代码部落中文站
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录