Flutter + Provider + Firebase Firestoreの値を更新(update)する

#はじめに
https://qiita.com/kokogento/items/76da17fb51806fbb7ff1

一応上記の続きということになります。
**※しかしながら、大幅にコードを変えているのであまり続き感はありません。。。**

動作はこんな感じ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">かなり無理矢理な方法ですが、なんとか実現したい動作は実装できた？？！😅 <a href="https://t.co/YUz0gvFicm">pic.twitter.com/YUz0gvFicm</a></p>&mdash; 高卒プログラマーげんと (@gento34165638) <a href="https://twitter.com/gento34165638/status/1396396645567266821?ref_src=twsrc%5Etfw">May 23, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**かなり無理やり実装した感があるので、「このやり方がおすすめ！という人は是非教えて下さいませ:relieved:**

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

```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:

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

```
lib/
　├ main.dart
　├ models
　├    └ user_model.dart
　├ providers
　│　  └ user_provider.dart
　├ widgets
　│　  └ mypage_view.dart
　└ screens
     ├ mypage_screen.dart
     └ mypage_edit_screen.dart 
```

#一番困ったところ

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

**このやり方でいくと値の更新がリアルタイムで画面に反映されませんでした。。。:sob:**
よくわかりませんが、下記動画を参考にとにかく**根本から作り替えてみることにしました。**

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

時として、今まで作り上げたコードを全て捨て去る時も重要かと。。

#実装
極力省けるところは省きます！

##user_provider.dart

```dart:models/user_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class UserModel {
  UserModel(DocumentSnapshot doc) {
    uid = doc.id;
    email = doc.data()['email'];
    displayName = doc.data()['displayName'];
    photoURL = doc.data()['photoURL'];
    introText = doc.data()['introText'];
  }
  String uid;
  String email;
  String displayName;
  String photoURL;
  String introText;
}
```

`UserModel`を「DocumentSnapshot」という型にしておきます。

##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 user;

  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();
    this.user = UserModel(docs);
    notifyListeners();
  }

  Future updateUserData(UserModel editedUserData) async {
    final doc = _firestore.doc('users/${editedUserData.uid}');
    await doc.update({
      'displayName': editedUserData.displayName,
      'email': editedUserData.email,
      'photoURL': editedUserData.photoURL,
      'introText': editedUserData.introText,
    });
    notifyListeners();
  }
}
```

##mypage_view.dart
userのデータを表示するだけのwidget。
これを後記する`mypage_screen.dart`で呼び出す。

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

class MyPageView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);
    return Consumer<UserProvider>(builder: (context, model, child) {
      final userData = model.user;

      return userData == null
          ? Container(
              child: CircularProgressIndicator(),
            )
          : 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:96-c/photo.jpg'),
                      )),
                ),
                SizedBox(
                  width: 5 * SizeConfig.blockSizeHorizontal,
                ),
                Flexible(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        userData.displayName != null
                            ? userData.displayName
                            : 'No Name',
                        style: Theme.of(context).textTheme.headline6,
                      ),
                      SizedBox(
                        height: 1 * SizeConfig.blockSizeVertical,
                      ),
                      Text(userData.introText != null
                          ? userData.introText
                          : 'No introduction'),
                    ],
                  ),
                )
              ],
            );
    });
  }
}
```

##mypage_screen.dart

```dart:screens/mypage_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../config/size_config.dart';
import '../../providers/user_provider.dart';
import '../../widgets/mypage/mypage_view.dart';
import './mypage_edit_screen.dart';

class MyPageScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('My Page'),
        actions: <Widget>[
            Consumer<UserProvider>(
              builder: (context, model, child) {
                final user = model.user;
                return IconButton(
                  icon: Icon(
                    Icons.edit_sharp,
                  ),
                  onPressed: () async {
                    await Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => MyPageEditScreen(
                            user: user,
                          ),
                          fullscreenDialog: true,
                        ));
                    model.fetchUserData();
                  },
                );
              },
            )
        ],
      ),
      body: Container(
        child: Padding(
          padding: const EdgeInsets.only(left: 30, right: 30, top: 30),
          child: Column(
            children: <Widget>[
              MyPageView(),
            // 省略
```

`AppBar`にアイコンを設置して、マイページの編集画面へと遷移させる。
その時、現在ログインしてるユーザーを`MyPageEditScreen`に渡す。


```dart
//「現在ログインしてるユーザー」とはここで取得してる部分
final user = model.user;
```

で、`model.user`がどこから来るかというと、`user_provider.dart`から。

```dart:providers/user_provider.dart
 UserModel user;
// 省略
 Future fetchUserData() async {
    final userId = _firebaseAuth.currentUser.uid;
    final docs = await _firestore.doc('users/${userId}').get();
    this.user = UserModel(docs);
    notifyListeners();
  }
```

##mypage_edit_screen.dart

```dart:screens/mypage_edit_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/user_model.dart';
import '../../config/size_config.dart';
import '../../providers/user_provider.dart';

class MyPageEditScreen extends StatelessWidget {
  //  mypage_screen.dartから受け取ったuser
  MyPageEditScreen({this.user});
  final UserModel user;
  String _displayName;
  String _email;
  String _photoURL;
  String _introText;
  final _form = GlobalKey<FormState>();

  Future _saveProfile(UserProvider model, BuildContext ctx) async {
    _form.currentState.save();
    // 「user」の情報を無理やり上書き
    user.displayName = _displayName;
    user.email = _email;
    user.photoURL = _photoURL;
    user.introText = _introText;

    await model.updateUserData(user);
   // 省略
  }
 
  @override
  Widget build(BuildContext context) {
    final nameEditingController = TextEditingController();
    final introEditingController = TextEditingController();
    // 初期値を入れる。エラー処理をしていないのでnullならエラー出る。。。
    nameEditingController.text = user.displayName;
    introEditingController.text = user.introText;
  
    // 保存した時に空っぽのまま更新されてしまうので、現在のuserの値を入れておく
    _email = user.email;
    _photoURL = user.photoURL;
    return Consumer<UserProvider>(builder: (context, model, child) {
      final userData = user;
      return Scaffold(
        appBar: AppBar(
          title: Text('Edit Profile'),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.check_sharp),
              onPressed: () {
                _saveProfile(model, context);
              },
            )
          ],
        ),
        body: Container(
          child: Padding(
            padding: const EdgeInsets.only(left: 30, right: 30, top: 30),
            child: Column(
              children: <Widget>[
                userData == null
                    ? Container(
                        child: CircularProgressIndicator(),
                      )
                    : Form(
                        key: _form,
                        child: Row(
                          children: <Widget>[
                           // 省略
                            SizedBox(
                              width: 5 * SizeConfig.blockSizeHorizontal,
                            ),
                            Flexible(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  TextFormField(
                                    controller: nameEditingController,
                                    style:
                                        Theme.of(context).textTheme.headline6,
                                    textInputAction: TextInputAction.done,
                                    onSaved: (value) {
                                      _displayName = value;
                                    },
                                  ),
                                  SizedBox(
                                    height: 1 * SizeConfig.blockSizeVertical,
                                  ),
                                  TextFormField(
                                    controller: introEditingController,
                                    textInputAction: TextInputAction.done,
                                    keyboardType: TextInputType.multiline,
                                    maxLines: null,
                                    onSaved: (value) {
                                      _introText = value;
                                    },
                                  )
                                ],
                              ),
                            ),
                          ],
                        ),
                      ),
              ],
            ),
          ),
        ),
      );
    });
  }
}
```

`model.updateUserData`で渡さないといけないのは`DocumentSnapshot`である`UserModel`なので、

```dart
  _editedUserData = UserModel(
      displayName: _editedUserData.displayName,
      email: user.email,
      photoURL: user.photoURL,
      introText: _editedUserData.introText,
    );
   model.updateUserData(_editedUserData);
```

みたいな形にすることができなかった。。（私が無理やっただけで、やり方によってはできるのかな？）

更新すると何故かemailとかが空っぽになってしまうので、無理やりだが

```dart
user.displayName = _displayName;
    user.email = _email;
    user.photoURL = _photoURL;
    user.introText = _introText;

    await model.updateUserData(user);
```

みたいな形にした。。。:sweat_smile:

##main.dart

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

#最後に
参考にした動画の通り`ChangeNotifierProvider`を使うのか、`StreamProvider`を使うのか、どっちを使えばいいのか今一わかりません！

今回はかなり無理やり実現しちゃった感がすごいので、、、どなたか同じような挙動を実現できるお勧めの方法があれば教えて下さい:bow_tone4: