6 sätt att hantera tillstånd i Flutter-appar (med kodexempel)

Hur man hanterar tillstånd i Flutter

Sidinnehåll

I den här artikeln kommer vi att utforska sex populära sätt att hantera tillstånd i Flutter appar, inklusive verkliga exempel och bästa praxis:

Hantering av tillstånd är ett av de viktigaste - och mest debatterade - ämnena inom Flutter-utveckling. Det bestämmer hur din app hanterar förändringar i data och uppdaterar gränssnittet effektivt. Flutter ger dig flera strategier för att hantera tillstånd — från enkla till högt skalbara.

Strategierna för hantering av tillstånd i Flutter är:

  1. 🧱 setState() — Den inbyggda, enklaste metoden
  2. 🏗️ InheritedWidget — Flutter:s grund för spridning av tillstånd
  3. 🪄 Provider — Det rekommenderade lösningen för de flesta appar
  4. 🔒 Riverpod — Modern, kompileringssäker utveckling av Provider
  5. 📦 Bloc — För skalbara, företagsgradiga applikationer
  6. GetX — Lättviktslösning all-i-ett

flutter troubles mega tracktor


1. Användning av setState() — Grunderna

Det enklaste sättet att hantera tillstånd i Flutter är att använda den inbyggda setState()-metoden i en StatefulWidget.

Den här metoden är idealisk för lokal UI-tillstånd — där tillståndet tillhör en widget och inte behöver delas över hela appen.

Exempel: Räknare-app med 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),
      ),
    );
  }
}

Fördelar

  • Mycket enkelt att implementera
  • Bra för lokal eller tillfällig UI-tillstånd
  • Inga externa beroenden

Nackdelar

  • Skalar inte för stora appar
  • Svårt att dela tillstånd mellan widgets
  • Logik blandad med UI

Använd det när:

  • Prototypande eller byggande av små widgets
  • Hantering av isolerad UI-tillstånd (t.ex. växling av knapp, visning av modal)

2. InheritedWidget — Flutter:s grund

InheritedWidget är den lågnivåmekanism som Flutter använder för att sprida data nedåt i widget-trädet. De flesta lösningar för hantering av tillstånd (inklusive Provider) är byggda på den.

Att förstå InheritedWidget hjälper dig att förstå hur Flutter:s hantering av tillstånd fungerar under ytan.

Exempel: Tema-hanterare med InheritedWidget

import 'package:flutter/material.dart';

// Den InheritedWidget som innehåller tema-data
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);

  // Metod för att få tag på närmaste AppTheme upp i widget-trädet
  static AppTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppTheme>();
  }

  @override
  bool updateShouldNotify(AppTheme oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

// Stateful-wrapper för att hantera tillståndsförändringar
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(
              'Aktuellt tema: ${theme!.isDarkMode ? "Mörkt" : "Ljust"}',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => theme.toggleTheme(),
              child: Text('Växla tema'),
            ),
          ],
        ),
      ),
    );
  }
}

Fördelar

  • Byggt in i Flutter — inga externa beroenden
  • Effektiva widget-uppdateringar
  • Grund för att förstå andra lösningar
  • Direkt kontroll över spridningslogik

Nackdelar

  • Verbos boilerplate-kod
  • Kräver wrapper StatefulWidget
  • Lätt att göra misstag
  • Inte nybörjarvänlig

Använd det när:

  • Du lär dig hur Flutter:s hantering av tillstånd fungerar internt
  • Du bygger anpassade lösningar för hantering av tillstånd
  • Du behöver mycket specifik kontroll över spridning av tillstånd

3. Provider — Det Flutter-rekommenderade lösningen

När din apps tillstånd behöver delas mellan flera widgets, kommer Provider till undsättning.

Provider är baserat på Inversion of Control — istället för att widgets äger tillståndet, exponerar en provider det för andra att konsumera. Flutter-teamet rekommenderar det officiellt för medelstora appar.

Installation

Lägg till detta i din pubspec.yaml:

dependencies:
  provider: ^6.0.5

Exempel: Räknare-app med 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),
      ),
    );
  }
}

Fördelar

  • Reaktiva och effektiva uppdateringar
  • Ren separation av UI och logik
  • Väl dokumenterad och community-stödd

Nackdelar

  • Lite mer boilerplate än setState()
  • Nästlade providers kan bli komplexa

Använd det när:

  • Du behöver delat tillstånd mellan flera widgets
  • Du vill ha ett reaktivt mönster utan komplexitet
  • Din app växer bortom prototyp-storlek

4. Riverpod — Providers moderna utveckling

Riverpod är en komplett omskrivning av Provider som tar bort dess beroende av BuildContext och lägger till kompileringssäkerhet. Det är utformat för att lösa Providers begränsningar samtidigt som samma filosofi bibehålls.

Installation

Lägg till detta i din pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.4.0

Exempel: Användarprofil med Riverpod

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Modell
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: 'Gäst', 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();
});

// Beräknad provider
final greetingProvider = Provider<String>((ref) {
  final profile = ref.watch(profileProvider);
  return 'Hej, ${profile.name}! Du är ${profile.age} år gammal.';
});

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 Profil')),
      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: 'Namn'),
              onChanged: (value) {
                ref.read(profileProvider.notifier).updateName(value);
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Ålder: ${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('-'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Fördelar

  • Inget beroende av BuildContext — åtkomst till tillstånd var som helst
  • Kompileringssäkerhet — fånga fel innan körning
  • Bättre testbarhet
  • Kraftfull tillståndskomposition
  • Inga minnesläckor

Nackdelar

  • Annan API än traditionella Flutter-mönster
  • Brantare inlärningskurva än Provider
  • Mindre community (men växer snabbt)

Använd det när:

  • Du startar ett nytt Flutter-projekt
  • Du vill ha typ-säkerhet och kompileringsgarantier
  • Komplexa tillståndsberoenden och kompositioner
  • Arbeta med medelstora-till-stora applikationer

5. BLoC (Business Logic Component) — För företagsappar

BLoC mönstret är en mer avancerad arkitektur som helt separerar affärslogiken från gränssnittet med hjälp av strömmmar.

Det är idealiskt för stora eller företagsappar, där förutsägbara och testbara tillståndsövergångar är avgörande.

Installation

Lägg till denna beroende i ditt projekt:

dependencies:
  flutter_bloc: ^9.0.0

Exempel: Räknare-app med BLoC

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// Händelser
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 Räknare')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('Räkning: $count', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

Fördelar

  • Skalbart och underhållbart för komplexa appar
  • Klar separation av lager
  • Lätt att enhetstesta

Nackdelar

  • Mer boilerplate
  • Inlärningskurvan är brantare

Använd det när:

  • Bygga företags- eller långsiktiga projekt
  • Du behöver förutsägbar och testbar logik
  • Flera utvecklare arbetar på olika appmoduler

6. GetX — Den lättviktslösa all-i-ett-lösningen

GetX är en minimalistisk men kraftfull lösning för hantering av tillstånd som också inkluderar routing, beroendeinjektion och verktyg. Det är känt för att ha minst boilerplate av alla lösningar.

Installation

Lägg till detta i din pubspec.yaml:

dependencies:
  get: ^4.6.5

Exempel: Inköpslista med GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// Kontroller
class ShoppingController extends GetxController {
  // Observerbar lista
  var items = <String>[].obs;

  // Observerbar räknare
  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 Inköpslista',
      home: ShoppingScreen(),
    );
  }
}

class ShoppingScreen extends StatelessWidget {
  // Initiera kontroller
  final ShoppingController controller = Get.put(ShoppingController());
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Inköpslista'),
        actions: [
          Obx(() => Padding(
                padding: EdgeInsets.all(16),
                child: Center(
                  child: Text(
                    'Antal: ${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: 'Ange varanamn',
                      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('Inga varor i din lista'),
                );
              }

              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: 'Rensa Lista',
                    middleText: 'Är du säker på att du vill rensa alla varor?',
                    textConfirm: 'Ja',
                    textCancel: 'Nej',
                    onConfirm: () {
                      controller.clearAll();
                      Get.back();
                    },
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('Rensa Allt'),
              ),
            ),
        ],
      ),
    );
  }
}

Fördelar

  • Minimal boilerplate — snabbast utveckling
  • All-i-ett-lösning (tillstånd, routing, DI, meddelanden, dialoger)
  • Inget BuildContext behövs
  • Mycket lättviktig och snabb
  • Lätt att lära

Nackdelar

  • Mindre “Flutter-lik” jämfört med Provider/BLoC
  • Kan leda till stark koppling om man inte är försiktig
  • Mindre ekosystem än Provider
  • Vissa utvecklare tycker det är “för magiskt”

Använd det när:

  • Snabb prototyp eller MVP-utveckling
  • Små till medelstora appar
  • Du vill ha minimal boilerplate
  • Teamet föredrar enkelhet framför strikt arkitektur

Val av rätt lösning för tillståndshantering

Lösning Komplexitet Boilerplate Inlärningskurva Bäst för
🧱 setState() Låg Minimal Lätt Enkel lokal tillstånd, prototyper
🏗️ InheritedWidget Medel Hög Medel Att lära sig Flutter-interner, anpassade lösningar
🪄 Provider Låg-Medel Låg Lätt De flesta appar, delat tillstånd
🔒 Riverpod Medel Låg-Medel Medel Moderna appar, typ säkerhet
📦 BLoC Hög Hög Brant Företagsappar, komplex affärslogik
GetX Låg Minimal Lätt Snabb utveckling, MVPs

Snabb beslutsguide:

App komplexitet Rekommenderad metod Exempelanvändning
🪶 Enkel setState() eller GetX Knappväxling, animationer, små widgetar
⚖️ Medel Provider eller Riverpod Delade teman, användarinställningar, datacaching
🏗️ Komplex BLoC eller Riverpod E-handel, chattappar, finansiella instrumentpaneler

Slutsatser

Det finns ingen allmänt passande lösning för tillståndshantering i Flutter.

Här är ett praktiskt tillvägagångssätt:

  • Börja enkelt med setState() för enkla widgetar och prototyper
  • Lär dig grunderna med InheritedWidget för att förstå hur Flutter fungerar internt
  • Gå vidare till Provider när din app växer och behöver delat tillstånd
  • Överväg Riverpod för nya projekt där typ säkerhet och moderna mönster är viktiga
  • Anamma BLoC för stora företagsappar med komplex affärslogik
  • Prova GetX när snabb utveckling och minimal boilerplate är prioriterat

Viktiga slutsatser:

✅ Överdriv inte - använd setState() tills du behöver mer ✅ Provider och Riverpod är utmärkta val för de flesta applikationer ✅ BLoC lyser i stora team och komplexa appar ✅ GetX är bra för hastighet, men var medveten om koppling ✅ Att förstå InheritedWidget hjälper dig att behärska vilken lösning som helst

Nyckeln är att balansera enkelhet, skalbarhet och underhållbarhet - och välja rätt verktyg för dina specifika behov, teamets expertis och projektkrav.

Länkar till Flutter State Management-bibliotek

Andra användbara länkar