[Flutter] SQLiteを使用してローカル保存を学ぶ

## はじめに
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でのローカルストレージには`SQLite`があります。ローカルストレージをサポートするパッケージは他にもたくさんあるので、興味のある方はぜひ調べてみてください。

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

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

![画面収録 0006-03-20 18.38.49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368449/2d292c12-dd60-ec51-9e91-41334ceb0fcb.gif)


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

https://pub.dev/packages/sqflite

### パッケージの導入

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

```dart
flutter pub add sqflite
```

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

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

### サンプルコード　

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SQLite Counter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const CounterPage(),
    );
  }
}

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

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

class CounterPageState extends State<CounterPage> {
  late Database _database;
  int _counter = 0;

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

  Future<void> _initDatabase() async {
    // データベースをオープン（存在しない場合は作成）
    _database = await openDatabase(
      'counter_database.db',
      version: 1,
      onCreate: (Database db, int version) {
        // データベース内にカウンターテーブルを作成
        return db.execute(
          'CREATE TABLE counters(id INTEGER PRIMARY KEY, count INTEGER)',
        );
      },
    ); 
    // データベースからカウンターの値を取得
    List<Map<String, dynamic>> records = await _database.query('counters');
    if (records.isNotEmpty) {
      setState(() {
        _counter = records.first['count'];
      });
    } else {
      _insertCount();
    }
  }

  // カウンター保存メソッド
  Future<void> _insertCount() async {
    // データベースに初期値を挿入
    await _database.insert('counters', {'count': _counter});
  }

  // カウンター増加メソッド
  Future<void> _incrementCounter() async {
    setState(() {
      _counter++;
    });
    // データベース内のカウンターの値を更新
    await _database.update('counters', {'count': _counter});
  }

  // カウンタークリアメソッド
  Future<void> _clearCounter() async {
    setState(() {
      _counter = 0;
    });
    // データベースからカウンターのデータを削除
    await _database.delete('counters');
  }

  @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('Counter Value:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                _clearCounter();
                _insertCount();
              },
              child: const Text('Clear Counter'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

```
上記のアプリはSQLiteを使用してカウンターの状態をローカル保存させた、シンプルなカウンタアプリです。プラスボタンを押すとカウンタが＋１されて、アプリを落とした後でも状態は保持されます。0に戻す場合は、クリアボタンを押してカウンターの値を削除します。

```dart: _initDatabase() 
_database = await openDatabase(
  'counter_database.db',
  version: 1,
  onCreate: (Database db, int version) {
    // データベース内にカウンターテーブルを作成
    return db.execute(
      'CREATE TABLE counters(id INTEGER PRIMARY KEY, count INTEGER)',
    );
  },
);
```
- `openDatabase` メソッドを使用してデータベースを初期化しています。 `path`　で指定したデータベースが存在しなかった場合、 `onCreate` 内の処理が呼び出されます。
- ここでは、`counters` テーブルを作成し、 `id`列を主キーに、 `count` 列を整数として定義しています。

```dart
// データベースからカウンターの値を取得
List<Map<String, dynamic>> records = await _database.query('counters');
```
- データベースから `counters` テーブルをクエリして、その結果を取得します。
- 結果は `Map<String, dynamic>` 型のリストとして宣言されている `records` 変数に格納されます。各要素はテーブルの行を表し、列名をキーとして値を持ちます。


```dart
if (records.isNotEmpty) {
  setState(() {
    _counter = records.first['count'];
  });
} else {
  _insertCount();
} 
```
- 取得したレコードが空でない場合、データベースから取得したカウンターの値を `_counter` 変数にセットします。
- `records.first['count']` では、取得した最初のレコードの `count` 列の値を取得しています。
- レコードが空の場合、つまりデータベース内にカウンターの値が保存されていない場合、 `_insertCount()` メソッドを呼び出します。

```dart: _insertCount()
  // カウンター保存メソッド
  Future<void> _insertCount() async {
    // データベースに初期値を挿入
    await _database.insert('counters', {'count': _counter});
  }
```
- データベースに値を保存するための非同期メソッドです。
メソッド内では、 `_counter` 変数の値を `counters` テーブルに挿入しています。

```dart: _incrementCounter() 
 // カウンター増加メソッド
  Future<void> _incrementCounter() async {
    setState(() {
      _counter++;
    });
    // データベース内のカウンターの値を更新
    await _database.update('counters', {'count': _counter});
  }
```
- カウンターの値を増加させるための非同期メソッドです。
メソッド内では、 `_counter` 変数の値を1つ増やした後、 `_counter` の新しい値をデータベースの `counters` テーブルに更新しています。これにより、カウンターの値が増加するたびにデータベース内の値も更新されるようになります。

```dart: _clearCounter()
  // カウンタークリアメソッド
  Future<void> _clearCounter() async {
    setState(() {
      _counter = 0;
    });
    // データベースからカウンターのデータを削除
    await _database.delete('counters');
  }
```
- カウンターの値をリセットし、データベースからカウンターのデータを削除するための非同期メソッドです。
- メソッド内では、`_counter` 変数の値を 0 にセットしています。これにより、カウンターの値がリセットされます。
- 次に、 `_database.delete('counters')` を呼び出して、データベースから counters テーブル内のすべてのデータを削除しtます。これにより、データベース内のカウンターのデータが完全に消去されます。

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

https://qiita.com/haru-qiita/items/c43cd90b80a2b33ad91c

## SharedPreferenceとSQLiteの違い

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


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

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

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

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

## 参考資料

https://yakiimosan.com/flutter-sqlite-basic/

https://note-tmk.hatenablog.com/entry/2022/08/18/112611

https://www.flutter-study.dev/recipe/local-data

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

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

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