1
头图
This article is updated very frequently, please check the latest content: latest content---GetX code generation IDEA plug-in function description

foreword

This article is not to write the use of the getx framework, but also to explain the function of its code generation IDEA plug-in

I have written two very long getx articles before

An introductory use: Flutter GetX use --- simple charm!

An in-depth analysis of principles: In-depth analysis of Flutter |

Fishing and fishing have already been handed over to you, so there is no need to repeat them.

img

At the same time, I also wrote a getx code generation plugin: getx_template , this tool is equivalent to a fishing seat (making you more comfortable fishing or eating fish?) Now! The initial function is very simple, that is to generate the corresponding module code for a single page, there is no memory option function, basically it is the level of a plastic seat

  • But with a lot of requests from , , , , this plugin has become a bit complicated.
  • Especially when it comes to the Select Function module, some people may not understand what the selected function button means, so just tick all of them. . .
  • Therefore, I want to talk to you about the functions of this tool in detail, hoping to help you save some development time.

Brothers, I really don't want to write hydrology; but this tool has a function button, and the code may be changed very little, and the things behind it may require a lot of pen and ink to describe. Filipinos, said.

img

This article has been updated for a long time. If you want to know the details of each update of the plugin, you can click here.

code generation

  • Search for getx in Plugins

image-20210906222922384

Compared

  • Early code generation pop-up box, optional functions are relatively few, and persistent storage was not supported at that time

    • gan, the icon is ugly too

20210130182809

  • This is the function selection pop-up window after many improvements

getx_new

I am a very good looking party, I have done a lot of consideration for this latest version of the page

  • On the home page, with the various needs mentioned by the pretty boys, Select Function has increased from the original two functions to the current seven functions.

    • With the increase of function buttons, tiled on the dialog, the height of the entire dialog will become quite long
    • The most important thing is: it will make users not clear what the key function buttons in Function are!
  • Based on the above thinking, I racked my brains to solve this problem

    • Option 1: I originally wanted to make a folding storage area, and the secondary function buttons were placed in the folding area

      • After writing with swing, I found that the effect is really ugly. When storing, there is also a problem with the height calculation: give up
    • Option 2: This is when I was flipping the swing control and found the tab control JBTabbedPane

      • The effect is simple and elegant, and the idea of folding is complete: use
  • This time I comprehensively improved the dialog layout problem

    • The length and width of the entire dialog in the past were written to death, which would cause problems on high-resolution screens.
    • This time, I discovered the magic of the pack method (the bitter tears of the swinging dog), the fully refactored interface layout logic
  • This time, on a 48-inch screen, the following situation will definitely not occur

圖片

I haven't tried it, but I have confidence in my code

img

Mode selection

Here are two large mode options: default, easy

see the difference

default mode

image-20210905174923566

  • view
class TestPage extends StatelessWidget {
  final logic = Get.put(TestLogic());
  final state = Get.find<TestLogic>().state;

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • logic
class TestLogic extends GetxController {
  final TestState state = TestState();
}
  • state
class TestState {
  TestState() {
    ///Initialize variables
  }
}
Easy Mode

image-20210905175435395

  • view
class TestPage extends StatelessWidget {
  final logic = Get.put(TestLogic());

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • logic
class TestLogic extends GetxController {

}
Summary

The above default mode and easy mode, from the code point of view, can still see the obvious difference

  • Default mode has one more State layer than Easy mode
  • State is specially used to store page variables and initialize related variable data

I have written a more complex module

  • There are hundreds of variables on the page (involving complex form submission), and there are dozens of event interactions with users
  • A lot of logic in the whole module is calibrated by related variables, and a lot of different data will be initialized. The code of the State layer is almost a thousand lines faster.
  • Therefore, when the business is gradually complicated, the State layer is not thin, it supports the logical calibration and reversal of the entire module

Unless it is a business minimal module visible to the naked eye, it is recommended to use the Easy module; otherwise, it is recommended to use the Default mode

main (main function)

useFolder,usePrefix

The functions of useFolder and usePrefix are relatively simple, so they will be discussed together here.

useFolder

This function is selected by default, and a folder will be created in addition to the multiple files created, which is convenient for management

useFolder

usePrefix

Some friends like to add a prefix to the module name (lowercase + underscore) at each layer: view, state, logic

Here is an optional feature for you

usePrefix

isPageView

Please note: the isPageView and autoDispose buttons cannot be selected at the same time, both of them can solve the problem stored in PageView, select one button, the other button will automatically uncheck

This is a very useful feature

If you use getx in PageView, you may find that GetXController in all subpages is injected all at once! Instead of switching to the corresponding page, inject the corresponding GetXController!

PageView(children: [
    FunctionPage(),
    ExamplePage(),
    SettingPage(),
])
analysis

We can analyze why this happens, let's take a look: FunctionPage

class FunctionPage extends StatelessWidget {
  final logic = Get.put(FunctionLogic());
  final state = Get.find<FunctionLogic>().state;

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

The step we inject is to place it in the scope of the member variable of the class

  • This scope takes effect before instantiating the constructor
  • So when we add the instanced Page, the scope of the member variable is triggered directly, and GetXController is injected

PageView trigger mechanism

  • PageView triggers the added Widget, which is the build method that triggers the corresponding Widget
  • Which Widget to switch to, trigger the build method of the corresponding Widget

With the above understanding, it is easy to solve the problem of PageView

  • Just put the injection process in the build method
  • Because we are using StatelessWidget, we don't need to consider its refresh problem, it will be refreshed only when its parent node refreshes
  • The putIfAbsent method used by GetX to store objects will only store the first injected object, and subsequent objects of the same class will be ignored directly, which can avoid many problems
handles

So this function only needs to change the injection location of GetXController in the View file (other files do not need to be changed)

class FunctionPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final logic = Get.put(FunctionLogic());
    final state = Get.find<FunctionLogic>().state;

    return Container();
  }
}
  • Compared

isPageView

addBinding

Binding is for unified management of GetXController, let's see the difference between binding and non-binding

addBinding

Non-Binding
  • view
class TestOnePage extends StatelessWidget {
  final logic = Get.put(TestOneLogic());

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • logic
class TestOneLogic extends GetxController {

}
Binding: Requires matching GetX route
  • binding
class TestTwoBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => TestTwoLogic());
  }
}
  • view
class TestTwoPage extends StatelessWidget {
  final logic = Get.find<TestTwoLogic>();

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • logic
class TestTwoLogic extends GetxController {

}
  • This binding needs to be bound in the routing module
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: RouteConfig.testOne,
      getPages: RouteConfig.getPages,
    );
  }
}

class RouteConfig {
  static const String testTwo = "/testTwo";

  static final List<GetPage> getPages = [
    GetPage(
      name: testTwo,
      page: () => TestTwoPage(),
      binding: TestTwoBinding(),
    ),
  ];
}
Summarize

In the binding file, lazy injection is used: when the find method is used, the real injection is made

So in the view, you need to change the put to find.

  • Add binding files and use lazy injection
  • view file, put change to find
  • You need to bind the binding instance on the corresponding page in the getx routing module

minor (minor feature)

addLifecycle

This is a very simple function, placed under the secondary functions tab

For some friends, the logic module needs to write the onReady and onClose callbacks often, and they are too lazy to write it every time; so the function of automatically supplementing these two callbacks has been added to the plug-in.

  • Only the Logic files are different. When this function is enabled: the onReady() and onClose() methods are automatically added
class TestLogic extends GetxController {
  final TestState state = TestState();

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

  @override
  void onClose() {
    // TODO: implement onClose
    super.onClose();
  }
}
  • Compared

addLifecycle

autoDispose

The function is exactly what the name says: auto-release GetXController; in fact, this is a very important function, but the implementation is too inelegant, so I moved it to the secondary function tab

When we use GetX, there may be no feeling that GetxController has not been released. This situation is because we generally use the set of route jump APIs (Get.to, Get.toName...) of getx. Class: To use Get.toName, you must use GetPage; if you use Get.to, you do not need to register in GetPage. There is an operation added to GetPageRoute inside Get.to

It can be seen from the above registration in GetPage, which means that when we jump to the page, GetX will store the page information and manage it. The following two scenarios will cause GetxController to fail to release.

  • Conditions under which GetxController can be automatically released

    • GetPage+Get.toName is used together and can be released
    • Use Get.to directly, releasable
  • GetxController cannot be automatically released scene

    • Do not use the routing jump provided by GetX: directly use the jump operation of the native routing api
    • This will directly cause GetX to be unable to perceive the life cycle of the corresponding page GetxController, which will cause it to fail to release
Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => XxxxPage()),
);

From this, it can be seen from the above that GetxController cannot be released: Do not use GetX routing

optimal solution

There is an optimal solution. Even if you don't use Getx routing, you can easily recycle GetXController on each page. Thanks to @法 for pointing out in the comments

  • Manually make getx aware of routes
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage,
      ///此处配置下!
      navigatorObservers: [GetXRouterObserver()],
    );
  }
}

///自定义这个关键类!!!!!!
class GetXRouterObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    RouterReportManager.reportCurrentRoute(route);
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) async {
    RouterReportManager.reportRouteDispose(route);
  }
}

To be honest, this principle is actually very simple, but the idea is very interesting; if you click on the two methods of reportCurrentRoute and reportRouteDispose , you probably know what is going on.

  • reportCurrentRoute is to assign the current route to GetX
  • When we enter a page, the corresponding GetXController will be initialized, and finally the _startController<S>({String? tag}) method will be called
  • _startController will call RouterReportManager.appendRouteByCreate(i) to save the injected GetXController
  • Saved in a map, the key is the current route route , the value is HashSet, multiple GetXControllers can be saved
  • ok, when the route is closed, it is recycled in the reportRouteDispose method, the key is the current route , and it traverses all the GetXController recycling in the value
  • I giao, based on this kind of thinking, everyone can do a lot of things! ! !
StatefulWidget scheme

If the above optimal solution can't help you solve the problem of GetXController recycling, you may encounter a special scenario. Generally speaking, you can basically analyze your own code by analyzing it.

If you are too lazy to analyze the reasons, try the following compromise solution; the granularity is extremely small, and it is solved for the single page dimension

Here I simulated the above scenario and wrote a solution

  • Jump to the first page
Navigator.push(
    Get.context,
    MaterialPageRoute(builder: (context) => AutoDisposePage()),
);
  • demo page

    • This place must use StatefulWidget, because in this case, the life cycle cannot be sensed, and the StatefulWidget life cycle needs to be used
    • At the dispose callback, delete the current GetxController from the entire GetxController management chain
class AutoDisposePage extends StatefulWidget {
  @override
  _AutoDisposePageState createState() => _AutoDisposePageState();
}

class _AutoDisposePageState extends State<AutoDisposePage> {
  final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: AppBar(title: const Text('计数器-自动释放')),
      body: Center(
        child: Obx(
          () => Text('点击了 ${logic.count.value} 次',
              style: TextStyle(fontSize: 30.0)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: const Icon(Icons.add),
      ),
    );
  }

  @override
  void dispose() {
    Get.delete<AutoDisposeLogic>();
    super.dispose();
  }
}

class AutoDisposeLogic extends GetxController {
  var count = 0.obs;

  ///自增
  void increase() => ++count;
}

Seeing this, you might think, ahhh! Why is it so troublesome, why do I have to write StatefulWidget, so troublesome!

n't worry, I also thought of this problem, I specially added the function of automatic recycling to the

  • If the page you write cannot be recycled, remember to check autoDispose

    • How to judge whether the GetxController of the page can be recycled? It's actually very simple. The unreleased scene above has been described more clearly. If you're not sure, just look at it again.

image-20210922112126153

Let's take a look at the code, the default mode is the same

  • view
class AutoDisposePage extends StatefulWidget {
  @override
  _AutoDisposePageState createState() => _AutoDisposePageState();
}

class _AutoDisposePageState extends State<AutoDisposePage> {
  final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());

  @override
    Widget build(BuildContext context) {
      return Container();
    }

  @override
  void dispose() {
    Get.delete<AutoDisposeLogic>();
    super.dispose();
  }
}
  • logic
class AutoDisposeLogic extends GetxController {

}
Optimize StatefulWidget scheme

The above is a general solution, you don't need to introduce anything else; but this solution uses StatefulWidget, and the code is a lot more, which makes me a little nervous

I have quite an obsessive-compulsive disorder. After thinking about it for a long time, starting from the outside, I wrote a general control to recycle the corresponding GetXController

GetBindWidget

  • The meaning of this control: Bind GetXController to the life cycle of the current page, and automatically recycle when the page is closed
  • The control can recycle a single GetXController (bind parameter), and can add the corresponding tag (tag parameter); it can also recycle multiple GetXController (binds), and can add multiple tags (tags parameter, please correspond to binds one by one; no tag In GetXController, tag can be written as null character: "")
import 'package:flutter/material.dart';
import 'package:get/get.dart';

/// GetBindWidget can bind GetxController, and when the page is disposed,
/// it can automatically destroy the bound related GetXController
///
///
/// Sample:
///
/// class SampleController extends GetxController {
///   final String title = 'My Awesome View';
/// }
///
/// class SamplePage extends StatelessWidget {
///   final controller = Get.put(SampleController());
///
///   @override
///   Widget build(BuildContext context) {
///     return GetBindWidget(
///       bind: controller,
///       child: Container(),
///     );
///   }
/// }
class GetBindWidget extends StatefulWidget {
  const GetBindWidget({
    Key? key,
    this.bind,
    this.tag,
    this.binds,
    this.tags,
    required this.child,
  })  : assert(
          binds == null || tags == null || binds.length == tags.length,
          'The binds and tags arrays length should be equal\n'
          'and the elements in the two arrays correspond one-to-one',
        ),
        super(key: key);

  final GetxController? bind;
  final String? tag;

  final List<GetxController>? binds;
  final List<String>? tags;

  final Widget child;

  @override
  _GetBindWidgetState createState() => _GetBindWidgetState();
}

class _GetBindWidgetState extends State<GetBindWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() {
    _closeGetXController();
    _closeGetXControllers();

    super.dispose();
  }

  ///Close GetxController bound to the current page
  void _closeGetXController() {
    if (widget.bind == null) {
      return;
    }

    var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
    GetInstance().delete(key: key);
  }

  ///Batch close GetxController bound to the current page
  void _closeGetXControllers() {
    if (widget.binds == null) {
      return;
    }

    for (var i = 0; i < widget.binds!.length; i++) {
      var type = widget.binds![i].runtimeType.toString();

      if (widget.tags == null) {
        GetInstance().delete(key: type);
      } else {
        var key = type + (widget.tags?[i] ?? '');
        GetInstance().delete(key: key);
      }
    }
  }
}
  • very simple to use
/// 回收单个GetXController
class TestPage extends StatelessWidget {
  final logic = Get.put(TestLogic());

  @override
  Widget build(BuildContext context) {
    return GetBindWidget(
      bind: logic,
      child: Container(),
    );
  }
}

/// 回收多个GetXController
class TestPage extends StatelessWidget {
  final logicOne = Get.put(TestLogic(), tag: 'one');
  final logicTwo = Get.put(TestLogic());
  final logicThree = Get.put(TestLogic(), tag: 'three');

  @override
  Widget build(BuildContext context) {
    return GetBindWidget(
      binds: [logicOne, logicTwo, logicThree],
      tags: ['one', '', 'three'],
      child: Container(),
    );
  }
}

/// 回收日志
[GETX] Instance "TestLogic" has been created with tag "one"
[GETX] Instance "TestLogic" with tag "one" has been initialized
[GETX] Instance "TestLogic" has been created
[GETX] Instance "TestLogic" has been initialized
[GETX] Instance "TestLogic" has been created with tag "three"
[GETX] Instance "TestLogic" with tag "three" has been initialized
[GETX] "TestLogicone" onDelete() called
[GETX] "TestLogicone" deleted from memory
[GETX] "TestLogic" onDelete() called
[GETX] "TestLogic" deleted from memory
[GETX] "TestLogicthree" onDelete() called
[GETX] "TestLogicthree" deleted from memory

LintNorm

pub: lint library

This function, at first glance, everyone is probably confused; if I hadn't written this, I would have been confused when I saw it.

img

However, this function is really the gospel for a small number of obsessive-compulsive patients

Because the author of getx, in the demo project, introduced the lint library, some small partners may also use this library

lint is a code base with strict rules. For the corresponding irregularities of the code, IDEA will give prompts; for many codes that we think are reasonable, sometimes corresponding warnings may also be given.

  • In the generated template code, a few lines will be warned under the lint rule

    • These two injection codes will automatically deduce the corresponding type; but under the lint rule, there will be a yellow underline warning

image-20210906174811659

  • This adjustment is required to get rid of the warning

image-20210919172158224

Selecting the lintNorm button will generate template code in the following form; so this function is a gospel for obsessive-compulsive patients. . .

For people who use strong rules like lint, I say:

img

pub:flutter_lints

Recently, Flutter has added the flutter_lints library by default in new projects. The rules of this library are much looser, and the rules are basically standard flutter writing.

  • In the generated template code, there will be a warning

image-20210918222832370

  • The following adjustments need to be made to remove the warning

image-20210918222957089

When you turn on lintNorm, it will also help you make up the constructor of the generated page

template (toggle template naming)

Scenes

This function provides the operation of switching template naming

Three sets of template names are provided, only three sets are provided, and there will be no more

Internally refactored the persistence module

  • No refactoring will work, adding a large number of persistent variables, and using all static variables is really inelegant
  • Added data classes to record a large number of repeated persistent data
Why does provide the function of switching template naming?

When the business becomes more and more complex, in many cases, complex general components can also be encapsulated by getx

  • But use the plugin to generate the corresponding module, the Widget of the view module may still be XxxPage
  • The above situation is not very friendly, you may need XxxComponent or XxxWidget
  • Although, you can rename the suffix name in the settings, but this may have an impact on the generation of the Page module
  • Therefore, here are three sets of template naming switches, you can quickly switch to the custom naming method you need

Demo

plugin window adds three sets of template switching
  • Select Template to provide three sets of switching template names: Page, Component, Custom

    • Default Page

image-20210914222313651

Three sets of template names support custom modification
  • The above switch corresponds to the three sets of custom general suffixes on the settings page

    • The setting page layout has also been rewritten, it looks more comfortable, and the overall space utilization rate is also higher

image-20210919122822352

example
  • Mode selection: Easy; Template selection: Component

image-20210914223626196

look at the code

  • view
class TestComponent extends StatelessWidget {
  final logic = Get.put(TestLogic());

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • logic
class TestLogic extends GetxController {

}

Wrap Widget

This is a very useful feature

Currently four types of Wrap Widgets are supported: GetBuilder, GetBuilder(autoDispose), Obx, GetX

Precautions for use: click the mouse on the Widget, then press alt+enter; do not double-click to select the widget name

  • GetBuilder

GetBuilder

  • GetBuilder(Auto Dispose)

    • assignId is set to true: GetBuilder will automatically recycle its specified generic GetXController when the page is recycled

GetBuilder(Auto Dispose)

  • Obx

    • Say why the arrow symbol is not used here. If the Widget to be wrapped is very long, after using the arrow symbol, the formatted code is not neat
    • Considering this situation, the return form is used

Obx

  • GetX

    • Although I don't like to use this component, but maybe there are friends who like to use it, so I added it

GetX

  • optional close

image-20210802160631405

Quick code generation

The plugin also provides you with the function of entering keywords to generate shortcut code snippets

Note: Keyword prefix is getx

routing module

  • getxroutepagemap

getxroutepagemap

  • getxroutename

getxroutename

  • getxroutepage

getxroutenpage

  • getxto,getxtoname

getxto

  • getxoff,getxoffall,getxoffnamed,getxoffallnameed

getxoff

dependency injection

  • put

getxput

  • find

getxfind

  • lazyPut

getxlazyput

Business Layer

  • GetxController

getxcontroller

  • getxfinal,getxfinal_

getxfinal

  • getxget,getxget_

getxget

  • getset,getset_

getset

other

  • getsnakebar,getdialog,getbottomsheet

getxdialog

  • getxbuilder,getxobx

getxobx

  • binding

getxbinding

There are some other shortcut codes, feel it yourself~~

Setting function

With the continuous increase of functions, some subdivided functions need to be placed in the setting module, and it is time to write down the detailed description.

lintNorm subdivision

  • When the lintNorm function is enabled, the generated template code supports two libraries: lint and flutter_lints

image-20211207102535779

  • Now the support is subdivided, and you can set it at will: support one of the libraries or support both

image-20210926112241600

useFolderSuffix

This function can choose whether to use the folder suffix or not, and it is turned off by default.

when useFolderSuffix is not selected
  • Settings page

image-20211207093055755

  • The generated folder is like this

image-20211207102221083

when useFolderSuffix is selected
  • Settings page

image-20211207101105948

  • Append suffix to folder

image-20211207101246498

  • The added suffix is the ViewName field in each Template

image-20211207101459335

Version update instructions

3.2.x
  • Added template switching function, greatly optimized the internal persistence method
  • Refactoring settings page layout
  • Support flutter_lints rules
  • Split lintNorm: lint and flutter_lints (custom support in settings)
  • Fix Template title missing (thanks to @ for helping test)
  • Add useFolderSuffix function in settings
3.1.x
  • Significantly improved overall page layout

    • There will be no more pit ratio problems on high-size screens
  • Support for lint rules (lintNorm)
  • Improve the quick code prompt function, the "get" prefix is changed to "getx"

    • The prefix get will make the prompt code overwhelmed by many system codes. After changing to getx , it can be seen at a glance
  • Plugin description page, add a link to this article
3.0.x
  • Migrate project code from Java to kotlin
  • ModuleName input: the first letter is lowercase, and the interior will be automatically marked as uppercase
  • Increase the generation of a large number of shortcut code snippets
  • Plug-in project logic refactoring, separation of interface layer and logic layer
  • Wrap Widget added: GetBuilder (Auto Dispose)

    • The corresponding GetXController can be automatically recycled
  • Add PageView solution
  • fix some bugs
2.1.x
  • Major update!
  • Add Wrap Widget: GetBuilder, Obx, GetX
  • Increase the generation of shortcut code snippets
  • Greatly optimized plugin layout
  • Add the perfect life cycle callback function (addLifecycle)
  • Add binding function (addBinding)
1.5.x
  • Added memory function (button for memory selection)
  • Add GetXController automatic recycling function (autoDispose)
  • Support for modifying common suffixes: view, logic, state
  • Adjust the plugin description and fix some bugs
1.3.x
  • Adapt to multiple versions of IDEA (only one IDEA version was adapted before, pit)
  • Add plugin logo
  • Add a getx English article (machine translation of your own blog article)
  • Improve plugin description
1.2
  • Adjust the description content
1.1
  • Fixed the problem of abnormal package import when adding prefix
1.0
  • You can use this plugin to generate a lot of getx framework code
  • This can greatly improve your efficiency
  • If you have any questions, please send me an issue; before you do: please think about it first, is it reasonable

At last

When I continue to improve this plugin, it is also a process I keep thinking about.

Thank you all for your various needs

img

This plug-in can be improved a little bit, so that now, it can really help the pretty boys save a little development time

img

series of articles + related address

小呆呆666
177 声望1.1k 粉丝

Android Flutter