質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.39%
Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Dart

Dartは、Googleによって開発されたJavaScriptの代替となることを目的に作られた、ウェブ向けのプログラミング言語である。

Q&A

解決済

2回答

377閲覧

Flutterで前の画面に遷移すると『Duplicate GlobalKey detected in widget tree.』とエラーが表示される

shoshinshadao

総合スコア6

Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Dart

Dartは、Googleによって開発されたJavaScriptの代替となることを目的に作られた、ウェブ向けのプログラミング言語である。

0グッド

0クリップ

投稿2024/07/31 01:12

編集2024/07/31 09:46

実現したいこと

こんにちは。現在RiverpodのStateNotifierProvider.autoDisposeを使用してログイン画面のViewModelを実装しています。ViewModel内にはフォーム用のGlobalKeyを持たせており、Widgetからそのキーを参照しています。

発生している問題・分からないこと

  1. Duplicate GlobalKeyエラー: ログイン後にログアウトして再びログイン画面に遷移すると、『Duplicate GlobalKey detected in widget tree.』というエラーが表示されます。
  2. 状態の維持: StateNotifierProvider.autoDisposeを使用しているにもかかわらず、画面遷移後も前回入力したログイン情報が残っており、状態が維持されています。状態は破棄されるべきだと思いますが、破棄されていないようです。
  3. onDisposeが呼び出されない: ref.onDisposeで破棄時にログを出力するように設定しましたが、画面遷移時にログが出力されません。他のautoDisposeのproviderを使用している画面でも同様にref.onDisposeが呼び出されていないことから、providerが破棄されていない原因があると考えています。

質問

  1. RiverpodのStateNotifier.autoDisposeを使用する際に、GlobalKeyをViewModel内で管理することは適切でしょうか?
  2. Duplicate GlobalKeyエラーを回避するためのベストプラクティスがあれば教えてください。
  3. autoDisposeが期待通りに動作しておらず、状態が維持されてしまう問題について、何か考えられる原因や解決策はありますか?

4.ref.onDisposeが呼び出されない理由について、考えられる点やデバッグ方法を教えてください。

アドバイスや経験談があれば教えていただけると助かります。

よろしくお願いします。

エラーメッセージ

error

1======== Exception caught by widgets library ======================================================= 2The following assertion was thrown while finalizing the widget tree: 3Duplicate GlobalKey detected in widget tree. 4 5The following GlobalKey was specified multiple times in the widget tree. This will lead to parts of the widget tree being truncated unexpectedly, because the second time a key is seen, the previous instance is moved to the new location. The key was: 6- [LabeledGlobalKey<FormState>#2ae82] 7This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key. 8The specific parent that did not update after having one or more children forcibly removed due to GlobalKey reparenting is: 9- Padding(padding: EdgeInsets(41.3, 0.0, 41.3, 0.0), dependencies: [Directionality], renderObject: RenderPadding#cf18c relayoutBoundary=up2 NEEDS-PAINT) 10A GlobalKey can only be specified on one widget at a time in the widget tree. 11When the exception was thrown, this was the stack: 12#0 BuildOwner.finalizeTree.<anonymous closure> (package:flutter/src/widgets/framework.dart:3235:15) 13#1 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3260:8) 14#2 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1143:19) 15#3 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:443:5) 16#4 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1392:15) 17#5 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1313:9) 18#6 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1171:5) 19#7 _invoke (dart:ui/hooks.dart:312:13) 20#8 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5) 21#9 _drawFrame (dart:ui/hooks.dart:283:31) 22==================================================================================================== 23

該当のソースコード

Dart

1import 'package:flutter/cupertino.dart'; 2import 'package:flutter/material.dart'; 3import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 5import 'package:freezed_annotation/freezed_annotation.dart'; 6import 'package:vparty/repositories/user_repository.dart'; 7import 'package:vparty/repositories/vrchat_repository.dart'; 8import 'package:vparty/services/user_service.dart'; 9import 'package:vparty/services/vrchat_service.dart'; 10import 'package:vparty/ui/screens/bottom_navigation_screen.dart'; 11import 'package:vparty/ui/screens/create_account_screen.dart'; 12import 'package:vparty/ui/screens/email_one_time_code_screen.dart'; 13import 'package:vparty/ui/utils/show_loading_dialog.dart'; 14import 'package:vparty/utils/shared_prefs.dart'; 15import 'package:vparty/view_model/base_view_model.dart'; 16 17part 'login_view_model.freezed.dart'; 18 19 20class LoginState with _$LoginState { 21 const factory LoginState({ 22 ("") String username, 23 ("") String password, 24 Exception? exception, 25 }) = _LoginState; 26} 27 28final loginProvider = 29 StateNotifierProvider.autoDispose<LoginViewModel, LoginState>((ref) { 30 ref.onDispose(() { 31 print('disposed'); 32 }); 33 return LoginViewModel(); 34}); 35 36class LoginViewModel extends BaseViewModel<LoginState> { 37 LoginViewModel() : super(const LoginState()); 38 39 GlobalKey<FormState> formKey = GlobalKey<FormState>(); 40 41 void inputUsername(String username) { 42 state = state.copyWith(username: username); 43 } 44 45 void inputPassword(String password) { 46 state = state.copyWith(password: password); 47 } 48 49 void login(BuildContext context) async { 50 if (!formKey.currentState!.validate()) return; 51 await LoadingDialog.show(context); 52 try { 53 final repository = await VRChatRepository.create(); 54 final service = VRChatService(repository); 55 final isRequireTwoFactorAuth = 56 await service.login(state.username, state.password); 57 58 Widget screen; 59 if (isRequireTwoFactorAuth) { 60 screen = const EmailOneTimeCodeScreen(); 61 } else { 62 //Firebase上にuserIDが存在するか確認する。 63 final userService = UserService(repository: UserRepository()); 64 final uid = await SharedPrefs.getVRChatUid(); 65 if (uid == null) throw Exception('uid is null'); 66 final isExistUserID = await userService.isExistUserID(uid); 67 screen = isExistUserID 68 ? BottomNavigationScreen() 69 : const CreateAccountScreen(); 70 } 71 72 Navigator.pushReplacement( 73 context, MaterialPageRoute(builder: (context) => screen)); 74 } catch (e) { 75 state = state.copyWith(exception: e as Exception); 76 } finally { 77 LoadingDialog.close(context); 78 } 79 } 80 81 82 void dismissException() { 83 state = state.copyWith(exception: null); 84 } 85} 86

Dart

1 2 Widget build(BuildContext context, ref) { 3 final notifier = ref.read(loginProvider.notifier); 4 final state = ref.watch(loginProvider); 5 showError(context, state.exception, notifier); 6 return LoginScaffold( 7 child: Stack( 8 children: [ 9 buildInfoIcon(onPressed: () { 10 Navigator.push(context, MaterialPageRoute(builder: (context) { 11 return const ContactUsScreen(); 12 })); 13 }), 14 Padding( 15 padding: EdgeInsets.symmetric(horizontal: SizeConfig.width * 11), 16 child: Form( 17 key: notifier.formKey, 18 child: Column( 19 children: [ 20 Gap(SizeConfig.height * 10), 21 Image.asset('assets/images/logos/VRChatLogo.png'), 22 Gap(SizeConfig.height * 10), 23 buildLoginTextField( 24 hintText: AppString.usernameOrEmail, 25 obscureText: false, 26 onChanged: (text) => notifier.inputUsername(text), 27 validator: Validators.validateUsernameOrEmail, 28 ), 29 Gap(SizeConfig.height * 5), 30 buildLoginTextField( 31 hintText: AppString.password, 32 obscureText: true, 33 onChanged: (text) => notifier.inputPassword(text), 34 validator: Validators.validatePassword, 35 ), 36 Gap(SizeConfig.height * 10), 37 buildLoginButton(onPressed: () => notifier.login(context)), 38 ], 39 ), 40 ), 41 ), 42 ], 43 )); 44 }

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

StackOverFlowでautodisposeをつけてporivderが破棄されないケースを調べましたが、よくわかりませんでした。

補足

先ほどNavigator.pushメソッドの前にNavigator.popメソッドを走らせたら、無事providerが破棄されました。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

自己解決

await LoadingDialog.show(context);でローディングダイアログが画面が遷移した後にpopされるのがいけないみたいみたいでした。
ダイアログをpop→画面遷移だと無事プロバイダーが破棄されました。
とんだ落とし穴でした。ありがとうございました!

投稿2024/08/02 14:09

shoshinshadao

総合スコア6

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

補足を見る限り、画面の遷移というか画面の設計に問題を含んでいるのではないかと思います。
提示されたソースコードが限定された状態なので、あくまで想像ですが。

今回の場合は、ログイン画面からログイン完了後ログイン画面を破棄していないから、再ログイン時に、ログイン画面が2画面プッシュされた状態になり、キーが重複しているとかViewModelのプロバイダーが破棄されないという問題になってるのではないかと思います。

ログイン完了後に、メイン画面に移るような場合はpushReplacementを使うのが適当ではないかな。

上記の遷移について正式側はわかりづらいので、わかりやすいかは不明ですがとりあえず概念が書いてあるサイトを紹介します。
https://flutter.ctrnost.com/basic/routing/

元の質問に関しては

  • RiverpodのStateNotifier.autoDisposeを使用する際に、GlobalKeyをViewModel内で管理することは適切でしょうか?
  • Duplicate GlobalKeyエラーを回避するためのベストプラクティスがあれば教えてください。

上2件については、GlobakKeyのヘルプを見る限りbuildで構築するウィジェットの上位側オブジェクトで管理するのが適切みたいな書き方でした。keyが適用されるウィジェットの構築前に存在し、そのウィジェットの破棄後に破棄されるというライフサイクルを守っていれば、いいんじゃないかと思います。

  • autoDisposeが期待通りに動作しておらず、状態が維持されてしまう問題について、何か考えられる原因や解決策はありますか?

これは、上に書いた画面遷移の問題で、ログイン画面がルートから除去されてなかったのでそれに紐づけされたプロバイダーも削除されなかったのだと思います。

  • ref.onDisposeが呼び出されない理由について、考えられる点やデバッグ方法を教えてください。

呼び出されない理由は画面遷移の問題だと思います。
デバッグ方法はちょっとわからないです。

投稿2024/08/01 01:09

ta.fu

総合スコア1716

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.39%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問