Flutter + Provider + Firebase　取得した値をConsumerで表示するまで

#はじめに
Flutter + FirebaseのアプリでProviderを使うためにはどうすればいいのか？
この記事では上記の疑問を、ほんの少しでも理解するための１つの参考例としてみていただければと思います。。:sweat_smile:
**かなり我流な気もするので「これが正しい書き方だ！」という方がいれば、コメントください！**

###追記

https://qiita.com/kokogento/questions/d670eb857d03e66e95f5

**このやり方でいくと値の更新がリアルタイムで画面に反映されませんでした。。。:sob:**
**もはやどうしたらいいのかわかりません！！！助けてください！！！！**

###追記 その２

https://qiita.com/kokogento/items/f0861b820ffcdf1133c3

かなり無理矢理ですが、なんとか更新できました。。。

#前提
バージョンは下記の通り

```yaml:pubspec.yaml
environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.3
  firebase_core: "0.7.0"
  firebase_auth: "^0.20.1"
  cloud_firestore: "^0.16.0+1"
```

この記事ではFirebaseとの連携部分は省略します:relieved:

#全体像
まずユーザーのプロフィールページをイメージしてください。
![スクリーンショット 2021-05-17 9.22.36.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/5681b366-da51-0f59-db71-338e79166e20.jpeg)
ドキュメント`users/${uid}`が保持する
・displayName
・email
・introText

①これらの値を`Provider`で監視！

②displayName変更のように「プロフィール情報」が変更されたときに`notifyListeners()`で変更を知らせる

③変更の通知を受け取ってConsumerで囲ってある「プロフィール情報」のStatelessWidgetが変わる！
**（状態は`Provider`から取得できるため、Widgetで保持する必要が無くなった。そのため、StatelessWidgetを使う。）**

#フォルダ構成
ざっくりとこんな感じで。。。

```console
lib/
　├ models/user_model.dart
　├ provider/user_provider.dart
　├ screen/my_page_screen.dart
　└ main.dart/
```

#一番困ったところ
ここがわかるとProvider と Firebaseの連携がとってもスムーズにいくかも。

Providerで監視するには「データモデル」みたいな型（データ構造を表すEntityとかいうやつかな？）を指定する必要があり、Firestoreから取得した値をそのままProviderに適応することはできないのです。。

**私の場合は「型の変換」という部分が一番わけがわからず迷走していた部分でした**:joy:
ここ重要なので覚えておきましょう！

#実装
長々と前置きを挟みましたが、ここから実際のコードを書きます。。

##user_model.dart

```dart:models/user_model.dart
class UserModel {
  String uid;
  String email;
  String displayName;
  String photoURL;
  String introText;

  UserModel({
    this.uid,
    this.email,
    this.displayName,
    this.photoURL,
    this.introText,
  });
}
```

##user_provider.dart

```dart:providers/user_provider.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/user_model.dart';

class UserProvider with ChangeNotifier {
  UserModel _userModels;

  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;

  Future fetchUserData() async {
    final userId = _firebaseAuth.currentUser.uid;
    final docs = await _firestore.doc('users/${userId}').get();
    final userModels = UserModel(
      uid: userId,
      displayName: docs.data()['displayName'],
      email: docs.data()['email'],
      photoURL: docs.data()['photoUrl'],
      introText: docs.data()['introText'],
    );
    _userModels = userModels;
    notifyListeners();
  }
}
```

```dart
 final docs = await _firestore.doc('users/${userId}').get();
```

ここでFirestoreから取得した`DocumentSnapshot`(docsという変数に格納)を`UserModel`に変換しないといけません。

変換している部分がここ

```dart
final userModels = UserModel(
      uid: userId,
      displayName: docs.data()['displayName'],
      email: docs.data()['email'],
      photoURL: docs.data()['photoUrl'],
      introText: docs.data()['introText'],
    );
```

ちなみに、今回は`DocumentSnapshot`（ドキュメント１つ）を変換しましたが、コレクションで取得して変換する場合は下記を参考に`map`を使ってみてください（ちょっと古いですが・・）

https://www.youtube.com/watch?v=xeaiMqPeyQk


##main.dart
状態を供給するには、アプリ全体をProviderでラップする必要があるので`main.dart`で**「MaterialApp」を「MultiProvider」で囲う**

また、`UserProvider()..fetchUserData()`で、createされると同時に`fetchUserData()`が実行されます。
**（「createされる」の意味があんまりわかっていないのですが、、:sweat_smile:誰か教えてください。。）**

```dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './screens/my_page_screen.dart';
import 'providers/user_provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';


void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // Create the initialization Future outside of `build`:
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          return Center(
            child: Text('読み込みでエラー発生' + snapshot.error),
          );
        }
        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done) {
          return Home();
        }
        // Otherwise, show something whilst waiting for initialization to complete
        return CircularProgressIndicator();
      },
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => UserProvider()..fetchUserData(),
        )
      ],
      child: MaterialApp(
       // 省略
      ),
    );
  }
}
```

##my_page_screen.dart
`SizeConfig`については下記を参考に:point_down:

https://qiita.com/kokogento/items/87aaf0a0fbc192e51504

`Consumer`のmodelを使って、`user_provider.dart`から値を受け取ります。

```dart:screen/my_page_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../config/size_config.dart';
import '../providers/user_provider.dart';

class MyPageScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);
    return Scaffold(
      body: Container(
        child: Padding(
          padding: const EdgeInsets.only(left: 30, right: 30, top: 30),
          child: Column(
            children: <Widget>[
              Consumer<UserProvider>(
                builder: (context, model, child) {
                  final userData = model.userModels;
                  return Row(
                    children: <Widget>[
                      Container(
                        height: 10 * SizeConfig.blockSizeVertical,
                        width: 20 * SizeConfig.blockSizeHorizontal,
                        decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            image: DecorationImage(
                              fit: BoxFit.fill,
                              image: NetworkImage(
                                  'https://lh5.googleusercontent.com/-rEw1ckfg8Sc/AAAAAAAAAAI/AAAAAAAAAAA/AMZuuckW028Ka5poorv9UE629d5mtR13CA/s96-c/photo.jpg'),
                            )),
                      ),
                      SizedBox(
                        width: 5 * SizeConfig.blockSizeHorizontal,
                      ),
                      Flexible(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              userData.displayName,
                              style: Theme.of(context).textTheme.headline6,
                            ),
                            SizedBox(
                              height: 1 * SizeConfig.blockSizeVertical,
                            ),
                            Text(userData.introText),
                          ],
                        ),
                      )
                    ],
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

#問題点

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">データの取得が終わってないのにウィジェットが作成されるからか、Consumerで一瞬エラーが発生する。。。 <a href="https://t.co/y8iJE1VsO9">pic.twitter.com/y8iJE1VsO9</a></p>&mdash; 高卒プログラマーげんと (@gento34165638) <a href="https://twitter.com/gento34165638/status/1394114676556849152?ref_src=twsrc%5Etfw">May 17, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

一瞬エラーが発生します。。。
どうすれば良いのでしょうか？？？？！！

###追記

https://qiita.com/kokogento/items/bdfc3fc9a6e7f3850831

こちらにでエラーを回避できました！:relaxed:





#参考
https://fireship.io/lessons/advanced-flutter-firebase/

https://www.youtube.com/watch?v=xeaiMqPeyQk