FlutterとSQLiteを活用したデータ管理: 初心者向け解説

モバイル開発

はじめに

Flutterでアプリを作るとき、データの永続化方法で悩むことはないでしょうか。 たとえばユーザー情報や設定を保存したり、オフラインでも動作させたいときにデータベースを利用すると便利ですね。 そんな場面でよく使われるのがSQLiteです。 SQLiteはスマートフォンの内部にデータを保存できるため、外部サーバーやインターネット環境なしでアプリが動くというメリットがあります。 しかもシンプルな構成で扱いやすいので、多くのFlutterアプリで採用されています。

本記事では、FlutterアプリケーションにSQLiteを取り入れる方法を初心者の方でもわかりやすいように解説します。 アプリの機能とSQLiteを連携させる具体例を通じて、実務でも応用できるような視点を提供できればと思っています。 これからFlutterを使って開発を始めようとしている皆さんにとって、SQLiteの基本を理解する機会になれば幸いです。

この記事を読むとわかること

  • FlutterでSQLiteを使うメリット
  • FlutterアプリとSQLiteの連携方法
  • CRUD操作の基本実装イメージ
  • 実務で役立つ具体的なテーブル設計方法
  • バグを防ぐためのポイント

ここからは、FlutterとSQLiteを連携させるための基本的なセットアップやコードの一例を紹介しながら、データベース操作の流れを確認していきましょう。

FlutterとSQLiteの特徴

Flutterは、1つのコードベースでAndroidやiOSなど複数のOS向けにアプリを作れる点が大きな特徴です。 見た目のデザインを統一しながら、動作が軽快であることから人気が高まっています。 データの永続化を考える際に登場するのが、端末内部にデータを保存できるデータベースとしてのSQLiteです。

SQLiteはファイルベースで動くデータベースです。 サーバーを用意せずにアプリ内部だけで動かせるので、オフラインアプリや簡単なメモ管理アプリなどに向いています。 さらにインストールや設定が難しくなく、スキーマ設計がシンプルなため初心者でも扱いやすいです。

他のデータベースと比べると、SQLiteは大規模な同時アクセスにはあまり向きません。 それでもモバイルアプリでの個人データや小規模なテーブルを取り扱うには十分な性能です。 Flutterとの組み合わせにより、ユーザーの端末上で必要な情報を管理できるようになります。

Flutterアプリでのデータの扱い方

通常、アプリのデータを扱う方法としては以下のような選択肢があります。

  • 端末内のファイルに直接書き込む
  • SharedPreferencesを使ってKey-Value形式で保存する
  • SQLiteのようなリレーショナルデータベースを使う

ファイルに直接書き込む場合、構造化されたデータを整理するのは少し手間がかかるかもしれません。 SharedPreferencesは設定情報などシンプルなデータの保存には向いていますが、複雑なデータ管理にはあまり適していないですね。

そこで、リレーショナルデータベースであるSQLiteを利用することで、複数テーブルやカラムの定義を柔軟に行いながらデータを一元管理できます。 アプリが成長して機能が増えたとしても、SQLというわかりやすいクエリ言語を使ってデータを操作できるので、保守性が高いのがポイントです。

FlutterアプリにSQLiteを組み込むまでの大まかな流れ

FlutterアプリとSQLiteを連携させるためには、以下のようなステップで進めることが一般的です。

  1. 必要なプラグインを導入する
  2. データベースやテーブルを作成し、初期化する
  3. CRUD (Create, Read, Update, Delete) 操作を実装する
  4. UIとデータ操作を繋げる

ここで、とくに大切なのはプラグイン選びとデータベースの初期化周りの実装です。 SQLiteにアクセスするための代表的なパッケージとして、たとえばsqfliteが挙げられます。 一例として、実装コードの流れを次の見出しで確認していきましょう。

プラグインの導入

FlutterプロジェクトでSQLiteを扱うには、sqfliteパッケージがよく使われます。 ほかにも類似のパッケージはありますが、まずは sqflite を使う方法を理解すると取り組みやすいです。

Flutterのプロジェクトフォルダ内にあるpubspec.yamlファイルに依存関係を追加したら、下記のようにパッケージをインポートして利用できます。

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

この2つを使うことで、端末上のファイルパスを取得したり、データベースにアクセスしたりできます。

データベースのパスを管理するときには、正しいファイルパスを取得するためにpath_providerなどが必要になる場合もあります。 パッケージを追加し忘れないように注意すると良いでしょう。

データベースを初期化する手順

FlutterでSQLiteを利用するときは、データベースの初期化処理が重要になります。 まず、アプリ起動時もしくはアプリ上で初めてデータベースが必要になったタイミングで、以下のようなフローで初期化を行います。

  1. 端末内の保存先パスを取得する
  2. データベースファイルが存在しない場合は作成する
  3. 必要なテーブルを生成するためのSQL文を実行する

以下のサンプルコードで、データベースのパスを指定して開く、あるいは新規作成する手順を簡単にまとめてみました。

Future<Database> initializeDatabase() async {
  final dbPath = await getDatabasesPath();
  final path = join(dbPath, 'sample.db');

  return openDatabase(
    path,
    version: 1,
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, quantity INTEGER)',
      );
    },
  );
}

onCreate内のSQL文でテーブルを作成しています。 このようにコードでテーブルの定義を記述すると、アプリ内にテーブルが準備されます。

CRUD操作の具体例

データベース操作でよく耳にするCRUDとは、次のような意味です。

  • Create: 新しいデータを追加する
  • Read: 既存のデータを読み取る
  • Update: 既存のデータを更新する
  • Delete: 不要なデータを削除する

SQLiteではSQL文を使ってこれらを実行します。 Flutterのコードからは、db.insertdb.queryなどのメソッドで操作できます。

データの追加(Create)

Future<void> insertItem(Database db, String name, int quantity) async {
  await db.insert(
    'items',
    {'name': name, 'quantity': quantity},
  );
}

データの取得(Read)

Future<List<Map<String, dynamic>>> getAllItems(Database db) async {
  return await db.query('items');
}

データの更新(Update)

Future<void> updateItem(Database db, int id, String newName, int newQuantity) async {
  await db.update(
    'items',
    {'name': newName, 'quantity': newQuantity},
    where: 'id = ?',
    whereArgs: [id],
  );
}

データの削除(Delete)

Future<void> deleteItem(Database db, int id) async {
  await db.delete(
    'items',
    where: 'id = ?',
    whereArgs: [id],
  );
}

これらのCRUD操作を組み合わせれば、アプリ内でさまざまなデータ操作が可能になります。

実務で役立つテーブル設計の考え方

テーブル設計をしっかり行うと、あとから複雑な機能を追加するときに柔軟に対応しやすくなります。 たとえば、買い物リストを管理するアプリを考える場合、itemsテーブルに以下のようなカラムを用意するとわかりやすいかもしれません。

  • id (INTEGER, PRIMARY KEY)
  • name (TEXT)
  • quantity (INTEGER)
  • note (TEXT)
  • created_at (TEXT)

このように最初の段階で必要そうなカラムを定義しておけば、あとで修正作業を減らせるでしょう。 とはいえ、大量のカラムをむやみに追加するのは混乱のもとです。 まずは本当に必要な情報だけを選び、アプリを成長させながら拡張していくほうがミスを防ぎやすいです。

UIとの連携

SQLiteの仕組みを理解したら、次にやることはUIとの連携です。 たとえばFlutterのListViewウィジェットを用いて、データベースから取得したデータをリスト表示させるイメージが考えられます。 ユーザーが操作するたびにデータが更新され、その結果を画面に反映するわけです。

画面とデータベースを結びつけるためには、FutureBuilderStreamBuilderを使う方法がよく利用されます。 これらを使うことで、非同期処理でデータを読み込んだ結果をリアルタイムにUIに反映できます。

非同期処理が増えるとコードが複雑になることもあります。 コードを整理するためにViewModelやRepositoryパターンなどを検討しておくと保守性が高まるでしょう。

サンプルコードを組み合わせたイメージ

では、初期化したデータベースに対して挿入・取得・更新・削除を実行し、リスト表示する流れを簡単にまとめてみましょう。 以下のイメージは、それぞれのステップを組み合わせてUIに反映する例です。

import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class Item {
  final int? id;
  final String name;
  final int quantity;

  Item({this.id, required this.name, required this.quantity});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'quantity': quantity,
    };
  }
}

class MyHomePage extends StatefulWidget {
  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Database? _database;
  List<Item> _items = [];

  
  void initState() {
    super.initState();
    initDbAndLoadItems();
  }

  Future<void> initDbAndLoadItems() async {
    _database = await initializeDatabase();
    await loadItems();
  }

  Future<Database> initializeDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'sample.db');
    return openDatabase(
      path,
      version: 1,
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, quantity INTEGER)',
        );
      },
    );
  }

  Future<void> loadItems() async {
    if (_database == null) return;
    final data = await _database!.query('items');
    setState(() {
      _items = data.map((itemData) {
        return Item(
          id: itemData['id'] as int?,
          name: itemData['name'] as String,
          quantity: itemData['quantity'] as int,
        );
      }).toList();
    });
  }

  Future<void> addItem(String name, int quantity) async {
    if (_database == null) return;
    await _database!.insert(
      'items',
      {'name': name, 'quantity': quantity},
    );
    await loadItems();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SQLite Sample'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () async {
              await addItem('New Item', 1);
            },
            child: Text('Add Item'),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _items.length,
              itemBuilder: (context, index) {
                final item = _items[index];
                return ListTile(
                  title: Text(item.name),
                  subtitle: Text('Quantity: ${item.quantity}'),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

このように、アプリを起動するとSQLiteデータベースが作成され、ボタンを押すと新しいアイテムが追加されます。 リスト表示する部分は簡素化していますが、基本的な流れをつかむには十分なイメージになります。

まとめ

FlutterとSQLiteを組み合わせることで、端末上にデータを保存しながら動作するアプリを構築できます。 オフラインであってもデータを扱えるのがSQLiteの大きな強みです。 とくに個人向けのメモアプリや日常的なデータ管理を必要とするアプリに向いています。

Flutterでの実装は、まずはプラグインを導入し、データベースの初期化とテーブル作成を行うところから始まります。 その後、CRUD操作をしっかり理解してUIと連携させると、ユーザーが操作したデータをスムーズに管理できます。

実際の開発では、コード量が増えるにつれてロジックとUIの分離が課題になりますが、設計パターンやクリーンアーキテクチャを意識すれば整理しやすくなるでしょう。 今回紹介した手順やサンプルコードを参考に、Flutter + SQLiteで必要なデータを取り扱うアプリを作成してみると良いのではないでしょうか。 皆さんがアプリ開発を進めていく上で、データベースの扱い方に慣れ、効率的に機能を拡張できるきっかけになれば嬉しいです。

Flutterをマスターしよう

この記事で学んだFlutterの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。