#03 개념 확장 - 상태 관리 (StatefulWidget & InheritedWidget)
목차
Ⅰ 상태관리의 필요성
01. 상태 관리
- 모바일 환경에서는 사용자와의 상호작용을 기반으로 실시간으로 변하는 데이터와 UI를 처리해야 함
- 사용자와 상호작용하는 순간마다 애플리케이션의 상태가 변경
- 상태를 효율적으로 관리해야 앱의 성능 저하를 막고, 예상치 못한 오류를 방지할 수 있음
- Flutter는 이러한 상태 관리를 효울적으로 처리할 수 있는 다양한 도구들을 제공
Ⅱ StatefulWidget을 이용한 상태 관리
01. StatefulWidget
StatefulWidget
- StatefulWidget은 Flutter에서 상태를 관리하는 가장 기본적인 방식
- 앱의 UI가 동적으로 변경될 필요가 있을 때 사용하는 위젯
구조와 특징
- StatefulWidget은 StatefulWidget과 State로 구성
- UI는 State 클래스에 정의되며, 상태가 변경되면 setState() 메서드를 호출해 업데이트
02. StatefulWidget 상태 관리 예제
- 도서 목록에서 +, - 버튼을 클릭해 도서를 선택하는 예제
- 사용자가 선택한 도서는 Cart 페이지에 표시
main
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(mySelectedBook),
);
}
}
HomeScreen
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int pageIndex = 0; // bottomNavigation을 위한 현재 선택된 페이지 index
List<String> mySelectedBook = []; // 사용자가 선택한 Book List (상태 관리 변수)
// HomeScreen 의 상태를 관리하기 위한 함수 (상태 변경 함수)
void _selectedBook(String book) {
// setState 메서드를 호출해 화면 재랜더링
setState(() {
if (mySelectedBook.contains(book)) {
mySelectedBook.remove(book);
} else {
mySelectedBook.add(book);
}
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(title: Text('상태관리')),
body: IndexedStack(
index: pageIndex,
children: [
// ListPage에 필요한 매개 변수 전달
ListPage(
onSelectedBook: _selectedBook,
selectedBook: mySelectedBook,
),
CartPage(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'list',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart),
label: 'cart',
),
],
onTap: (index) {
setState(() {
pageIndex = index;
});
},
),
),
);
}
}
ListPage
class ListPage extends StatelessWidget {
// 버튼을 선택했을 때 호출될 함수 (부모 위젯의 상태 변경 함수)
final Function onSelectedBook;
final List<String> selectedBook;
final List<String> books = ['호모사피엔스', '다트입문', '홍길동전', '코드리팩토링', '전치사도감'];
ListPage(
{required this.onSelectedBook, required this.selectedBook, super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: books.map((book) {
final isSelectedBool = selectedBook.contains(book);
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.redAccent,
borderRadius: BorderRadius.circular(8.0),
),
),
title: Text(
book,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
trailing: IconButton(
// 버튼을 클릭할 때 부모 위젯의 상태 관리 함수 호출
onPressed: () {
onSelectedBook(book);
},
icon: Icon(isSelectedBool ? Icons.remove_circle : Icons.add),
color: isSelectedBool ? Colors.red : Colors.green,
),
);
}).toList(),
);
}
}
CartPage
class CartPage extends StatelessWidget {
final List<String> selectedBook;
const CartPage(this.selectedBook, {super.key});
@override
Widget build(BuildContext context) {
return ListView(
children:
selectedBook.map((book) => ListTile(title: Text(book))).toList(),
);
}
}
코드 흐름
- 부모 위젯에서 상태를 관리하는 변수와 상태를 변경하는 메서드를 정의
- 상태를 관리하는 변수와 상태를 변경하는 메서드를 자식 위젯에게 전달
- 자식 위젯에서 전달 받은 상태 변경 메서드를 호출
- 부모 위젯의 상태 변경 메서드에서 setState() 메서드를 이용해 상태 변경하고 UI를 갱신
- 부모 위젯에서 변경된 상태는 자식 위젯과 동기화되어, 자식 위젯은 최신 상태로 UI를 랜더링
Ⅲ InheritedWidget을 이용한 상태 관리
01. InheritedWidget
InheritedWidget
- 위젯 트리에서 데이터를 상위 위젯으로부터 하위 위젯으로 효율적으로 전달하기 위해 사용
- 상태 관리의 기본 원리와 Provider 같은 라이브러리의 기반을 이해하는 데 중요한 역할을 함
구조와 특징
- 데이터 트리를 전체에서 공유해야 할 때 사용
- 데이터가 변경되면 이를 참조하는 모든 위젯이 재빌드
- 성능 최적화가 가능하며, 적절한 설계가 중요
02. InheritedWidget 상태 관리 예제
- 도서 목록에서 +, - 버튼을 클릭해 도서를 선택하는 예제
- 사용자가 선택한 도서는 Cart 페이지에 표시
main
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
HomeScreen
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int pageIndex = 0;
List<String> mySelectedBook = [];
// 상태를 변경하는 메서드
void _toggleSaveStates(String book) {
setState(() {
if (mySelectedBook.contains(book)) {
// 리스트를 불변 데이터로 관리하여 상태 변경을 감지
mySelectedBook = List.from(mySelectedBook)..remove(book);
} else {
mySelectedBook = List.from(mySelectedBook)..add(book);
}
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('상태관리'),
backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
),
body: InheritedParent(
state: mySelectedBook,
onPressed: _toggleSaveStates,
child: IndexedStack(
index: pageIndex,
children: [
BookListPage(),
BookCartPage(),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
onTap: (index) {
setState(() {
pageIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.list), label: 'book-list'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'cart'),
],
),
),
);
}
}
BookListPage
class BookListPage extends StatelessWidget {
BookListPage({super.key});
final List<String> books = ['호모사피엔스', '다트입문', '홍길동전', '코드리팩토링', '전치사도감'];
@override
Widget build(BuildContext context) {
// InheritedParent 위젯에 접근하여 상태 가져오기
InheritedParent? inheritedParent = context.dependOnInheritedWidgetOfExactType();
List<String> selectedBook = inheritedParent?.state ?? []; // 선택된 책 목록 초기값 설정
return inheritedParent == null
? Center(child: Text('Data Null'))
: ListView(
children: books.map(
(book) {
final isSelectedBool = selectedBook.contains(book);
return ListTile(
leading: Container(
width: 35,
height: 35,
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.black),
),
),
title: Text(
book,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
trailing: IconButton(
onPressed: () {
inheritedParent.onPressed(book);
},
icon: Icon(
isSelectedBool ? Icons.remove_circle : Icons.add,
color: isSelectedBool ? Colors.red : Colors.green,
),
),
);
},
).toList(),
);
}
}
BookCartPage
class BookCartPage extends StatelessWidget {
const BookCartPage({super.key});
@override
Widget build(BuildContext context) {
// InheritedParent 위젯에 접근하여 상태 가져오기
InheritedParent? inheritedParent = context.dependOnInheritedWidgetOfExactType();
List<String> selectedBook = inheritedParent?.state ?? []; // 선택된 책 목록 초기값 설정
return ListView(
children:
selectedBook.map((book) => ListTile(title: Text(book))).toList(),
);
}
}
InheritedParent
class InheritedParent extends InheritedWidget {
List<String> state; // 공유 상태 데이터 관리
void Function(String book) onPressed; // 상태 변경 메서드
InheritedParent(
{required this.state, required this.onPressed, required super.child});
// 상태가 변경되었는지 여부를 판단하는 메서드
// 주의점 : InheritedWidget을 상속받아 재정의한 InheritedParent을 넣어주자
@override
bool updateShouldNotify(covariant InheritedParent oldWidget) {
print('InheritedParent - updateShouldNotify() 호출');
print('state : ${state.toString()}');
print('oldWidget.state : ${oldWidget.state.toString()}');
// 상태가 달라졌음을 판단하는 방법 - 현재의 state와 oldWidget의 state를 비교
if (state.length != oldWidget.state.length) {
print('상태 변경됨');
return true;
}
for (int i = 0; i < state.length; i++) {
if (state[i] != oldWidget.state[i]) {
print('상태 변경됨');
return true;
}
}
print('상태 변경 안됨');
// 상태 변경이 있으면 true, 없으면 false -> bool 여부에 따라 자식 위젯의 build 재호출 결정
return false;
}
}
코드 흐름
- InheritedParent는 상태를 관리하는 변수와 상태를 변경하는 메서드를 정의하고 전달하는 역할
- 부모 위젯인 HomeScreen 위젯에서 전체 상태를 관리
- InheritedParent를 통해 하위 위젯에 상태와 상태 변경 메서드를 전달
- 하위 위젯은 InheritedParent를 통해 상태를 참조하며, 상태 변경 메서드를 호출해 상태 변경
- 상태를 변경하면 부모에서 관리하는 상태가 업데이트 되고, InheritedParent가 이를 감지해 하위 위젯을 재랜더링 하여 동기화