Riverpod, StateNotifier에서 응용 테마 전환 기능 구현

12월 22일(화) 업데이트
・ 시동 가능한 샘플 창고를 제작했다.
flutter_theme_mode_selector_example | GitHub
안녕하세요.
이 글은 Flutter #1 Advent Calendar 2020 | Qiita 21일째 글입니다.💪
4월에는 메디움에'ChangeNotifier Provider로 Fluter의 Theme 전환 기능 구현'이라는 기사를 썼다.
8개월이 지났고, Flutter 자체의 업데이트와 내가 사용하는 상태 관리의 변천이 있었다.
2020년 12월에 현재 사용하고 있는 상태 관리 방법으로 다시 써보고 싶어요.
前回:ChangeNotifier + Provider
今回:StateNotifier + Riverpod + Flutter Hooks
Flutter Hooks를 사용하지 않은 사람은 적당히HookWidgetConsumerWidget로 바꾸십시오.
또 지난번에 Theememode로 기본적으로 정의된 System/Light/Dark 외에 주제를 추가한 예도 썼다.
이번에는 세 가지 주제를 전환하여 더욱 간단하고 많은 기본 설정을 사용하여 실시할 것이다.
4가지 이상의 주제 상태를 이루고 싶은 분들은 지난번 기사를 참조하세요.

개요


이 설정을 저장할 수 있도록 시스템, 라이트, Dark 세 가지 설정을 응용 프로그램에서 전환합니다.

소상히 설명하지 않은 일


테마 설정 상세 정보(색상 지정 방법 및 상세 테마 데이터의 정의 방법)

필요한 포장 사용


사용자 선택 항목 저장preferences를 사용합니다.pubspec.yaml에서 보충flutter pub get.
pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

    flutter_hooks: any # 問題無ければ最新バージョンを指定(以下同様)
    flutter_state_notifier: any
    hooks_riverpod: any
    shared_preferences: any

주제 설정 데이터 준비


오른쪽 테마용, 어두운 테마용 TheemeData를 정의합니다.
light_theme_data.dart
import 'package:flutter/material.dart';
// デフォルトのテーマで問題無い(カスタムしない)場合は `copyWith()` は不要です。
final ThemeData lightThemeData = ThemeData.light().copyWith(
  // 好みの色やテキストのテーマを設定する
  colorScheme: /* 定義 */,
  textTheme: /* 定義 */,
  buttonTheme: /* 定義 */,
);
dark_theme_data.dart도 똑같이 제작됐다.

테마 선택을 정의하는 StateNotifier

RiverpodStateNotifier를 사용하여 선택한 테마로 전체 응용 프로그램을 업데이트합니다.ThemeSelector 클래스를 생성할 때 저장된 테마 정보가 있으면 읽고 상태에 반영합니다.
사용자가 테마를 변경할 때 change 방법으로 상태를 업데이트하고 shared_preferences에 저장합니다.
theme_controller.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:state_notifier/state_notifier.dart';

/// テーマ選択のProvider
final themeSelectorProvider = StateNotifierProvider(
  (ref) => ThemeSelector(ref.read),
);

/// テーマの変更・記憶を行うStateNotifier
class ThemeSelector extends StateNotifier<ThemeMode> {
  ThemeSelector(this._reader) : super(ThemeMode.system) {
    initialize();
  }
  /// SharedPreferences で使用するテーマ記憶用のキー
  static const themePrefsKey = 'selectedTheme';

  // 現状、他のProviderを読み込むことは無いので削除しても良い
  // ignore: unused_field
  final Reader _reader;

  /// 選択されたテーマの記憶があれば取得して反映
  Future initialize() async {
    final themeIndex = await _themeIndex;
    state = ThemeMode.values.firstWhere(
      (e) => e.index == themeIndex,
      orElse: () => ThemeMode.system,
    );
  }

  /// テーマの変更を行い、永続化
  Future change(ThemeMode theme) async {
    await _save(theme.index);
    state = theme;
  }

  /// 現在選択中のテーマを`SharedPreferences`から取得
  Future<int> get _themeIndex async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt(themePrefsKey);
  }

  /// 選択した`SharedPreferences`に保存
  Future<void> _save(int themeIndex) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(themePrefsKey, themeIndex);
  }
}

MaterialApp으로 테마 지정


테마의 변경 사항을 전체 프로그램에 반영하기 위해서 MaterialApp 테마의 선택 상태를 가져와 설정합니다.
main.dart
class MyApp extends HookWidget {
  const MyApp();
  
  Widget build(BuildContext context) {
    // 現在のテーマを取得
    final themeMode = useProvider(themeSelectorProvider.state);
    return MaterialApp(
      // ダークモードであればダークテーマを指定、
      // ライトモードかシステムモードであればライトテーマを指定することで、
      // ダークモード時は常時ダークテーマが使用されるようになります。
      theme: themeMode == ThemeMode.dark
          ? darkThemeData
          : lightThemeData,
      // 反対に、ライトモードであればライトテーマを指定、
      // ダークモードかシステムモードであればダークモードを指定することで、
      // ライトモード時は常時ライトテーマが使用されることになります。
      darkTheme: themeMode == ThemeMode.light
          ? lightThemeData
          : darkThemeData,
          // 以下略
    )
  }
}

주제 선택 페이지 만들기


응용 프로그램에서 사용자의 테마를 변경하고 전용 페이지를 만듭니다.
여기, 우리는 ThemeSelectionPage라는 이름의 StatelessWidget을 선택했다.
(이 화면의 주요 부분ThemeListView은 다른 Widget으로 다음과 같이 정의됨)
theme_selection_page.dart
import 'package:flutter/material.dart';

import 'theme_list_view.dart';

class ThemeSelectionPage extends StatelessWidget {
  const ThemeSelectionPage();

  static const String routeName = '/theme-selection';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('テーマ設定')),
      body: const SafeArea(
        child: ThemeListView(),
      ),
    );
  }
}
다음은 테마 선택 부분의 작은 위젯입니다.
그 전에 기본적으로 준비한 ThemeMode(システム・ライト・ダーク)에는 일본어의 라벨과 설명 등이 정의되어 있지 않기 때문에 extension 기능으로 표시됩니다.
theme_mode_ext.dart
import 'package:flutter/material.dart';

extension ThemeModeExt on ThemeMode {
  /// タイトル文字列
  String get title {
    switch (this) {
      case ThemeMode.system:
        return 'System';
      case ThemeMode.light:
        return 'Lignt';
      case ThemeMode.dark:
        return 'Dark';
    }
    throw AssertionError();
  }

  /// サブタイトル文字列
  String get subtitle {
    switch (this) {
      case ThemeMode.system:
        return '端末のシステム設定に追従';
      case ThemeMode.light:
        return '白を基調とした明るいテーマ';
      case ThemeMode.dark:
        return '黒を基調とした暗いテーマ';
    }
    throw AssertionError();
  }

  /// アイコン
  IconData get iconData {
    switch (this) {
      case ThemeMode.system:
        return Icons.autorenew;
      case ThemeMode.light:
        return Icons.wb_sunny;
      case ThemeMode.dark:
        return Icons.nightlife;
    }
    throw AssertionError();
  }
}
그러면 이것은 테마 선택 부분의 Widget입니다.
theme_list_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'theme_selector.dart';

/// テーマを選択できるリストWidget
class ThemeListView extends HookWidget {
  const ThemeListView();

  
  Widget build(BuildContext context) {
    final themeSelector = useProvider(themeSelectorProvider);
    final currentThemeMode = useProvider(themeSelectorProvider.state);
    return ListView.builder(
      itemCount: ThemeMode.values.length,
      itemBuilder: (_, index) {
        final themeMode = ThemeMode.values[index];
        return RadioListTile<ThemeMode>(
          value: themeMode,
          groupValue: currentThemeMode,
          onChanged: (newTheme) {
            themeSelector.change(newTheme);
          },
          title: Text(themeMode.title),
          subtitle: Text(themeMode.subtitle),
          secondary: Icon(themeMode.iconData),
          controlAffinity: ListTileControlAffinity.platform,
        );
      },
    );
  }
}
목록을 작성하여 선택할 수 있는 목록은 ThemeMode 수(시스템 램프/어둡기)만 표시됩니다.
현재 선택한 테마는 useProvider(themeSelectorProvider.state)에서 얻을 수 있습니다.RadioListTileWidget의 줄을 눌렀을 때 useProvider(themeSelectorProvider).change(newTheme) 방법으로 테마의 변경 사항을 반영하고 저장합니다.
끝까지 열람해 주셔서 감사합니다!
트위터에는 주로 Flutter Firebase·iOS/Swift에 대한 잔소리가 담겨 있다.
팔로우 해주시면 감사하겠습니다.☺️ → 🐦촌송룡

수정 내역


2020년 12월 27일light_theme_data.dart 하지만 사용ThemeData.dark()을 수정했습니다!
왕림해 주셔서 감사합니다🙏

좋은 웹페이지 즐겨찾기