출시·고도화 중
Flutter 안내서 · 4/6
앱이 커지면 "어떤 데이터를 어디에 두고, 바뀌었을 때 어떤 위젯을 다시 그릴지"가 중요해집니다. 이 장에서는 위젯 안의 지역 상태부터 여러 화면이 함께 쓰는 공유 상태, 비동기 처리, 네트워크 요청, 기기 안 저장소까지 차례로 다룹니다.
한 위젯 안에서만 쓰는 상태(입력 중인 글자, 펼침 여부, 선택된 탭 등)는 StatefulWidget과 setState로 충분합니다. 상태를 필요로 하는 위젯들의 가장 가까운 공통 부모에 상태를 두고, 자식에게는 값과 콜백을 생성자로 넘깁니다. 이를 "상태 끌어올리기"라고 합니다.
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final List<String> _items = [];
void _add(String text) => setState(() => _items.add(text));
@override
Widget build(BuildContext context) {
return Column(
children: [
TodoInput(onSubmit: _add),
Expanded(child: TodoList(items: _items)),
],
);
}
}여러 단계 아래의 위젯에 값을 계속 생성자로 넘기는 것은 번거롭습니다. InheritedWidget은 트리 아래쪽 어디서든 값을 찾을 수 있게 해 주는 기본 장치로, Theme.of(context)와 MediaQuery도 이 방식으로 동작합니다.
class UserScope extends InheritedWidget {
const UserScope({super.key, required this.userName, required super.child});
final String userName;
static UserScope of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserScope>()!;
}
@override
bool updateShouldNotify(UserScope oldWidget) => userName != oldWidget.userName;
}
// 아래쪽 어디서든
final name = UserScope.of(context).userName;dependOnInheritedWidgetOfExactType로 값을 읽은 위젯은 값이 바뀌면(updateShouldNotify가 true일 때) 다시 그려집니다. 다만 직접 쓰기에는 코드가 많아서, 실제 앱에서는 이를 감싼 패키지를 주로 씁니다.
Provider는 InheritedWidget을 쉽게 쓰도록 감싼 패키지로, Flutter 공식 문서의 상태 관리 예제에도 쓰입니다. ChangeNotifier로 상태 클래스를 만들고 값을 바꾼 뒤 notifyListeners()를 호출하면, 그 값을 지켜보는 위젯이 다시 그려집니다.
// flutter pub add provider
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
void add(String item) {
_items.add(item);
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
// 읽고 지켜보기(값이 바뀌면 다시 그림)
final count = context.watch<CartModel>().items.length;
// 메서드만 호출(다시 그리지 않음)
context.read<CartModel>().add('사과');Riverpod은 Provider를 만든 개발자가 새로 설계한 별도의 패키지입니다. 프로바이더를 위젯 트리 밖의 전역 선언으로 정의하고, BuildContext 대신 ref로 읽습니다. 컴파일 시점에 타입이 검사되고, 비동기 데이터와 테스트용 교체가 쉽다는 장점이 있습니다. 앱 루트를 ProviderScope로 감싸야 합니다.
// flutter pub add flutter_riverpod
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterText extends ConsumerWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return TextButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}이 밖에 Bloc 같은 패키지도 널리 쓰입니다. 어느 것을 고르든 "상태를 UI에서 분리하고, 바뀐 부분만 다시 그린다"는 원리는 같습니다.
Dart의 비동기 처리는 두 가지 타입으로 표현됩니다. Future는 나중에 한 번 도착하는 값(네트워크 응답, 파일 읽기)이고, Stream은 시간에 따라 여러 번 도착하는 값(센서, 채팅 메시지)입니다. async·await로 동기 코드처럼 쓸 수 있으며, 위젯에서는 FutureBuilder와 StreamBuilder로 결과를 표시합니다.
import 'dart:convert';
import 'package:http/http.dart' as http;
class Post {
const Post({required this.id, required this.title});
final int id;
final String title;
factory Post.fromJson(Map<String, dynamic> json) =>
Post(id: json['id'] as int, title: json['title'] as String);
}
Future<Post> fetchPost(int id) async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'));
if (response.statusCode != 200) {
throw Exception('불러오기 실패: ${response.statusCode}');
}
return Post.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}FutureBuilder에 넘길 Future는 build 안에서 만들지 말고, State의 필드로 한 번만 만들어 둡니다(initState에서 대입하거나 아래처럼 late final로 선언). 그렇지 않으면 다시 그릴 때마다 요청이 반복됩니다.
late final Future<Post> _post = fetchPost(1);
@override
Widget build(BuildContext context) {
return FutureBuilder<Post>(
future: _post,
builder: (context, snapshot) {
if (snapshot.hasError) return Text('오류: ${snapshot.error}');
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text(snapshot.data!.title);
},
);
}Android 릴리스 빌드에서 네트워크를 쓰려면 android/app/src/main/AndroidManifest.xml에 인터넷 권한을 넣어야 하고(디버그 빌드에는 기본으로 들어 있습니다), macOS 앱은 네트워크 클라이언트 권한(entitlement)을 추가해야 합니다.
| 용도 | 패키지 |
|---|---|
| 설정값 같은 작은 키-값 데이터 | shared_preferences |
| 문서·캐시 폴더 경로 찾기(파일 저장) | path_provider |
| 관계형 데이터(SQLite) | sqflite, drift |
| 토큰 같은 민감한 값 | flutter_secure_storage |
import 'package:shared_preferences/shared_preferences.dart';
final prefs = SharedPreferencesAsync();
Future<void> saveTheme(bool dark) => prefs.setBool('darkMode', dark);
Future<bool> loadTheme() async => await prefs.getBool('darkMode') ?? false;setState, 여러 위젯이 함께 쓰는 상태는 InheritedWidget을 바탕으로 한 Provider나 Riverpod 같은 패키지로 관리합니다.FutureBuilder·StreamBuilder로 표시합니다.http 패키지, 기기 저장은 shared_preferences·sqflite 등으로 처리합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.