[Flutter] カウンターアプリから学ぶローカル保存 (Shared Preference)

## はじめに
Flutterを網羅的に学習するにあたって[Roadmap](https://roadmap.sh/flutter)を使って学習を進めることにしました。

この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、**ローカルストレージ**についてまとめています。

RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひ[Roadmap](https://roadmap.sh/)を利用して学習してみてください。

## Roadmapとは

簡潔に言えば、Roadmap.shは学習者にとっての**ガイドブック**であり、**学習の方向性を提供する学習ロードマップサイト**です。

初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。

学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。

## Storage
FlutterロードマップStorageでは以下の5つのサイトが紹介されています。興味のある方はぜひお読みください。

- Storage - Flutter Tutorial: https://www.youtube.com/watch?v=UpKrhZ0Hppk
- Cloud Storage in Flutter: https://firebase.flutter.dev/docs/storage/overview/
- sqflite - pub.dev package: https://pub.dev/packages/sqflite
- shared_preferences - pub.dev package: https://pub.dev/packages/shared_preferences
- How do I access Shared Preferences?: https://docs.flutter.dev/get-started/flutter-for/android-devs#how-do-i-access-shared-preferences

## ストレージ (Storage)とは
ストレージとは、データを一時的または永続的に保管するための方法です。ストレージは、データの読み取り、書き込み、保持、および整理に使用されます。一般的に、ストレージは**ローカルストレージ**と**リモートストレージ**の2つに分類されます。

### ローカルストレージの特徴
ローカルストレージは、**デバイス自体にデータを保存する**方法です。主にユーザーのデバイスにアクセスするために使用され、**オフラインデータへのアクセス**が可能です。以下はローカルストレージの特徴です。

- オフラインデータへの高速アクセス
- データの永続性
- プライバシーの確保

#### メリット

- **高速アクセス**: ローカルストレージに保存されたデータへのアクセスは高速で、通常、オンライン接続を必要としません。
- **データの永続性**: ローカルストレージはデバイスにデータを永続的に保存できます。再起動してもデータが保持されます。
- **プライバシー**: ローカルストレージ内のデータは通常、アプリ内でのみアクセス可能で、セキュアな状態で保存ができます。

#### デメリット

- **デバイスに制約**: ローカルストレージはデバイスに依存するため、異なるデバイス間でデータを共有することはできません。
- **データ損失**: デバイスの故障、紛失、またはアンインストールされることによって、データ損失のリスクがあります。

### リモートストレージの特徴

リモートストレージは、データを**リモートサーバーやクラウド上に保存する**方法です。データへのアクセスは**ネットワーク接続**を必要とし、データは通常**複数のデバイス間で共有**されます。以下はリモートストレージの特徴です。

- デバイスに依存しないデータ共有
- バックアップと復元
- データの拡張性

#### メリット

- **デバイスに依存しない**: リモートストレージはデバイスに依存せず、異なるデバイス間でデータを共有することができます。
- **バックアップと復元**: リモートストレージはデータのバックアップと復元を容易に行うことができます。
- **拡張性**: クラウドストレージはデータの拡張に適しており、大規模なデータセットを管理することができます。

#### デメリット

- **ネットワーク接続が必要**: リモートストレージへのアクセスにはインターネット接続が必要なため、オフライン時にアクセスすることができません。
- **セキュリティ懸念**: ローカルストレージと比較して、セキュリティとプライバシーが問題となる可能性があるため、適切なセキュリティ対策が必要になります。

## Flutter ローカルストレージ
Flutterでのローカルストレージには`Shared Preference`があります。ローカルストレージをサポートするパッケージは他にもたくさんあるので、興味のある方はぜひ調べてみてください。

ここでは、Flutterでローカルストレージを使用した保存機能を実装する際、どういった手順で導入していくのかを解説していきます。

## Shared Preference
今回は以下のサンプルアプリを作成していきます。

![Simulator Screen Shot - iPhone SE (3rd generation) - 2023-10-17 at 23.17.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368449/8d872050-b4cd-539f-8827-bf1f0b101fae.png)


まずFlutterで`Shared Preference`を使用するには、shared_preferencesパッケージという外部パッケージを活用します。

https://pub.dev/packages/shared_preferences

### パッケージの導入

1. パッケージを追加します。

```dart
flutter pub add shared_preferences
```

２. 使用するファイル内でimportします。

```dart
import 'package:shared_preferences/shared_preferences.dart';
```

### サンプルコード　

```dart: カウンターアプリ
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterPage(),
    );
  }
}

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  CounterPageState createState() => CounterPageState();
}

class CounterPageState extends State<CounterPage> {
  int _count = 0;

  @override
  void initState() {
    super.initState();
    _loadCounter();
  }

  void _loadCounter() async {
    final int count = await SharedPreferencesHelper.getCount();
    setState(() {
      _count = count;
    });
  }

  void _incrementCounter() {
    setState(() {
      _count++;
      SharedPreferencesHelper.setCount(_count);
    });
  }

  void _resetCounter() {
    setState(() {
      _count = 0;
      SharedPreferencesHelper.clearCount();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Count:',
              style: TextStyle(fontSize: 20.0),
            ),
            Text(
              '$_count',
              style: const TextStyle(fontSize: 40.0),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                ElevatedButton(
                  onPressed: _incrementCounter,
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _resetCounter,
                  child: const Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class SharedPreferencesHelper {
  static const String _keyCount = 'count';

  static Future<int> getCount() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getInt(_keyCount) ?? 0;
  }

  static Future<void> setCount(int count) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(_keyCount, count);
  }

  static Future<void> clearCount() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.remove(_keyCount);
  }
}
```
上記のアプリは`Shared Preference`を使用してカウンターの状態をローカル保存させた、シンプルなカウンタアプリです。増加ボタンを押すとカウンタが＋１されて、アプリを落とした後でも状態は保持されます。0に戻す場合は、リセットボタンを押してカウンターの値を削除します。

```dart: initStateメソッド
  @override
  void initState() {
    super.initState();
    _loadCounter();
  }
```
ウィジェットの初期化時に呼び出されるメソッドで、`_loadCounter()`メソッドを呼び出してローカルストレージからカウンターの値を読み込みます。

```dart: _loadCounterメソッド
  void _loadCounter() async {
    final int count = await SharedPreferencesHelper.getCount();
    setState(() {
      _count = count;
    });
  }
```
`SharedPreferencesHelper.getCount()`メソッドを使用して非同期にローカルストレージからカウンターの値を読み込みます。読み込んだ値を`_count`にセットして、ウィジェットの状態を更新します。

```dart: _incrementCounterメソッド
  void _incrementCounter() {
    setState(() {
      _count++;
      SharedPreferencesHelper.setCount(_count);
    });
  }
```
カウンターの値を増加させるメソッドです。`_count`の値を増やし、新しい値をローカルストレージに保存します。

```dart:_resetCounterメソッド
  void _resetCounter() {
    setState(() {
      _count = 0;
      SharedPreferencesHelper.clearCount();
    });
  }
```
カウンターの値をリセットするメソッドです。`_count`の値を0にリセットし、ローカルストレージからカウンターの値を削除します。

```dart: SharedPreferencesHelperクラス
class SharedPreferencesHelper {
  static const String _keyCount = 'count';

  static Future<int> getCount() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getInt(_keyCount) ?? 0;
  }

  static Future<void> setCount(int count) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(_keyCount, count);
  }

  static Future<void> clearCount() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.remove(_keyCount);
  }
}
```

カウンターの値をローカルストレージで保存、取得、削除するためのクラスです。`_keyCount`はカウンターのキー名です。
- `getCount()`メソッド: ローカルストレージからカウンターの値を取得します。
- `setCount()`メソッド: 新しいカウンターの値をローカルストレージに保存します。
- `clearCount()`メソッド: ローカルストレージからカウンターの値を削除します。

## SQLite
有名なローカルストレージのパッケージは `Shared Preference` の他に
`SQLite` というパッケージもあります。 以下の記事でまとめているので、
興味のある方はぜひ見てください。

https://qiita.com/haru-qiita/items/146c4cd89ac5cda2a3f2

## SharedPreferenceとSQLiteの違い

### データの保存形式
- SharedPreferences: キーと値のペアを保存するシンプルなストレージです。設定やユーザーの状態など、軽量のデータを保存するために使用されます。
- SQLite: リレーショナルデータベース(RDB[^1])として動作する組み込みSQLデータベースです。テーブルを使用して構造化されたデータを保存し、複雑なクエリやデータの関係性をサポートします。


### データの複雑さ
- SharedPreferences: シンプルなキーと値ペアのみを保存するため、データ構造が比較的単純です。
- SQLite: テーブルとの関係性を使用してデータを保存することができるため、複雑なデータ構造やリレーションシップを持つデータを保存できます。

### パフォーマンス
- SharedPreferences: 単純な読み取り/書き込み操作が高速であり、メモリ内でデータが管理されます。複雑なクエリや大量のデータの処理には適していません。
- SQLite: インデックス、クエリの最適化など、高度なデータベース機能を提供しているため、大規模なデータセットの処理に適しています。

### sqfliteを利用すべきケース
- データ構造が複雑
- データが大量かつソートや走査などを行う
- データ間で何かしら相互依存関係がある
- ユーザー投稿など

### shared_preferencesを利用するケース
- データ構造が単純
- データ量が少しでソートなどの操作を行わない
- 設定データなど


## 参考資料
https://virment.com/how-to-use-shared-preferences-in-flutter/

https://zenn.dev/joo_hashi/articles/22e7f7d0897645

https://note-tmk.hatenablog.com/entry/2022/08/13/164942


[^1]: リレーショナルデータベース (RDB)
関係データベースと訳され、データを複数の表として管理し、表と表の関係を定義することで、複雑なデータの関連性を扱えるようにしたデータベース管理方式。
[こちら](https://it-trend.jp/words/rdb#:~:text=Relational%20Database%EF%BC%88%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%8A%E3%83%AB%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%EF%BC%89%E3%81%AE,%E7%89%B9%E5%BE%B4%E3%81%A8%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%80%82)にわかりやすい記事があったので、興味のある方是非見てください。

## 告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。

https://eda-inc.jp/recruit/

みなさまからのご応募をお待ちしております。