6 sposobów zarządzania stanem w aplikacjach Flutter (z przykładami kodu)

Jak zarządzać stanem w Flutterze

Page content

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:

  1. 🧱 setState() — wbudowany, najprostszy sposób
  2. 🏗️ InheritedWidget — fundament Fluttera do propagacji stanu
  3. 🪄 Provider — zalecane rozwiązanie dla większości aplikacji
  4. 🔒 Riverpod — nowoczesna, bezpieczna w czasie kompilacji ewolucja Provider
  5. 📦 Bloc — dla skalowalnych, aplikacji typu enterprise
  6. GetX — lekki, kompletny w rozwiązaniu

flutter troubles mega tracktor


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

Inne przydatne linki