
InheritedWidget을 사용한 shared state management를 구현하던 도중 Navigator.push로 띄운 route에서 상위 widget인 InheritedWidget에 접근하지 못하는 문제BuildContext.dependOnInheritedWidgetOfExactType이 null을 반환하여 state 및 action method에 접근할 수 없음Navigator.push를 사용할 때, BuildContext에 따라 route로 사용되는 widget이 widget tree에 추가되는 위치가 달라지는 것을 이해하여, InheritedWidget이 widget tree에서 자신에 접근하려는 child widget들의 공통 ancestor widget이 되도록 만들어 문제를 해결한다.MaterialApp은 내부에서 routing을 담당하는 최상위 Navigator를 만들고 widget tree에 추가한다.Navigator.of(context)는 현재 widget의 widget tree에서 가장 가까운 ancestor Navigator widget을 얻는다.build() method로 전달되는 BuildContext를 통해 Navigator.of(context)를 얻으면 MaterialApp 아래에 있는 최상위 Navigator를 얻는다.Navigator.push를 통해 화면을 전환하면, Screen A와 B는 같은 level로 widget tree에 추가된다. (sibling 관계)Navigator를 가지고 routing 해야 한다. 따라서, Screen A widget이 내부에서 Navigator widget을 별도로 생성하고, MaterialPageRoute의 builder 함수로 Screen A의 UI를 구성하는 widget을 반환한다.Navigator를 추가하지 않으면서 Screen B가 Screen A와 InheritedWidget을 통한 shared state에 접근하려면, InheritedWidget이 MaterialApp보다 상위에 있어야 한다.ProviderScope는 MyApp을 감싸는 widget tree의 root로 추가해야 하는데,ProviderScope는 내부 build() method에서 InheritedWidget을 상속받는 UncontrolledProviderScope를 반환한다.InheritedWidget을 놓으려는 시도를 한 것 같다.문제를 재현하는 간단한 counter app
HomeScreen 및 DetailScreen은 버튼을 눌러서 count 증가
HomeScreen과 DetailScreen은 count 값(state) 공유
공유 상태를 하위 widget에 공유하기 위한 CounterProvider를 InheritedWidget으로 생성
class CounterProvider extends InheritedWidget {
const CounterProvider({
super.key,
required super.child,
required this.counter,
required this.incrementCounter,
});
final int counter; // ✅ count 값 공유
final void Function() incrementCounter; // ✅ count action 공유
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
static CounterProvider? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<CounterProvider>();
}
공유 상태를 관리하기 위한 CounterScope widget을 StatefulWidget으로 생성
class CounterScope extends StatefulWidget {
const CounterScope({
super.key,
required this.child,
});
final Widget child;
State<CounterScope> createState() => _CounterScopeState();
}
class _CounterScopeState extends State<CounterScope> {
// ✅ 실제 상태 값 관리 : 상태 저장 및 갱신(setState)
int counter = 0;
void incrementCounter() {
setState(() {
counter++;
});
}
Widget build(BuildContext context) {
return CounterProvider(
counter: counter,
incrementCounter: incrementCounter,
child: widget.child,
);
}
}
MyApp의 home에서 CounterScope를 HomeScreen의 상위 widget으로 추가
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigator Inherited Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
// ✅ CounterScope가 HomeScreen의 상위 widget이 됨
home: const CounterScope(child: HomeScreen()),
);
}
}
HomeScreen에서는 CounterProvider에 접근할 수 있음
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: ...,
body: Center(
child: CountLabel(
// ✅
counter: CounterProvider.of(context)?.counter ?? -1,
),
),
floatingActionButton: IncrementButton(
// ✅
onPressed: CounterProvider.of(context)?.incrementCounter,
),
);
}
}
Navigator.push로 DetailScreen으로 이동
// 버튼 callback
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailScreen(),
),
),
DetailScreen에서는 CounterProvider에 접근할 수 없음
// 버튼 callback
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Detail Page'),
),
body: Center(
child: CountLabel(
// ❌ : null을 반환하므로 label에 '-1' 표시
counter: CounterProvider.of(context)?.counter ?? -1,
),
),
floatingActionButton: IncrementButton(
// ❌ : null을 반환하므로 counter가 동작하지 않음
onPressed: CounterProvider.of(context)?.incrementCounter,
),
);
}
}
BuildContext.dependOnInheritedWidgetOfExactType이 null을 반환한다는 것은 ancestor에 해당 type의 InheritedWidget이 존재하지 않는다는 것DetailScreen과 CounterProvider는 ancestor-descendant 관계가 아닌 sibling 관계
CounterProvider는 DetailScreen의 ancestor가 아니기 때문에 null을 반환했다.HomeScreen은 CounterProvider의 child widget이므로 HomeScreen에서 BuildContext.dependOnInheritedWidgetOfExactType는 CounterProvider 객체를 정상적으로 반환할 수 있었음HomeScreen에서 Navigator.push로 전달한 route에서 사용되는 DetailScreen은 왜 HomeScreen의 child widget이 아닌 sibling widget이 되었는가? (왜 다른 widget tree를 형성했을까?)MaterialApp 등의 WidgetsApp이 위치해야 함MaterialApp은 widget tree의 최상위에 Navigator를 갖고 있고, home에 전달되는 widget을 Navigator가 관리하는 route stack의 첫 번째 element로 설정함MaterialApp
ㄴ Navigator
ㄴ HomeScreenHomeScreen에서 Navigator.of(context)로 현재 widget tree 상에서 가장 가까운 ancestor navigator를 가져오게 되는데, 이 때 최상위 MaterialApp 아래의 최상위 Navigator를 사용함HomeScreen에서 최상위 Navigator를 사용하여 DetailScreen으로 routing 하기 때문에 HomeScreen과 DetailScreen은 최상위 Navigator의 children이 된다.MaterialApp
ㄴ Navigator
ㄴ HomeScreen
ㄴ DetailScreen // => push된 DetailScreen이 HomeScreen의 sibling이 됨CounterProvider와 DetailScreen을 ancestor-descendant 관계로 만들어야 한다.
HomeScreen에서 DetailScreen routing을 위해 사용하는 Navigator를 하위에 추가하여 사용한다.CounterProvider를 최상위 Navigator보다 상위 level로 추가한다. (MaterialApp 위)HomeScreen을 아래와 같이 수정한다.
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
// ✅ HomeScreen 하위에 Navigator 추가
// 이제 `Navigator.of(context)`는 이 Navigator를 반환한다.
return Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => ..., // 원래 widget 덩어리 반환
),
);
}
}
HomeScreen의 build() method에서 Navigator를 반환한다.Navigator의 onGenerateRoute 함수에서 MaterialPageRoute를 반환한다.MaterialPageRoute의 builder 함수에서 HomeScreen을 구성하는 Scaffold widget 덩어리를 반환한다.이제 HomeScreen에서 Navigator.of(context)를 호출하면 최상위 Navigator가 아닌 HomeScreen의 하위 Navigator를 사용하므로 DetailScreen이 HomeScreen의 child widget으로 추가된다.
DetailScreen이 MaterialApp 아래에서 CounterProvider 및 HomeScreen과 병렬적으로 widget tree를 형성하는 것이 문제이므로, CounterProvider가 MaterialApp의 상위 widget이 될 수 있도록 CounterScope의 위치를 최상위로 변경한다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
// ✅ CounterScope와 CounterProvider가 MaterialApp보다 상위 widget이 된다.
return CounterScope(
child: MaterialApp(
title: 'Navigator Inherited Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
// ✅ `HomeScreen`을 감싸고 있던 CounterScope는 제거한다.
home: const HomeScreen(),
),
);
}
}
이제 HomeScreen과 DetailScreen이 sigling 관계여도 CounterProvider를 공통 ancestor로 가지므로, 두 widget 모두 BuildContext를 통해 CounterProvider에 접근할 수 있다.