State management and route management play an important role in the application framework of Flutter. At present, the mainstream solutions include Google’s official Provider, tripartite GetX, Bloc, fish-Redux, etc. GetX stands out from the rest.

GetX is a lightweight and powerful solution with high performance state management, intelligent dependency injection, and easy route management.

This article will teach you how to build your own Flutter application framework by integrating GetX from scratch.

0. GetX integration

Add the dependent

Add the GetX dependency to the pubspec.yaml file as follows:

dependencies:
  flutter:
    sdk: flutter
  get: ^ 4.5.1
Copy the code

Initialize the GetX

To use GetX, GetX needs to be initialized. Replace the default MaterialApp with GetMaterialApp as follows:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage(), ); }}Copy the code

1. Status management

GetX provides two methods of reactive state management: reactive variable mode and state manager mode.

Responsive variable

define

To define a responsive variable, add a.obs to the end of the variable:

var count = 0.obs;
Copy the code

Reactive variables can be used on any type:

final name = ' '.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String.int>{}.obs;

// Custom class - can be any class
final user = User().obs;
Copy the code
Gets the value of a reactive variable

Call value to get the value of the variable. You do not need to add.value to List or Map.

String nameValue = name.value
bool isLoggedValue = isLogged.value
int countValue = count.value
double numberValue = number.value
String item = items[0] //.value is not required
int value = myMap['key'] //.value is not required
String name = user.value.name
Copy the code
Update data:

For base data types, simply reassign value to update the data and refresh the interface via Obx:

name.value = "123"
isLogged.value = true
count.value = 1
number.value = 12.0
Copy the code

For other data types, call update or variable methods as follows:

user.update((value) { value? .name ="123";
});
Copy the code

Or reassign an object using the variable name method. For example, if the variable is user, the user() method can be used to update it:

user(User(name: "abcd", age: 25));
Copy the code
Refresh the interface

To use reactive variables on the interface, simply wrap Obx on the control that uses variables to achieve reactive update, that is, automatically refresh the interface when the value of the variable changes:

Obx(() => Text("${count.value}"))
Copy the code
Data change monitor

In addition to using Obx to automatically refresh interface data, GetX provides a variety of manual ways to monitor the data changes of responsive variables, and perform custom logic when data changes, such as re-request interface after data changes.

  • Ever is triggered when data changes
  • EverAll is much like “ever “except that it listens for changes in multiple responsive variables and triggers a callback when one of them changes
  • Once is called only the first time a variable is changed
  • Debounce, which is called after a certain amount of time and only the last change within a specified time will trigger a callback. If the time is set to 1 second and three data changes occur at an interval of 500 milliseconds, only the last change will trigger the callback.
  • Only the last change in the interval triggers a callback. If the interval is set to 1 second, no matter how many times you click within 1 second, only the last callback will be triggered and then the next interval will be entered.

Usage:

///Every time`count`Called when changes.
ever(count, (newValue) => print("$newValue has been changed"));

///It is called only if the variable count is changed for the first time.
once(count, (newValue) => print("$newValue was changed once"));

///Anti-ddos - Called whenever the user stops typing for 1 second, for example.
debounce(count, (newValue) => print("debouce$newValue"), time: Duration(seconds: 1));

///Ignore all changes for 1 second, and only the last one triggers the callback.
interval(count, (newValue) => print("interval $newValue"), time: Duration(seconds: 1));
Copy the code
The sample

Counter functionality with reactive variables:

class CounterPage extends StatelessWidget {
  var count = 0.obs;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Counter"),
      ),
      body: Center(
        child: Obx(() => Text("${count.value}", style: const TextStyle(fontSize: 50))),
      ),
      floatingActionButton: FloatingActionButton(
        child: constIcon(Icons.add), onPressed: () => count ++, ), ); }}Copy the code

This code implements a simple counter function, and a closer look shows that the count can be updated automatically without using the StatefulWidget. That’s the power of reactive variables.

State manager

GetX also provides the use of Controller to manage state, implement a custom Controller class inherited from GetxController, Controller for business logic processing, Update () is called when state data needs to be changed to notify it of the change.

Implementation method:

class CounterController extends GetxController{
  int count = 0;
  voidincrement(){ count ++ ; update(); }}Copy the code

When used in the interface, you need to wrap it with GetBuilder so that when using data changes in the Controller, the update() call will refresh the interface control.

GetBuilder<CounterController>(init: CounterController(), (controller) { return Text("${controller.count}", style: const TextStyle(fontSize: 50)); })Copy the code

If you use a Controller for the first time, you need to initialize it. If you use the same Controller again, you don’t need to initialize it.

After initialization, you can use get.find () to find the corresponding Controller:

The sample

Implement counters with Controller:

class CounterController extends GetxController{
  int count = 0;

  voidincrement(){ count ++ ; update(); }}class CounterPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text("Counter"),
      ),
      body: Center(
        child: Column(
            mainAxisSize:MainAxisSize.min,
          children: [
            
            GetBuilder<CounterController>(
              init: CounterController(), /// Initialize the Controller
              builder: (controller) {
                return Text("${controller.count}", style: const TextStyle(fontSize: 50));
              }),
            
            GetBuilder<CounterController>(  ///No initialization
                builder: (controller) {
                  return Text("${controller.count}", style: const TextStyle(fontSize: 50));
                }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          ///Use find to find Controller
          CounterController controller = Get.find();
          ///Call the Controller methodcontroller.increment(); },),); }}Copy the code

There are two numeric controls implemented, one that initializes the CounterController and the other that is used directly.

2. Dependency management

GetX dependency management is already used in the previous section. After initializing the Controller in GetBuilder, you can use get.find () to find the corresponding Controller elsewhere. GetX dependency management can inject instances of any type and provides multiple ways to insert/register dependencies.

Insert/register dependencies

Get.put

Insert dependent objects into GetX using PUT:

Get.put<CounterController>(CounterController());
Get.put<CounterController>(CounterController(), permanent: true);
Get.put<CounterController>(CounterController, tag: "counter");
Copy the code

When inserting a dependency, you can set additional parameters in addition to the instance of the dependency class:

  • Permanent: Indicates whether the instance is permanent. The default value is false. The instance is destroyed when it is no longer used
  • Tag: The tag used to distinguish different instances of the same class.
Get.lazyPut

Lazy initialization, where instance objects are not initialized until they are needed, that is, the first time a class is found.

///Only the first time you use get.find<CounterController>Then CounterController will be called.
Get.lazyPut<CounterController>(() => CounterController());

Get.lazyPut<CounterController>(
  () {
    // ... some logic if needed
    return CounterController();
  },
  tag: Math.random().toString(),
  fenix: true
)
Copy the code

LazyPut also takes extra parameters, which are basically the same as PUT.

  • Fenix: Similar to ‘permanent’, except that when not in use, the instance is discarded, but when needed again, Get recreates the instance

  • Tag: The tag used to distinguish different instances of the same class.

Get.putAsync

PutAsync can register an instance asynchronously. Used when some instance needs to be initialized asynchronously, such as SharedPreferences:

Get.putAsync<SharedPreferences>(() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setInt('counter'.12345);
  return prefs;
});
Copy the code

Put has the same permanent and tag parameters as PUT.

Get.create

Create is similar to put, except that permanent is true by default.

Get.create<CounterController>(() => CounterController());
Copy the code

use

Get the dependent instance with the find() method:

final controller = Get.find<CounterController>();
/ / or
CounterController controller = Get.find();

///Get by tag
final controller = Get.find<CounterController>("counter");
Copy the code

It is also possible to manually remove injected instances of dependencies using the delete() method. In most cases, this method is not called manually; GetX handles it internally and removes it when it is no longer needed

Get.delete<CounterController>();
Copy the code

3. Route management

Routing is also an important part of the Flutter project. Page hopping in a Flutter is achieved through routing. GetX provides common and alias routes.

Common routing

  • to: Go to the next screen
Get.to(CounterPage());
Copy the code

Use arguments to pass arguments:

Get.to(CounterPage(), arguments: count);
Copy the code

Use arguments to pass arguments of any type.

Get the parameters on the next page:

dynamic args = Get.arguments;
Copy the code
  • off: Goes to the next page, and the navigation does not return
Get.off(CounterPage());
Copy the code
  • offAll: Go to the next screen and cancel all previous routes
Get.offAll(CounterPage());
Copy the code
  • backReturns the
Get.back();
Copy the code

Return to pass parameter:

Get.back(result: 'success');
Copy the code

Get the return argument:

var data = await Get.to(CounterPage());
Copy the code

The alias routing

Create a RouteGet class to configure the route mapping: RouteGet (RouteGet)

class RouteGet {
  /// page name
  static const String counter = "/counter";

  ///pages map
  static final List<GetPage> getPages = [
    GetPage(
        name: counter, 
        page: () => CounterPage(), 
    )
  ];
}
Copy the code

GetPage defines the mapping between the alias and the page.

Then initialRoute and getPages are configured in GetMaterialApp, that is, the initial page and route mapping set:

class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return GetMaterialApp( title: 'Flutter Demo', initialRoute: RouteGet.counter, getPages: RouteGet.getPages, theme: ThemeData( primarySwatch: Colors.blue, ) ); }}Copy the code
Jump and parameter passing

The use method is basically the same as the common route, but the method is more Named

  • Route jump:
Get.toNamed(RouteGet.login);
Copy the code
  • Route parameter transmission:
Get.toNamed(RouteGet.login, arguments: {"name":"aaaa"});
Copy the code

Route alias can also be directly followed by a parameter, similar to the way the Url get parameter is passed:

Get.toNamed("/NextScreen? device=phone&id=354&name=Enzo");
Copy the code
  • Receiving parameters:

    Arguments (); Get arguments (); Get arguments ();

dynamic args = Get.arguments;
Copy the code

Get.parameters: get.parameters: get.parameters: get.parameters: get.parameters

Get.parameters['device']
Copy the code

Bindings

Bindings is mainly used with routing. When entering the page through GetX route, the dependencies method will be automatically called, and the dependencies can be registered here.

class CounterBinding implements Bindings {
  @override
  voiddependencies() { Get.lazyPut<CounterController>(() => CounterController()); Get.put<Service>(()=> Api()); }}Copy the code

Common routes:

Get.to(CounterPage(), binding: CounterBinding());
Copy the code

Alias routing is used, and the corresponding Bindings of routes are set in GetPage

 ///pages map
  static final List<GetPage> getPages = [
    GetPage(
        name: counter, 
        page: () => CounterPage(), 
      	binding: CounterBinding() ///Set the Binding)];Copy the code

The alias route is then used the same way

For more operations related to routes, see route_management

At this point, the integration of GetX and the use of the key features of state management, dependency management, and route management are implemented and the project is ready to begin.

4. Use of GetX plug-in

To make it easy to use GetX in your project, you can install the GetX plug-in, which allows you to quickly create page templates for GetX and quickly use getX-related functions with shortcut keys. The plug-in is shown as follows:

After installation, right-click the directory -> New, there will be GetX menu, select GetX in the popup interface can quickly create page template, plug-in use as shown in the picture:

After clicking OK, four files binding, Controller, State and View will be generated in the corresponding directory, as shown below:

The name of the file can be set in plug-in Settings.

The corresponding file content is as follows:

  • **binding: ** is used to lazily load corresponding controllers
class CounterBinding extends Bindings {
  @override
  voiddependencies() { Get.lazyPut(() => CounterController()); }}Copy the code
  • Controller: Write interface business logic code, including lifecycle callback functions
class CounterController extends GetxController {
  final CounterState state = CounterState();

  @override
  void onReady() {
    // TODO: implement onReady
    super.onReady();
  }

  @override
  void onClose() {
    // TODO: implement onClose
    super.onClose(); }}Copy the code
  • State: stores interface status data
class CounterState {
  CounterState() {
    ///Initialize variables}}Copy the code
  • View: interface control, mainly for interface development
class CounterPage extends StatelessWidget {
  final controller = Get.find<CounterController>();
  final state = Get.find<CounterController>().state;

  @override
  Widget build(BuildContext context) {
    returnContainer(); }}Copy the code

The Bindings method used here automatically registers controllers in Bindings and generates find Controller code in the Page.

In addition, state is used here, in order to extract all state data separately and put them in state when there are too many state data on the page, which is more convenient for maintenance and avoids overcrowding of Controller.

For more information about the use of GetX plug-in, see the author’s article introduction: GetX code generation IDEA plug-in, super detailed function explanation (through the phenomenon to see the essence).

Internationalization of 5.

GetX provides multilingual internationalization processing to facilitate multilingual management and switching within a project.

  • Start by creating a language resource class that inherits from GetXTranslationsTo realizeget keys:
class StringRes extends Translations{

  @override
  Map<String.Map<String.String>> get keys => {
    'zh_CN': {
      'hello': Hello world
    },
    'en_US': {'hello': 'Hello World'}}; }Copy the code

Return a multi-language configuration in keys, key is the language identifier, format is [country]_[language], value is a Map, store our actual text resources.

  • Then, inGetMaterialAppTo configure:
GetMaterialApp(
      translations: StringRes(),
      locale: const Locale('zh'.'CN'),
  		fallbackLocale: Locale('en'.'US')... ;Copy the code

Translations pass in objects that we’ve defined as inheriting from the translations class, and locale is the language we use by default, FallbackLocale is a resource that is configured to use fallbackLocale when we don’t have the default language resource.

Locale can also be used to get the system locale:

import 'dart:ui' as ui;

return GetMaterialApp(
    locale: ui.window.locale,
);
Copy the code
  • use

Use key. STR of the corresponding resource as follows:

Text('hello'.tr);
Copy the code
  • Change the language

Use Get. UpdateLocale to change the language:

Get.updateLocale(const Locale('en'.'US'));
Copy the code
To optimize the

After the above configuration, the project realized multi-language, and can switch it, but found that if all languages are written into a file too much content is not easy to manage, so we can split the corresponding language resources, one file for each language:

str_res_zh.dart:

const zh_CN_res = {
  'hello': Hello world};Copy the code

str_res_en:

const en_US_res = {
  'hello': 'Hello World'};Copy the code

The StringRes is then modified as follows:

class StringRes extends Translations{
  @override
  Map<String.Map<String.String>> get keys => {
    'zh_CN': zh_CN_res,
    'en_US':en_US_res
  };
}
Copy the code

So it’s easier to manage. ‘hello’.tr is used every time. This is not a friendly manual method, it is not prompt, and it can be written incorrectly, so we can optimize it by defining a class that holds a String key as we do with the Android String resource:

str_res_keys.dart

class SR{
  static const hello = 'hello';
}
Copy the code

Modify the language resource configuration as follows:

const zh_CN_res = {
  SR.hello: Hello world};const en_US_res = {
  SR.hello: 'Hello World'};Copy the code

Then use the following:

Text(SR.hello.tr);
Copy the code

In this way, the multi-language configuration of the project is completed. The overall directory is shown in the figure below:

6. Other functions of GetX

snackbar

GetX provides a quick and easy way to use snackbar. Use the following methods:

Get.snackbar("title"."message");
Copy the code

The default pop-up is above, you can use the snackPosition to change the pop-up position, the effect is shown in the following figure:

In addition to the position, you can also set a number of properties, such as text color, background color, etc. The details of the properties can be set as follows:


    String title,
    String message, {
    Color? colorText,
    Duration? duration = const Duration(seconds: 3),

    /// with instantInit = false you can put snackbar on initState
    bool instantInit = true,
    SnackPosition? snackPosition,
    Widget? titleText,
    Widget? messageText,
    Widget? icon,
    bool? shouldIconPulse,
    double? maxWidth,
    EdgeInsets? margin,
    EdgeInsets? padding,
    double? borderRadius,
    Color? borderColor,
    double? borderWidth,
    Color? backgroundColor,
    Color? leftBarIndicatorColor,
    List<BoxShadow>? boxShadows,
    Gradient? backgroundGradient,
    TextButton? mainButton,
    OnTap? onTap,
    bool? isDismissible,
    bool? showProgressIndicator,
    DismissDirection? dismissDirection,
    AnimationController? progressIndicatorController,
    Color? progressIndicatorBackgroundColor,
    Animation<Color>? progressIndicatorValueColor,
    SnackStyle? snackStyle,
    Curve? forwardAnimationCurve,
    Curve? reverseAnimationCurve,
    Duration? animationDuration,
    double? barBlur,
    double? overlayBlur,
    SnackbarStatusCallback? snackbarStatus,
    Color? overlayColor,
    Form? userInputForm,
  }
Copy the code

It can be set according to your requirements.

dialog

GetX provides a quick way to use a dialog, either by passing in the Widget displayed on the Dialog or by using the dialog style provided by GetX by default:

The first:

Get.dialog(Widget)
Copy the code

The second:

Get.defaultDialog(title: "title", middleText: "this is dialog message");
Copy the code

Effect:

In addition to title and middleText, you can also set the OK button, cancel button and corresponding callback, rounded corner, background color and other parameters as follows:

Future<T? > defaultDialog<T>({String title = "Alert",
    EdgeInsetsGeometry? titlePadding,
    TextStyle? titleStyle,
    Widget? content,
    EdgeInsetsGeometry? contentPadding,
    VoidCallback? onConfirm,
    VoidCallback? onCancel,
    VoidCallback? onCustom,
    Color? cancelTextColor,
    Color? confirmTextColor,
    String? textConfirm,
    String? textCancel,
    String? textCustom,
    Widget? confirm,
    Widget? cancel,
    Widget? custom,
    Color? backgroundColor,
    bool barrierDismissible = true,
    Color? buttonColor,
    String middleText = "Dialog made in 3 lines of code",
    TextStyle? middleTextStyle,
    double radius = 20.0.// ThemeData themeData,
    List<Widget>? actions,

    // onWillPop Scope
    WillPopCallback? onWillPop,

    // the navigator used to push the dialog
    GlobalKey<NavigatorState>? navigatorKey,
  })
Copy the code

bottomSheet

Use as follows:

Get.bottomSheet(Container(
  height: 200,
  color: Colors.white,
  child: const Center(
    child: Text("bottomSheet"),),));Copy the code

Effect:

A closer look reveals that neither a Snackbar, dialog, nor bottomSheet needs a context, which means you can call it anywhere in the project.

If you want to cancel snackbar, dialog, bottomSheet you can use get.back ().

GetUtils

GetX also provides a number of utility methods that can be called using GetUtils, such as checking whether it is a mailbox or a file format type, as shown in the following figure:

GetX also provides some extension methods:

// Check which platform the application is running on.
GetPlatform.isAndroid
GetPlatform.isIOS
GetPlatform.isMacOS
GetPlatform.isWindows
GetPlatform.isLinux
GetPlatform.isFuchsia

// Check the device type
GetPlatform.isMobile
GetPlatform.isDesktop
// All platforms are independently Web-enabled!
// You can tell if you are running in the browser.
// On Windows, iOS, OSX, Android, etc.
GetPlatform.isWeb


// Equivalent to.mediaquery.of (context).size. Height,
// But cannot be changed.
Get.height
Get.width

// Provide the current context.
Get.context

/ / anywhere in your code, at the front desk provide snackbar/dialog/bottomsheet context.
Get.contextOverlay

// Note: the following methods are extensions to the context.
Since the context is accessible anywhere in your UI, you can use it anywhere in your UI code.

// If you need a variable height/width (e.g. the desktop or browser window can be zoomed), you will need to use context.
context.width
context.height

// Lets you define half of the page, one-third of the page, and so on.
// Useful for reactive applications.
// Parameters: dividedBy (double) Optional - Default: 1
// Parameter: reducedBy (double) Optional - Default: 0.
context.heightTransformer()
context.widthTransformer()

/// Similar to mediaQuery.of (context).size.
context.mediaQuerySize()

/// Similar to mediaQuery.of (context).padding.
context.mediaQueryPadding()

/// Similar to mediaQuery.of (context).viewPadding.
context.mediaQueryViewPadding()

/// Similar to mediaQuery.of (context).viewinsets.
context.mediaQueryViewInsets()

/// Similar to MediaQuery. Of (context). Orientation;
context.orientation()

///Check whether the device is in landscape mode
context.isLandscape()

///Check whether the device is in longitudinal mode.
context.isPortrait()

///Similar to mediaQuery.of (context).devicepixelRatio.
context.devicePixelRatio()

///Similar to mediaQuery.of (context).textScalefactor.
context.textScaleFactor()

///Query the shortest edge of a device.
context.mediaQueryShortestSide()

///True if the width is greater than 800.
context.showNavbar()

///True if the shortest edge is less than 600p.
context.isPhone()

///True if the shortest edge is greater than 600p.
context.isSmallTablet()

///True if the shortest side is greater than 720p.
context.isLargeTablet()

///True if the current device is a tablet
context.isTablet()

///Returns a value based on the page size<T>.
///The value can be given as:
///Watch: If the shortest edge is less than 300
///Mobile: if the shortest side is less than 600
///Tablet: If the shortestSide is less than 1200
///Desktop: if the width is greater than 1200
context.responsiveValue<T>()
Copy the code

Source: flutter_app_core

For more information, see the official documentation: GetX

References:

  • Use Flutter GetX — simple charm!
  • GetX code to generate IDEA plug-in, super detailed function explanation (through the phenomenon to see the essence)