[Flutter翻译]Flutter和MobX:黑暗/光明模式切换器

538 阅读5分钟

原文地址:developer.school/flutter-mob…

原文作者:developer.school/author/paul…

发布时间:2020年4月27日 - 6分钟阅读

在这篇文章中,我们将创建一个使用Flutter MobX和Provider在两个ThemeData状态之间切换的小程序。我们将查看以下关键概念。

  1. 如何在暗模式和亮模式之间切换。
  2. 如何创建一个ThemeStore,它将负责发射动作(s)和管理可观察值。
  3. 如何将ThemeStore注入到我们的Widget树中。
  4. 如何根据应用程序在黑暗或光明模式下的反应来显示SnackBar。

项目设置

让我们继续创建一个新的Flutter项目并安装我们所需的依赖关系。

# New Flutter project
$ flutter create mobx_theme

# Open in VS Code
$ cd mobx_theme && code .

一旦我们打开了项目,我们就可以更新pubspec.yaml,添加以下的依赖关系和dev_dependencies。

dependencies:
  flutter:
    sdk: flutter

  provider: ^4.0.5
  mobx: ^1.1.1
  flutter_mobx: ^1.1.0
  shared_preferences: ^0.5.6+3

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  mobx_codegen: ^1.0.3

在黑暗和光明模式之间切换

在实施任何状态管理方案之前,我们如何在暗模式和亮模式之间切换?Flutter通过在选定的ThemeData中改变亮度,让它变得简单。

这里分别以lightTheme和darkTheme为例。

 ThemeData get lightTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.deepPurpleAccent,
        brightness: Brightness.light,
        scaffoldBackgroundColor: Color(0xFFecf0f1),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  ThemeData get darkTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.tealAccent,
        brightness: Brightness.dark,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

这只代表了一小部分可配置的主题选项,我们鼓励你在这里创建自己的主题。

主题库

我们将从创建一个IThemeRepository接口开始。

/// lib/domain/theme/interfaces/i_theme_repository.dart
import 'package:flutter/material.dart';

abstract class IThemeRepository {
  Future<String> getThemeKey();
  Future<void> setThemeKey(Brightness brightness);
}

然后,我们将创建一个ThemeKey类来保存与Theme相关的常量键。

class ThemeKey {
  static const String THEME = "theme";
}

将来,你可能会想把这个功能抽象出来,放到一个Preferences repository里,就像我的另一篇文章:developer.school/how-to-save…

我们的实现可以在这里看到。

/// lib/infrastructure/theme/datasources/theme_repository.dart
import 'dart:ui';

import 'package:mobx_theme/domain/theme/constants/theme_keys.dart';
import 'package:mobx_theme/domain/theme/interfaces/i_theme_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThemeRepository implements IThemeRepository {
  @override
  Future<void> setThemeKey(Brightness brightness) async {
    (await SharedPreferences.getInstance()).setString(
      ThemeKey.THEME,
      brightness == Brightness.light ? "light" : "dark",
    );
  }

  @override
  Future<String> getThemeKey() async {
    return (await SharedPreferences.getInstance()).getString(ThemeKey.THEME);
  }
}

本质上,我们要么根据传入的亮度设置一个SharedPreferences值,要么是亮的,要么是暗的。我们也能够检索这个值,以便在将来设置合适的主题。

主题服务

我们现在已经有了保存和检索主题键的能力。我们可以继续创建一个主题服务(ThemeService),用它来为我们的用户返回一个合适的主题。

/// lib/application/theme/services/theme_service.dart
import 'package:flutter/material.dart';
import 'package:mobx_theme/domain/theme/interfaces/i_theme_repository.dart';

class ThemeService {
  ThemeService(IThemeRepository themeRepository)
      : _themeRepository = themeRepository;

  IThemeRepository _themeRepository;

  ThemeData get lightTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.deepPurpleAccent,
        brightness: Brightness.light,
        scaffoldBackgroundColor: Color(0xFFecf0f1),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  ThemeData get darkTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.tealAccent,
        brightness: Brightness.dark,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  Future<ThemeData> getTheme() async {
    final String themeKey = await _themeRepository.getThemeKey();

    if (themeKey == null) {
      await _themeRepository.setThemeKey(lightTheme.brightness);

      return lightTheme;
    } else {
      return themeKey == "light" ? lightTheme : darkTheme;
    }
  }

  Future<ThemeData> toggleTheme(ThemeData theme) async {
    if (theme == lightTheme) {
      theme = darkTheme;
    } else {
      theme = lightTheme;
    }

    await _themeRepository.setThemeKey(theme.brightness);
    return theme;
  }
}

接下来,我们将在ThemeStore中使用这个服务来处理这些值的反应性。

主题商店

商店将负责将我们当前的主题暴露给我们的MaterialApp。我们还为isDark创建了一个计算后的getter,我们可以在任何时候使用它来确定当前是否处于黑暗模式。

/// lib/application/theme/store/theme_store.dart
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_theme/application/theme/services/theme_service.dart';

part 'theme_store.g.dart';

class ThemeStore extends _ThemeStore with _$ThemeStore {
  ThemeStore(ThemeService themeService) : super(themeService);
}

abstract class _ThemeStore with Store {
  _ThemeStore(this._themeService);

  final ThemeService _themeService;

  @computed
  bool get isDark => theme.brightness == Brightness.dark;

  @observable
  ThemeData theme;

  @action
  Future<void> getTheme() async {
    theme = _themeService.lightTheme;
    theme = await _themeService.getTheme();
  }

  @action
  Future<void> toggleTheme() async {
    theme = await _themeService.toggleTheme(theme);
  }
}

由于MobX需要生成一些代码,我们需要用mobx_codegen运行build_runner。在你的终端中运行以下内容。

$ flutter pub run build_runner build 

提供我们的商店

我们现在能够切换亮度,但是,我们需要为我们的MaterialApp提供主题的当前值。我们可以通过使用Provider.Dart来实现。

///lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx_theme/application/theme/services/theme_service.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:mobx_theme/infrastructure/theme/datasources/theme_repository.dart';
import 'package:mobx_theme/presentation/pages/splash_screen.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider<ThemeStore>(
            create: (_) =>
                ThemeStore(ThemeService(ThemeRepository()))..getTheme())
      ],
      child: Consumer<ThemeStore>(
        builder: (_, ThemeStore value, __) => Observer(
          builder: (_) => MaterialApp(
            debugShowCheckedModeBanner: false,
            title: 'MobX Theme Switcher',
            theme: value.theme,
            home: SplashPage(),
          ),
        ),
      ),
    );
  }
}

在这里,我们将ThemeStore提供到我们的Widget树中,并立即使用Consumer来获取当前的theme.value。

由于我们使用MobX创建了一个@observable的主题,任何对主题的改变都将是被动的,因为我们已经将MaterialApp包装在一个观察者widget中。

创建我们的SplashPage

由于我们在本文中严格处理的是暗模式和亮模式之间的切换能力,所以我创建了一个页面--SplashPage,它有一个简单的标题/副标题。我们能够通过点击浮动动作按钮来切换暗模式和亮模式。

下面是我们如何实现的。

/// lib/presentation/pages/splash_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:provider/provider.dart';

class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  ThemeStore themeStore;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    themeStore ??= Provider.of<ThemeStore>(context);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: themeStore.toggleTheme,
        child: themeStore.isDark
            ? Icon(Icons.brightness_high)
            : Icon(Icons.brightness_2),
      ),
      body: buildSplash(context),
    );
  }

  Widget buildSplash(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: themeStore.isDark
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 20,
            ),
            Text(
              "Foodie",
              style: TextStyle(fontSize: 26),
            ),
            SizedBox(
              height: 4,
            ),
            Text(
              "The best way to track your nutrition.",
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

这里没有太多不寻常的地方。我们在didChangeDependencies中访问我们的themeStore,并在FAB被按下时使用themeStore.toggleTheme动作。

使用MobX Reactions

我想不出有什么用处,但是如果你想在主题改变的时候显示一个Snackbar(或者其他反应),那会怎么样呢?这里有一个例子,说明这可能是什么样子的。

这一点在MobX中很容易实现。我们必须将我们的ReactionDisposer注册到我们想要反应的Observable中。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:provider/provider.dart';

class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  ThemeStore themeStore;

  GlobalKey<ScaffoldState> _scaffoldKey;
  List<ReactionDisposer> _disposers;

  @override
  void initState() {
    super.initState();
    _scaffoldKey = GlobalKey<ScaffoldState>();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    themeStore ??= Provider.of<ThemeStore>(context);
    _disposers ??= [
      reaction((fn) => themeStore.isDark, (isDark) {
        _scaffoldKey.currentState?.removeCurrentSnackBar();

        if (isDark) {
          _scaffoldKey.currentState.showSnackBar(SnackBar(
            content: Text("Hello, Dark!"),
          ));
        } else {
          _scaffoldKey.currentState.showSnackBar(SnackBar(
            content: Text("Hello, Light!"),
          ));
        }
      })
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      floatingActionButton: FloatingActionButton(
        onPressed: themeStore.toggleTheme,
        child: themeStore.isDark
            ? Icon(Icons.brightness_high)
            : Icon(Icons.brightness_2),
      ),
      body: buildSplash(context),
    );
  }

  Widget buildSplash(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: themeStore.isDark
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 20,
            ),
            Text(
              "Foodie",
              style: TextStyle(fontSize: 26),
            ),
            SizedBox(
              height: 4,
            ),
            Text(
              "The best way to track your nutrition.",
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _disposers.forEach((disposer) => disposer());
    super.dispose();
  }
}

每当我们注册一个反应时,它都会返回一个ReactionDisposer,这个ReactionDisposer可以被调用来处理这个反应。我们在这里只注册了一个反应,但为了简单起见,我们使用了一个List来使它更灵活。

我们的应用程序现在能够对isDark的变化做出反应。

总结

在这篇文章中,我们看了一个潜在的方法来实现MobX的动态主题。我很乐意听到你的想法,如何改进这个方法和/或你希望我调查的任何未来的库。

本文代码:github.com/PaulHallida…


通过( www.DeepL.com/Translator )(免费版)翻译