🐟

【Flutter】【Firestore】データ更新ページに遷移後、遷移元画面に戻ってきたときに画面が再描画されないときに読む記事

2024/04/13に公開

データ更新ページに遷移後、遷移元画面に戻ってきたときに画面が再描画されない事象発生

個人開発中のレシピアプリでは、レシピの詳細ページ(detail_page.dart)から、登録したレシピの修正ページ(revise_page.dart)へと遷移します。
修正ページでデータを修正・更新したら、詳細ページに戻って更新したデータが反映されることを想定していました。しかし、詳細ページに戻っても、更新前のデータが表示される事象が発生しました。

うまくいかないコード

detail_page.dartの①でrevise_page.dartに遷移し、revise_page.dartの③でjson型で取得した更新レシピデータをRecipe型に変換、④でRecipe型の更新データをdetail_page.dartに返却して画面が戻るコードになっています。

この時点で、Firestoreのデータは更新されているのと、detail_page.dartの②の出力結果が以下の通りなので、データの更新自体ができていることは確認できました。

flutter: ぶり
flutter: ぶりりあん
flutter: ぶりりあん
  • データ更新前
  • データ更新後:「ぶりりあん」と表示されるはずが更新前の「ぶり」が表示されている
detail_page.dart
// 前略

class DetailPage extends StatefulWidget {
  final Recipe recipe;
  const DetailPage(this.recipe,{super.key});

  
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  List<String> categoryList = ['主食', '主菜', '副菜', 'スープ', 'デザート', '飲み物'];
  List<String> styleList = ['和食', '洋食', '中華', 'その他'];
  Recipe? recipe; //null safety対応

  
  Widget build(BuildContext context) {
    recipe = widget.recipe;
    final url1 = Uri.parse(recipe!.url1);
    final url2 = Uri.parse(recipe!.url2);
    final url3 = Uri.parse(recipe!.url3);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(recipe!.name)
      ),
      body: Center(

        // 中略

                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Container(
                      width: 200,
                      child: ElevatedButton(
                        child: const Text('修正する'),
                        onPressed: () async {
                          // ①
                          final revisedRecipe = await Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => RevisePage(recipe!)),
                          );
                          // ②
                          setState(() {
                            print(recipe!.name);
                            recipe = revisedRecipe;
                            print(recipe!.name);
                            print(revisedRecipe.name);
                          });
                        },
                      ),
                    ),
                    Container(
                      width: 200,
                      child: ElevatedButton(
                        child: const Text('削除する'),
                        onPressed: () async {
                          try {
                            _checkDialog("削除しますか?", "削除", context, recipe);
                          } catch (e) {
                            print(e);
                          }
                        },
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

// 後略
revise_page.dart
// 前略

Future _reviseFirebase() async {
    final db = FirebaseFirestore.instance;
    final recipe = <String, dynamic>{
      "name": name,
      "category": category,
      "style": style,
      "memo": memo,
      "url1": url1,
      "url2": url2,
      "url3": url3,
      "picture" : picture
    };
    await db.collection('recipe').doc(id).update(recipe);
    // ③
    final revisedRecipeMap = await db.collection('recipe').doc(id).get();
    final revisedRecipe = Recipe.fromJson(id, revisedRecipeMap.data()!);
    return revisedRecipe;
  }

// (中略)

// ④
final revisedRecipe = await _reviseFirebase();
Navigator.pop(context, revisedRecipe);

// 後略

原因

detail_page.dartの↓ここ!!↓が悪さをしていました!build関数の中にrecipe = widget.recipe;を書いているところです。
②でsetStateを実行するとbuild関数が再度呼ばれるため、毎回↓ここ!!↓で更新前の初期値が代入されてしまっていました。

detail_page.dart
// 前略

class DetailPage extends StatefulWidget {
  final Recipe recipe;
  const DetailPage(this.recipe,{super.key});

  
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  List<String> categoryList = ['主食', '主菜', '副菜', 'スープ', 'デザート', '飲み物'];
  List<String> styleList = ['和食', '洋食', '中華', 'その他'];
  Recipe? recipe; //null safety対応

  
  Widget build(BuildContext context) {
    // ↓ここ!!↓
    recipe = widget.recipe;
    final url1 = Uri.parse(recipe!.url1);
    final url2 = Uri.parse(recipe!.url2);
    final url3 = Uri.parse(recipe!.url3);

    // 中略

        onPressed: () async {
                          // ①
                          final revisedRecipe = await Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => RevisePage(recipe!)),
                          );
                          // ②
                          setState(() {
                            print(recipe!.name);
                            recipe = revisedRecipe;
                            print(recipe!.name);
                            print(revisedRecipe.name);
                          });
                        },

// 後略

解決方法

以下のようにinitState() の中でrecipe = widget.recipe;を呼ぶことで、画面が表示された初回のみwidget.recipeが代入されるようにしたところ、無事画面の再描画がされるようになりました!

detail_page.dart
  
  void initState() {
    // TODO: implement initState
    super.initState();
    recipe = widget.recipe;
  }

  
  Widget build(BuildContext context) {
    final url1 = Uri.parse(recipe!.url1);
    final url2 = Uri.parse(recipe!.url2);
    final url3 = Uri.parse(recipe!.url3);

うまくいくコード(全体)

detail_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cook_collect/list_page.dart';
import 'package:cook_collect/revise_page.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'Recipe.dart';
import 'package:url_launcher/url_launcher.dart';

class DetailPage extends StatefulWidget {
  final Recipe recipe;
  const DetailPage(this.recipe,{super.key});

  
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  List<String> categoryList = ['主食', '主菜', '副菜', 'スープ', 'デザート', '飲み物'];
  List<String> styleList = ['和食', '洋食', '中華', 'その他'];
  Recipe? recipe; //null safety対応

  
  void initState() {
    // TODO: implement initState
    super.initState();
    recipe = widget.recipe;
  }

  
  Widget build(BuildContext context) {
    final url1 = Uri.parse(recipe!.url1);
    final url2 = Uri.parse(recipe!.url2);
    final url3 = Uri.parse(recipe!.url3);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(recipe!.name)
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Image.network(recipe!.picture),
                Row(
                  children: [
                    Text(categoryList[recipe!.category], style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
                    SizedBox(width: 16),
                    Text(styleList[recipe!.style], style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),)
                  ],
                ),
                Text('作った日', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
                Text(DateFormat('yyyy/MM/dd').format(recipe!.date.toDate()), style: TextStyle(fontSize: 16)),
                Text(
                  recipe!.memo == '' ? '' : 'メモ',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
                Text(recipe!.memo, style: TextStyle(fontSize: 16)),
                Text(
                  url1.toString() == '' && url2.toString() == '' && url3.toString() == '' ? '' : 'レシピリンク',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                TextButton(
                  onPressed: () {launchUrl(url1);},
                    child: Text(recipe!.url1,style: TextStyle(fontSize: 16))
                ),
                TextButton(
                    onPressed: () {launchUrl(url1);},
                    child: Text(recipe!.url2,style: TextStyle(fontSize: 16))
                ),
                TextButton(
                    onPressed: () {launchUrl(url1);},
                    child: Text(recipe!.url3,style: TextStyle(fontSize: 16))
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Container(
                      width: 200,
                      child: ElevatedButton(
                        child: const Text('修正する'),
                        onPressed: () async {
                          final revisedRecipe = await Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => RevisePage(recipe!)),
                          );
                          setState(() {
                            print(recipe!.name);
                            recipe = revisedRecipe;
                            print(recipe!.name);
                            print(revisedRecipe.name);
                          });
                        },
                      ),
                    ),
                    Container(
                      width: 200,
                      child: ElevatedButton(
                        child: const Text('削除する'),
                        onPressed: () async {
                          try {
                            _checkDialog("削除しますか?", "削除", context, recipe);
                          } catch (e) {
                            print(e);
                          }
                        },
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
  _alertDialog(message1, message2, context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(message1),
        content: Text(message2),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text("とじる"),
          )
        ],
      ),
    );
  }
  _checkDialog(error_message, ok_message, context, recipe) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("確認"),
        content: Text(error_message),
        actions: [
          TextButton(
            onPressed: () async {
              try {
                final db = FirebaseFirestore.instance;
                await db.collection('recipe').doc(recipe.id).delete();
                Navigator.of(context).pop();
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => ListPage()),
                );
              } catch (e) {
                print(e);
              }
            },
            child: Text(ok_message),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text("キャンセル"),
          )
        ],
      ),
    );
  }
}
revice_page.dart
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as path;
import 'Recipe.dart';
import 'list_page.dart';

class RevisePage extends StatefulWidget {
  final Recipe recipe;
  const RevisePage(this.recipe,{super.key});

  
  State<RevisePage> createState() => _RevisePageState();
}

class _RevisePageState extends State<RevisePage> {
  String uid = FirebaseAuth.instance.currentUser!.uid;
  String id = '';
  String name = '';
  String memo = '';
  String url1 = '';
  String url2 = '';
  String url3 = '';
  int category = 0;
  int style = 0;
  List<String> categoryList = ['主食', '主菜', '副菜', 'スープ', 'デザート', '飲み物'];
  List<String> styleList = ['和食', '洋食', '中華', 'その他'];
  String picture = 'https://3.bp.blogspot.com/-5ccEpI-5qwQ/VvXe5ps8DBI/AAAAAAAA5Jo/S_Kil6RUsCUx_4zZKZYqt2ZYh6w2lBMKw/s800/fish_buri2.png';
  late File file;
  String fileName = '';
  User? user = FirebaseAuth.instance.currentUser;
  final regUrl = RegExp(
    caseSensitive: false,
    r"https?://[\w!?/+\-_~;.,*&@#$%()'[\]]+",
  );

  void _upload() async {
    // imagePickerで画像を選択する
    final pickerFile =
    await ImagePicker().pickImage(source: ImageSource.gallery);
    file = File(pickerFile!.path);
    fileName = path.basename(file.path);
    final metadata = SettableMetadata(
      contentType: 'image/jpeg',
      customMetadata: {'picked-file-path': file.path},
    );
    UploadTask uploadTask;
    FirebaseStorage storage = FirebaseStorage.instance;
    Reference ref = storage.ref("images").child(fileName);
    try {
      uploadTask = ref.putData(await file.readAsBytes(),metadata);
      fileName = await (await uploadTask).ref.getDownloadURL();
    } catch (e) {
      print(e);
    }
    setState(() {
      picture = fileName;
    });
  }

  
  Widget build(BuildContext context) {
    Recipe recipe = widget.recipe;
    uid = recipe.uid;
    id = recipe.id;
    name = recipe.name;
    memo = recipe.memo;
    url1 = recipe.url1;
    url2 = recipe.url2;
    url3 = recipe.url3;
    picture = recipe.picture;
    return Scaffold(
      appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text("作った料理を追加")
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                hintText: '料理名',
              ),
              controller: TextEditingController(text: name),
              onChanged: (text) {
                name = text;
              },
            ),
            Column(
              children: [
                Image.network(
                  picture,
                  height: 150,
                  width: 150,
                ),
                ElevatedButton(
                  onPressed: () async {
                    _upload();
                  },
                  child: Text('画像を選択'),
                ),
              ],
            ),
      // Text(fileName),
      SizedBox(
        width: 200,
        child: DropdownButton(
          items: const[
            DropdownMenuItem(
              value: 0,
              child: Text('主食'),
            ),
            DropdownMenuItem(
              value: 1,
              child: Text('主菜'),
            ),
            DropdownMenuItem(
              value: 2,
              child: Text('副菜'),
            ),
            DropdownMenuItem(
              value: 3,
              child: Text('スープ'),
            ),
            DropdownMenuItem(
              value: 4,
              child: Text('デザート'),
            ),
            DropdownMenuItem(
              value: 5,
              child: Text('飲み物'),
            ),
          ],
            hint: const Text('ジャンルを選択'),
          value: category,
            isExpanded: true,
          onChanged: (int? value) {
            setState(() {
              category = value!;
            });
          }),
      ),
            SizedBox(
              width: 200,
              child: DropdownButton(
                  hint: const Text('カテゴリを選択'),
                  items: const[
                    DropdownMenuItem(
                      value: 0,
                      child: Text('和食'),
                    ),
                    DropdownMenuItem(
                      value: 1,
                      child: Text('洋食'),
                    ),
                    DropdownMenuItem(
                      value: 2,
                      child: Text('中華'),
                    ),
                    DropdownMenuItem(
                      value: 3,
                      child: Text('その他'),
                    ),
                  ],
                  value: style,
                  isExpanded: true,
                  onChanged: (int? value) {
                    setState(() {
                      style = value!;
                    });
                  }),
            ),
            TextField(
              controller: TextEditingController(text: memo),
              decoration: InputDecoration(
                hintText: 'メモ',
              ),
              onChanged: (text) {
                memo = text;
              },
            ),
            TextField(
              controller: TextEditingController(text: url1),
              decoration: InputDecoration(
                hintText: 'レシピURL1',
              ),
              onChanged: (text) {
                url1 = text;
              },
            ),
            TextField(
              controller: TextEditingController(text: url2),
              decoration: InputDecoration(
                hintText: 'レシピURL2',
              ),
              onChanged: (text) {
                url2 = text;
              },
            ),
            TextField(
              controller: TextEditingController(text: url3),
              decoration: InputDecoration(
                hintText: 'レシピURL3',
              ),
              onChanged: (text) {
                url3 = text;
              },
            ),
            ElevatedButton(
              onPressed: () async {
                if (name == '') {
                  _alertDialog('エラー', '料理名を入力してください');
                  return;
                } else if (url1 != '' && !regUrl.hasMatch(url1)) {
                  _alertDialog('エラー', 'レシピURL1にはURLを入力してください');
                  return;
                } else if (url2 != '' && !regUrl.hasMatch(url2)) {
                  _alertDialog('エラー', 'レシピURL2にはURLを入力してください');
                  return;
                } else if (url3 != '' && !regUrl.hasMatch(url3)) {
                  _alertDialog('エラー', 'レシピURL3にはURLを入力してください');
                  return;
                };
                final revisedRecipe = await _reviseFirebase();
                Navigator.pop(context, revisedRecipe);
              },
              child: Text('保存する'),
            ),
          ],
        ),
      ),
    );
  }
  _alertDialog(message1, message2) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(message1),
        content: Text(message2),
        actions: [
          TextButton(
            onPressed: () async {
              await Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) {
                    return const ListPage();
                  },
                ),
              );
            },
            child: const Text("とじる"),
          )
        ],
      ),
    );
  }
  Future _reviseFirebase() async {
    final db = FirebaseFirestore.instance;
    final recipe = <String, dynamic>{
      "name": name,
      "category": category,
      "style": style,
      "memo": memo,
      "url1": url1,
      "url2": url2,
      "url3": url3,
      "picture" : picture
    };
    await db.collection('recipe').doc(id).update(recipe);
    final revisedRecipeMap = await db.collection('recipe').doc(id).get();
    final revisedRecipe = Recipe.fromJson(id, revisedRecipeMap.data()!);
    return revisedRecipe;
  }
}
  • データ更新前
  • データ更新後:更新後の値である「ぶりりあん」と表示

Discussion