GetX簡介
GetX是Flutter的輕便而強大的解決方案,它結合了高性能狀態管理,智能依賴注入和快速實用的路由管理。
更新時間:4.18
一、響應式控件
(一)Obx
關鍵詞:StatelessWidget、.obs、Obx()
[ 例:一個加法按鈕 —— 使用.obs變量和Obx對象構建響應式控件 ]
下面這個例子,是基于可觀察的變量和控件Obx構建的一個簡單視圖,點擊按鈕可以改變頁面中的數字;
該例子表明GetX框架可在頁面未進行setState()刷新的情況下,實現由build方法構建的控件的狀態更新。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// ignore: must_be_immutable
class GetXPage extends StatelessWidget {
var count = 0.obs;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Obx(() => Text("$count"))
),
floatingActionButton: FloatingActionButton(
onPressed: () => count++,
child: Icon(Icons.add),
),
);
}
}
如果沒有使用GetX框架(即變量count無后綴.obs 或Text沒有包含在控件Obx中),則點擊按鈕+,屏幕中的數字不會增加;這是由于,方法build僅執行了一次,onPressed所引起的 count值的變化 并沒有進入控件Text,因而屏幕中的數字顯示不會變化。
[ 編寫步驟 ]
- 使用StatelessWidget創建Widget對象;
- 聲明一個"可觀察變量"count,在其后綴.obs,此變量將使用于控件Obx中 ;
- 使用 控件Obx() 包裹 包含變量count的控件Text() ,常用格式為Obx( () => 目標更新狀態的控件 );
注:使用動態類型的可觀察變量無法用于有類型要求的構造傳參;
(二)GetX
關鍵詞:.obs、GetX()
[ 例:使用別人家的按鈕(一) —— 基于可觀察變量的視圖-邏輯分離 ]
使用GetX框架可以加載并使用非當前類的控制器來更新當前視圖控件的狀態,實現視圖與邏輯的分離;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Controller {
var count = 0.obs;
void increment() {
count++;
}
}
class TestGetX extends StatelessWidget {
final controller = Controller();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: EdgeInsets.all(170),
child: GetX(builder: (_) => Text('clicks: ${controller.count}')),
),
FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => controller.increment(),
),
],
),
);
}
}
通過導包本身即能夠在他類構造實例以訪問類中的變量和方法;但如果沒有使用GetX框架,方法increment所引起的 count的值變化,將無法傳遞更新到控件,實現視圖狀態刷新;除了構造實例創建控制器外,該例子的實現還需要將相應變量聲明為 "可觀察的" 。
[ 編寫步驟 ]
1、在控制器中定義 可觀察變量 ;
- 定義業務邏輯類Controller;
- 變量count將保存需要更新的數據,因而后綴".obs ";
- 方法increment能夠引起變量count的值變化;
- 變量count的"可觀察"特性,意味著當其值發生變化,變量能夠自動響應并更新值變化;
2、將 包含可觀察變量的視圖控件作為 控件GetX的子控件;
- 創建視圖頁面:使用StatelessWidget;
- 構建響應控件:將目標更新狀態或顯示變化的控件 傳入 控件GetX的屬性builder中;
3、使用控制器更新變量的值,觀察視圖控件的 狀態響應和變化;
- 創建動作控件:在FloatingActionButton的onPressed屬性中設置更新觸發
- 每次點擊將執行一次controller的方法increment;
- 當count發生變化時,由builder構建的顯示變量count的控件狀態將響應更新;
注:使用控件GetX的前提是,聲明變量可觀察,并非定義繼承GetxController及使用Get.put進行注入;
(三)GetxController & GetBuilder
關鍵詞:GetxController、update、Get.put()、GetBuilder<s>()
[ 例:使用別人家的按鈕(二) —— 使用GetBuilder構建視圖-邏輯分離的響應式控件 ]
使用GetBuilder實現響應控件與使用GetX的差別在于:后者基于變量可觀察,而前者則通過加載指定控制器來更新控件狀態;
import 'package:get/get.dart';
class Controller extends GetxController {
var count = 0;
void increment() {
count++;
update();
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'test_controller.dart';
class TestGetBuilder extends StatelessWidget {
final controller = Get.put(Controller());
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(170.0),
child: GetBuilder<Controller>(
builder: (_) => Text(
'clicks: ${controller.count}',
)),
),
FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => controller.increment(),
),
],
),
);
}
}
通過GetBuilder實現響應式控件,控制器必須繼承自GetxController,所定義的目標狀態變量之后無需后綴".obs ",但需要定義方法update;并且加載指定控制器,不僅需要使用Get.put進行注入,而且GetBuilder還需要通過指定泛型綁定目標注入的控制器。
[ 編寫步驟 ]
- 定義邏輯控制類Controller,繼承自GetxController;
- 定義變量count,保存狀態數據
- 定義方法increment,改變狀態數據
- 在方法increment中,執行方法update,更新數據狀態;
- 使用StatelessWidget創建視圖,將目標顯示變化的控件作為GetBuilder的構建對象;
- 通過 Get.put() 加載 Controlle實例,控制器將對當前Widget下的所有子路由可用;
- 將響應變化的控件傳入,泛型指定為Controller的GetBuilder的屬性builder中;
- 創建動作控件FloatingActionButton,在onPressed中設置每次觸擊將執行controller的方法increment;
- 當count發生變化,由GetBuilder構建的控件將響應變化并更新狀態;
(四)Get put & find
關鍵詞:Get.put、Get.find
[ 例:最直觀的跨組件狀態通信 —— 在這個頁面觸發事件,在另一個頁面狀態變化 ]
我們在例子-使用別人家的按鈕(二) 中,已了解到Get x框架中的Get.put可以用來注入非當前類的控制器;使用GetBuilder構建響應式控件,不僅需要通過Get.put進行控制器注入,還要求控制器必須作為GetxController實現類實例,且控件GetBuilder必須指定了泛型才能夠真正獲取到控制器,實現在當前視圖使用另一個類中定義的業務邏輯對控件狀態進行變化和更新;
但Get.find使這些步驟得到了魔術般的簡化:使用Get.find能夠發現并獲取到已經通過Get.put注入的控制器,它不僅并不要求控制器來自GetxController的實現類,也無須使用Obx、GetX、GetBuilder等控件 —— 通過Get.find獲取的控制器可以直接應用于當前Widget下的所有子路由,實現真正意義上的跨組件狀態通信。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Controller {
var count = 0;
}
class FirstPage extends StatelessWidget {
final controller = Get.put(Controller());
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Next Route'),
onPressed: () {
controller.count++;
},
);
}
}
class SecondPage extends StatelessWidget {
final Controller ctrl = Get.find();
@override
Widget build(context) {
return Scaffold(body: Center(child: Text("${ctrl.count}")));
}
}
class HomePage extends StatelessWidget {
@override
Widget build(context) => Scaffold(
body: SlidePage(),
);
}
// ignore: must_be_immutable
class SlidePage extends StatelessWidget {
List<Widget> pages = <Widget>[FirstPage(), SecondPage()];
@override
Widget build(BuildContext context) {
return PageView.custom(
scrollDirection: Axis.vertical,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return pages[index];
},
childCount: 2,
),
);
}
}
使用Get.find的局限在于,它必須應用在兩個或兩個以上的widget中,若在同一widget中將無法實現視圖的狀態響應和刷新,這是因為,Get.find的使用前提是:獲取已經通過Get.put在另一個widget完成了注入的控制器。
[ 編寫步驟 ]
1、定義業務邏輯類Controller
- 定義變量count,保存狀態數據(可以不用".obs "或方法update)
- Controller可以不必繼承GetxController
2、使用 Get.put 注入控制器,通過控制改變count數據
使用StatelessWidget創建界面
創建控制器:在Get.put()構造Controller實例controller,使后者對當下所有子路由可用;
3、使用 Get.find 獲取控制器,共享其中的數據狀態
-
調用Get.find從另一個Widget中即頁面FirstPage中,發現已注入的控制器;
Get作為GetInterface子類私有對象的引用,調用父類拓展中的find,等同于一個GetInstance調用find
通過find方法初始化控制器,并將其保存為ctrl;然后通過ctrl共享變量count中的數據狀態;
4、實現跨頁面的狀態聯動:創建滑動頁面,點擊頁面FirstPage中的Button,頁面SecondPgae中展示的數值將發生變化;
(五)Get.to
[ 例:使用Get.to實現路由跳轉 ]
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(GetMyApp());
}
// 使用Get.to來進行頁面路由,需要在GetMaterialApp下構建控件樹
class GetMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
home: GetPage(),
// SlidePage(),
);
}
}
// 創建Get式可路由頁面
class GetPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 通過RaisedButton按鈕onPressed事件觸發跳轉
return RaisedButton(
child: Text('Next Route'),
onPressed: () {
// 將目標調整頁面作為參數對象傳入Get.to;
Get.to(TargetPage());
},
);
}
}
// 創建目標跳轉頁
class TargetPage extends StatelessWidget {
@override
Widget build(context) {
return Scaffold(body: Center(child: Text("TargetPage")));
}
}
[ 整合例:使用Get.to + Get.put + Get.find 實現頁面跳轉計數 ]
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
// testPrint();
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
home: GetPageTwo(),
// SlidePage(),
);
}
}
class Controller extends GetxController {
var count = 0;
}
class GetPageTwo extends StatelessWidget {
final controller = Get.put(Controller());
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Next Route'),
onPressed: () {
controller.count++;
Get.to(Second());
},
);
}
}
class Second extends StatelessWidget {
final Controller ctrl = Get.find();
@override
Widget build(context) {
return Scaffold(body: Center(child: Text("${ctrl.count}")));
}
}
二、可伸縮應用程序
(一)Bindings
Bindings是擁有可伸縮應用程序的第一步;
Bindings是管理依賴注入的類,將依賴注入抽離出widget樹,使視圖層更純粹地用于布置widget控件,使代碼更整潔、有條理;
注入在Bindings的控制器,允許任何沒有上下文的地方被訪問;
打開頁面上的Bindings文件,將可以清楚地看將注入頁面的內容;
import 'package:get/get.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<Controller>(() => Controller());
}
}
使用作為私有實例GetInstance的引用Get,能夠調用其父類GetInterface拓展中的方法lazyPut
[ 編寫步驟 ]
- 創建一個類HomeBinding,繼承Bindings,并重寫方法dependencies(Bindings是抽象類,方法dependencies須重寫);
- 在HomeBinding使用Get.lazyPut(),惰性地注入一個Controller實例,注意類型一致;
- Bindings類的實例可與路由綁定,綁定后所注入的控制器將對所有子路由可用;
(二)GetView
GetView,是具有“ controller”屬性的StatelessWidget;
在可伸縮應用程序中使用GetView創建界面;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';
class Home extends GetView<HomeController> {
@override
Widget build(context) => Scaffold(
appBar: AppBar(title: Text("counter")),
body: Center(
child: Obx(() => Text("${controller.count}")),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: controller.increment,
));
}
[ 編寫步驟 ]
- 創建頁面類Home,繼承自GetView,后者通過指定泛型來確定目標控制器來源;
- 導入控制器類型文件,在GetView頁面中不需要構造控制器,因而也不需要Get.put()和Get.find();
- 將有數據狀態更新的控件構造放到obx()中進行,當count發生變化時,obx將自動響應接收新的Text();
- 設置操作控件FloatingActionButton,在onPressed設置每次觸擊將調用一次controller的方法increment;
(三)GetPage & GetMaterialApp
將GetMaterialApp設置為widget視圖頂端;
控件GetPage可以用來構造一個能夠設置具體依賴注入綁定的命名路由頁面;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../home_view.dart';
import '../home_bindings.dart';
void main() {
runApp(GetMaterialApp(
initialRoute: '/home',
getPages: [
GetPage(name: '/home', page: () => Home(), binding: HomeBinding()),
],
));
}
[ 編寫步驟 ]
- 使用GetMaterialApp代替MaterialApp(記住,盡可能地避免使用StatefulWidget);
- 在屬性getPages中構造路由控件GetPage,命名為'/home',它將跳轉至頁面Home;
- 屬性initialRoute,將對'/home'路由進行初始化并綁定它的依賴注入為HomeBinding;
(四)構建可伸縮應用程序的基本步驟
1、定義業務邏輯類Controller
import 'package:get/get.dart';
class HomeController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
2、定義Bindings類,注入依賴
import 'package:get/get.dart';
import 'home_controller.dart';
class HomeBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut(() => HomeController());
}
}
3、使用GetView創建界面
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';
class Home extends GetView<HomeController> {
@override
Widget build(context) => Scaffold(
appBar: AppBar(title: Text("counter")),
body: Center(
child: Obx(() => Text("${controller.count}")),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: controller.increment,
));
}
4、將GetMaterialApp設置為widget視圖頂端;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../home_view.dart';
import '../home_bindings.dart';
void main() {
runApp(GetMaterialApp(
initialRoute: '/home',
getPages: [
GetPage(name: '/home', page: () => Home(), binding: HomeBinding()),
],
));
}
附:使用GetX制作的一個計數器
官方文檔例
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(GetMaterialApp(
// It is not mandatory to use named routes, but dynamic urls are interesting.
initialRoute: '/home',
defaultTransition: Transition.native,
translations: MyTranslations(),
locale: Locale('pt', 'BR'),
getPages: [
//Simple GetPage
GetPage(name: '/home', page: () => First()),
// GetPage with custom transitions and bindings
GetPage(
name: '/second',
page: () => Second(),
customTransition: SizeTransitions(),
binding: SampleBind(),
),
// GetPage with default transitions
GetPage(
name: '/third',
transition: Transition.cupertino,
page: () => Third(),
),
],
));
}
class MyTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en': {
'title': 'Hello World %s',
},
'en_US': {
'title': 'Hello World from US',
},
'pt': {
'title': 'Olá de Portugal',
},
'pt_BR': {
'title': 'Olá do Brasil',
},
};
}
class Controller extends GetxController {
int count = 0;
void increment() {
count++;
// use update method to update all count variables
update();
}
}
class First extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.add),
onPressed: () {
Get.snackbar("Hi", "I'm modern snackbar");
},
),
title: Text("title".trArgs(['John'])),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<Controller>(
init: Controller(),
// You can initialize your controller here the first time. Don't use init in your other GetBuilders of same controller
builder: (_) => Text(
'clicks: ${_.count}',
)),
RaisedButton(
child: Text('Next Route'),
onPressed: () {
Get.toNamed('/second');
},
),
RaisedButton(
child: Text('Change locale to English'),
onPressed: () {
Get.updateLocale(Locale('en', 'UK'));
},
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Get.find<Controller>().increment();
}),
);
}
}
class Second extends GetView<ControllerX> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('second Route'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetX<ControllerX>(
// Using bindings you don't need of init: method
// Using Getx you can take controller instance of "builder: (_)"
builder: (_) {
print("count1 rebuild");
return Text('${_.count1}');
},
),
GetX<ControllerX>(
builder: (_) {
print("count2 rebuild");
return Text('${controller.count2}');
},
),
GetX<ControllerX>(builder: (_) {
print("sum rebuild");
return Text('${_.sum}');
}),
GetX<ControllerX>(
builder: (_) => Text('Name: ${controller.user.value.name}'),
),
GetX<ControllerX>(
builder: (_) => Text('Age: ${_.user.value.age}'),
),
RaisedButton(
child: Text("Go to last page"),
onPressed: () {
Get.toNamed('/third', arguments: 'arguments of second');
},
),
RaisedButton(
child: Text("Back page and open snackbar"),
onPressed: () {
Get.back();
Get.snackbar(
'User 123',
'Successfully created',
);
},
),
RaisedButton(
child: Text("Increment"),
onPressed: () {
Get.find<ControllerX>().increment();
},
),
RaisedButton(
child: Text("Increment"),
onPressed: () {
Get.find<ControllerX>().increment2();
},
),
RaisedButton(
child: Text("Update name"),
onPressed: () {
Get.find<ControllerX>().updateUser();
},
),
RaisedButton(
child: Text("Dispose worker"),
onPressed: () {
Get.find<ControllerX>().disposeWorker();
},
),
],
),
),
);
}
}
class Third extends GetView<ControllerX> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {
controller.incrementList();
}),
appBar: AppBar(
title: Text("Third ${Get.arguments}"),
),
body: Center(
child: Obx(() => ListView.builder(
itemCount: controller.list.length,
itemBuilder: (context, index) {
return Text("${controller.list[index]}");
}))),
);
}
}
class SampleBind extends Bindings {
@override
void dependencies() {
Get.lazyPut<ControllerX>(() => ControllerX());
}
}
class User {
User({this.name = 'Name', this.age = 0});
String name;
int age;
}
class ControllerX extends GetxController {
final count1 = 0.obs;
final count2 = 0.obs;
final list = [56].obs;
final user = User().obs;
updateUser() {
user.update((value) {
value.name = 'Jose';
value.age = 30;
});
}
/// Once the controller has entered memory, onInit will be called.
/// It is preferable to use onInit instead of class constructors or initState method.
/// Use onInit to trigger initial events like API searches, listeners registration
/// or Workers registration.
/// Workers are event handlers, they do not modify the final result,
/// but it allows you to listen to an event and trigger customized actions.
/// Here is an outline of how you can use them:
/// made this if you need cancel you worker
Worker _ever;
@override
onInit() {
/// Called every time the variable $_ is changed
_ever = ever(count1, (_) => print("$_ has been changed (ever)"));
everAll([count1, count2], (_) => print("$_ has been changed (everAll)"));
/// Called first time the variable $_ is changed
once(count1, (_) => print("$_ was changed once (once)"));
/// Anti DDos - Called every time the user stops typing for 1 second, for example.
debounce(count1, (_) => print("debouce$_ (debounce)"),
time: Duration(seconds: 1));
/// Ignore all changes within 1 second.
interval(count1, (_) => print("interval $_ (interval)"),
time: Duration(seconds: 1));
}
int get sum => count1.value + count2.value;
increment() => count1.value++;
increment2() => count2.value++;
disposeWorker() {
_ever.dispose();
// or _ever();
}
incrementList() => list.add(75);
}
class SizeTransitions extends CustomTransition {
@override
Widget buildTransition(
BuildContext context,
Curve curve,
Alignment alignment,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return Align(
alignment: Alignment.center,
child: SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: curve,
),
child: child,
),
);
}
}
編程小白,如有謬誤,歡迎指出,由衷感謝!