👂 ListenableBuilder — Connect Your Data to the Screen!
🧒 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!"
- ✅ 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
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
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:
@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:
- Loading — Shows a spinning circle. Happens while fetching data.
- Error — Shows an error icon, the error message, and a "Retry" button that calls
loadRandomArticle()again. - No data yet — Shows a prompt telling the user to tap a button (we'll add the button in the next step).
- 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:
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 callsviewModel.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
- Initial state: "Tap the button to load an article!" (no data yet)
- Tap the refresh icon in the app bar
- Loading state: A spinning circle appears while fetching
- Success state: A random Wikipedia article appears with title, description, and extract!
- Tap refresh again: A different random article loads!
- 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
Objective: Automatically load an article when the app starts
📋 Requirements:
- In
MainApp.build, after creating the ViewModel, callviewModel.loadRandomArticle() - But wait —
build()can't be async! How do you call it? - Use
WidgetsBinding.instance.addPostFrameCallback((_) { viewModel.loadRandomArticle(); }); - This schedules the call after the first frame is rendered
- 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
Objective: Display the Wikipedia article's thumbnail image
📋 Requirements:
- The
Summaryclass has athumbnailproperty with asourceURL - In the success state of ArticleView, add an Image.network widget above the title
- Check
summary.hasImagebefore showing the image - Use
summary.thumbnail!.sourceas the image URL - Set a fixed height (like 200) and use
fit: BoxFit.cover - Wrap it in a
ClipRRectwith rounded corners for a polished look
🎯 Expected Output:
Articles that have images now show a beautiful thumbnail at the top of the article view!
if (summary.hasImage) to conditionally show the image. Use ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(...)) for rounded corners.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!