Как четвертая статья в серии, данная работа рассматривает использование Redux в Flutter и демонстрирует реализацию реального времени для смены тем и многозначной локализации.
Flutter как реактивная платформа, использует state
для реализации логики отрисовки между кадрами, что может ассоциироваться с React и React Native. В React, популярный подход к управлению состоянием — это Redux, который также применим и в Flutter.
Мы достигнем следующего эффекта, соответствующий код доступен в GSYGithubAppFlutter. В данной статье используется библиотека Redux flutter_redux.
Основная идея Redux — это управление состоянием. Почему же нам нужен Redux, если у нас уже есть state
? Преимущество использования Redux заключается в возможности общего управления состоянием и единого источника данных.
Рассмотрите ситуацию, когда в приложении несколько мест используют данные авторизованного пользователя. Если где-то эти данные будут изменены, то обновление всех страниц станет сложной задачей.Однако после внедрения Redux, изменения данных пользователя на одной странице автоматически приведут к обновлению всех связанных с Redux компонентов. Такое решение значительно упрощает работу и обеспечивает удобство управления данными в едином источнике. Это аналогично тому, как мы будем использовать темы и множество языков в дальнейшем.
Как показано на рисунке выше, Redux состоит из трех основных частей: Store, Action, Reducer.
Поэтому общая последовательность действий выглядит так:
Сначала нам следует создать Store. Как показано ниже, для создания Store требуется reducer
, который представляет собой метод, принимающий state
и action
и возвращающий новое состояние. Поэтому нам сначала нужно создать объект состояния (GSYState
) класса, который будет хранить данные для совместного использования, такие как:
Затем нам нужно определить метод Reducer
, называемый appReducer
: связать каждый параметр внутри GSYState
со соответствующими действиями (actions
). В результате возвращается полное состояние GSYState
. Таким образом, мы определяем состояние и редуктор для создания хранилища.
/// Объект глобального Redux хранилища, который сохраняет данные состояния
class GSYState {
/// Информация пользователя
User userInfo;
}
``````markdown
/// Тема
ThemeData themeData;
/// Язык
Locale locale;
/// Конструктор
GSYState({this.userInfo, this.themeData, this.locale});
}
/// Создание Reducera
/// В исходном коде Reducer - это метод typedef State Reducer<State>(State state, dynamic action)
/// Мы создаем свой собственный appReducer для создания хранилища
GSYState appReducer(GSYState state, action) {
return GSYState(
/// Через пользовательский UserReducer связываем userInfo внутри GSYState с action
userInfo: UserReducer(state.userInfo, action),
/// Через пользовательский ThemeDataReducer связываем themeData внутри GSYState с action
themeData: ThemeDataReducer(state.themeData, action),
/// Через пользовательский LocaleReducer связываем locale внутри GSYState с action
locale: LocaleReducer(state.locale, action),
);
}
Как показано выше, каждый параметр GSYState
возвращается через отдельный пользовательский редуктор. Например, themeData
генерируется методом ThemeDataReducer
, который фактически связывает ThemeData
со всеми темами, связанными действиями (actions
), чтобы разделить его от других параметров. Таким образом, можно независимо управлять каждым параметром внутри GSYState
.
Продолжая этот процесс, как показано ниже, используя combineReducers
и TypedReducer
из библиотеки flutter_redux
, связываем класс RefreshThemeDataAction
с методом _refresh
, что в конечном итоге вернет экземпляр ThemeData
. Это значит, что при каждом отправлении действия RefreshThemeDataAction
вызывается метод _refresh
, который затем обновляет параметр themeData
внутри GSYState
.
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
/// Using combineReducers from flutter_redux, we create a Reducer<State>
final ThemeDataReducer = combineReducers<ThemeData>([
/// We bind the Action, method for handling the Action and State
TypedReducer<ThemeData, RefreshThemeDataAction>(_refresh),
]);
/// Define the method to handle the Action, returning a new State
ThemeData _refresh(ThemeData themeData, action) {
themeData = action.themeData;
return themeData;
}
/// Define the Action class
/// Bind this Action to the handler method in the Reducer
class RefreshThemeDataAction {
final ThemeData themeData;
RefreshThemeDataAction(this.themeData);
}
```
Отлично, теперь мы можем создать **Store**. Как показано ниже, при создании Store мы инициализируем GSYState через `initialState`, а затем используем `StoreProvider` для загрузки Store и его применения к `MaterialApp`. **На этом завершается инициализация в Redux.**
```
void main() {
runApp(new FlutterReduxApp());
}
class FlutterReduxApp extends StatelessWidget {
/// Create a Store using appReducer from GSYState to create a Reducer
/// initialState is used to initialize the State
final store = new Store<GSYState>(
appReducer,
initialState: new GSYState(
userInfo: User.empty(),
themeData: new ThemeData(
primarySwatch: GSYColors.primarySwatch,
),
locale: Locale('ru', 'RU')),
);
FlutterReduxApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// Apply the Store with StoreProvider
return new StoreProvider(
store: store,
child: new MaterialApp(),
);
}
}
```Итак, далее следует использование. Как показано ниже, связывание данных и компонентов осуществляется путём использования `StoreConnector` в `build`, где данные `store.state` преобразуются через `converter`, а затем возвращаются необходимые для отображения компоненты через `builder`. Конечно, вы также можете использовать `StoreBuilder`.```
class DemoUseStorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// Связываем GSYState с User с помощью StoreConnector
return new StoreConnector<GSYState, User>(
/// Преобразуем данные из GSYState через конвертер
converter: (store) => store.state.userInfo,
/// Возвращаем компоненты для отображения из userInfo
builder: (context, userInfo) {
return new Text(
userInfo.name,
);
},
);
}
}
```
Наконец, чтобы запустить процесс обновления, используйте следующий код:
```markdown
StoreProvider.of<User>(context).dispatch(new UpdateUserAction(newUserInfo));
```
Таким образом, либо в случае простой бизнес-логики, Redux может не иметь никаких преимуществ и даже выглядеть избыточным. Однако, как только архитектура создана, при сложной бизнес-логике это становится особенно удобным.
## Второй раздел: Тема
Flutter предоставляет официальную поддержку тем для своих приложений через `MaterialApp`, которая предлагает параметр `theme` для установки темы. После этого можно использовать `Theme.of(context)` для получения текущего объекта `ThemeData` и его использования для настройки цветов и шрифтов компонентов.
Создание объекта `ThemeData` позволяет указывать множество параметров, среди которых ключевым является параметр `primarySwatch`. Объект `primarySwatch` представляет собой объект типа **MaterialColor**, содержащий десять различных оттенков одного цвета, что делает его идеальным выбором для основного тона темы.Как показано ниже на схеме и коде, Flutter по умолчанию предлагает множество готовых тем, но также позволяет создавать пользовательские темы с помощью `MaterialColor`.

```dart
MaterialColor primarySwatch = const MaterialColor(
primaryValue,
const <int, Color>{
50: const Color(primaryLightValue),
100: const Color(primaryLightValue),
200: const Color(primaryLightValue),
300: const Color(primaryLightValue),
400: const Color(primaryLightValue),
500: const Color(primaryValue),
600: const Color(primaryDarkValue),
700: const Color(primaryDarkValue),
800: const Color(primaryDarkValue),
900: const Color(primaryDarkValue),
},
);
```
А как реализовать мгновенную смену темы? Конечно же, с использованием Redux!
Ранее мы уже создали поле `themeData` в классе `GSYState`, теперь нам нужно передать его в качестве параметра `theme` для `MaterialApp`. Затем, используя `dispatch`, мы можем менять значение `themeData`, чтобы осуществить смену темы.
Обратите внимание, поскольку ваш `MaterialApp` тоже является `StatefulWidget`, вам потребуется обернуть его в `StoreBuilder`, как показано ниже:
```dart
@Override
Widget build(BuildContext context) {
// Применяем store через StoreProvider
return new StoreProvider(
store: store,
child: new StoreBuilder<GSYState>(builder: (context, store) {
return new MaterialApp(
theme: store.state.themeData);
}),
);
}
```
Теперь вы можете использовать `dispatch` для изменения темы и `Theme.of(context).primaryColor` для получения значения основного цвета темы.---
ThemeData тема = new ThemeData(primarySwatch: цвета[index]);
store.dispatch(new ОбновлениеТемы(тема));
Переведём недопереведённые части:
```dart
MaterialColor primarySwatch = const MaterialColor(
primaryValue,
const <int, Color>{
50: const Color(primaryLightValue),
100: const Color(primaryLightValue),
200: const Color(primaryLightValue),
300: const Color(primaryLightValue),
400: const Color(primaryLightValue),
500: const Color(primaryValue),
600: const Color(primaryDarkValue),
700: const Color(primaryDarkValue),
800: const Color(primaryDarkValue),
900: const Color(primaryDarkValue),
},
);
```
Теперь текст полностью переведён и оформлен согласно правилам.```

## 3. Интернационализация
Интернационализация в Flutter по официальной документации [интернационализация](https://flutterchina.club/tutorials/internationalization) выглядит немного сложной и не упоминает реальное время смену языка, поэтому здесь представлен быстрый способ реализации. Конечно же, это невозможно без использования Redux!

Как показано на приведённой выше схеме, основной процесс осуществляется через настройку дефолтного `MaterialApp`, а также требует создания объектов **`LocalizationsDelegate`** и **`Localizations`**. В конечном итоге этот процесс использует `Localizations` для загрузки этого `delegate` с использованием `Locale`. Поэтому нам нужно сделать следующее:
* Реализовать **LocalizationsDelegate**.
* Реализовать **Localizations**.
* Использовать **Store** для смены языка.
Как показано ниже, создание пользовательского делегата требует наследования от объекта `LocalizationsDelegate`, где главным образом реализуется метод `load`. Мы можем использовать параметр `locale` для определения необходимого языка и вернуть наш пользовательский объект многоязычия `GSYLocalizations`. Наконец, мы предоставляем `LocalizationsDelegate` через статический `delegate`.
```
/**
* Делегат многоязычия
* Создан Гuoshyu
* Дата: 2018-08-15
*/
class GSYLocalizationsDelegate extends LocalizationsDelegate<GSYLocalizations> {
``` GSYLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
/// Поддерживает русский и английский
return ['en', 'ru'].contains(locale.languageCode);
}
/// Создает объект для предоставления текущего языка
@override
Future<GSYLocalizations> load(Locale locale) {
return new SynchronousFuture<GSYLocalizations>(new GSYLocalizations(locale));
}
@override
bool shouldReload(LocalizationsDelegate<GSYLocalizations> old) {
return false;
}
/// Глобальный статический делегат
static GSYLocalizationsDelegate delegate = new GSYLocalizationsDelegate();
}
Вышеупомянутый `GSYLocalizations` является пользовательским объектом, который, как показано ниже, зависит от `Locale`, используя `locale.languageCode` для определения соответствующего языкового объекта: *реализации класса GSYStringBase*. Поскольку объект **GSYLocalizations** в конечном итоге загружается через `Localizations`, то и объект `Locale` также присваивается через делегат. В этом контексте можно получить `GSYLocalizations` с помощью `Localizations.of`, например: `GSYLocalizations.of(context).currentLocalized.app_name`.
```
/// Класс для реализации многоязычности
class GSYLocalizations {
final Locale locale;
GSYLocalizations(this.locale);
/// Загрузка соответствующих локализаций в зависимости от locale.languageCode
/// Где GSYStringEn и GSYStringRu наследуются от GSYStringBase
static Map<String, GSYStringBase> _localizedValues = {
'en': new GSYStringEn(),
'ru': new GSYStringRu(),
};
GSYStringBase get currentLocalized {
return _localizedValues[locale.languageCode];
}
/// Получение текущего экземпляра GSYLocalizations через Localizations
/// и получение соответствующего GSYStringBase
static GSYLocalizations of(BuildContext context) {
return Localizations.of(context, GSYLocalizations);
}
}
```/// Абстрактный базовый класс для языковых строк
abstract class GSYStringBase {
String app_name;
}
/// Реализация класса для английского языка
class GSYStringEn extends GSYStringBase {
@override
String app_name = "GSYGithubAppFlutter";
}
/// Пример использования
GSYLocalizations.of(context).currentLocalized.app_name
```
Разговорившись о делегатах, теперь переходим к `Localizations`. На диаграмме выше видно, что `Localizations` предоставляет метод `override`, который позволяет создать новый экземпляр `Localizations`. Внутри этого метода можно установить значение `locale`, а мы хотим обеспечить **динамическое переключение языка в реальном времени**.
Ниже представлен пример создания `Widget` для `GSYLocalizations`, используя `StoreBuilder` для связи со `Store`, а затем оборачивание нужной страницы с помощью `Localizations.override` для связывания значения `locale` из `Store` с `locale` в `Localizations`.
```
class GSYLocalizations extends StatefulWidget {
final Widget child;
GSYLocalizations({Key key, this.child}) : super(key: key);
@override
State<GSYLocalizations> createState() {
return new _GSYLocalizations();
}
}
class _GSYLocalizations extends State<GSYLocalizations> {
@override
Widget build(BuildContext context) {
return new StoreBuilder<GSYState>(builder: (context, store) {
/// Реализация динамической многоязычности с помощью StoreBuilder и Localizations
return new Localizations.override(
context: context,
locale: store.state.locale,
child: widget.child,
);
});
}
}
```
Ниже приведён код, в котором объект `GSYLocalizations` используется в `MaterialApp`. Для смены локали можно использовать метод `store.dispatch`.
``````markdown
```dart
@Override
Widget build(BuildContext context) {
// Применяем store через StoreProvider
return new StoreProvider(
store: store,
child: new StoreBuilder<GSYState>(builder: (context, store) {
return new MaterialApp(
// Реализация многоязычия
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GSYLocalizationsDelegate.delegate,
],
locale: store.state.locale,
supportedLocales: [store.state.locale],
routes: {
HomePage.sName: (context) {
// Обёртка через Localizations.override --- здесь
return new GSYLocalizations(
child: new HomePage(),
);
},
});
}),
);
}
// Метод для изменения локали
static changeLocale(Store<GSYState> store, int index) {
Locale locale = store.state.platformLocale;
switch (index) {
case 1:
locale = Locale('ru', 'RU'); // Например, для русской локали
break;
case 2:
locale = Locale('en', 'US');
break;
}
store.dispatch(RefreshLocaleAction(locale));
}
```
При изменении локали состояние должно сохраняться, а при запуске приложения значение должно быть выдано после dispatch, что завершает настройку темы и многоязычия.
> Таким образом, четвертая часть наконец завершена! (///▽///)
### Рекомендации по материалам:
* GitHub: <https://github.com/CarGuo/>
* **Открытый проект на Flutter: https://github.com/CarGuo/GSYGithubAppFlutter**
* **Множество примеров на Flutter для обучения: https://github.com/CarGuo/GSYFlutterDemo**
* **Открытая электронная книга по Flutter: https://github.com/CarGuo/GSYFlutterBook**
#### Рекомендации по полному открытому проекту:
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp)
```
```
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )