Flutter에서 상태를 여러 위젯에 공유하려면 단순히 setState만으로는 한계가 있다. 예를 들어, 부모에서 만든 데이터를 자식 여러 개가 동시에 접근하려면 트리 전체를 따라 값 전달을 해야 하는데, 이는 번거롭고 복잡하다.
이 문제를 해결하기 위해 InheritedWidget이 등장하였다. 트리에 Provider를 배치하고, 필요한 곳에서 참조하는 구조이다.
핵심은 BuildContext.dependOnInheritedWidgetOfExactType<T>() 메서드이다.
- Consumer 위젯이 이 메서드를 호출하면, Flutter는 **해당 Consumer가 Provider를 “구독”**하게 만들어 준다.
- 이후 Provider의 값이 바뀌면, 구독 중인 Consumer가 다시 build되면서 최신 값을 반영한다.
즉, 구독과 변경 알림이 자동으로 연결되는 것이 InheritedWidget의 핵심 원리이다.
InheritedWidget을 직접 사용할 때는 보통 3개의 역할이 필요하다:
- 상태 관리자 (State Manager) • 값 보관 및 변경 책임. • 변경되면 Provider에 새로운 상태를 바인딩한다.
- Provider (InheritedWidget 확장) • 위젯 트리에 상태와 메서드를 배포한다. • updateShouldNotify를 통해 Consumer에게 갱신 여부를 알려준다.
- Consumer (데이터를 소비하는 위젯) - context.dependOnInheritedWidgetOfExactType로 Provider에 접근한다. - Provider로부터 받은 데이터를 UI에 반영한다.
import 'package:flutter/material.dart';
main() => runApp(MyApp());
// MARK: State Manager 역할
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int count = 0;
void _increment() {
setState(() {
count = count + 1;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterProvider(
_increment,
count: count,
child: MainScreen(),
),
);
}
}
// MARK: Consumer 역할
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
final provider = CounterProvider.of(context);
return Scaffold(
appBar: AppBar(title: Text('Main Screen')),
body: Center(child: Text('Count: ${provider.count}')),
floatingActionButton: FloatingActionButton(
onPressed: provider.increment,
child: Icon(Icons.add),
),
);
}
}
// MARK: Provider 역할
class CounterProvider extends InheritedWidget {
final int count;
final void Function() increment;
const CounterProvider(
this.increment, {
super.key,
required super.child,
required this.count,
});
static CounterProvider of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<CounterProvider>();
assert(result != null, 'No CounterProvider found in context');
return result!;
}
@override
bool updateShouldNotify(CounterProvider oldWidget) {
return oldWidget.count != count;
}
}- MyApp (State Manager)
- count 값과 _increment 메서드를 관리한다.
- 값이 바뀔 때마다 새로운 상태를 CounterProvider에 주입한다.
- CounterProvider (Provider)
- count와 increment를 트리에 배포한다.
- updateShouldNotify로 변경 여부를 Consumer에게 알려준다.
- MainScreen (Consumer)
- CounterProvider.of(context)로 상태를 받아온다.
- provider.count를 화면에 출력한다.
- provider.increment를 버튼에 연결해 값 갱신을 수행한다.
- setState → 위젯 하나 내부 전용
- ChangeNotifier → 여러 위젯 구독
- InheritedWidget → 트리 전역 또는 특정 영역 공유
- StatefulWidget 내부에서만 동작하는 가장 단순한 방식이다.
- 호출 시 해당 위젯 전체가 다시 빌드된다.
- 적합한 경우: 작은 위젯 내부에서만 상태를 관리할 때.
- 여러 위젯이 같은 상태를 구독할 수 있는 옵저버 패턴이다.
- notifyListeners() 호출 시 구독 중인 위젯만 다시 빌드된다.
- 이 위젯들의 setState가 실행된다고 이해하면 된다.
- InheritedNotifier나 Provider와 결합해 전역 공유에 자주 사용된다.
- 적합한 경우: 여러 위젯이 동시에 상태를 공유해야 할 때.
- 트리 자체에 상태를 심어 하위 위젯이 context로 접근한다.
- 구독 중인 위젯만 다시 빌드된다.
- 적합한 경우: 테마, 사용자 정보, 로케일 같은 전역적 상태 공유.
📌 방식별로 보면
- setState → “나 바뀌었어! 내 build() 다시 실행해!”
- ChangeNotifier → “나 바뀌었어! 나를 구독 중인 애들만 다시 빌드해!”
- InheritedWidget → “값이 달라지면 트리 아래 구독자 위젯만 빌드해!”
- Riverpod/Bloc → 같은 원리, 다만 더 구조적이고 확장성 있게 래핑한다.
- setState → 기초
- ValueNotifier → 단일 값 옵저버 패턴
- InheritedWidget → 상태를 트리 전역에 공유
- ChangeNotifier → 다수 상태 관리 + 옵저버 패턴
- InheritedNotifier → InheritedWidget + ChangeNotifier (Provider의 기초)
Provider라는 이름은 원래 CS 전반에서 "무언가를 공급하는 역할"이라는 의미로 자주 쓰인다.
Flutter에서도 이 컨벤션을 따라, InheritedWidget을 감싸는 클래스를 만들 때 보통 ~Provider라는 이름을 붙인다.
예: CounterProvider, ThemeProvider, AuthProvider
한편, Flutter의 공식 상태관리 라이브러리인 provider 패키지는 InheritedWidget과 InheritedNotifier를 기반으로 만들어졌다. 단순 값부터 ChangeNotifier, Future, Stream까지 다양한 상태를 Provider로 감싸 공급할 수 있고, 하위 위젯은 context.watch<T>(), context.read<T>(), Consumer<T> 같은 API를 통해 간단하게 접근할 수 있다.
InheritedWidget은 특정 부분의 위젯들끼리 데이터를 나눌 때 적합하다. 앱 전체 상태는Provider나Riverpod같은 도구를 사용하는 것이 더 편리하다.updateShouldNotify를 잘 설계하여 불필요하게 다시 그리는 일을 줄여야 한다.of(context)는 반드시InheritedWidget아래에서 호출해야 한다. 빌드 과정 외부에서는 주의해야 한다.- 상태가 자주 바뀌면
InheritedNotifier(예:ChangeNotifier)를 사용하면 더 세밀하게 제어할 수 있다.
InheritedNotifier<T extends Listenable>는 내부에 Listenable(예: ChangeNotifier)를 넣어서, 상태가 바뀌면 알림을 보내고 필요한 위젯만 다시 그리게 한다. Provider 패키지가 이런 방법을 사용한다.
class AppState extends InheritedNotifier<MyViewModel> {
const AppState({Key? key, required MyViewModel notifier, required Widget child})
: super(key: key, notifier: notifier, child: child);
static MyViewModel of(BuildContext context) {
final inherited = context.dependOnInheritedWidgetOfExactType<AppState>();
assert(inherited != null, 'No AppState found in context');
return inherited!.notifier!;
}
}ThemeInheritedWidget을 만들어서 앱에서 라이트 모드와 다크 모드를 변경해 보아야 한다. 위젯들이 자동으로 변경되어야 한다.- 기존에
ChangeNotifier를 사용하던 동적 리스트 관리 예제를InheritedNotifier로 변경해 보아야 한다. 다시 그려지는 부분이 어떻게 달라지는지 비교해야 한다. updateShouldNotify가 항상false를 반환하도록 변경한 뒤 앱이 어떻게 동작하는지 관찰하고 결과를 정리해야 한다.
import 'package:flutter/material.dart';
main() => runApp(AppRoot());
class AppRoot extends StatefulWidget {
const AppRoot({super.key});
@override
State<AppRoot> createState() => _AppRootState();
}
class _AppRootState extends State<AppRoot> {
bool _isDark = false;
void _toggleThemes() {
setState(() {
_isDark = !_isDark;
});
}
@override
Widget build(BuildContext context) {
return ThemeProvider(_isDark, _toggleThemes, child: const MyApp());
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
void _decrement() {
setState(() {
_count--;
});
}
@override
Widget build(BuildContext context) {
final provider = ThemeProvider.of(context);
return MaterialApp(
theme: provider.isDark ? ThemeData.dark() : ThemeData.light(),
home: CounterProvider(
_count,
_increment,
_decrement,
child: MainScreen(),
),
);
}
}
class ThemeProvider extends InheritedWidget {
final bool isDark;
final void Function() themeToggle;
const ThemeProvider(
this.isDark,
this.themeToggle, {
super.key,
required super.child,
});
static ThemeProvider of(BuildContext context) {
final ThemeProvider? result = context
.dependOnInheritedWidgetOfExactType<ThemeProvider>();
assert(result != null, 'No ThemeProvider found in context');
return result!;
}
@override
bool updateShouldNotify(ThemeProvider oldWidget) {
return isDark != oldWidget.isDark;
}
}
class CounterProvider extends InheritedWidget {
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
const CounterProvider(
this.count,
this.onIncrement,
this.onDecrement, {
super.key,
required super.child,
});
static CounterProvider of(BuildContext context) {
final CounterProvider? result = context
.dependOnInheritedWidgetOfExactType<CounterProvider>();
assert(result != null, 'No CounterProvider found in context');
return result!;
}
@override
bool updateShouldNotify(CounterProvider oldWidget) {
return count != oldWidget.count;
}
}
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
final provider = CounterProvider.of(context);
final themeProvider = ThemeProvider.of(context);
return Scaffold(
appBar: AppBar(title: Text('Inherited Widget')),
body: Center(child: Text('counter : ${provider.count}')),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: provider.onIncrement,
child: Icon(Icons.add),
),
SizedBox(height: 30),
FloatingActionButton(
onPressed: provider.onDecrement,
child: Icon(Icons.remove),
),
SizedBox(height: 30),
FloatingActionButton(
onPressed: themeProvider.themeToggle,
child: Icon(
themeProvider.isDark ? Icons.toggle_off : Icons.toggle_on,
),
),
],
),
);
}
}import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<int> items = [];
void increment() {
setState(() {
items = [...items, items.length + 1];
});
}
void decrement() {
setState(() {
if (items.isNotEmpty) {
final copy = List<int>.of(items);
copy.removeLast();
items = copy;
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ListProvider(
items,
increment,
decrement,
child: const MainScreen(),
),
);
}
}
class ListProvider extends InheritedWidget {
final List<int> items;
final void Function() increment;
final void Function() decrement;
const ListProvider(
this.items,
this.increment,
this.decrement, {
super.key,
required super.child,
});
static ListProvider of(BuildContext context) {
final ListProvider? result = context
.dependOnInheritedWidgetOfExactType<ListProvider>();
assert(result != null, 'No listProvider found in context');
return result!;
}
@override
bool updateShouldNotify(ListProvider oldWidget) {
return items != oldWidget.items;
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
final provider = ListProvider.of(context);
return Scaffold(
appBar: AppBar(title: Text('MainScreen')),
body: Center(
child: ListView.builder(
itemCount: provider.items.length,
itemBuilder: (context, index) {
return ListTile(title: Text('Item ${provider.items[index]}'));
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: provider.increment,
child: Icon(Icons.add),
),
SizedBox(height: 30),
FloatingActionButton(
onPressed: provider.decrement,
child: Icon(Icons.remove),
),
],
),
);
}
}
- 250820 : 최초 작성
- 250821 : 각 상태관리 방법 비교 추가