【Flutter】データベースisarをRiverpodでどこからでも呼び出せるようにする方法

Nです。

最近 flutterを使ってアプリの開発をしています。その際にRiverpodと呼ばれるアプリ内のさまざまなwidgetから変数やメソッドにアクセスするためのライブラリとisarと呼ばれるデータベースにチャレンジしてみました。

その際に初心者ということもあって結構苦労したのでメモを残しておきたいと思います。もしもっといい方法があるよという優しい方いらっしゃいましたらコメントで教えていただけますと幸いです。

めちゃくちゃ参考にさせていただいた記事
Flutterの状態管理ツールをproviderからriverpodに移行しました
providerからriverpodに移行した話の雑感(その内記事にする)
【Flutter】hiveの後継Isarを試してみる

今回検証に使用したコードはgithubにもあげてあります。

スポンサーリンク

やりたいこと:どこからでもデータベースにアクセスしてデータベース関連のメソッドも使いたい。

今回主に使ったライブラリ

データベース:isar: ^2.5.0
採用理由:いろんなプラットフォームに対応している。せっかくクロスプラットフォームでアプリをつくるのでいろんな環境で使えると嬉しい。sqfliteよりも対応プラットフォームが多かったので選んでみました(多分)。

どこからでもアクセス:flutter_riverpod: ^2.0.0-dev.9
採用理由:なんかよく使われてるみたいだったから

これらを使って投稿をカードで並べていくようなアプリをデモとして作成してみました。
初心者にはなかなか大変な道のりでした。

アプリのイメージ

スポンサーリンク

ポイント1:isarデータベースのインスタンスをmainで作成(main.dart)

以下のコードのようにisarデータベースのインスタンスをmain()内で作成します。
isarをopenするためにはschemaと保存するdirectoryへのpathが必要となります。
pathはgetApplicationSupportDirectory()で取得しています。
今回schemaはpost.dartフォルダ内でPostSchemaとして定義していますが、今回はRiverpodとisarを組み合わせるところに注力したいのでその方法の詳細はisar公式を確認してください。

以下main.dartファイルの中身です。

import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart'; //getApplicationSupportDirectory()を使うため
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'post.dart';// schemaの定義
import 'post_list_page.dart';
import 'isar_provider.dart';

Future<void> main() async {
  // Isarデータベースを開く===========================================
  WidgetsFlutterBinding.ensureInitialized();
  final dir = await getApplicationSupportDirectory();//chromeでエラー?
  Isar isar = await Isar.open(
      schemas: [PostSchema], directory: dir.path, inspector: true);
  // ==============================================================
  return runApp(MyApp(
    isar: isar,
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key, required this.isar}) : super(key: key);
  final Isar isar;

  @override
  Widget build(BuildContext context) {
    //この中でriverpodを使いますという宣言
    return ProviderScope(
      overrides: [
        // Providerが使用するインスタンスを指定する
        // ここでisarのインスタンスを渡すことでプロバイダで使えるようになる
        isarProvider.overrideWithValue(isar),
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const PostListPage(),
      ),
    );
  }
}

ポイント2:isarデータベースのインスタンスをProviderScopeのoverridesであらかじめ作成したプロバイダー(今回はisarProvider)に渡す。

ProviderScopeは普段この中でRiverpodのプロバイダーを使えるようにするという意味で書くのですが今回はさらにoverridesを使うことでインスタンスを用意したプロバイダー(今回はisarProvider)に継承させています。isarProviderはisar_provider.dartファイル内で用意しています。

これでisarProviderとしてisarがref.watch(isarProvider)という形でどこからでも使えるようになりました。

ポイント3:isarデータベースの操作メソッドをまとめたStateNotifireProviderを作成する

StateNotifireProviderとはユーザー操作により変化する状態を管理するためのものみたいです。今回はユーザー操作によりデータベースの状態を変更したり読み出したりするためこれを使っています。

StateNotifierProviderを作るためには不変なStateとStateNotifierが必要となります。今回Stateはpost_service_model.dart内でfreezedパッケージを使って作成しています。状態としてisar, title list, datalistを持っています。Stateにデータベースを更新したタイミングで変化する状態を持たせることでデータベースが更新した際にproviderがウィジェットを自動で更新してくれるようになります。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:isar/isar.dart';
part 'post_service_model.freezed.dart';


@freezed
class PostModel with _$PostModel {
  const factory PostModel({
    required Isar isar,
    required List<String> titleList,
    required List<DateTime> dateList,
  }) = _PostModel;
}

StateNotifierにはデータベースを操作するためのメソッドとデータベースの状態を取得して自身のStateを更新するメソッドを入れました。データベースを操作してもisarインスタンス自体には変化がないみたいなので、その他の状態titlelistなどを更新するように_resetPostState();を書くメソッドの最後に置いています。こうすることでデータベースの更新時にウィジェットの更新ができました。

isar_provider.dartファイルの中身

import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'post.dart';
import 'post_service_model.dart';

// ダミーのProviderを用意する
final isarProvider = Provider<Isar>((_) {
  debugPrint('run isarprovider');
  throw throw UnimplementedError("アプリケーション起動時にmainでawaitして生成したインスタンスを使用する");
});

class PostStateNotifier extends StateNotifier<PostModel> {
  PostStateNotifier(this.isar)
      : super(PostModel(
          isar: isar,
          titleList: isar.posts.where().titleProperty().findAllSync(),
          dateList: isar.posts.where().dateProperty().findAllSync(),
        ));
  final Isar isar;

  //データベースからの状態の読み込み
  void _resetPostState() async {
    List<String> titleList = await isar.posts.where().titleProperty().findAll();
    List<DateTime> dateList = await isar.posts.where().dateProperty().findAll();
    state = state.copyWith(
      titleList: titleList,
      dateList: dateList,
    );
  }
  //データベースに追加
  Future<void> add({required String title, String? avatarUri}) async {
    final post = Post()
      ..title = title
      ..date = DateTime.now();
    int val = await isar.writeTxn((isar) => isar.posts.put(post));
    _resetPostState();
  }
  //データベースの中身を全て削除
  Future<void> removeAll() async {
    await isar.writeTxn((isar) => isar.posts.clear());
    _resetPostState();
  }
}

final postStateNotifierProvider =
    StateNotifierProvider<PostStateNotifier, PostModel>((ref) {
  return PostStateNotifier(ref.watch(isarProvider));
});

作成したStateNotifierProviderを使ってみる

作成したpostStateNotifierProviderを投稿ページ(PostPage)と投稿結果を表示するページ(PostListPage)で使っていきます。

メソッドが使いたい時:ref.watch(プロバイダ名.notifier)を使う

今回投稿ページではデータベースに投稿を追加する機能とデータベースの中身を全て削除する機能を持たせます。そのために先ほど作成したpostStateNotifierProviderのメソッドが使いたいのでWidget build内でfinal postStateNotifier = ref.watch(postStateNotifierProvider.notifier);としメソッドが使いたいところでpostStateNotifier.メソッド名という形で使っています。(===で囲んだところに注目してください)

post_page.dartファイルの中身

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'isar_provider.dart';

class PostPage extends ConsumerWidget {
  const PostPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // postStateNotifierProviderの状態を操作するメソッドを使用する準備 ============
    final postStateNotifier = ref.watch(postStateNotifierProvider.notifier);
    // ======================================================================
    String inputText = '';
    return Scaffold(
      appBar: AppBar(
        title: const Text('リスト追加'),
      ),
      body: Container(
        padding: const EdgeInsets.all(64),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // テキスト入力
            TextField(
              // 入力されたテキストの値を受け取る(valueが入力されたテキスト)
              onChanged: (String value) {
                inputText = value;
              },
            ),
            const SizedBox(height: 8),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  // databaseにaddするメソッドを使用=========
                  postStateNotifier.add(title:inputText);
                  // =====================================
                  debugPrint('pressed add list icon');
                  Navigator.of(context).pop();
                },
                child:
                    const Text('リスト追加', style: TextStyle(color: Colors.white)),
              ),
            ),
            const SizedBox(height: 8),
            SizedBox(
              width: double.infinity,
              // キャンセルボタン
              child: TextButton(
                // ボタンをクリックした時の処理
                onPressed: () {
                  // "pop"で前の画面に戻る
                  Navigator.of(context).pop();
                },
                child: const Text('キャンセル'),
              ),
            ),
            const SizedBox(height: 8),
            SizedBox(
              width: double.infinity,
              // リスト削除ボタン
              child: ElevatedButton(
                onPressed: () {
                  // databaseの中身を全て消すメソッドを使用 ===
                  postStateNotifier.removeAll();
                  // =====================================
                  debugPrint('pressed delete list icon');
                  Navigator.of(context).pop();
                },
                child:
                    const Text('リスト削除', style: TextStyle(color: Colors.white)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

状態が使いたい時:ref.watch(プロバイダ名)を使う

リストを表示するページではデータベースの中身を使って状態を表示しています。そのためfinal postState = ref.watch(postStateNotifierProvider);としてpostState.状態名みたいな形で使っています。

post_list_page.dartファイルの中身

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'post_page.dart';
import 'isar_provider.dart';

// Postリスト一覧画面用Widget
class PostListPage extends ConsumerWidget {
  const PostListPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // postStateNotifirerProviderの状態を使ってカードを作成するのでここで呼び出しておく ===
    final postState = ref.watch(postStateNotifierProvider);
    // ===========================================================================
    return Scaffold(
      // postStateNotifierProvide
      body: ListView.builder(
        itemCount: postState.titleList.length,//titleリストの長さだけカードを作成する
        itemBuilder: (context, index) {
          final title = postState.titleList[index];//カードのタイトルはpostListの各タイトルを使用
          return Card(
            child: ListTile(
              title: Text(title),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          debugPrint('Pressed icon');
          // "push"で新規画面に遷移
          Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
              // 遷移先の画面としてリスト追加画面を指定
              return const PostPage();
            }),
          );
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

まとめ

以上どうでしたでしょうか。

正直flutterを初めて数週間しか経ってないですし、まだまだ変なところたくさんあるかと思いますが、とりあえず動かしてみるの精神でやっていきたいと思います。そして問題が見つかり次第頑張って直していきます。

(優しい方、もっといい方法等教えていただけるとすごく嬉しいです。)

コメント

タイトルとURLをコピーしました