実現したいこと
こんにちは。現在RiverpodのStateNotifierProvider.autoDisposeを使用してログイン画面のViewModelを実装しています。ViewModel内にはフォーム用のGlobalKeyを持たせており、Widgetからそのキーを参照しています。
発生している問題・分からないこと
- Duplicate GlobalKeyエラー: ログイン後にログアウトして再びログイン画面に遷移すると、『Duplicate GlobalKey detected in widget tree.』というエラーが表示されます。
- 状態の維持: StateNotifierProvider.autoDisposeを使用しているにもかかわらず、画面遷移後も前回入力したログイン情報が残っており、状態が維持されています。状態は破棄されるべきだと思いますが、破棄されていないようです。
- onDisposeが呼び出されない: ref.onDisposeで破棄時にログを出力するように設定しましたが、画面遷移時にログが出力されません。他のautoDisposeのproviderを使用している画面でも同様にref.onDisposeが呼び出されていないことから、providerが破棄されていない原因があると考えています。
質問
- RiverpodのStateNotifier.autoDisposeを使用する際に、GlobalKeyをViewModel内で管理することは適切でしょうか?
- Duplicate GlobalKeyエラーを回避するためのベストプラクティスがあれば教えてください。
- 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が破棄されました。
回答2件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。