Home State in Flutter Apps Use ListenableBuilder to Update App UI

👂 ListenableBuilder — Connect Your Data to the Screen!

⏱️ 30-35 minutes 📊 Intermediate — The final connection! 📦 2 Projects included 🏷️ ListenableBuilder, Reactive UI, View, Completion

🧒 The Final Piece — Making the UI Listen to Changes!

🔌

Your ViewModel Is Broadcasting — But Nobody's Watching!

In the last lesson, you built an ArticleViewModel that fetches data and calls notifyListeners() when it's ready. But there's still one missing piece: the UI doesn't know how to listen!

Your ViewModel is like a TV station broadcasting a show 📺, but your app doesn't have a TV to watch it on. ListenableBuilder is that TV — it automatically rebuilds whenever the ViewModel says "I have new data!"

🎯
Today you'll finally complete the Wikipedia Reader!
  • ✅ Learn what ListenableBuilder is
  • ✅ Build the ArticleView widget (the final MVVM layer)
  • ✅ Connect the ViewModel to the UI — data flows automatically!
  • ✅ Complete the full Wikipedia Reader app 🎉
🔄

ListenableBuilder — The "Auto-Rebuild" Widget

ListenableBuilder is a widget that takes a Listenable (like ChangeNotifier) and a builder function. Whenever the listenable calls notifyListeners(), the builder function runs again and rebuilds its widgets!

🔍 The ListenableBuilder Pattern

How ListenableBuilder works
ListenableBuilder(
    listenable: viewModel,              // ← "Listen to this!"
    builder: (context, child) {         // ← "Rebuild this when notified!"
        if (viewModel.isLoading) {
            return CircularProgressIndicator();  // Show spinner
        }
        if (viewModel.errorMessage != null) {
            return Text('Error: ${viewModel.errorMessage}');  // Show error
        }
        return Text(viewModel.summary!.titles.display);  // Show data!
    },
)
🏁

The Complete Wikipedia Reader — All Layers Connected!

By the end of this lesson, you'll have built all three MVVM layers:

🗄️

ArticleModel

Fetches random Wikipedia articles via HTTP. Handles network errors and JSON parsing.

🔔

ArticleViewModel

Holds state (summary, loading, error). Calls notifyListeners() when data changes.

👂

ListenableBuilder + View

Listens to the ViewModel. Automatically rebuilds to show loading, error, or article data.

This is professional app architecture! Clean, testable, and maintainable — the same patterns used in production apps worldwide.

1 Create the ArticleView — The UI Layer

The View is a StatelessWidget that receives a ViewModel and uses ListenableBuilder to react to state changes.

Step 1.1 The ArticleView Class

Add this code to your main.dart file, below the ArticleViewModel class:

🔍 ArticleView — Line by Line

lib/main.dart — Add below ArticleViewModel
class ArticleView extends StatelessWidget {
    const ArticleView({super.key, required this.viewModel});

    final ArticleViewModel viewModel;  // ← Takes a ViewModel!

    @override
    Widget build(BuildContext context) {
        return ListenableBuilder(           // ← The magic widget!
            listenable: viewModel,         // ← Listen to the ViewModel
            builder: (context, child) {    // ← Rebuild when notified
                // We'll fill this in next!
                return Container();
            },
        );
    }
}

What each part means:

  • extends StatelessWidget — The View itself is stateless! It doesn't need its own state because the ViewModel manages all state.
  • required this.viewModel — The View needs a ViewModel to display data from.
  • ListenableBuilder — The star of the show! It takes a listenable and a builder function.
  • listenable: viewModel — Tells the builder: "Watch this ViewModel. Whenever it calls notifyListeners(), rebuild."
  • builder: (context, child) { ... } — This function runs every time the ViewModel notifies. Whatever it returns gets displayed!

Step 1.2 Fill in the Builder — Handle All Three States

Replace the return Container(); with logic that handles loading, error, and success:

Complete builder function
@override
Widget build(BuildContext context) {
    return ListenableBuilder(
        listenable: viewModel,
        builder: (context, child) {
            // STATE 1: Loading — show a spinner
            if (viewModel.isLoading) {
                return const Center(
                    child: CircularProgressIndicator(),
                );
            }

            // STATE 2: Error — show error message with retry button
            if (viewModel.errorMessage != null) {
                return Center(
                    child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                            const Icon(Icons.error_outline, size: 48, color: Colors.red),
                            const SizedBox(height: 16),
                            Text('Error: ${viewModel.errorMessage}'),
                            const SizedBox(height: 16),
                            ElevatedButton(
                                onPressed: () { viewModel.loadRandomArticle(); },
                                child: const Text('Retry'),
                            ),
                        ],
                    ),
                );
            }

            // STATE 3: No data yet — show initial prompt
            if (viewModel.summary == null) {
                return const Center(
                    child: Text('Tap the button to load an article!'),
                );
            }

            // STATE 4: Success — show the article!
            final summary = viewModel.summary!;
            return SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                        Text(
                            summary.titles.display,
                            style: Theme.of(context).textTheme.headlineMedium,
                        ),
                        if (summary.description != null) ...[
                            const SizedBox(height: 8),
                            Text(
                                summary.description!,
                                style: Theme.of(context).textTheme.titleMedium?.copyWith(
                                    color: Colors.grey,
                                ),
                            ),
                        ],
                        const SizedBox(height: 16),
                        Text(summary.extract),
                    ],
                ),
            );
        },
    );
}

The four states your UI handles:

  1. Loading — Shows a spinning circle. Happens while fetching data.
  2. Error — Shows an error icon, the error message, and a "Retry" button that calls loadRandomArticle() again.
  3. No data yet — Shows a prompt telling the user to tap a button (we'll add the button in the next step).
  4. Success! — Shows the article title, description, and extract in a scrollable view.

2 Update MainApp — Wire Everything Together

Now update your MainApp to create the ViewModel and pass it to the ArticleView with a load button.

Step 2.1 The Complete MainApp

Replace your MainApp class with this final version:

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

    @override
    Widget build(BuildContext context) {
        final viewModel = ArticleViewModel();  // ← Create ViewModel

        return MaterialApp(
            home: Scaffold(
                appBar: AppBar(
                    title: const Text('Wikipedia Flutter'),
                    actions: [
                        IconButton(                          // ← Load button in app bar
                            icon: const Icon(Icons.refresh),
                            onPressed: () { viewModel.loadRandomArticle(); },
                        ),
                    ],
                ),
                body: ArticleView(viewModel: viewModel),   // ← The View!
            ),
        );
    }
}

What's happening:

  • final viewModel = ArticleViewModel(); — Creates ONE ViewModel instance. The entire app shares this.
  • IconButton(icon: Icon(Icons.refresh)) — A refresh button in the app bar. When tapped, it calls viewModel.loadRandomArticle().
  • ArticleView(viewModel: viewModel) — Passes the ViewModel to the View. The View listens and displays accordingly!

3 Run Your App — The Complete Wikipedia Reader!

Hot reload and test the full flow. Here's what should happen:

Step 3.1 What You Should See

  1. Initial state: "Tap the button to load an article!" (no data yet)
  2. Tap the refresh icon in the app bar
  3. Loading state: A spinning circle appears while fetching
  4. Success state: A random Wikipedia article appears with title, description, and extract!
  5. Tap refresh again: A different random article loads!
  6. If the network fails: An error message appears with a "Retry" button

Every state transition happens automatically — the ViewModel calls notifyListeners(), and ListenableBuilder rebuilds the UI. No manual setState needed!

📝 What You Learned Today

ListenableBuilder

Used ListenableBuilder to automatically rebuild UI when a ChangeNotifier calls notifyListeners().

Built ArticleView

Created the View layer with four distinct states: loading (spinner), error (with retry), no data (prompt), and success (article display).

Connected All Layers

Wired Model → ViewModel → View together. The complete MVVM architecture is now working!

Completed Wikipedia Reader!

Your app fetches real Wikipedia data, manages state professionally, and displays it beautifully. 🎉

🧠 Test Yourself!

Q1 What does ListenableBuilder do?

Q2 How many different UI states does the ArticleView handle?

📦 Project 1: Add a "Load on Startup" Feature

Project 1Beginner

Objective: Automatically load an article when the app starts

📋 Requirements:

  1. In MainApp.build, after creating the ViewModel, call viewModel.loadRandomArticle()
  2. But wait — build() can't be async! How do you call it?
  3. Use WidgetsBinding.instance.addPostFrameCallback((_) { viewModel.loadRandomArticle(); });
  4. This schedules the call after the first frame is rendered
  5. Now when the app opens, it immediately starts loading a random article!

🎯 Expected Output:

When the app launches, it automatically shows a loading spinner, then displays a random Wikipedia article without the user needing to tap anything.

📦 Project 2: Add Article Image Display

Project 2Intermediate

Objective: Display the Wikipedia article's thumbnail image

📋 Requirements:

  1. The Summary class has a thumbnail property with a source URL
  2. In the success state of ArticleView, add an Image.network widget above the title
  3. Check summary.hasImage before showing the image
  4. Use summary.thumbnail!.source as the image URL
  5. Set a fixed height (like 200) and use fit: BoxFit.cover
  6. Wrap it in a ClipRRect with rounded corners for a polished look

🎯 Expected Output:

Articles that have images now show a beautiful thumbnail at the top of the article view!

💡
Hint: Not all Wikipedia articles have images. Use if (summary.hasImage) to conditionally show the image. Use ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(...)) for rounded corners.

🚀 What's Next?

🎉 Congratulations! You've completed the "State in Flutter Apps" topic! You now know:

  • ✅ MVVM architecture (Model-View-ViewModel)
  • ✅ Making HTTP requests with async/await
  • ✅ Managing state with ChangeNotifier
  • ✅ Building reactive UI with ListenableBuilder

In the next topic, you'll dive into Flutter UI 102 — advanced UI features and adaptive layouts!

🎉

Topic Complete!

You've built a complete Wikipedia Reader app with professional MVVM architecture! You can now fetch data from the internet, manage state, and build reactive UIs — skills used by professional Flutter developers every day!

Click the button above to track your progress!