Zum Inhalt springen

Imperative versus Declarative in Flutter

Summary

There are two common ways to build screens. Imperative code tells the computer exactly what to do, step by step. Declarative code describes what the screen should look like for the current state, and the framework updates it for you. Flutter encourages the declarative way. This post keeps things simple, shows tiny examples, and gives a clear path to try the ideas on your own device.

A calm starting point

Imagine a small screen that shows a list of books. There is a loading spinner, and sometimes an error message. On a busy day the old list stays visible under the error card. You fix one path, another breaks. It feels fragile. A change of style can help.

Two ways to think

  • Imperative: change what already exists. Show the spinner, hide the list, replace the text.
  • Declarative: say what the screen should be for this state. If loading, return a spinner. If ready, return a list.

An everyday picture helps. If a friend asks for directions to your flat:

  • Imperative sounds like this. Turn left at the bakery, walk past the park, take the third right, press the second buzzer.
  • Declarative sounds like this. Number 18, Willow Road, the blue door.

One is a list of steps. The other is the intended result.

Imperative in one tiny glance

This short example is from the classic Android view system. You flip visibility by hand and push data into the list.

kotlin

// Small, imperative example in Kotlin

sealed interface ScreenState
object Loading : ScreenState
data class Ready(val items: List<String>) : ScreenState

fun render(
  state: ScreenState,
  spinner: View,
  list: RecyclerView,
  adapter: ListAdapter<String, *>
) {
  spinner.visibility = if (state is Loading) View.VISIBLE else View.GONE
  list.visibility    = if (state is Ready)   View.VISIBLE else View.GONE

  if (state is Ready) {
    adapter.submitList(state.items)
  }
}

It is clear when there are only two states. It becomes easy to miss a line when more states appear. That is when yesterday’s content sometimes remains visible.

Declarative with the setState function in Flutter

Now the same idea with Flutter. We describe the whole screen from the current state. There is a single source of truth, the screen value.

How to try this now

  1. „Create a new Flutter project.“
  2. „Replace lib/main.dart with the code below.“
  3. „Run the app and tap the button.“
import 'package:flutter/material.dart';

// Two simple states for the screen
enum Screen { loading, ready }

// A tiny value object for books
class Book {
  final String title;
  final String author;
  const Book(this.title, this.author);
}

void main() => runApp(const MaterialApp(home: BooksDemo()));

class BooksDemo extends StatefulWidget {
  const BooksDemo({super.key});
  @override
  State<BooksDemo> createState() => _BooksDemoState();
}

class _BooksDemoState extends State<BooksDemo> {
  Screen screen = Screen.loading;
  List<Book> items = const [];

  Future<void> load() async {
    setState(() => screen = Screen.loading);
    await Future<void>.delayed(const Duration(seconds: 1));
    setState(() {
      items = const [
        Book('Practical Flutter', 'Sara Conte'),
        Book('Patterns for Mobile', 'Leo Ahmed'),
      ];
      screen = Screen.ready;
    });
  }

  @override
  void initState() {
    super.initState();
    load(); // start loading as soon as the screen appears
  }

  @override
  Widget build(BuildContext context) {
    // Describe the whole screen from the current state
    if (screen == Screen.loading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    // Ready state
    return Scaffold(
      appBar: AppBar(title: const Text('Books')),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (_, i) => ListTile(
          title: Text(items[i].title),
          subtitle: Text('By ${items[i].author}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: load,
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

What to notice
There is no manual show or hide. We do not try to remember what was visible before. We return the right tree for now. When screen changes, Flutter rebuilds for us.

A very gentle user input example

Here is a tiny settings screen. It has a dark mode switch. When the switch changes, the app theme changes. No manual updates to labels. No hidden toggles.

import 'package:flutter/material.dart';

void main() => runApp(const ThemeDemo());

class ThemeDemo extends StatefulWidget {
  const ThemeDemo({super.key});
  @override
  State<ThemeDemo> createState() => _ThemeDemoState();
}

class _ThemeDemoState extends State<ThemeDemo> {
  bool dark = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: dark ? ThemeMode.dark : ThemeMode.light,
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: Scaffold(
        appBar: AppBar(title: const Text('Settings')),
        body: ListTile(
          title: const Text('Dark mode'),
          trailing: Switch(
            value: dark,
            onChanged: (v) => setState(() => dark = v),
          ),
        ),
      ),
    );
  }
}

The widget tree is a clear reflection of state. If dark is true, the dark theme is used. If it is false, the light theme is used.

A simple form that reacts to state

This shows how a small form can feel declarative. The message appears when the name is too short. There is no manual show or hide scattered around.

class NameForm extends StatefulWidget {
  const NameForm({super.key});
  @override
  State<NameForm> createState() => _NameFormState();
}

class _NameFormState extends State<NameForm> {
  String name = '';

  @override
  Widget build(BuildContext context) {
    final isValid = name.trim().length >= 3;

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        TextField(
          decoration: const InputDecoration(labelText: 'Your name'),
          onChanged: (v) => setState(() => name = v),
        ),
        const SizedBox(height: 8),
        if (!isValid)
          const Text('Please enter at least three characters',
              style: TextStyle(color: Colors.red)),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: isValid ? () {} : null,
          child: const Text('Continue'),
        ),
      ]),
    );
  }
}

The button enables itself when the state is valid. The error text appears only when needed. Clear and predictable.

Key differences you can feel

  • Readability: Declarative code reads like a small list of states. Loading, ready, failed. The intent is visible.
  • Single source of truth: Keep a single state value. The tree is built from that value. Old values do not leak into new frames.
  • Fewer side effects: You are not nudging old widgets into shape. You are returning the correct shape for now.
  • Easier testing: You can say, given this state, I expect a spinner, or this text, or a certain number of tiles.

When to choose which

  • Small, local screens. Use the setState function. It is simple and fine.
  • Shared or cross screen state. Move state into a small model with Provider or Riverpod.
  • Many transitions and tricky flows. Consider Bloc for explicit events and clearer steps.
    If you are unsure, begin with the setState function. When the widget starts to feel busy, lift state into a model. You will feel the difference.

A gentle way to migrate an old screen

  • Write down the states. Loading, empty, ready, failed.
  • Create one state value and a single if or switch that returns the right body.
  • Extract small helper widgets for each branch.
  • Remove old show or hide code one piece at a time.
  • Add an explicit empty state if the list can be empty. That small decision prevents confusion later.

This is steady work. It pays off quickly.

Takeaways you can use today

  • Describe the screen from state rather than patching it.
  • Keep a single source of truth.
  • Start small with the setState function. Lift state into a model when needed.
  • Name states up front. It saves time and reduces accidental drift.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert