Released · improving
Flutter guide · 4/6
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.
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 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.
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.
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.
| Need | Package |
|---|---|
| Small key-value settings | shared_preferences |
| Document and cache paths for files | path_provider |
| Relational data (SQLite) | sqflite, drift |
| Secrets such as tokens | 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 for state inside one widget; for shared state, use packages built on InheritedWidget such as Provider, or Riverpod.FutureBuilder and StreamBuilder.http for network calls and shared_preferences or sqflite for local storage.
0 comments
Sign in · Sign in to leave a comment.
Be the first to comment.