Я изучаю Flutter&Dart некоторое время, сначала я думал, что это вложенный ад, но теперь я думаю, что это неплохо! Это не кажется таким уж страшным.После того, как я постепенно освоился с Flutter, я научился начинать инкапсулировать Виджеты, научился абстрагировать Функции, научился добавлять Сервисы.Я постепенно почувствовал, что это не так сложно освоить, и я тоже стал нравиться Flutter для сборки приложения, потому что он удобный, набор кодов для Android, IOS, веба все сделано, несовместимости нет, все плавно. . .
Недавно я увидел структуру управления состоянием Flutter,flutter_bloc,MobX,GetX, я использовал первый из этих 3-х фреймворков, но MobX не использовал, но после прочтения ReadMe будет куча codegen кодов, так что мне это не очень нравится, поэтому дальше не стал, но когда я увидел GetX, я обнаружил, что это тоже слишком просто, так легко получить управление состоянием, не так ли? Сегодня давайте сделаем стартовый проект с GetX в качестве управления состоянием. Скриншот выглядит следующим образом, мы сделали заставку, затем введите логин и регистрация, а затем перейдите на домашнюю страницу. Несмотря на простоту, он охватывает предопределенную структуру папок, темы стилей, доступ к API, управление состоянием, маршрутизацию и зависимости и т. д. В нем должно быть все, что должно быть в малых и средних проектах.
1. Что такое GetX? как использовать?
-
GetX — это легкое и мощное решение на Flutter: высокопроизводительное управление состоянием, интеллектуальное внедрение зависимостей и удобное управление маршрутизацией.
-
GetX имеет 3 основных принципа:
- представление:GetX фокусируется на производительности и минимальном потреблении ресурсов. Размер упакованного apk и использование памяти во время выполнения GetX сопоставимы с другими плагинами управления состоянием. Если интересно, вот одинТестирование производительности.
- эффективность:Синтаксис GetX очень прост и поддерживает чрезвычайно высокую производительность, что может значительно сократить время разработки.
- структура:GetX может полностью отделить интерфейс, логику, зависимости и маршрутизацию, упрощая использование, делая логику более понятной и простой в обслуживании.
-
GetX не раздутый, но легкий. Если вы используете только управление состоянием, будет скомпилирован только модуль управления состоянием, больше ничего не будет скомпилировано в ваш код. Он имеет множество функций, но все эти функции находятся в отдельных контейнерах, которые запускаются только после использования.
-
Getx имеет огромную экосистему, которая работает на Android, iOS, в Интернете, Mac, Linux, Windows и на вашем сервере с одним и тем же кодом.пройти черезGet ServerВы можете полностью повторно использовать код, который вы написали во внешнем интерфейсе, в своем бэкэнде.
Из приведенного выше описания мы знаем, что такое GetX, так для чего используется GetX? Это также очень просто.В качестве примера возьмем официальный счетчик Flutter, чтобы написать счетчик GetX, который требует следующих трех шагов:
Шаг 1: Добавьте «Get» перед вашим MaterialApp, чтобы сделать его GetMaterialApp.
void main() => runApp(GetMaterialApp(home: Home()));
Шаг 2: Создайте свой класс бизнес-логики и поместите в него все переменные, методы и контроллеры. Вы можете сделать любую переменную наблюдаемой с помощью простого «.obs».
class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}
Шаг 3: Создайте свой интерфейс, сэкономьте немного памяти с помощью StatelessWidget, используйте GET, вам может больше не понадобиться использовать StateFulWidget.
class Home extends StatelessWidget {
@override
Widget build(context) {
// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());
return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),
// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}
class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();
@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}
видеть это? Очень лаконичная реализация официального проекта счетчика. После знакомства с GetX давайте перейдем к сути и создадим наш начальный проект GetX flutter_getx_boilerplate.
2. Инициализируйте проект Flutter.
Создайте проект инициализации с помощью следующей командной строки, откройте проект с помощью кода VS, и теперь ваш проект является полным официальным проектом счетчика Flutter.
flutter create flutter_getx_boilerplate
3. Структура папок проекта сборки следующая. Пожалуйста, внимательно прочитайте описание каждой папки и файла.
lib/
|- api - 全局Restful api请求,包括请求拦截器等
|- interceptors - 拦截器,包括auth、request、response拦截
|- api.dart - Restful api导出文件
|- lang - 国际化,包含翻译文件,翻译服务文件等
|- lang.dart - 语言导出文件
|- models - 各种结构化实体类,分为request和response两种类型的实体
|- models.dart - 实体类导出文件
|- modules - 业务模块文件夹
|- auth - 登录&注册模块
|- home - 首页模块
|- splash - splash模块
|- modules.dart - 模块导出文件
|- routes - 路由模块
|- app_pages.dart - 路由页面配置
|- app_routes.dart - 路由名称
|- routes.dart - 路由导出文件
|- Shared - 全局共享文件夹,包括静态变量、全局services、utils、全局Widget等
|- shared.dart - 全局共享导出文件
|- theme - 主题文件夹
|- app_bindings.dart - 在app运行之前启动的服务等,如Restful api
|- di.dart - 全局依赖注入对象,如SharedPreferences等
|- main.dart - 导出类,用作外面调用api请求主入口
4. Добавлен модуль Splash.
Как правило, в проекте мы добавим страницу-заставку, эта страница аналогична роли страницы приветствия, в этом проекте роль этой страницы заключается в том, чтобы определить, вошел ли текущий пользователь в систему, если вы не вошли в систему. Страница параметров регистрации или перейдите непосредственно на домашнюю страницу (советы, конечно, мы не можем написать свою собственную страницу-заставку, паб над родным пакетом может использовать страницу приветствия,flutter_native_splash).
Модуль Splash содержит следующие файлы 4. Каждый из наших модулей будет содержать как минимум эти файлы. Это основано на GetX.ПримерВнесла некоторые изменения в свои привычки.
|- Splash - Splash模块文件夹
|- splash_binding.dart - Splash依赖绑定文件,也就是这个模块依赖的Controller,Service都可以在这里注入进去。
|- splash_controller.dart - Controller文件主要处理当前模块的业务逻辑,应该把所有的业务逻辑写在这里面,保证UI与业务完全分离。
|- splash_screen.dart - 当前模块的页面UI文件。
|- splash.dart - Splash模块的导出文件,导出这个模块下面的所有文件,方便引用。
А. splash_binding.dart, нам нужно полагаться только на контроллер для модуля заставки, поэтому используйте Get.put, чтобы добавить его, чтобы контроллер можно было импортировать позже через Get.find().
import 'package:get/get.dart';
import 'splash_controller.dart';
class SplashBinding extends Bindings {
@override void dependencies() {
Get.put<SplashController>(SplashController());
}
}
b. splash_controller.dart, Контроллер определяет, следует ли входить в систему, определяя, существует ли токен. Обратите внимание, что здесь мы использовали переходGet.toNamed()метод, вы нашли, что контекст здесь не нужен, да, GetX не нужен! Кроме того, здесь мы используем дополнительную задержку для имитации некоторых трудоемких операций, например, вам нужно запросить фоновый API для получения некоторых основных данных.
import 'package:flutter_getx_boilerplate/routes/routes.dart';
import 'package:flutter_getx_boilerplate/shared/shared.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SplashController extends GetxController {
@override
void onReady() async {
super.onReady();
await Future.delayed(Duration(milliseconds: 2000));
var storage = Get.find<SharedPreferences>();
try {
if (storage.getString(StorageConstants.token) != null) {
Get.toNamed(Routes.HOME);
} else {
Get.toNamed(Routes.AUTH);
}
} catch (e) {
Get.toNamed(Routes.AUTH);
}
}
}
c. splash_screen.dart, мы использовали простую загрузку для страницы-заставки.
import 'package:flutter/material.dart';
import 'package:flutter_getx_boilerplate/shared/shared.dart';
class SplashScreen extends StatelessWidget {
@override Widget build(BuildContext context) {
SizeConfig().init(context);
return Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.hourglass_bottom,
color: ColorConstants.darkGray,
size: 30.0,
),
Text(
'loading...',
style: TextStyle(fontSize: 30.0),
),
],
),
);
}
}
г. splash.dart, экспортировать все файлы текущего модуля.
export 'splash_binding.dart';
export 'splash_controller.dart';
export 'splash_screen.dart';
На этом сплэш-модуль готов.Подводя итог, каждый раз, когда мы добавляем модуль, нам нужно создавать как минимум 4 файла, привязка — привязка зависимостей, контроллер — обработка бизнес-логики, экран — пользовательский интерфейс страницы, файлы экспорта.
5. Добавьте модуль «Маршруты».
В приведенном выше контроллере модуля-заставки мы написали некоторую логику перехода, чтобы перейти на страницу выбора входа и регистрации или на домашнюю страницу. Теперь давайте определим маршрут, GetX также включаетмодуль маршрутизацииИ очень удобно использовать, вы можете прыгать без контекста.
А. Перейдите на следующую страницу
Get.toNamed("/NextScreen");
б. Просмотрите и удалите предыдущую страницу
Get.offNamed("/NextScreen");
в. Просмотрите и удалите все предыдущие страницы
Get.offAllNamed("/NextScreen");
г. Навигация и ввод параметров, затем получение параметров на экране или контроллере
Get.toNamed("/NextScreen", arguments: 'Get is the best');
print(Get.arguments);
//print out: Get is the best
Очень лаконично и естественно.Если мы используем нативную навигацию Flutter, это будет выглядеть следующим образом.
Navigator.pushNamed(context, "/NextScreen", arguments: "Get is the best");
Итак, кратко представили функцию маршрутизации GetX, мы определяем собственный модуль маршрутизации.
А. app_routes.dart, определите имя маршрута, у нас есть корневая страница (заставка), страница выбора входа и регистрации, страница входа, страница регистрации и домашняя страница.
part of 'app_pages.dart';
abstract class Routes {
static const SPLASH = '/';
static const AUTH = '/auth';
static const LOGIN = '/login';
static const REGISTER = '/register';
static const HOME = '/home';
}
б) app_pages.dart определяет маршрут GetX, мы замечаем GetPage и содержащиеся в нем параметры, каждая GetPage является определением маршрута, и каждое определение маршрута содержит имя, страницу страницы и зависимости привязки, поэтому мы помещаем зависимости Binding в указанный route, у каждого маршрута будет указанная зависимость, конечно, мы также можем добавить глобальную initialBinding, эта зависимость является глобальной зависимостью, мы поговорим о ней позже в основном файле записи. В Routes.AUTH мы также используем подмаршруты, auth и его подмаршруты, к которым мы можем получить доступ следующим образом.
Get.toNamed(Routes.AUTH); // 跳转到登录&注册选择页面
// 注意到,我们这里的参数传入了controller到子路由,因为我们的Routes.Auth主路由和子路由使用了同一个controller。
Get.toNamed(Routes.AUTH + Routes.LOGIN, arguments: controller); // 进入登录页面
Get.toNamed(Routes.AUTH + Routes.REGISTER, arguments: controller); // 进入注册页面
app_pages.dart
import 'package:flutter_getx_boilerplate/modules/auth/auth.dart';
import 'package:flutter_getx_boilerplate/modules/home/home.dart';
import 'package:flutter_getx_boilerplate/modules/modules.dart';
import 'package:get/get.dart';
part 'app_routes.dart';
class AppPages {
static const INITIAL = Routes.SPLASH;
static final routes = [
GetPage(
name: Routes.SPLASH,
page: () => SplashScreen(),
binding: SplashBinding(),
),
GetPage(
name: Routes.AUTH,
page: () => AuthScreen(),
binding: AuthBinding(),
children: [
GetPage(name: Routes.REGISTER, page: () => RegisterScreen()),
GetPage(name: Routes.LOGIN, page: () => LoginScreen()),
],
),
GetPage(
name: Routes.HOME,
page: () => HomeScreen(),
binding: HomeBinding(),
),
];
}
6. Добавьте модуль API в проект.
Модуль API Мы используем бесплатный Restful APIREQ|RESЧтобы имитировать наш бизнес-логин, регистрацию и информацию о пользователе и т. д. В то же время мы использовали встроенный http-модуль GetX для сборки модуля API, мы добавили провайдера, репозиторий, перехватчики и т. д. Здесь, поскольку это шаблонный проект GetX, мы не различали API в соответствии с модуль.
а) base_provider.dart, обеспечивающий функцию перехватчиков.Провайдеры могут наследовать base_provider.dart для инициализации перехватчиков.
import 'package:get/get.dart';
import 'api.dart';
class BaseProvider extends GetConnect {
@override void onInit() {
httpClient.baseUrl = ApiConstants.baseUrl;
httpClient.addAuthenticator(authInterceptor);
httpClient.addRequestModifier(requestInterceptor);
httpClient.addResponseModifier(responseInterceptor);
}
}
B. Перехватчики, мы добавили 3 типа перехватчиков, перехватчики аутентификации, запроса и ответа, здесь вы можете выполнить некоторую предварительную обработку для HTTP-запросов, такую как получение и сохранение токена, обработка исключений, запрос на добавление заголовков jwt, добавление запроса на загрузку и т. д. .
auth_interceptor.dart
import 'dart:async';
import 'package:get/get_connect/http/src/request/request.dart';
FutureOr<Request> authInterceptor(request) async {
// final token = StorageService.box.pull(StorageItems.accessToken);
// request.headers['X-Requested-With'] = 'XMLHttpRequest';
// request.headers['Authorization'] = 'Bearer $token';
return request;
}
request_interceptor.dart
import 'dart:async';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get_connect/http/src/request/request.dart';
FutureOr<Request> requestInterceptor(request) async {
// final token = StorageService.box.pull(StorageItems.accessToken);
// request.headers['X-Requested-With'] = 'XMLHttpRequest';
// request.headers['Authorization'] = 'Bearer $token';
EasyLoading.show(status: 'loading...');
return request;
}
response_interceptor.dart
import 'dart:async';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_getx_boilerplate/models/models.dart';
import 'package:flutter_getx_boilerplate/shared/shared.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
FutureOr<dynamic> responseInterceptor(
Request request, Response response) async {
EasyLoading.dismiss();
if (response.statusCode != 200) {
handleErrorStatus(response);
return;
}
return response;
}
void handleErrorStatus(Response response) {
switch (response.statusCode) {
case 400:
final message = ErrorResponse.fromJson(response.body);
CommonWidget.toast(message.error);
break;
default:
}
return;
}
в) api_provider.dart, здесь только Restful api, также можно добавить db_provider.dart, cache_provider.dart и т.д. Здесь мы наследуем BaseProvider, так что мы добавим три вышеуказанных перехватчика перед первым вызовом приобретенного интерфейса.
import 'package:flutter_getx_boilerplate/api/base_provider.dart';
import 'package:flutter_getx_boilerplate/models/models.dart';
import 'package:get/get.dart';
class ApiProvider extends BaseProvider {
Future<Response> login(String path, LoginRequest data) {
return post(path, data.toJson());
}
Future<Response> register(String path, RegisterRequest data) {
return post(path, data.toJson());
}
Future<Response> getUsers(String path) {
return get(path);
}
}
г. api_repository.dart, для обработки данных, в этом классе мы обрабатываем только успешные запросы, а все неудачные запросы передаются перехватчику.
import 'dart:async';
import 'package:flutter_getx_boilerplate/models/models.dart';
import 'package:flutter_getx_boilerplate/models/response/users_response.dart';
import 'api.dart';class ApiRepository {
ApiRepository({required this.apiProvider});
final ApiProvider apiProvider;
Future<LoginResponse?> login(LoginRequest data) async {
final res = await apiProvider.login('/api/login', data);
if (res.statusCode == 200) {
return LoginResponse.fromJson(res.body);
}
}
Future<RegisterResponse?> register(RegisterRequest data) async {
final res = await apiProvider.register('/api/register', data);
if (res.statusCode == 200) {
return RegisterResponse.fromJson(res.body);
}
}
Future<UsersResponse?> getUsers() async {
final res = await apiProvider.getUsers('/api/users?page=1&per_page=12');
if (res.statusCode == 200) {
return UsersResponse.fromJson(res.body);
}
}
}
Что ж, с модулем API мы теперь можем войти в наш бизнес-модуль обработки.
7. Бизнес-модуль аутентификации.
Модуль аутентификации имитирует вход и регистрацию пользователя, потому что каждый проект отличается от пользовательского интерфейса страницы, это не разговор, если вам нужно увидеть себяисходный код, чтобы сосредоточиться на привязке и контроллере, а также на том, как пользовательский интерфейс страницы использует контроллер.
A. auth_binding.dart, модуль аутентификации зависит от контроллера. Обратите внимание, что контроллер имеет параметр apiRepository. Мы напрямую используем метод Get.find() для его получения. Ранее мы упоминали, что Get.put() требуется, прежде чем мы можно использовать Get.find() для получения соответствующего экземпляра, здесь apiRepository мы зарегистрировали глобальную зависимость, то есть initialBinding в файле входа main.dart, поэтому мы можем получить apiRepository.
import 'package:get/get.dart';
import 'auth_controller.dart';
class AuthBinding implements Bindings {
@override void dependencies() {
Get.lazyPut<AuthController>(
() => AuthController(apiRepository: Get.find()));
}
}
B. auth_controller.dart, файл контроллера модуля аутентификации, в основном связан с бизнес-логикой, такой как проверка, вход в систему и регистрация Мы помещаем все TextEditingControllers в контроллер, чтобы мы могли обрабатывать бизнес в контроллере.
import 'package:flutter/material.dart';
import 'package:flutter_getx_boilerplate/api/api.dart';
import 'package:flutter_getx_boilerplate/models/models.dart';
import 'package:flutter_getx_boilerplate/routes/app_pages.dart';
import 'package:flutter_getx_boilerplate/shared/shared.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthController extends GetxController {
final ApiRepository apiRepository;
AuthController({required this.apiRepository});
final registerFormKey = GlobalKey<FormState>();
final registerEmailController = TextEditingController();
final registerPasswordController = TextEditingController();
final registerConfirmPasswordController = TextEditingController();
bool registerTermsChecked = false;
final loginFormKey = GlobalKey<FormState>();
final loginEmailController = TextEditingController();
final loginPasswordController = TextEditingController();
@override void onReady() {
super.onReady();
}
void register(BuildContext context) async {
AppFocus.unfocus(context);
if (registerFormKey.currentState!.validate()) {
if (!registerTermsChecked) {
CommonWidget.toast('Please check the terms first.');
return;
}
final res = await apiRepository.register(
RegisterRequest(
email: registerEmailController.text,
password: registerPasswordController.text,
),
);
final prefs = Get.find<SharedPreferences>();
if (res!.token.isNotEmpty) {
prefs.setString(StorageConstants.token, res.token);
print('Go to Home screen>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
}
}
}
void login(BuildContext context) async {
AppFocus.unfocus(context);
if (loginFormKey.currentState!.validate()) {
final res = await apiRepository.login(
LoginRequest(
email: loginEmailController.text,
password: loginPasswordController.text,
),
);
final prefs = Get.find<SharedPreferences>();
if (res!.token.isNotEmpty) {
prefs.setString(StorageConstants.token, res.token);
Get.toNamed(Routes.HOME);
}
}
}
}
в. Как пользовательский интерфейс страницы использует контроллер?GetView,Obx. Используйте эти два класса, предоставляемые GetX, для обеспечения бесшовного соединения между пользовательским интерфейсом страницы и контроллером.
GetView - это виджет безделька, поэтому мы можем использовать GetView As as stainteleswidget, но getview есть универсальный getter, может получить соответствующий контроллер, чтобы мы могли использовать контроллер на нашей странице класс UI. Заимствование пример чиновника GetX.
class AwesomeController extends GetController {
final String title = 'My Awesome View';
}
// ALWAYS remember to pass the `Type` you used to register your controller!
class AwesomeView extends GetView<AwesomeController> {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
child: Text(controller.title), // just call `controller.something`
);
}
}
Obx, используя его, в вашем проекте не будет StatefulWidget, потому что до тех пор, пока контент, обернутый Obx, может реагировать на изменения в контроллере в режиме реального времени. Таким образом, вам нужно только обернуть контент, который нужно изменить, и вы можете написать то, что хотите написать, если изменений нет. Это также пример, взятый с официального сайта.
// This is your count variable:
var name = 'Jonatas Borges';// To make it observable, you just need to add ".obs" to the end of it:
var name = 'Jonatas Borges'.obs;// And in the UI, when you want to show that value and update the screen whenever the values changes, simply do this:
Obx(() => Text("${controller.name}"));
Видишь, это так просто!
Как и на официальном сайте GetX, нам просто нужно заменить MaterialApp на GetMaterialApp, и все готово!
Ранее мы говорили о внедрении глобальной зависимости initialBinding, чтобы мы могли использовать эту зависимость глобально через Get.find(). Эта глобальная зависимость размещается в параметрах GetMaterialApp.В то же время GetMaterialApp также предоставляет initialRoute, smartManagement, локаль, переводы и т. д., которые можно изучить самостоятельно.
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_getx_boilerplate/shared/shared.dart';
import 'package:get/get.dart';
import 'app_binding.dart';
import 'di.dart';
import 'lang/lang.dart';
import 'routes/routes.dart';
import 'theme/theme.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await DenpendencyInjection.init();
runApp(App());
}
class App extends StatelessWidget {
@override Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
enableLog: true,
initialRoute: Routes.SPLASH,
defaultTransition: Transition.fade,
getPages: AppPages.routes,
initialBinding: AppBinding(),
smartManagement: SmartManagement.keepFactory,
title: 'Flutter GetX Boilerplate',
theme: ThemeConfig.lightTheme,
locale: TranslationService.locale,
fallbackLocale: TranslationService.fallbackLocale,
translations: TranslationService(),
);
}
}
Кроме того, наряду с нашим основным файлом основной записи, есть также файл di.dart, В этом файле я зарегистрировал хранимую службу, которая хранится глобально, например, локально хранимый токен, userInfo и другую информацию.
import 'package:get/get.dart';
import 'shared/services/services.dart';
class DenpendencyInjection {
static Future<void> init() async {
await Get.putAsync(() => StorageService().init());
}
}
Что ж, это основное введение во весь шаблонный проект! Подводя итог, наш проект включает в себя модуль API Restful для обработки http-запросов, общий модуль добавляет некоторые глобально используемые утилиты, константы, сервисы и виджеты, а также модули маршрутизации, бизнес-функций, тем, интернационализации и другие модули. Наконец, что касается исходного кода, все желающие могут оставить комментарии и предложения!