[Flutter-error] Could not find the correct Provider<ViewModel> above this _Body Widget

Hee Tae Shin·2023년 2월 14일
post-thumbnail

상황

현재 만들고있는 토이프로젝트에서 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),
  ];
}

view 단

  1. homeScreen
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;
  	...
  }
}
  1. search
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 에서만 접근하도록이요.
profile
안녕하세요

0개의 댓글