LoginSignup
2
2

More than 1 year has passed since last update.

Flutter Shared Preferencesでユーザー情報をローカル保存

Last updated at Posted at 2022-09-01

まえがき

このページについて

  • Flutterアプリで、ユーザー設定などの簡易な情報を保存、呼び出しをする実装のサンプルを紹介しています
  • アプリはAndroid, iOS, Linux, macOS, web, Windowsの全て対応だと思いますが、動作確認はiOS Simulatorでしか行っていません
  • FlutterやDart、その他開発環境に関する基礎的な説明は対象範囲外です

記載のサンプルアプリについて

  • flutter createで生成されるカウンターアプリをベースにしています
  • int型の値を端末に保存し、次回アプリ起動時には値を自動で呼び出しビューに反映します
  • パッケージshared_preferencesを利用します
  • アプリ全体のGithubリポジトリ、最下部にはmain.dart全文を記載してます
  • 呼び出した値の反映にはFutureBuilderを利用しています
  • (他で紹介されている記事ではsetStateのみで値をビューに反映している物がありますが、厳密には不正を起こす可能性があるのでオススメしません!)

完成イメージ

  • 今回はFlutterの新規作成後のカウンターアプリで、タップした回数をshared_preferencesで保存するという実装です
  • 今回はint型の値を扱いますが、例えばダークモードの設定などを保存する場合はboolなどに置き換える方法も考えられます
  • 画像はiOSのSimulatorの画面で、再起動後もタップした回数が保持されている様子です

Simulator Screen Shot - iPhone 13 mini - 2022-08-31 at 09.png

環境

  • flutter doctorの出力
[✓] Flutter (Channel stable, 3.0.5, on macOS 12.5.1 21G83 darwin-x64, locale en-JP)
[✓] Xcode - develop for iOS and macOS (Xcode 13.4.1)
[✓] Chrome - develop for the web
[✓] VS Code (version 1.70.2)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

shared_preferencesについて

  • iOSやmacOSのNSUserDefaults、AndroidのSharedPreferencesに相当する機能を提供する公式パッケージ
  • データをKeyとValueのセットで保存し、保存した値はKeyを指定して呼び出します
  • 実装も簡単で、アプリのユーザー設定など簡易的なデータの読み書きに向いています
  • 読み書きできる値の型はint, double, bool, String, List<String>です
  • 通常のDBとしてのデータの管理にはSQLiteなどを利用してください

実装

手順

  1. shared_preferencesの追加
  2. MyHomePageFutureBuilder
  3. CounterViewWidgetの実装

1. shared_preferencesの追加

  • Flutterアプリのルートディレクトリからflutter pub addを実行
  • yamlファイルにdependenciesが追加される
flutter pub add shared_preferences
pubspec.yaml
# ...
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  shared_preferences: ^2.0.15
# ...
  • shared_preferencesをdartファイルにimport
main.dart
import 'package:shared_preferences/shared_preferences.dart';

2. MyHomePageFutureBuilder

ここで行うこと

  • MyHomePageStatelessWidget
  • 値の呼び出しメソッドを実装
  • MyHomePagereturnFutureBuilderに変更

詳しい手順

  1. MyHomePageStatelessWidget

    • FutureBuildersetStateで再描画する必要がないためStatelessWidget
    • エディタ上でstlesと打ちエンターを押すと自動で雛形生成されます
    • 生成されたWidgetの名前をMyHomePageとし、先にあったMyHomePageは削除してください
  2. 値呼び出しメソッドの実装

    • 非同期で値を呼び出す_futureLoadedValue()を実装
    • 戻り値はFuture<int>
    main.dart
    class MyHomePage extends StatelessWidget {
      const MyHomePage({Key? key}) : super(key: key);
      
      // ---------- 追加ここから ----------
      
      // 保存した値呼び出し
      Future<int> _futureLoadedValue() async {
        // SharedPreferencesインスタンスを生成
        SharedPreferences prefs = await SharedPreferences.getInstance();
        return prefs.getInt('counterValue') ?? 0; // 保存した値がなければゼロ
      }
    
      // ---------- ここまで ----------
    
      @override
      Widget build(BuildContext context) {
        // ...
      }
    }
    
  3. returnFutureBuilder

    • 上記メソッドの実行完了後にビルドするようにFutureBuilderを利用します
    • future:には上記メソッドの戻り値
    • builder:以下に、処理状況に応じた分岐と、それぞれにWidgetを実装
    main.dart
    class MyHomePage extends StatelessWidget {
      const MyHomePage({Key? key}) : super(key: key);
      // 保存した値ロード
      Future<int> _futureLoadedValue() async {
        // ...
      }    
    
      @override
      Widget build(BuildContext context) {
    
        // ---------- 追加ここから ----------
    
        // 保存した値呼び出し
        final futureLoadedValue = _futureLoadedValue();
    
        return FutureBuilder(
          future: futureLoadedValue,
          builder: (context, AsyncSnapshot<int> snapshot) {
            if (snapshot.connectionState != ConnectionState.done) {
              // 値呼び出し中プログレスバー
              return Scaffold(
                appBar: AppBar(title: const Text('SP COUNTER APP')),
                body: const Center(child: CircularProgressIndicator()),
              );
            } else if (snapshot.hasError) {
              // 呼び出し失敗
              return Scaffold(
                appBar: AppBar(title: const Text('SP COUNTER APP')),
                body: Center(child: Text(snapshot.error.toString())),
              );
            } else {
              // 呼び出し後
              return CounterView(initialValue: snapshot.data!); // このWidgetは未実装です
            }
          },
        );
    
        // ---------- ここまで ----------
      }
    }
    

3. CounterViewWidgetの実装

ここで行うこと

  1. 新しいStatefulWidgetを生成する (デフォルトカウンターアプリのMyHomePageに相当)
  2. エディタ上でstfulと打ちエンターを押すと自動で雛形生成されます
  3. 必要な変数と、インクリメント&保存のメソッドを実装
  4. returnするWidget部分を実装 (カウンターアプリとほぼ同じ)

詳しい手順

  1. 新しいStatefulWidgetを生成

    • stfulStatefulWidgetの雛形を生成
    • CounterViewと命名
    • 引数initialValueを定義 (呼び出し後の値を親から受け取るため)
    main.dart
    // 以下すべて追加
    
    // ロード後のメインのビュー
    class CounterView extends StatefulWidget {
      const CounterView({Key? key, required this.initialValue}) : super(key: key);
      // 親から受け取る初期値
      final int initialValue;
    
      @override
      State<CounterView> createState() => _CounterViewState();
    }
    
    class _CounterViewState extends State<CounterView> {
      @override
      Widget build(BuildContext context) {
        // ...
      }
    }
    
    
  2. 必要な変数と、インクリメント&保存のメソッドを実装

    • counterlateとし、初期値は初回ビルド時に代入
    • firstBuildで初回ビルドかどうかを判定する
      これにより、setStateによる再描画でinitialValueが繰り返し代入されるのを防ぐ
    • 値の保存は非同期で行われます
    • 重要度や必要に応じてawaitによる同期化や、エラー処理の追加をしてください
    • インクリメントは値を更新とsetStateに加えて、保存処理メソッド実行
    main.dart
    // ロード後のメインのビュー
    class CounterView extends StatefulWidget {
      // ...
    }
    
    class _CounterViewState extends State<CounterView> {
      // ---------- 追加ここから ----------
      bool firstBuild = true; // 初回ビルド判定
      late int counter; // カウンターとして表示する数 (ここでは初期値を定義しない)
      // ---------- ここまで ----------
    
      @override
      Widget build(BuildContext context) {
    
        // ---------- 追加ここから ----------
        
        // 初回ビルド時はcounterに初期値代入
        if (firstBuild) {
          counter = widget.initialValue;
          firstBuild = false;
        }
    
        // 値の保存
        void _saveValue() async {
          SharedPreferences prefs = await SharedPreferences.getInstance();
          prefs.setInt('counterValue', counter);
        }
    
        // 値のインクリメント&保存まで実行
        void _incrementCounter() {
          setState(() {
            counter = counter + 1;
          });
          _saveValue();
        }
    
        // ---------- ここまで ----------
    
        // ...
      }
    }
    
    
  3. Widget部分を実装

    • 初期カウンターアプリとほぼ同等のScaffold部を実装
main.dart
class CounterView extends StatefulWidget {
  // ...
}

class _CounterViewState extends State<CounterView> {
  int? counter;
  @override
  Widget build(BuildContext context) {

    bool firstBuild = true;
    late int counter;

    if (firstBuild) { // ... }
    void _saveValue() async { // ... }
    void _incrementCounter() { // ... }

    // ---------- 追加ここから ----------

    return Scaffold(
      appBar: AppBar(title: const Text('SP COUNTER APP')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );

    // ---------- ここまで ----------

  }
}

あとがき

いかがだったでしょうか?

shared_preferences自体はとてもシンプルで使いやすいパッケージではあるのですが、初回の呼び出し時にはFutureBuilderを使う点や、不要な再描画がかからないようにするガード的に実装などは、Flutterやアプリ開発に慣れてない開発者の方には参考にしていただける部分があったのではないかと思います

今回はintのみを扱いましたが、アプリ起動時に呼び出し必要なユーザー設定系は全て同様に実装可能だと思いますので、みなさんもぜひご自身のアプリでお試しください!

いいねとご質問お待ちしております^ ^

main.dartのコード全体

  • 上の解説パートとコメントなどが若干異なる部分があります
  • ソースコードはこちらのリポジトリからご覧ください
main.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.pink,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);
  // 保存した値ロード
  Future<int> _futureLoadedValue() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // 保存した値がなければゼロ
    return prefs.getInt('counterValue') ?? 0;
  }

  @override
  Widget build(BuildContext context) {
    // 保存した値ロード
    final futureLoadedValue = _futureLoadedValue();

    return FutureBuilder(
      future: futureLoadedValue,
      builder: (context, AsyncSnapshot<int> snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          // 値ロード中プログレスバー
          return Scaffold(
            appBar: AppBar(title: const Text('SP COUNTER APP')),
            body: const Center(child: CircularProgressIndicator()),
          );
        } else if (snapshot.hasError) {
          // ロード失敗
          return Scaffold(
            appBar: AppBar(title: const Text('SP COUNTER APP')),
            body: Center(child: Text(snapshot.error.toString())),
          );
        } else {
          // ロード後
          return CounterView(initialValue: snapshot.data!);
        }
      },
    );
  }
}

// ロード後のメインのビュー
class CounterView extends StatefulWidget {
  const CounterView({Key? key, required this.initialValue}) : super(key: key);
  final int initialValue;

  @override
  State<CounterView> createState() => _CounterViewState();
}

class _CounterViewState extends State<CounterView> {
  // 初回ビルド判定
  bool firstBuild = true;
  late int counter;

  @override
  Widget build(BuildContext context) {
    if (firstBuild) {
      counter = widget.initialValue;
      firstBuild = false;
    }

    // 値の保存
    void _saveValue() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      prefs.setInt('counterValue', counter);
    }

    // 値のインクリメント&保存まで実行
    void _incrementCounter() {
      setState(() {
        counter = counter + 1;
      });
      _saveValue();
    }

    return Scaffold(
      appBar: AppBar(title: const Text('SP COUNTER APP')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2