🌎

riverpodを使ってFireStoreの値を取得する

2022/05/15に公開
3

riverpodとは?

今流行りの、Flutterの状態管理ライブラリーというやつですね。
今回、FireStoreの値を取得することに成功したので、記事を書いて記録を残しておきます。

*開発環境

flutter2.10.3
Android OS

設定はこんな感じでしました。動作は保証できません😇

app/src/build.gradle

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'com.google.gms.google-services'

android {
    compileSdkVersion flutter.compileSdkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.book_list"
        minSdkVersion 23 // 23に修正
        targetSdkVersion 31 // 31に修正
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        multiDexEnabled true
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation platform('com.google.firebase:firebase-bom:30.0.1')
    implementation "androidx.multidex:multidex:2.0.1"
}

pabspec.yaml

name: book_list
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: ">=2.16.1 <3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  # riverpod用のパッケージ
  flutter_riverpod: ^1.0.3
  # Firebase用のパッケージ
  cloud_firestore: ^3.1.13
  firebase_core: ^1.15.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^1.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware.

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

フォルダ構成

.
├── generated_plugin_registrant.dart
├── main.dart
├── model
│   ├── book_data.dart
│   ├── book_state.dart
│   └── book_store.dart
├── provider
│   └── global_provider.dart
└── view
    └── book_data_page.dart

modelフォルダのファイル

book_data.dart
// 本のタイトルクラス
class BookData {
  String documentId;
  String title;
  String auther;

  // コンストラクター
  BookData({this.documentId = '', required this.title, required this.auther});

  // factoryを利用する場合、インスタンスは処理内で作成して返却する必要がある
  // 今回は引数(Firestoreの情報)からインスタンスを生成する
  // また引数については、処理内で再設定されないようfinalを追記
  factory BookData.toModel(
      final String docId, final Map<String, dynamic> data) {
    // ここでインスタンスを生成し返却する
    return BookData(
      documentId: docId,
      title: data['title'],
      auther: data['auther']
      );
  }

  // toJsonメソッドは
  // 呼び出されるインスタンスの各フィールド値をMap形式に変換します

  Map<String, Object?> toJson() {
    return {
      'title': title,
      'auther': auther
    };
  }
}
book_state.dart
// 状態管理用のクラス
// BookData情報をリスト型で保持する
import 'package:book_list/model/book_data.dart';

class BookDataState {
  List<BookData> books = [];
}

book_store.dart

// 状態管理クラス
import 'package:book_list/model/book_data.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class BookStore extends StateNotifier<List<BookData>> {
  BookStore() : super([]) {
    // これは初期化処理1度だけ実行されます
    // ここでFirestoreの情報を取得
    debugPrint('init call📞');
    _readFirebaseDocument();
  }

  // Firebaseの読み込みを行う関数
  // 対象DocumentはBooks
  Future<void> _readFirebaseDocument() async {
    // まずはFirebaseを利用するためにinstanceを取得
    // 以降設定することがないのでfinalで定義
    final store = FirebaseFirestore.instance;
    final document = await store
        .collection('books')
        .withConverter<BookData>(
          fromFirestore: (snapshot, _) =>
              BookData.toModel(snapshot.id, snapshot.data()!),
          toFirestore: (book, _) => book.toJson(),
        )
        .get();
    // 取得したDocument情報からTodo情報を取得して
    // stateに設定する
    // document.docs.map((bookData) => todoData.data()).toList()
    // ではdocument.docsの中身をtodoDataで受けて
    // bookData.data()でBook.toModelを実行した結果にアクセスする
    // その戻り値を.toList()でList形式に変換する
    final List<BookData> list =
        document.docs.map((bookData) => bookData.data()).toList();
    state = list; // mapで変換したデータが入った変数
  }
}

providerフォルダのファイル

global_provider.dart
import 'package:book_list/model/book_data.dart';
import 'package:book_list/model/book_store.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 状態管理用のプロバイダー
// StateNotifierProvider<[状態管理クラス:extends StateNotifier<状態クラス>],状態クラス>を指定する
// 今回でいえばStateNotifierProvider<BookStore, List<BookData>

// final bookDataStateProvider =
//     StateNotifierProvider<BookStore, List<BookData>>((ref) => BookStore()); // 状態管理クラスを返却する
// このように書き換えることもできる
final bookDataStateProvider =
    StateNotifierProvider<BookStore, List<BookData>>((ref) {
  return BookStore();
});

viewフォルダのファイル

book_data_page.dart
import 'package:book_list/model/book_data.dart';
import 'package:book_list/provider/global_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class BookDataPage extends ConsumerWidget {
  BookDataPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("BOOK-APP"),
      ),
      body: Column(
        children: <Widget>[
          // Expandedを使えばColumnやRowなどの空いたスペースをレスポンシブに埋めることができます!
          Expanded(
            child: _listViewBuilder(ref),
          ),
        ],
      ),
    );
  }

  /// 引数が存在する場合はこんな形で[]で囲むとよい
  /// [ref] WidgetRef
  /// create List View
  Widget _listViewBuilder(WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(bookDataStateProvider).length,
        itemBuilder: (BuildContext context, int index) {
          return _bookItem(context, ref, index);
        });
  }

  Widget _bookItem(BuildContext context, WidgetRef ref, int index) {
    // リストの各要素をWidget化
    // 今回は基本的に状態リスト自体を書き換えているので、ここはreadで問題ない
    // 状態リストが書き換わると、
    // 本メソッドの呼び出し元 itemCount: ref.watch(bookDataStateProvider).length
    // でリビルドが発生するため、中身の各要素に応じたWidgetもリビルド対象となる
    BookData bookData = ref.read(bookDataStateProvider)[index];
    return Container(
      child: ListTile(
        subtitle: Text('著者: ${bookData.auther}'),
        title: Text('本のタイトル: ${bookData.title}'),
      ),
    );
  }
}
main.dart
import 'package:book_list/view/book_data_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(
    // Riverpodでデータを受け渡しが可能な状態にするために必要
    ProviderScope(
      child: MaterialApp(
        title: 'Book App',
        theme: ThemeData(
          primaryColor: Colors.blueAccent
        ),
        home: BookDataPage(),
      ),
    ),
  );
}
generated_plugin_registrant.dart
//
// Generated file. Do not edit.
//

// ignore_for_file: directives_ordering
// ignore_for_file: lines_longer_than_80_chars

import 'package:cloud_firestore_web/cloud_firestore_web.dart';
import 'package:firebase_core_web/firebase_core_web.dart';

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

// ignore: public_member_api_docs
void registerPlugins(Registrar registrar) {
  FirebaseFirestoreWeb.registerWith(registrar);
  FirebaseCoreWeb.registerWith(registrar);
  registrar.registerMessageHandler();
}

最後に

単純なサンプルですが、誰かの役にお役に立てればと思いこの記事を書きました。私も最近エンジニアになったばかりでわからないことだらけです。
毎日outputしております🧑‍💻

Discussion

めろんぺんめろんぺん

質問です。
このコードだと新規追加があった時はリアルタイムで更新されますか?

JboyHashimotoJboyHashimoto

こちらだと、されてなかった気がしますね。
随分前に勉強用に書き換えていたコードなので、あまりわかってないです😅
StreamProvider使ったアプリはリアルタイムで更新されてましたよ!