As an app grows, the key questions are where data lives and which widgets rebuild when it changes. This chapter covers local and shared state, async code, networking and storage.
Local state: setState
State used by one widget, such as a selected tab, fits in a StatefulWidget with setState. Keep it in the nearest common parent of the widgets that need it and pass values and callbacks down (lifting state up).
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 lets any descendant look a value up without passing it through every constructor; Theme.of(context) works this way.
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;
}
// Anywhere below it
final name = UserScope.of(context).userName;
Widgets that read it this way rebuild when updateShouldNotify returns true. Using it directly is verbose, so most apps use a package built on it.
Provider and Riverpod
Provider wraps InheritedWidget in an easier API and is used in the state management examples of the official docs. Model state with a ChangeNotifier; after changing data, call notifyListeners() and the widgets watching it rebuild.
// 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(),
),
);
}
// Read and listen (rebuilds on change)
final count = context.watch<CartModel>().items.length;
// Call a method without listening
context.read<CartModel>().add('Apple');
Riverpod is a separate package by the author of Provider. Providers are global declarations outside the widget tree, read through ref instead of BuildContext, with compile-time safety, built-in async support and easy test overrides. Wrap the app root in 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 and others are also popular; all keep state out of the UI and rebuild only what changed.
Async: Future and Stream
A Future delivers one value later (a network response); a Stream delivers many values over time (chat messages). Write async code with async and await, and show results with FutureBuilder and StreamBuilder.
Networking with the http package
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('Request failed: ${response.statusCode}');
}
return Post.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
Create the Future once as a State field (assign it in initState or declare it late final as below), never inside build, or every rebuild repeats the request.
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('Error: ${snapshot.error}');
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text(snapshot.data!.title);
},
);
}
Release Android builds need the internet permission in android/app/src/main/AndroidManifest.xml (debug builds include it by default), and macOS apps need the network client entitlement.
On-device 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;
Summary
- Use
setState for state inside one widget; for shared state, use packages built on InheritedWidget such as Provider, or Riverpod. - Handle async values with Future (one value) and Stream (many), shown via
FutureBuilder and StreamBuilder. - Use
http for network calls and shared_preferences or sqflite for local storage.
0 comentários
Fazer login · Faça login para deixar um comentário.