Skip to content
Boring Doc
Official documentation (snapshot)doc

Rebuild UI when state changes

Instructions on how to manage state with ChangeNotifiers.

Source last updated Oct 21, 2026 · last synchronized 3 weeks ago · commit b3045439b7c0 · read on docs.flutter.dev

This is a rendered snapshot of the official page as stored by Boring Doc for provenance and search. Site-specific components (embedded demos, DartPad, images with relative paths) may not render; use the official page for the full experience. Content © The Flutter Authors, CC BY 3.0.

Learn to use ListenableBuilder to automatically rebuild UI and handle all possible states with switch expressions.


Introduction

The view layer is your UI, and in Flutter, that refers to your app's widgets. As it pertains to this tutorial, the important part is wiring up your UI to respond to data changes from the ViewModel. ListenableBuilder is a widget that can "listen" to a ChangeNotifier, and automatically rebuilds when it's provided ChangeNotifier calls notifyListeners().

Create the article view widget

Create the ArticleView widget that manages your page's layout and ViewModel lifecycle. Because it must explicitly initialize data fetching before rendering, implement it as a StatefulWidget.

Start by creating the basic stateful structure:

import 'package:material_ui/material_ui.dart';

class ArticleView extends StatefulWidget {
  const ArticleView({super.key});

  @override
  State<ArticleView> createState() => _ArticleViewState();
}

class _ArticleViewState extends State<ArticleView> {
  // The view model will be instantiated here next.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Wikipedia Flutter')),
      body: const Center(child: Text('Loading...')),
    );
  }
}

Instantiate the article view model

Next, initialize your ArticleViewModel mapping it to the state's lifecycle. Provide the ViewModel and execute fetchArticle() within initState():

class ArticleView extends StatefulWidget {
  const ArticleView({super.key});

  @override
  State<ArticleView> createState() => _ArticleViewState();
}

class _ArticleViewState extends State<ArticleView> {
  final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());

  @override
  void initState() {
    super.initState();
    viewModel.fetchArticle();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Wikipedia Flutter')),
      body: const Center(child: Text('Loading...')),
    );
  }
}

Update your app to include the article view

Connect everything together by updating your MainApp to include your completed ArticleView.

Replace your existing MainApp with this updated version:

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: ArticleView());
  }
}

This change switches from the console-based test to the full UI experience with proper state management.

Listen for state changes

Wrap your UI in a ListenableBuilder to listen for state changes, and pass it a ChangeNotifier object. In this case, the ArticleViewModel extends ChangeNotifier.

class ArticleView extends StatefulWidget {
  const ArticleView({super.key});

  @override
  State<ArticleView> createState() => _ArticleViewState();
}

class _ArticleViewState extends State<ArticleView> {
  final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());

  @override
  void initState() {
    super.initState();
    viewModel.fetchArticle();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Wikipedia Flutter')),
      body: ListenableBuilder(
        listenable: viewModel,
        builder: (context, child) {
          return const Center(child: Text('Loading...'));
        },
      ),
    );
  }
}

ListenableBuilder uses the builder pattern, which requires a callback rather than a child widget to build the widget tree below it. These widgets are flexible because you can perform operations within the callback, building different widgets based on the state.

Handle possible view model states

Recall the ArticleViewModel, which has three properties that the UI is interested in:

  • Summary? summary
  • bool isLoading
  • Exception? error

Depending on the combined state of these properties, the UI can display different widgets. Use Dart's support for switch expressions to handle all possible combinations in a clean, readable way:

class ArticleView extends StatefulWidget {
  const ArticleView({super.key});

  @override
  State<ArticleView> createState() => _ArticleViewState();
}

class _ArticleViewState extends State<ArticleView> {
  final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());

  @override
  void initState() {
    super.initState();
    viewModel.fetchArticle();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Wikipedia Flutter')),
      body: Center(
        child: ListenableBuilder(
          listenable: viewModel,
          builder: (context, _) {
            return switch ((
              viewModel.isLoading,
              viewModel.summary,
              viewModel.error,
            )) {
              (true, _, _) => const CircularProgressIndicator(),
              (_, _, final Exception e) => Text('Error: $e'),
              (_, final summary?, _) => ArticlePage(
                summary: summary,
                nextArticleCallback: viewModel.fetchArticle,
              ),
              _ => const Text('Something went wrong!'),
            };
          },
        ),
      ),
    );
  }
}

This is an excellent example of how a declarative, reactive framework like Flutter and a pattern like MVVM work together: The UI is rendered based on the state and updates when a state changes demands it, but it doesn't manage any state or the process of updating itself. The business logic and rendering are completely separate from each other.

Complete the UI

The only thing remaining is to use the properties and methods provided by the view model to build the UI.

Now create a ArticlePage widget that displays the actual article content. This reusable widget takes summary data and a callback function:

class ArticlePage extends StatelessWidget {
  const ArticlePage({
    super.key,
    required this.summary,
    required this.nextArticleCallback,
  });

  final Summary summary;
  final VoidCallback nextArticleCallback;

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Article content will be displayed here...'),
    );
  }
}

Add a scrollable layout

Replace the placeholder with a scrollable column layout:

class ArticlePage extends StatelessWidget {
  const ArticlePage({
    super.key,
    required this.summary,
    required this.nextArticleCallback,
  });

  final Summary summary;
  final VoidCallback nextArticleCallback;

  @override
  Widget build(BuildContext context) {
    return const SingleChildScrollView(
      child: Column(
        children: [Text('Article content will be displayed here...')],
      ),
    );
  }
}

Add article content and button

Complete the layout with an article widget and navigation button:

class ArticlePage extends StatelessWidget {
  const ArticlePage({
    super.key,
    required this.summary,
    required this.nextArticleCallback,
  });

  final Summary summary;
  final VoidCallback nextArticleCallback;

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          ArticleWidget(summary: summary),
          ElevatedButton(
            onPressed: nextArticleCallback,
            child: const Text('Next random article'),
          ),
        ],
      ),
    );
  }
}

Create the ArticleWidget

The ArticleWidget handles the display of the actual article content with proper styling and conditional rendering.

Set up the basic article structure

Start with the widget that accepts a summary parameter:

class ArticleWidget extends StatelessWidget {
  const ArticleWidget({super.key, required this.summary});

  final Summary summary;

  @override
  Widget build(BuildContext context) {
    return const Text('Article content will be displayed here...');
  }
}

Add padding and column layout

Wrap the content in proper padding and layout:

class ArticleWidget extends StatelessWidget {
  const ArticleWidget({super.key, required this.summary});

  final Summary summary;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Column(
        spacing: 10,
        children: [const Text('Article content will be displayed here...')],
      ),
    );
  }
}

Add conditional image display

Add the article image that only shows when available:

class ArticleWidget extends StatelessWidget {
  const ArticleWidget({super.key, required this.summary});

  final Summary summary;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Column(
        spacing: 10,
        children: [
          if (summary.hasImage) Image.network(summary.originalImage!.source),
          const Text('Article content will be displayed here...'),
        ],
      ),
    );
  }
}

Complete with styled text content

Replace the placeholder text with a properly styled title, description, and extract:

class ArticleWidget extends StatelessWidget {
  const ArticleWidget({super.key, required this.summary});

  final Summary summary;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Column(
        spacing: 10,
        children: [
          if (summary.hasImage) Image.network(summary.originalImage!.source),
          Text(
            summary.titles.normalized,
            overflow: TextOverflow.ellipsis,
            style: Theme.of(context).textTheme.displaySmall,
          ),
          if (summary.description != null)
            Text(
              summary.description!,
              overflow: TextOverflow.ellipsis,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          Text(summary.extract),
        ],
      ),
    );
  }
}

This widget demonstrates a few important UI concepts:

  • Conditional rendering: The if statements show content only when available.
  • Text styling: Different text styles create visual hierarchy using Flutter's theme system.
  • Proper spacing: The spacing parameter provides consistent vertical spacing.
  • Overflow handling: TextOverflow.ellipsis prevents text from breaking the layout.

Run the complete app

Hot reload your app one final time. You should now see:

  1. A loading spinner while the initial article loads.
  2. The article's title, description, and summary extract.
  3. An image (if the article has one).
  4. A button to load another random article.

To see the reactive UI in action, click the Next random article button. The app shows a loading state, fetches new data, and updates the display automatically.

Review

Test yourself

Was this useful?

Content from flutter/website, © The Flutter Authors, used under the Creative Commons Attribution 3.0 License; code samples under the BSD License.

Rebuild UI when state changes (official source) · Boring Doc