【Flutter/Dart】データベースを導入する

Flutterに戻る

目次

概要

Flutterでデータベースを実装するためのライブラリに「sqflite」というものがある。
SQLiteを使用することができるものだ。
今回はその導入部分を実装していく。

準備

まずはターミナルで以下を実行しよう。

$ flutter pub add sqflite

そうすると、pubspec.yamlファイルにsqfliteが追加されていることを確認する。

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  flutter_riverpod: ^2.6.1
  sqflite: ^2.4.2

ここまでの確認ができることで準備完了だ。
そして、アーキテクチャはriverpodを使用しているため、こちらの記事を参考にしてもらえるとわかりやすいと思う。

ソースコード

Entity(データベースのクラス)

import 'dart:ui';
import 'package:flutter/material.dart';

enum AttackType {
  normal,
  explosion,
  penetration,
  mystery,
  vibration;

  factory AttackType.fromInt(int type) {
    switch (type) {
      case 0:
        return AttackType.explosion;
      case 1:
        return AttackType.penetration;
      case 2:
        return AttackType.mystery;
      case 3:
        return AttackType.vibration;
      default:
        return AttackType.normal;
    }
  }

  String description() {
    switch (this) {
      case AttackType.explosion:
        return "爆発";
      case AttackType.penetration:
        return "貫通";
      case AttackType.mystery:
        return "神秘";
      case AttackType.vibration:
        return "振動";
      case AttackType.normal:
        return "ノーマル";
    }
  }

  Color typeColor() {
    switch (this) {
      case AttackType.explosion:
        return Colors.red;
      case AttackType.penetration:
        return Colors.yellow;
      case AttackType.mystery:
        return Colors.blue;
      case AttackType.vibration:
        return Colors.purple;
      case AttackType.normal:
        return Colors.indigo;
    }
  }
}

class BlueArchiveStudentEntity {
  final int id;
  final String name;
  final int level;
  final AttackType attackType;
  final DateTime registerAt;

  BlueArchiveStudentEntity({
    required this.id,
    required this.name,
    required this.level,
    required this.attackType,
    required this.registerAt,
  });

  factory BlueArchiveStudentEntity.fromData(dynamic data) {
    final int id = data['id'];
    final String name = data['name'];
    final int level = data['level'];
    final int attackType = data['attack_type'];
    final DateTime registerAt = DateTime.parse(data['register_at']);

    final entity = BlueArchiveStudentEntity(
        id: id,
        name: name,
        level: level,
        attackType: AttackType.fromInt(attackType) ,
        registerAt: registerAt);

    return entity;
  }
}

Service

import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:sqflite/sqflite.dart';

import 'package:flutter_demo_application/Entity/blue_archive_student_entity.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final homeServiceProvider = Provider<HomeService>((ref) {
  return HomeService();
});

class HomeService {
  static late Database _database;
  static late String _databasePath;

  Future<void> initDatabase() async {
    try {
      _databasePath = await getDatabasesPath();
      await Directory(_databasePath).create(recursive: true);
      debugPrint(_databasePath);
    } on Exception catch (exception) {
      throw Exception(exception);
    }

    try {
      HomeService._database = await openDatabase(
        'demo_application.db',
        version: 1,
        onCreate: (Database db, int version) async {
          return await db.execute(
              '''CREATE TABLE 
                IF NOT EXISTS blue_archive_student (
                  id INTEGER PRIMARY KEY, 
                  name TEXT, 
                  level INTEGER, 
                  attack_type INTEGER,
                  register_at DATETIME
              )''');
        },
      );
    } on Exception catch (exception) {
      Exception(exception);
    }
  }

  Future<List<BlueArchiveStudentEntity>> getStudentDataService() async {
    List<BlueArchiveStudentEntity> studentList = [];
    try {
      await initDatabase();
      List<Map<String, Object?>> students = await HomeService._database
          .rawQuery('SELECT * FROM blue_archive_student;');
      for (var student in students) {
        BlueArchiveStudentEntity entity = BlueArchiveStudentEntity.fromData(student);
        studentList.add(entity);
      }
    } on Exception catch (exception) {
      throw Exception(exception);
    }
    return studentList;
  }
}

Repository

import 'package:flutter_demo_application/Entity/blue_archive_student_entity.dart';
import 'package:flutter_demo_application/service/home_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final homeRepositoryProvider = Provider<HomeRepository>(
  (ref) => HomeRepository(service: ref.read(homeServiceProvider)),
);

class HomeRepository {
  final HomeService service;
  HomeRepository({required this.service});

  Future<void> initDatabase() async {
    await service.initDatabase();
  }

  Future<List<BlueArchiveStudentEntity>> getStudentData() async {
    try {
      final data = await service.getStudentDataService();
      return data;
    } on Exception catch (exception) {
      throw Exception(exception);
    }
  }
}

ViewModel

import 'package:flutter_demo_application/Entity/blue_archive_student_entity.dart';
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_demo_application/repository/home_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final homeViewModelProvider = FutureProvider<HomeViewModel>((ref) async {
  final viewModel = await HomeViewModel(
    repository: ref.read(homeRepositoryProvider),
  );
  return viewModel;
});

final homeViewModelStudentStateNotifierProvider =
StateNotifierProvider<StudentEntityStateNotifier, List<BlueArchiveStudentEntity>>((ref) {
  return StudentEntityStateNotifier(ref);
});

class StudentEntityStateNotifier extends StateNotifier<List<BlueArchiveStudentEntity>> {
  StudentEntityStateNotifier(this.ref) : super(<BlueArchiveStudentEntity>[]);

  final Ref ref;

  Future<List<BlueArchiveStudentEntity>> getStateStudentData() async {
    final repository = ref.read(homeRepositoryProvider);
    final studentList = await repository.getStudentData();
    return studentList;
  }
}

class HomeViewModel {
  final HomeRepository repository;
  HomeViewModel({required this.repository});

  late List<ZipCloudEntity> _zipCloudData;
  List<ZipCloudEntity> get zipCloudData => _zipCloudData;

  late List<BlueArchiveStudentEntity> _studentData = <BlueArchiveStudentEntity>[];
  List<BlueArchiveStudentEntity> get studentData => _studentData;

  bool isLoading = false;

  Future initDatabase() async {
    await repository.initDatabase();
  }

  Future getStudentData() async {
    try {
      await repository.initDatabase();
      final data = await repository.getStudentData();
      _studentData = data;
    } on Exception catch (exception) {
      Exception(exception);
    }
  }
}

View

import 'package:flutter/material.dart';
import 'package:flutter_demo_application/Entity/blue_archive_student_entity.dart';
import 'package:flutter_demo_application/Entity/zip_cloud_entity.dart';
import 'package:flutter_demo_application/view_model/home_view_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomeView extends ConsumerWidget {
  HomeView({super.key});

  List<ZipCloudEntity> zipCloudList = <ZipCloudEntity>[];
  List<BlueArchiveStudentEntity> studentList = <BlueArchiveStudentEntity>[];

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var button = OutlinedButton(
      onPressed: () {
        Future<List<BlueArchiveStudentEntity>> studentStateProvider =
            ref.watch(homeViewModelStudentStateNotifierProvider.notifier).getStateStudentData();
        studentStateProvider.then((list) {
          studentList = list;
          if (studentList.isEmpty) {
            debugPrint("データなし");
          } else {
            debugPrint(studentList.first.name);
          }
          ref.invalidate(homeViewModelProvider);
        });
      },
      style: OutlinedButton.styleFrom(
        backgroundColor: Colors.white10,
        foregroundColor: Colors.black,
        disabledBackgroundColor: Colors.black26,
        disabledForegroundColor: Colors.black54,
      ),
      child: Text("データ取得"),
    );

    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.grey),
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text(''),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            button,
            ref.watch(homeViewModelProvider).when(
              data: (_) => ListView.builder(
                scrollDirection: Axis.vertical,
                shrinkWrap: true,
                itemCount: studentList.length,
                itemBuilder: (_, index) {
                  return studentDataCell(studentList[index]);
                  },
              ),
              error: (error, _) => const Center(child: Text('通信エラー')),
              loading: () => const Center(child: CircularProgressIndicator()),
            ),
          ],
        ),
      ),
    );
  }

  Widget studentDataCell(BlueArchiveStudentEntity entity) {
    return Container(
      padding: const EdgeInsets.all(12.0),
      decoration: BoxDecoration(
        color: entity.attackType.typeColor().withAlpha(128),
        border: const Border(bottom: BorderSide(color: Colors.grey, width: 1.0)),
      ),
      child: Padding(
        padding: const EdgeInsets.only(bottom: 8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text("${entity.name} Lv.${entity.level}"),
            Text("${entity.attackType.description()}"),
          ],
        ),
      ),
    );
  }
}

デモ動画

詳細

今回は準備、導入の話なので以下の話になる。

  • ライブラリの導入
  • データベースの作成
  • テーブルの作成
  • データの取得

ライブラリの導入は準備のところで話したので割愛しよう。

データベースの作成・テーブルの作成

まず、データベースを保存するための場所(ディレクトリ)を作成している処理は以下だ。

Future<void> initDatabase() async {
  try {
    _databasePath = await getDatabasesPath();
    await Directory(_databasePath).create(recursive: true);
    debugPrint(_databasePath);
  } on Exception catch (exception) {
    throw Exception(exception);
  }

// 省略

見ての通りだが、データベースのファイル(.dbファイル)を保存する場所を設定し、そこにデータベースのファイルをcreateメソッドで作成する。

そして、以下が本題。
データベースを作成する。同時にテーブルも併せて作成する。

try {
  HomeService._database = await openDatabase(
    'demo_application.db',
    version: 1,
    onCreate: (Database db, int version) async {
      return await db.execute(
        '''CREATE TABLE 
          IF NOT EXISTS blue_archive_student (
          id INTEGER PRIMARY KEY, 
          name TEXT, 
          level INTEGER, 
          attack_type INTEGER,
          register_at DATETIME
        )''');
      },
  );
}

openDatabaseメソッドを使用して、データベースを作成&データベースの参照の行う。

データの取得

現段階ではデータが登録されていないので、上記で記載されているコードではデータが取得できないので注意。
データの登録方法は別ページで解説する。今回は.dbファイルを直接操作してデータを登録する。
また、データの方法は本ページでは簡単にすべてのデータを取得するという処理を行う。

.dbファイルの場所はinitDatabaseメソッドの以下の出力箇所を見れば保存場所が出てくる。

_databasePath = await getDatabasesPath();
await Directory(_databasePath).create(recursive: true);
debugPrint(_databasePath); // ここ

今回Macで開いた時はこんな感じだ。
「DB Browser for SQLite」でデータを追加した。

そして、以下のコードでデータベースからデータを取得している。
SELECT文ですべてのデータを取得しているが、条件付きのデータ取得などは別のページに記載する。

List<Map<String, Object?>> students = await HomeService._database
          .rawQuery('SELECT * FROM blue_archive_student;');

取得したデータはList<MAP<String, Object>>型、日本語で言うなら連想配列なので、以下のようにレスポンスクラスの変換メソッドを定義し、レスポンスクラスのList型のオブジェクトにする。

factory BlueArchiveStudentEntity.fromData(dynamic data) {
  final int id = data['id'];
  final String name = data['name'];
  final int level = data['level'];
  final int attackType = data['attack_type'];
  final DateTime registerAt = DateTime.parse(data['register_at']);

  final entity = BlueArchiveStudentEntity(
      id: id,
      name: name,
      level: level,
      attackType: AttackType.fromInt(attackType) ,
      registerAt: registerAt);

  return entity;
}
for (var student in students) {
  BlueArchiveStudentEntity entity = BlueArchiveStudentEntity.fromData(student);
  studentList.add(entity);
}

こうすることで以下のようにViewで表示する時に使いやすい状態になる。

Widget studentDataCell(BlueArchiveStudentEntity entity) {
  return Container(
    padding: const EdgeInsets.all(12.0),
    decoration: BoxDecoration(
      color: entity.attackType.typeColor().withAlpha(128),
      border: const Border(bottom: BorderSide(color: Colors.grey, width: 1.0)),
    ),
    child: Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text("${entity.name} Lv.${entity.level}"),
          Text("${entity.attackType.description()}"),
        ],
      ),
     ),
  );
}

参考ページ

Flutterに戻る