Hacker News
In this example we will rely on the ObservableFuture to keep track of the
fetch calls to the HackerNews API. The entire state of the application will be
defined in terms of the ObservableFuture.
The complete example can be seen here
Observable State​
We will wrap our calls to the HackerNews API within a simple HNApi wrapper.
For the observable state, we will take a different perspective.
The hacker-news client essentially shows a list of items for the latest and the
top news items submitted on the
HackerNews website. Instead of defining the
observable state as two separate lists of items, we will just create two
separate ObservableFutures. The result field of an ObservableFuture will
point to the List<FeedItem>, which is the first page of the latest and top
news feed.
Thus the reactive state of this example looks like so:
import 'package:mobx/mobx.dart';
import 'package:mobx_examples/hackernews/hn_api.dart';
import 'package:url_launcher/url_launcher.dart';
part 'news_store.g.dart';
enum FeedType { latest, top }
class HackerNewsStore = _HackerNewsStore with _$HackerNewsStore;
abstract class _HackerNewsStore with Store {
final _hnApi = HNApi();
ObservableFuture<List<FeedItem>>? latestItemsFuture;
ObservableFuture<List<FeedItem>>? topItemsFuture;
Future fetchLatest() => latestItemsFuture = ObservableFuture(_hnApi.newest());
Future fetchTop() => topItemsFuture = ObservableFuture(_hnApi.top());
void loadNews(FeedType type) {
if (type == FeedType.latest && latestItemsFuture == null) {
fetchLatest();
} else if (type == FeedType.top && topItemsFuture == null) {
fetchTop();
}
}
// ignore: avoid_void_async
void openUrl(String? url) async {
if (await canLaunch(url ?? '')) {
await launch(url!);
} else {
print('Could not open $url');
}
}
}
Note that in the background we are running the build_runner to generate the
*.g.dartfile. This command is run in the folder containing the project.flutter pub run build_runner watch --delete-conflicting-outputs> ```
The two actions fetchLatest() and fetchTop() create a new instance of the
ObservableFuture, fetching the refreshed results. Note that the result field
of each stores the list: List<FeedItem>.
Observer Widgets​
The root widgets is simple a tab-container of two tabs, one of Latest and one for Top news:
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_examples/hackernews/hn_api.dart';
import 'package:mobx_examples/hackernews/news_store.dart';
class HackerNewsExample extends StatefulWidget {
const HackerNewsExample();
_HackerNewsExampleState createState() => _HackerNewsExampleState();
}
class _HackerNewsExampleState extends State<HackerNewsExample>
with SingleTickerProviderStateMixin {
final HackerNewsStore store = HackerNewsStore();
late TabController _tabController;
final _tabs = [FeedType.latest, FeedType.top];
void initState() {
_tabController = TabController(length: 2, vsync: this)
..addListener(_onTabChange);
store.loadNews(_tabs.first);
super.initState();
}
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Hacker News'),
bottom: TabBar(
controller: _tabController,
tabs: const [Tab(text: 'Newest'), Tab(text: 'Top')],
),
),
body: SafeArea(
child: TabBarView(controller: _tabController, children: [
FeedItemsView(store, FeedType.latest),
FeedItemsView(store, FeedType.top),
]),
));
void _onTabChange() {
store.loadNews(_tabs[_tabController.index]);
}
}
The most interesting Widget above is the FeedItemsView, an
Observer-Widget that tracks the load of the specific feed type.
FeedItemsView​
FeedItemsView tracks the specific observable-future depending on the
FeedType. An ObservableFuture has three distinct states:

Thus for visual feedback, it is important to show each of these three states
separately. Pending is normally shown with some loading-indicator. Fulfilled
will be shown as as ListView of FeedItems and finally Rejected is the
error state. You can see each of these in the following snippet:
class FeedItemsView extends StatelessWidget {
const FeedItemsView(this.store, this.type);
final HackerNewsStore store;
final FeedType type;
// ignore: missing_return
Widget build(BuildContext context) => Observer(builder: (_) {
final future = type == FeedType.latest
? store.latestItemsFuture
: store.topItemsFuture;
if (future == null) {
return const CircularProgressIndicator();
}
switch (future.status) {
case FutureStatus.pending:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
Text('Loading items...'),
],
);
case FutureStatus.rejected:
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Failed to load items.',
style: TextStyle(color: Colors.red),
),
ElevatedButton(
child: const Text('Tap to try again'),
onPressed: _refresh,
)
],
);
case FutureStatus.fulfilled:
final List<FeedItem> items = future.result;
return RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
leading: Text(
'${item.score}',
style: const TextStyle(fontSize: 20),
),
title: Text(
item.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text('- ${item.author}'),
onTap: () => store.openUrl(item.url),
);
}),
);
}
});
Future _refresh() =>
(type == FeedType.latest) ? store.fetchLatest() : store.fetchTop();
}
We have also added the Pull to Refresh behavior for fetching the latest
updates to the news. This can be seen with the use of RefreshIndicator.

Summary​
The HackerNews example makes good use of the ObservableFuture and uses it to
show the different states of fetching the news. Since we are not really storing
the news for anything else, we could pull if off with a just the
ObservableFuture<List<FeedItem>>. As the use cases change, we may have to
store it in a List<FeedItem>, or even an ObservableList<FeedItem>.
The complete example can be seen here