1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-4.md 24 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Как четвертая статья в серии, данная работа рассматривает использование Redux в Flutter и демонстрирует реализацию реального времени для смены тем и многозначной локализации.

Ссылка на статью:

Коллекция статей по Flutter

Популярные статьи по Flutter

Flutter как реактивная платформа, использует state для реализации логики отрисовки между кадрами, что может ассоциироваться с React и React Native. В React, популярный подход к управлению состоянием — это Redux, который также применим и в Flutter.

Мы достигнем следующего эффекта, соответствующий код доступен в GSYGithubAppFlutter. В данной статье используется библиотека Redux flutter_redux.

Давайте сделаем это!

1. Redux

Основная идея Redux — это управление состоянием. Почему же нам нужен Redux, если у нас уже есть state? Преимущество использования Redux заключается в возможности общего управления состоянием и единого источника данных.

Рассмотрите ситуацию, когда в приложении несколько мест используют данные авторизованного пользователя. Если где-то эти данные будут изменены, то обновление всех страниц станет сложной задачей.Однако после внедрения Redux, изменения данных пользователя на одной странице автоматически приведут к обновлению всех связанных с Redux компонентов. Такое решение значительно упрощает работу и обеспечивает удобство управления данными в едином источнике. Это аналогично тому, как мы будем использовать темы и множество языков в дальнейшем.Общий процесс

Как показано на рисунке выше, Redux состоит из трех основных частей: Store, Action, Reducer.

  • Action используется для определения запроса на изменение данных.
  • Reducer используется для создания нового состояния на основе действия, обычно это метод.
  • Store используется для хранения и управления состоянием.

Поэтому общая последовательность действий выглядит так:

  1. Widget связан с состоянием в Store.
  2. Widget отправляет действие через Action.
  3. Reducer создает новое состояние на основе действия.
  4. Обновленное состояние в Store обновляет связанный Widget.

Сначала нам следует создать 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`.

![image.png](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image4)

```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),
    },
  );
```
Теперь текст полностью переведён и оформлен согласно правилам.```



![Удобная смена](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image5)

## 3. Интернационализация

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

![Основной процесс](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image6)

Как показано на приведённой выше схеме, основной процесс осуществляется через настройку дефолтного `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)
```![Увидимся ли еще раз?](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image8)
```

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master