【Flutter】設定機能を構築してみる

2023/04/03に公開

まえがき

いきなり入力機能追加に伴い、はじめて設定機能を構築しました。
設定の保存にはshared_preferencesが便利なのは知ってたけど使い方がわからない。
そこでいまはやりのChatGPTに聞いてみました。
とりあえずダイレクトに聞いたら、なかなかいい答えが返ってきた。
だけどproviderを使って状態管理してるからStatefulWidgetじゃ使えないから聞き直し。
質問の仕方が悪くてごめんね。
そうしたらほぼ求めてる答えが返ってきました。
あとはジChatGPTが書いてくれたコードをコピペしてっと。
コードにちょっと手を加えたらあっさり完成しました。

そうだ、これを記事にしよう!!
と思ったらChatGPTに聞いた履歴を消してしまってた・・・。
せっかくのChatGPTネタだったのにぃ〜。

はい!というわけで、起動時に表示する画面を切り替える設定を構築します。
具体的にはページAとページBと設定画面を用意して設定画面で起動時の画面をAとBで切り替えできるようにします。
それでは始めましょう。

プロジェクトを作成

まずはプロジェクト名settings_sampleで新規プロジェクトを作成します。
以下の記事を参考にしてプロジェクト名をsettings_sampleに置き換えて作成してください。

https://zenn.dev/arafipro/books/flutter-dog-list-app-ui/viewer/flutter-dog-list-app-ui-01

またプロジェクトのテンプレートを「Application(empty)」を選択すると最小限のコードが生成されるのでこちらを選びます。

パッケージをインストール

今回は設定の保存に使うshared_preferencesと状態管理のproviderpubspec.yamlに追加します。


(https://pub.dev/packages/shared_preferences/install)


(https://pub.dev/packages/provider/install)
ターミナルから以下のコマンドを実行します。

flutter pub add shared_preferences
flutter pub add provider

またはpubspec.yamldependenciesのところにshared_preferences: ^2.0.18provider: ^6.0.5を入力してターミナルでflutter pub getを実行します。

dependencies:
  shared_preferences: ^2.0.18
  provider: ^6.0.5

ファイル構成

  • lib
    • view
      • page_a.dart
      • page_b.dart
      • settings.dart
    • viewmodel
      • setting_model.dart
    • main.dart

libディレクトリ内にviewディレクトリとviewmodelディレクトリを作成します。
viewディレクトリ内には各画面を作成します。
viewmodelディレクトリ内にはSettingModelを作成します。

viewディレクトリ内のファイルを作成

ページAとページB

ページAをファイル名page_a.dartで作成、ページBをファイル名page_b.dartで作成します。

page_a.dart
import "package:flutter/material.dart";

class PageA extends StatelessWidget {
  const PageA({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: const Icon(
            Icons.settings,
          ),
        ),
      ),
      body: const Center(
        child: Text(
          "PageA",
          style: TextStyle(
            fontSize: 60,
            color: Colors.red,
          ),
        ),
      ),
    );
  }
}
page_b.dart
import "package:flutter/material.dart";

class PageB extends StatelessWidget {
  const PageB({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: const Icon(
            Icons.settings,
          ),
        ),
      ),
      body: const Center(
        child: Text(
          "PageB",
          style: TextStyle(fontSize: 60),
        ),
      ),
    );
  }
}

ページA、Bと両方のアプリケーションバーと画面中央に"PageA""PageB"を表示します。
まずはAppBarウィジェットを使ってアプリケーションバーをおきます。
アプリケーションバーにはleadingプロパティでIconButtonクラスを呼び出します。
leadingプロパティはアプリケーションバーの左側にアイコンを表示できます。
onPressedプロパティはアイコンがタップされたときに実行される動作を指定します。
とりあえず、(){}としてタップだけ有効にしておきます。
iconプロパティでIconクラスを呼び出しIcons.settingsを表示します。
Icons.settingsは歯車のアイコンを表示します。
あとで歯車のアイコンをタップして設定画面に遷移するようにコードを追加します。
次にCenterウィジェットを使って画面中央にテキストを置くことができます。
そしてTextウィジェットを使って各テキストを表示します。
styleプロパティからTextStyleクラスを呼び出してfontSizeプロパティを60にしてフォントサイズを変更します。
また"PageA"だけcolorプロパティをColors.redにしてフォントカラーを赤色にして区別しやすくします。

設定画面

設定画面をファイル名setting.dartで作成します。

setting.dart
import "package:flutter/material.dart";

class SettingPage extends StatelessWidget {
  const SettingPage({super.key});
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: const [
          ListTile(
            title: Text("起動時画面切り替え"),
            subtitle: Text("起動時の画面をAとBで切り替えます"),
            trailing: Switch(
              value: true,
              onChanged: null,
            ),
          ),
        ],
      ),
    );
  }
}

main.dartを変更

まずはmain.dartを以下のようにスッキリしておきます。

main.dart
import "package:flutter/material.dart";
import "package:settings_sample/views/page_a.dart";

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: PageA(),
    );
  }
}

import 'package:settings_sample/views/page_a.dart';を追加します。
page_a.dartPageAウィジェットを参照します。
MainAppStatelessWidgetに変更します。
MainAppMaterialAppウィジェットを返します。
Scaffoldウィジェットを子要素としてbodyプロパティからPageAウィジェットを使用します。
これにより、PageAがアプリケーションのメイン画面に表示されます。

設定画面に移動

歯車のアイコンをタップして設定画面へ移動できるようにします。
以下のFlutterのドキュメントを参考にして実装します。

https://docs.flutter.dev/cookbook/navigation/navigation-basics#2-navigate-to-the-second-route-using-navigatorpush

ドキュメントを見ると以下のコードを追加するようになっています。

onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => const SecondRoute()),
  );
}

ここではSecondRoute()SettingPage()に置き換えて、page_a.dartpage_b.dartonPressedに追加します。

  onPressed: () {
+   Navigator.push(
+     context,
+     MaterialPageRoute(builder: (context) => const SettingPage()),
+   );
  }

あと、setting.dartをインポートします。

+ import "package:settings_sample/views/setting.dart";

ViewModelを作成

設定画面での設定状態をshared_preferencesで保存します。
保存したデータをproviderで状態管理するためにsetting_model.dartを作成します。

setting_model.dart
import "package:flutter/material.dart";
import "package:shared_preferences/shared_preferences.dart";

class SettingModel with ChangeNotifier {
  // 初期値
  bool _startPageA = true;
  bool get startPageA => _startPageA;

  Future<void> getSetting() async {
    final prefs = await SharedPreferences.getInstance();
    final startPageAValue = prefs.getBool("startPageA") ?? false;
    _startPageA = startPageAValue;
    notifyListeners();
  }

  Future<void> setStartPageA(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool("startPageA", value);
    _startPageA = value;
    notifyListeners();
  }
}

アプリがPageAで開始するかどうかを決定するbool型の_startPageA変数があります。
また初期値はtrueにしてページAが表示されるようにしています。
getterメソッドで_startPageAの値を取得します。
getSetting()メソッドはSharedPreferencesからユーザーの保存された設定を取得し_startPageA変数を更新します。
setStartPageA()メソッドは_startPageA変数を更新しSharedPreferencesに保存します。
変数_startPageAに変更がある場合にはnotifyListeners()メソッドが呼び出されて状態に変更があったことをリスナーに通知します。

スイッチの設定

設定画面のスイッチを動作するようにsetting.dartを変更します。

setting.dart
  import "package:flutter/material.dart";
+ import "package:provider/provider.dart";
+ import "package:settings_sample/viewmodels/setting_model.dart";

  class SettingPage extends StatelessWidget {
    const SettingPage({super.key});
    
    Widget build(BuildContext context) {
-     return Scaffold(
-       body: ListView(
-         children: const [
-           ListTile(
-             title: Text("起動時画面切り替え"),
-             subtitle: Text("起動時の画面をAとBで切り替えます"),
-             trailing: Switch(
-               value: true,
-               onChanged: null,
-             ),
-           ),
-         ],
-       ),
-     );
+     return ChangeNotifierProvider<SettingModel>(
+       create: (_) => SettingModel()..getSetting(),
+       child: Scaffold(
+         body: Consumer(
+           builder: (
+             BuildContext context,
+             SettingModel model,
+             Widget? child,
+           ) =>
+               ListView(
+             children: [
+               ListTile(
+                 title: const Text("起動時画面切り替え"),
+                 subtitle: const Text("起動時の画面をAとBで切り替えます"),
+                 trailing: Switch(
+                   value: model.startPageA,
+                   onChanged: (value) => {
+                     model.setStartPageA(value),
+                     debugPrint(value.toString()),
+                   }
+                 ),
+               ),
+             ],
+           ),
+         ),
+       ),
+     );
    }
  }

SettingModelクラスのインスタンスを作成し​getSetting()メソッドを呼び出してアプリの設定情報を取得します。
そして​ChangeNotifierProviderを使用して​SettingModelクラスのインスタンスを作成します。
これによりアプリ内の他のウィジェットでSettingModelクラスのインスタンスを使用して設定情報を取得できます。
ConsumerウィジェットはChangeNotifierProviderで状態を監視し変更された場合にUIを再ビルドするために使用されます。
このコードではSettingModelクラスのインスタンスを使用してアプリの設定情報を取得しUIに反映します。
builderプロパティはUIを構築するためのコールバック関数を指定します。
このコールバック関数はBuildContextSettingModel、およびchildウィジェットを引数として受け取りUIを構築するために使用されます。
このように、Consumerウィジェットを使用することでアプリ内の他のウィジェットでSettingModelクラスのインスタンスを使用してアプリの設定情報を取得できます。
SettingModelクラスのstartPageAプロパティを使用してスイッチの初期値を設定します。
onChangedプロパティはスイッチの値が変更されたときに呼び出されるコールバック関数を指定します。
このコードではSettingModelクラスのsetStartPageAメソッドを使用してスイッチの値を更新します。
そしてdebugPrint関数を使用してスイッチの値をデバッグコンソールに出力します。

それでは一度実行してみましょう。
そして設定画面に移動してスイッチを動かしてみます。
そうするとデバッグコンソールでスイッチを動かすとtruefalseを交互に表示します。

main.dartを再び変更

最後にトップページを設定の値によって変更されるようにします。

main.dart
  import "package:flutter/material.dart";
+ import "package:provider/provider.dart";
+ import "package:settings_sample/viewmodels/setting_model.dart";
  import "package:settings_sample/views/page_a.dart";
+ import "package:settings_sample/views/page_b.dart";

  void main() {
    runApp(const MainApp());
  }

  class MainApp extends StatelessWidget {
    const MainApp({super.key});

    
    Widget build(BuildContext context) {
-     return const MaterialApp(
+     return ChangeNotifierProvider<SettingModel>(
-       home: PageA(),
+       create: (_) => SettingModel()..getSetting(),
+       child: MaterialApp(
+         home: Consumer(
+           builder: (
+             BuildContext context,
+             SettingModel model,
+             Widget? child,
+           ) =>
+               Scaffold(
+             body: model.startPageA ? const PageA() : const PageB(),
+           ),
+         ),
+       ),
      );
    }
  }

setting.dartを変更と同様に​ChangeNotifierProviderを使用して​SettingModelクラスのインスタンスを作成してConsumerウィジェットを導入します。
bodyプロパティには3項演算子を利用して"PageA""PageB"を切り替えます。
3項演算子とはif-else文と同じように条件分岐を行う演算子です。
具体的には次のように書くことができます。

条件式 ? 式1 : 式2

この式では条件式がtrueならば式1が選択されます。条件式がfalseならば式2が選択されます。

model.startPageA ? const PageA() : const PageB()

今回の場合はmodel.startPageAtrueでならばPageA()falseならばPageB()が選択されます。
これで設定画面から起動時の画面を設定できるようになりました。

スマホアプリ「ひとこと投資メモ」シリーズをリリース

Flutter学習のアウトプットの一環として「日本株ひとこと投資メモ」「米国株ひとこと投資メモ」を公開しています。
今回の設定機能の構築も活用しています。

簡単に使えるライトな投資メモアプリです。
iPhone、Android両方に対応しています。
みなさんの投資ライフに少しでも活用していただきれば幸いです。
以下のリンクからそれぞれのサイトに移動してダウンロードをお願いします。
https://jpstockminimemo.arafipro.com/
https://usstockminimemo.arafipro.com/

GitHubで編集を提案

Discussion