まえがき
このページについて
- 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の画面で、再起動後もタップした回数が保持されている様子です
環境
-
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などを利用してください
実装
手順
-
shared_preferences
の追加 -
MyHomePage
のFutureBuilder
化 -
CounterView
Widgetの実装
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. MyHomePage
のFutureBuilder
化
ここで行うこと
-
MyHomePage
をStatelessWidget
化 - 値の呼び出しメソッドを実装
-
MyHomePage
のreturn
をFutureBuilder
に変更
詳しい手順
-
MyHomePage
をStatelessWidget
化-
FutureBuilder
はsetState
で再描画する必要がないためStatelessWidget
化 - エディタ上で
stles
と打ちエンターを押すと自動で雛形生成されます - 生成されたWidgetの名前を
MyHomePage
とし、先にあったMyHomePage
は削除してください
-
-
値呼び出しメソッドの実装
- 非同期で値を呼び出す
_futureLoadedValue()
を実装 - 戻り値は
Future<int>
型
main.dartclass 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) { // ... } }
- 非同期で値を呼び出す
-
return
のFutureBuilder
化- 上記メソッドの実行完了後にビルドするように
FutureBuilder
を利用します -
future:
には上記メソッドの戻り値 -
builder:
以下に、処理状況に応じた分岐と、それぞれにWidget
を実装
main.dartclass 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. CounterView
Widgetの実装
ここで行うこと
- 新しい
StatefulWidget
を生成する (デフォルトカウンターアプリのMyHomePage
に相当) - エディタ上で
stful
と打ちエンターを押すと自動で雛形生成されます - 必要な変数と、インクリメント&保存のメソッドを実装
-
return
するWidget部分を実装 (カウンターアプリとほぼ同じ)
詳しい手順
-
新しい
StatefulWidget
を生成-
stful
でStatefulWidget
の雛形を生成 -
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) { // ... } }
-
-
必要な変数と、インクリメント&保存のメソッドを実装
-
counter
はlate
とし、初期値は初回ビルド時に代入 -
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(); } // ---------- ここまで ---------- // ... } }
-
-
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),
),
);
}
}