
현재 만들고있는 토이프로젝트에서 MVVM 패턴으로 클린 아키텍쳐를 신경써 작업을 해주던 상황이었다.
그 상황에서 2개의 screen(home, search)에서 각각 Provider 를 접근해 상태값에 저장된 값들을 출력을해주려고 했으나, 이상하게 search 에서는 주입해둔 Provider를 잘 가져오는데, home 에서는 주입해둔 Provider 를 접근을 못하는 상황이 되어버린것이다.
======== Exception caught by widgets library =======================================================
The following ProviderNotFoundException was thrown building _Body(dirty):
Error: Could not find the correct Provider<HomeViewModel> above this _Body Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that _Body is under your MultiProvider/Provider<HomeViewModel>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>().toString()),
);
}
consider using `builder` like so:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context, child) {
// No longer throws
return Text(context.watch<Example>().toString());
}
);
}
If none of these solutions work, consider asking for help on StackOverflow:
https://stackoverflow.com/questions/tagged/flutter
The relevant error-causing widget was:
When the exception was thrown, this was the stack:
#0 Provider._inheritedElementOf (package:provider/src/provider.dart:343:7)
#1 Provider.of (package:provider/src/provider.dart:293:30)
#2 WatchContext.watch (package:provider/src/provider.dart:693:21)
#3 _Body.build (package:what_do_you_want_to_sing/presentation/home/home_screen.dart:79:35)
#4 StatelessElement.build (package:flutter/src/widgets/framework.dart:4949:49)
#5 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4878:15)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#7 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2667:19)
#8 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
#9 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#10 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#11 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9)
#12 SchedulerBinding.scheduleWarmUpFrame.<anonymous closure> (package:flutter/src/scheduler/binding.dart:881:7)
(elided 4 frames from class _RawReceivePortImpl, class _Timer, and dart:async-patch)
Provider 생성 및 선언
// 이곳에서 의존성을 한번에 불러 생성한 다음, main.dart 에 주입하겠다.
List<ChangeNotifierProvider> getProviders() {
final dio = Dio();
SongRepository repository = SongRepository(dio);
UseCases useCases = UseCases(
getSearchSong: GetSearchSongUseCase(repository: repository),
getSearchSinger: GetSearchSingerUseCase(repository: repository),
getRecentlySongsList: GetRecentlySongsListUseCase(repository: repository),
);
SearchViewModel searchViewModel = SearchViewModel(useCases: useCases);
HomeViewModel homeViewModel = HomeViewModel(useCases: useCases);
return [
ChangeNotifierProvider(create: (_) => searchViewModel),
ChangeNotifierProvider(create: (_) => homeViewModel),
];
}
Provider 주입
// main.dart
void main() {
// provider 호출
final providers = getProviders();
runApp(
MultiProvider(
providers: providers,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'title',
home: HomeScreen(),
);
}
}
List<ChangeNotifierProvider> getProviders() {
final dio = Dio();
SongRepository repository = SongRepository(dio);
UseCases useCases = UseCases(
getSearchSong: GetSearchSongUseCase(repository: repository),
getSearchSinger: GetSearchSingerUseCase(repository: repository),
getRecentlySongsList: GetRecentlySongsListUseCase(repository: repository),
);
SearchViewModel searchViewModel = SearchViewModel(useCases: useCases);
HomeViewModel homeViewModel = HomeViewModel(useCases: useCases);
return [
// 타입추가
ChangeNotifierProvider<SearchViewModel>(create: (_) => searchViewModel),
// 타입추가
ChangeNotifierProvider<HomeViewModel>(create: (_) => homeViewModel),
];
}
class _Body extends StatelessWidget {
const _Body({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final homeViewModel = Provider.of<HomeViewModel>(context);
final state = homeViewModel.state;
...
}
}
class _SearchScreenState extends State<SearchScreen> {
Widget build(BuildContext context) {
final searchViewModel = context.watch<SearchViewModel>();
final state = searchViewModel.state;
...
}
}
getProviders 의 함수의 리턴 배열안에 각 viewModel의 타입을 추가해주고, main.dart 에서 주입을 시켜주니! 각 screen 에서 Provider 접근에러가 해결된 것을 볼 수 있다.
배열을 리턴하는 하나의 getProviders 함수 안에 두개의 모델을 넣었고 그 점을 생각을 못했다.
강사님에게도 달린 답변이다.
context 가 중첩되서 그렇습니다. 대표적인 안티패턴 코드인데요.
아마 Builder 위젯으로 감싸면 될 건데 오히려 코드가 복잡해 집니다.
_Body StatelessWidget 대신 Widget을 리턴하는 일반 헬퍼 함수로 변경하시거나,
_Body 는 ViewModel에 접근하지 않고 생성자를 통해서 homeViewModel.state 를 전달받도록 수정하시면 됩니다.
HomeViewModel 은 HomeScreen 에서만 접근하도록이요.