https://docs.flutter.dev/data-and-backend/state-mgmt/intro
안드로이드, ios는 명령형 UI 프레임워크를 채택했다. 하나하나 알려줘야 한다.
플러터는 선언적 UI 프레임워크다. 상태가 바뀌면 UI가 바뀐다.
https://docs.flutter.dev/get-started/flutter-for/declarative
앱이 실행될 때 메모리에 올라가 있는 모든 것이 상태다.
그러나 모든 상태를 개발자가 관리하지는 않는다. 플러터만 건드리는 상태도 있다. (e.g, 텍스쳐)
사용자가 관리하는 상태는 두 가지로 분류할 수 있다.
= UI상태, 로컬 상태
한 위젯에 포함될 수 있는 상태.
임시라는 말 그대로 UI를 다 쓰고 난 뒤에 저장될 필요도 없다.
(앱 껐다가 다시 키면 초기화되는게 맞다)
e.g., 게시판 페이지 넘버, 애니메이션 진행도
= 공유 상태
임시 상태와 대치된다.
e.g., 유저 설정, 로그인 정보, 쇼핑몰 장바구니, 메일함 unread/read 여부.
알아서 결정하면 된다. 정해진거 없다. 실제로 개발하는 사람들만이 판단할 수 있음. 원래는 임시 상태였던게 시간이 지나서 앱 상태가 돼야 할 수도 있다.
어떤 상태든, 상태 관리를 위해 State클래스와 setState메소드를 쓰면 된다.
대충 일반적인 가이드라인을 잡아주는 다이어그램은 아래와 같이 그려볼 수 있다.
플러터가 선언적 UI를 채택했다는 점을 생각하면 상태를 어느 계층에 둘 것인지 판단할 수 있다. (데이터에 접근하기 쉬운 방법을 채택)
다른 위젯의 상태를 가져와서 처리하고, 각자 위젯 마다 같은 내용의 다른 상태변수를 업데이트 하는 것
--> 이거 하면 인생이 피곤해진다. 이럴거면 프레임워크 쓰지마라.
--> 차라리 트리 상위계층에서 하나의 상태를 관리하고, 하위 위젯에 뿌려줘라
자신보다 위에 있는 위젯의 데이터 접근 방법
: 위에서 접근점 역할을 할 수 있는 콜백함수를 내려준다.
다만 트리 깊이가 깊어질 수록 웹-프론트엔드의 props-drilling같은 일이 발생할 수 있는데, 플러터는 자식보다 아래에 위치한 위젯에 파라미터를 전달하기 위한 기능을 제공한다. 그 기능은 위젯으로 제공된다.
이 위젯들을 사용하는 대신 더 쉬운 방법인 Provider로 전역상태 관리 문제를 해결할 수 있다.
flutter pub add provider
아래의 기능을 사용해 상태를 관리한다.
notifyListeners()
를 호출class CounterModel extends ChangeNotifier {
int _num = 0; // 언더스코어는 private이란 뜻
int get num => _num; // getter를 이렇게 쓴다.
// // setter 쓰고싶으면 이렇게 할 수 있다
// set num (int a) {_num = a;}
void increment() {
_num++;
notifyListeners();
}
}
ChangeNotifier로 상태를 관리하는 모델을 만들었다면,
이제 그 모델을 어떤 위젯이 접근할 수 있도록 앱 내에 박아줘야 한다.
아래 경우는 MaterialApp이 모델(의 상태와 메소드)에 접근할 수 있도록 하는 코드.
이 경우 제공할 모델이 하나라서 ChangeNotifierProvider<T>
를 사용했다.
만약 모델이 여러 개라면 MultiProvider
를 사용할 수 있다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return ChangeNotifierProvider<CounterModel>(
create: (_) => CounterModel(),
child: MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
),
);
}
}
이제 ChangeNotifier가 ChangeNotifierProvider(혹은 MultiProvider)를 통해 앱에 박혔다면, 하위 위젯에서 써먹어야 한다.
세 가지 방법으로 나누어 아래에 적는다.
context.watch는 상태가 변경되면 다시 빌드한다.
read는 다시 빌드하지 않는다.
class DemoBody extends StatelessWidget {
const DemoBody({super.key});
Widget build(BuildContext context) {
final int num = context.watch<CounterModel>().num;
return Center(
child: ElevatedButton(
onPressed: () {
context.read<CounterModel>().increment();
},
child: Text('$num'),
),
);
}
}
context.watch/read와 유사한 사용법
옛날에는 어떤 제약때문에 지금처럼 언제나 일대일대응이 되지는 못했다고 한다.
class DemoBody extends StatelessWidget {
const DemoBody({super.key});
Widget build(BuildContext context) {
final int num = Provider.of<CounterModel>(context).num;
return Center(
child: ElevatedButton(
onPressed: () {
Provider.of<CounterModel>(context, listen: false).increment();
},
child: Text('$num'),
),
);
}
}
이렇게도 된다.
class DemoBody extends StatelessWidget {
const DemoBody({super.key});
Widget build(BuildContext context) {
return Center(
child: Consumer<CounterModel>(
builder: (_, counter, __) {
final int num = counter.num;
return ElevatedButton(
onPressed: () {
counter.increment();
},
child: Text('$num'),
);
},
),
);
}
}
Provider말고도 상태관리 도구 많이 있다. 알아서 골라써라.
https://docs.flutter.dev/data-and-backend/state-mgmt/options
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return ChangeNotifierProvider<CounterModel>(
create: (_) => CounterModel(),
child: MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
),
);
}
}
class DemoBody extends StatelessWidget {
const DemoBody({super.key});
// ---------------------------------------------
// context.read/watch로 구현
// @override
// Widget build(BuildContext context) {
// final int num = context.watch<CounterModel>().num;
// return Center(
// child: ElevatedButton(
// onPressed: () {
// context.read<CounterModel>().increment();
// },
// child: Text('$num'),
// ),
// );
// }
// ---------------------------------------------
// // Provider.of로 구현
// @override
// Widget build(BuildContext context) {
// final int num = Provider.of<CounterModel>(context).num;
// return Center(
// child: ElevatedButton(
// onPressed: () {
// Provider.of<CounterModel>(context, listen: false).increment();
// },
// child: Text('$num'),
// ),
// );
// }
// ---------------------------------------------
// Consumer로 구현
Widget build(BuildContext context) {
return Center(
child: Consumer<CounterModel>(
builder: (_, counter, __) {
final int num = counter.num;
return ElevatedButton(
onPressed: () {
counter.increment();
},
child: Text('$num'),
);
},
),
);
}
}
class CounterModel extends ChangeNotifier {
int _num = 0;
int get num => _num;
void increment() {
_num++;
notifyListeners();
}
}