https://docs.flutter.dev/get-started/fundamentals
플러터 핵심에 관한 공식 문서를 읽고 배운 점을 요약해본다. 공부 목적은 아래 질문을 답하기 위해서이다. (아직 5,6번은 해결 덜 됨)
- build()는 어떤 의미일까?
- State는 왜 별도 클래스로 선언할까?
- constraints는 뭘 가리키는 걸까?
- build()와 setState(), 그리고 무한루프
- 위젯의 생명주기
- MVVM, Callback Notifier, Inherited Widget 이 셋의 연결고리
https://docs.flutter.dev/get-started/fundamentals/widgets
https://docs.flutter.dev/get-started/fundamentals/layout#understanding-layout-in-flutter
build() 메서드: 모든 위젯은 자기 객체에 build 메서드를 가지고 있어야하며(override), 이 build 메서드는 또 다른 위젯을 반환해야한다.https://docs.flutter.dev/get-started/fundamentals/layout
플러터가 UI 툴킷이라는 점을 고려했을 때, 플러터 개발자는 대부분 위젯으로 레이아웃을 만드는 데 시간을 쓰게 된다. 특히 가장 흔한 에러 중 하나인 "unbounded constraints" 에러도 자주 만나게 될 것이다.
TBC... https://docs.flutter.dev/get-started/fundamentals/layout#box-types
https://docs.flutter.dev/get-started/fundamentals/widgets#widget-state
https://docs.flutter.dev/get-started/fundamentals/state-management
아래에서는 상태 관리, 즉 앱 안에서 이런 정보 자원들을 어떻게 관리할지 설계하기 위해서 사용할 수 있는 방법을 크게 3가지로 나누어 다룬다:
상태를 관리하는 가장 간단한 방법. 상태를 위젯 안에 저장하는 방식.
| StatefulWidget | State | |
|---|---|---|
| 변경(mutable) | X | O |
build() | X | O |
예시 코드
class MyCounter extends StatefulWidget {
const MyCounter({super.key});
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
int count = 0;
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
TextButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
)
],
);
}
}
위 예시는 아래 두 가지 컨셉을 충족한다:
MyCounter 는 count 변수에 대해 몰라도 된다_MyCounterState 객체와 그 안의 count 변수는, MyCounter 가 처음 빌드될 때 생성되었다가 MyCounter 가 화면에서 사라질 때 제거된다. (임시 상태 - "ephemeral state"라고 부른다. ephemeral 은 수명이 짧다는 뜻인데 한글권에서는 주로 임시라고 번역하는 듯.)InhertiedWidget 사용 (혹은 "provider" 등 유사한 패키지)InheritedWidget 을 상속하고 static 메서드인 of() 와 updateShouldNotify() 를 구현한다. of() 호출. updateShouldNotify() 를 통해 새 데이터를 비교하고, 테마 등이 이 InheritedWidget의 대표적인 예시.
ValueChanged<T> 타입 사용: typedef ValueChanged<T> = void Function(T value);class MyState extends InheritedWidget {
const MyState({
super.key,
required this.data,
required super.child,
});
final String data;
static MyState of(BuildContext context) {
// This method looks for the nearest `MyState` widget ancestor.
final result = context.dependOnInheritedWidgetOfExactType<MyState>();
assert(result != null, 'No MyState found in context');
return result!;
}
// This method should return true if the old widget's data is different
// from this widget's data. If true, any widgets that depend on this widget
// by calling `of()` will be re-built.
bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
var data = MyState.of(context).data;
return Scaffold(
body: Center(
child: Text(data),
),
);
}
}
플러터는 Listenable라는 추상 클래스를 지원한다. Listenable은 한 개 이상의 리스너들을 업데이트할 수 있다. Listenable 을 사용하는 방법 중 대표적인 몇 가지를 소개하자면 (Listenable을 구현하는 클래스들):
ChangeNotifier를 선언하고 ListenableBuilder로 이 Notifier를 구독한다.ValueNotifier와 ValueListenableBuilder를 사용한다.ChangeNotifier 를 상속하는 클래스를 만들고, 상태가 변경되는 곳에서(변경 되었다고 알려야할 때) notifyListeners() 를 호출한다.ListenableBuilder 에 위 객체를 전달하고, builder 함수에서 서브트리를 생성한다.class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
Column(
children: [
ListenableBuilder(
listenable: counterNotifier,
builder: (context, child) {
return Text('counter: ${counterNotifier.count}');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.increment();
},
),
],
)
ChangeNotifier 을 상속하는 클래스로, 더 심플한 버전. 값을 하나만 저장한다 (single value).
Then use the value field to read or update the value, and notify any listeners that the value has changed. Because ValueNotifier extends ChangeNotifier, it is also a Listenable and can be used with a ListenableBuilder. But you can also use ValueListenableBuilder, which provides the value in the builder callback:
ValueNotifier 인스턴스를 생성한다.ValueListenableBuilder 에 위 객체를 전달하고, value 필드를 이용해 값을 읽거나 업데이트한다.ValueNotifier 는 ChangeNotifier 상속한 것이므로, ValueListenableBuilder 대신 ListenableBuilder 를 사용할 수도 있다. 다만 ValueListenableBuilder 는 builder() 함수에서 value 인자를 넘겨주므로 더 편리한 점이 있다.
ValueNotifier<int> counterNotifier = ValueNotifier(0);
Column(
children: [
ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return Text('counter: $value');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.value++;
},
),
],
)
InheritedWidget 을 직접 쓰면:
InheritedWidget 이 들고 있음of(context) 로 접근InheritedWidget 자체를 새로운 인스턴스로 교체해야 함 → 그걸 감지한 위젯들이 재빌드즉, 상태가 바뀌면 전체 InheritedWidget 이 rebuild 되어야 하고, 상태를 바꿀 때마다 항상 새 위젯 생성. 그래서 상태 변경이 자주 일어나는 경우 매우 번거롭고 boilerplate가 많아짐.
Listenable:
Listenable을 듣고 있는 객체만 리빌드됨. 즉 Notifier+Listenable은 값이 바뀌었다는 사실만 전달, 값 자체를 전달하지는 않음.
상태를 클래스로 묶고(ChangeNotifier), 그 상태가 바뀌는 걸 notifyListeners 로 알리면 → UI 코드가 상태 클래스에 덜 의존하고 깔끔해짐. InheritedWidget만 쓰면 상태 변경 로직이 UI 쪽에 더 퍼지기 십상.
모델
import 'package:http/http.dart';
class CounterData {
CounterData(this.count);
final int count;
}
class CounterModel {
Future<CounterData> loadCountFromServer() async {
final uri = Uri.parse('https://myfluttercounterapp.net/count');
final response = await get(uri);
if (response.statusCode != 200) {
throw ('Failed to update resource');
}
return CounterData(int.parse(response.body));
}
Future<CounterData> updateCountOnServer(int newCount) async {
// ...
}
}