6 sposobów zarządzania stanem w aplikacjach Flutter (z przykładami kodu)
Jak zarządzać stanem w Flutterze
W tym artykule omówimy sześć popularnych sposobów zarządzania stanem w Flutter aplikacji, w tym rzeczywiste przykłady i najlepsze praktyki:
Zarządzanie stanem to jedno z najważniejszych – i najbardziej debatowanych – tematów w tworzeniu aplikacji w Flutter. Określa, jak aplikacja radzi sobie z zmianami danych i aktualizuje interfejs użytkownika w sposób efektywny. Flutter oferuje kilka strategii zarządzania stanem – od prostych po bardzo skalowalne.
Strategie zarządzania stanem w Flutter to:
- 🧱
setState()
— wbudowany, najprostszy sposób - 🏗️ InheritedWidget — fundament Fluttera do propagacji stanu
- 🪄 Provider — zalecane rozwiązanie dla większości aplikacji
- 🔒 Riverpod — nowoczesna, bezpieczna w czasie kompilacji ewolucja Provider
- 📦 Bloc — dla skalowalnych, aplikacji typu enterprise
- ⚡ GetX — lekki, kompletny w rozwiązaniu
1. Użycie setState()
— Podstawy
Najprostszym sposobem zarządzania stanem w Flutter jest użycie wbudowanego metody setState()
w StatefulWidget
.
Ten podejście jest idealne do lokalnego stanu interfejsu użytkownika — gdzie stan należy do jednego widgeta i nie musi być udostępniany w całej aplikacji.
Przykład: Aplikacja licznika z setState()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('setState Counter')),
body: Center(
child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Zalety
- Bardzo łatwe do zaimplementowania
- Świetne do lokalnego lub tymczasowego stanu interfejsu użytkownika
- Brak zależności zewnętrznych
Wady
- Nie skaluje się dla dużych aplikacji
- Trudno udostępniać stan między widgetami
- Logika mieszana z interfejsem użytkownika
Użyj, gdy:
- Prototypowanie lub budowanie małych widgetów
- Obsługa izolowanego stanu interfejsu użytkownika (np. przełączanie przycisku, wyświetlanie modalnego okna)
2. InheritedWidget — Fundament Fluttera
InheritedWidget
to niskopoziomowy mechanizm, który Flutter używa do propagowania danych w drzewie widgetów. Większość rozwiązań zarządzania stanem (w tym Provider) jest zbudowana na jego bazie.
Zrozumienie InheritedWidget pomaga zrozumieć, jak działa zarządzanie stanem w Flutterze.
Przykład: Menedżer motywów z InheritedWidget
import 'package:flutter/material.dart';
// InheritedWidget, który przechowuje dane motywów
class AppTheme extends InheritedWidget {
final bool isDarkMode;
final Function toggleTheme;
const AppTheme({
Key? key,
required this.isDarkMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
// Metoda dostępu do najbliższego AppTheme w drzewie widgetów
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
return isDarkMode != oldWidget.isDarkMode;
}
}
// Wrapper do zarządzania zmianami stanu
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
_ThemeManagerState createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeManager(
child: Builder(
builder: (context) {
final theme = AppTheme.of(context);
return MaterialApp(
theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: HomeScreen(),
);
},
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget Theme')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => theme.toggleTheme(),
child: Text('Toggle Theme'),
),
],
),
),
);
}
}
Zalety
- Wbudowany w Flutter — brak zależności zewnętrznych
- Efektywne odświeżanie widgetów
- Fundament do zrozumienia innych rozwiązań
- Prosta kontrola nad logiką propagacji
Wady
- Wielu powtarzających się fragmentów kodu
- Wymaga wrappera StatefulWidget
- Łatwo popełnić błędy
- Nie jest przyjazny dla początkujących
Użyj, gdy:
- Nauczanie się, jak działa zarządzanie stanem wewnętrznie w Flutterze
- Budowanie niestandardowych rozwiązań zarządzania stanem
- Potrzebujesz bardzo konkretnych kontrolek nad propagacją stanu
3. Provider — zalecane rozwiązanie w Flutter
Kiedy stan aplikacji musi być udostępniany między wieloma widgetami, Provider przychodzi na ratunek.
Provider opiera się na odwróceniu kontroli — zamiast widgetów posiadania stanu, dostawca go udostępnia innym do użycia. Zespół Fluttera oficjalnie zaleca go dla średnich aplikacji.
Konfiguracja
Dodaj to do swojego pubspec.yaml
:
dependencies:
provider: ^6.0.5
Przykład: Aplikacja licznika z Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: context.read<CounterModel>().increment,
child: Icon(Icons.add),
),
);
}
}
Zalety
- Reaktywne i efektywne aktualizacje
- Czysta separacja interfejsu użytkownika i logiki
- Dobrze udokumentowane i wspierane przez społeczność
Wady
- Słusznie więcej powtarzających się fragmentów kodu niż
setState()
- Zagnieżdżone dostawcy mogą stać się skomplikowane
Użyj, gdy:
- Potrzebujesz udostępnionego stanu między wieloma widgetami
- Chcesz wzorzec reaktywny bez złożoności
- Twoja aplikacja rośnie poza etap prototypowania
4. Riverpod — nowoczesna ewolucja Provider
Riverpod to kompletny przepis Provider, który usuwa zależność od BuildContext i dodaje bezpieczeństwo w czasie kompilacji. Jest zaprojektowany tak, aby rozwiązać ograniczenia Provider, zachowując tę samą filozofię.
Konfiguracja
Dodaj to do swojego pubspec.yaml
:
dependencies:
flutter_riverpod: ^2.4.0
Przykład: Profil użytkownika z Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class UserProfile {
final String name;
final int age;
UserProfile({required this.name, required this.age});
UserProfile copyWith({String? name, int? age}) {
return UserProfile(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
}
// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
return ProfileNotifier();
});
// Computed provider
final greetingProvider = Provider<String>((ref) {
final profile = ref.watch(profileProvider);
return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ProfileScreen());
}
}
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(profileProvider);
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Profile')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(greeting, style: TextStyle(fontSize: 20)),
SizedBox(height: 30),
TextField(
decoration: InputDecoration(labelText: 'Name'),
onChanged: (value) {
ref.read(profileProvider.notifier).updateName(value);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(profileProvider.notifier).updateAge(profile.age + 1);
},
child: Text('+'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (profile.age > 0) {
ref.read(profileProvider.notifier).updateAge(profile.age - 1);
}
},
child: Text('-'),
),
],
),
],
),
),
);
}
}
Zalety
- Brak zależności od BuildContext — dostęp do stanu wszędzie
- Bezpieczeństwo w czasie kompilacji — wykrywanie błędów przed uruchomieniem
- Lepsza testowalność
- Potężna kompozycja stanu
- Brak przecieków pamięci
Wady
- Inny API niż tradycyjne wzorce Fluttera
- Trudniejszy do nauki niż Provider
- Mniejsza społeczność (ale szybko rosnąca)
Użyj, gdy:
- Rozpoczynanie nowego projektu Flutter
- Chcesz bezpieczeństwo typów i gwarancje w czasie kompilacji
- Złożone zależności i kompozycje stanu
- Pracujesz nad aplikacjami średnich lub dużych rozmiarów
5. BLoC (Business Logic Component) — dla aplikacji typu enterprise
BLoC pattern to bardziej zaawansowana architektura, która całkowicie oddziela logikę biznesową od interfejsu użytkownika za pomocą strumieni.
To idealne rozwiązanie dla dużych lub aplikacji typu enterprise, gdzie przewidywalne i testowalne przejścia stanu są kluczowe.
Konfiguracja
Dodaj tę zależność do swojego projektu:
dependencies:
flutter_bloc: ^9.0.0
Przykład: Aplikacja licznika z BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(home: CounterScreen()),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Counter')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Count: $count', style: TextStyle(fontSize: 24));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
);
}
}
Zalety
- Skalowalne i utrzyjmowalne dla złożonych aplikacji
- Jasna separacja warstw
- Łatwe testowanie jednostkowe
Wady
- Więcej powtarzających się fragmentów kodu
- Trudniejszy do nauki
Użyj, gdy:
- Budowanie aplikacji typu enterprise lub długoterminowych projektów
- Potrzebujesz przewidywalnej i testowalnej logiki
- Wiele programistów pracuje nad różnymi modułami aplikacji
6. GetX — lekki, kompletny w rozwiązaniu
GetX to minimalistyczne, ale potężne rozwiązanie zarządzania stanem, które również obejmuje routing, iniekcję zależności i narzędzia. Wiadomo, że ma najmniej powtarzających się fragmentów kodu spośród wszystkich rozwiązań.
Konfiguracja
Dodaj to do swojego pubspec.yaml
:
dependencies:
get: ^4.6.5
Przykład: Lista zakupów z GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Controller
class ShoppingController extends GetxController {
// Obserwowana lista
var items = <String>[].obs;
// Obserwowany licznik
var totalItems = 0.obs;
void addItem(String item) {
if (item.isNotEmpty) {
items.add(item);
totalItems.value = items.length;
}
}
void removeItem(int index) {
items.removeAt(index);
totalItems.value = items.length;
}
void clearAll() {
items.clear();
totalItems.value = 0;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Shopping List',
home: ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
// Inicjalizacja kontrolera
final ShoppingController controller = Get.put(ShoppingController());
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Shopping List'),
actions: [
Obx(() => Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Items: ${controller.totalItems}',
style: TextStyle(fontSize: 18),
),
),
)),
],
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Enter item name',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
controller.addItem(textController.text);
textController.clear();
},
child: Icon(Icons.add),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.items.isEmpty) {
return Center(
child: Text('No items in your list'),
);
}
return ListView.builder(
itemCount: controller.items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(controller.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => controller.removeItem(index),
),
);
},
);
}),
),
if (controller.items.isNotEmpty)
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
Get.defaultDialog(
title: 'Clear List',
middleText: 'Are you sure you want to clear all items?',
textConfirm: 'Yes',
textCancel: 'No',
onConfirm: () {
controller.clearAll();
Get.back();
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(double.infinity, 50),
),
child: Text('Clear All'),
),
),
],
),
);
}
}
Zalety
- Minimalna ilość powtarzających się fragmentów kodu — naj szybszy rozwój
- Kompletna w rozwiązaniu (stan, routing, DI, snackbars, dialogi)
- Nie potrzebny BuildContext
- Bardzo lekki i szybki
- Łatwy do nauki
Wady
- Mniej “Flutter-like” niż Provider/BLoC
- Może prowadzić do silnego sprzężenia, jeśli nie będzie ostrożny
- Mniejsza ekosystema niż Provider
- Niektórzy programiści uważają, że “zbyt magiczny”
Użyj, gdy:
- Szybkie prototypowanie lub rozwój MVP
- Małe do średnie aplikacje
- Chcesz minimalną ilość powtarzających się fragmentów kodu
- Zespół preferuje prostotę nad ściślejszą architekturą
Wybór odpowiedniego rozwiązania zarządzania stanem
Rozwiązanie | Złożoność | Powtarzające się fragmenty | Kurwa uczenia | Najlepsze do |
---|---|---|---|---|
🧱 setState() |
Niski | Minimalny | Łatwy | Prosty lokalny stan, prototypy |
🏗️ InheritedWidget |
Średni | Wysoki | Średni | Nauczanie się wewnętrznych mechanizmów Fluttera, niestandardowe rozwiązania |
🪄 Provider |
Niski-Średni | Niski | Łatwy | Większość aplikacji, udostępnianie stanu |
🔒 Riverpod |
Średni | Średni | Średni | Nowoczesne aplikacje, bezpieczeństwo typów |
📦 BLoC |
Wysoki | Wysoki | Trudny | Aplikacje typu enterprise, złożona logika biznesowa |
⚡ GetX |
Niski | Minimalny | Łatwy | Szybki rozwój, MVP |
Szybki przewodnik decyzyjny:
Złożoność aplikacji | Zalecany podejście | Przykład przypadku użycia |
---|---|---|
🪶 Prosta | setState() lub GetX |
Przełączanie przycisków, animacje, małe widgety |
⚖️ Średnia | Provider lub Riverpod |
Udostępnianie motywów, ustawień użytkownika, buforowanie danych |
🏗️ Złożona | BLoC lub Riverpod |
E-commerce, aplikacje czatu, panele finansowe |
Ostateczne myśli
Nie ma jednego rozwiązywania wszystkiego podejścia do zarządzania stanem w Flutter.
Oto praktyczne podejście:
- Zaczynaj od prostego
setState()
dla prostych widgetów i prototypów - Naucz się podstaw z InheritedWidget, aby zrozumieć, jak działa Flutter wewnętrznie
- Przejdz do Provider w miarę rozwoju aplikacji i potrzeb udostępniania stanu
- Rozważ Riverpod dla nowych projektów, gdzie bezpieczeństwo typów i nowoczesne wzorce mają znaczenie
- Zastosuj BLoC dla dużych aplikacji typu enterprise z złożoną logiką biznesową
- Spróbuj GetX kiedy szybki rozwój i minimalna ilość powtarzających się fragmentów kodu są priorytetem
Główne wnioski:
✅ Nie nadkomplikuj — używaj setState()
dopóki nie potrzebujesz więcej
✅ Provider i Riverpod to świetne wybory dla większości aplikacji
✅ BLoC świetnie sprawdza się w dużych zespołach i złożonych aplikacjach
✅ GetX jest świetny dla szybkości, ale uważaj na sprzężenie
✅ Zrozumienie InheritedWidget pomaga w opanowaniu każdego rozwiązania
Kluczem jest zrównoważenie prostoty, skalowalności i utrzyjmowalności — oraz wybór odpowiedniego narzędzia dla konkretnych potrzeb, wiedzy zespołu i wymagań projektu.
Linki do bibliotek zarządzania stanem w Flutter
- Oficjalna dokumentacja Flutter na temat zarządzania stanem
- Pakiet Provider
- Dokumentacja Riverpod
- Biblioteka BLoC
- Dokumentacja GetX