Github Repos
In this example, we will use the package to fetch the repositories for a user. This will also show the use of an ObservableFuture
with an Observer
.
info
The full source code can be seen here
Define the observable state
The first step is to define the reactive (aka observable) state of your UI. This gives you a good grounding on what is to be expected eventually on the UI itself.
In this case, we want a simple UI where we accept the github-ID as input. We use that to fetch the user's repositories. This call will be made using the package, the results for which are stored as a List<Repository>
. The GithubStore
class so far looks like so:
import 'package:github/github.dart';
import 'package:mobx/mobx.dart';
part 'github_store.g.dart';
class GithubStore = _GithubStore with _$GithubStore;
abstract class _GithubStore with Store {
final GitHub client = GitHub();
List<Repository> repositories = [];
@observable
String user = '';
}
Notice that we are not making the repositories
into an observable field. Instead, we will make use of the ObservableFuture
to track it.
Dealing with async
The call to fetch repositories is an async operation. We do want to track and show the visual feedback while this network call is in progress. Such async-operations are normally tracked with an ObservableFuture
.
An
ObservableFuture
is a wrapper around aFuture
and gives you a way to react to thestatus
changes.
In our case, we will use one like below. The ObservableFuture
is tracking the results, which is a List<Repository>
.
abstract class _GithubStore with Store {
// ...
// We are starting with an empty future to avoid a null check
@observable
ObservableFuture<List<Repository>> fetchReposFuture = emptyResponse;
@computed
bool get hasResults =>
fetchReposFuture != emptyResponse &&
fetchReposFuture.status == FutureStatus.fulfilled;
static ObservableFuture<List<Repository>> emptyResponse =
ObservableFuture.value([]);
// ...
}
A few things to note here:
- The
fetchReposFuture
is marked as an@observable
, because it will change its value when the network call is invoked. We start with anemptyResponse
to avoid making null checks. - The
@computed
fieldhasResults
is an easy way to know if there are results available. It is normally a good practice to create such computed-fields to simplify the logic.
Adding the actions
Now that we have our reactive state ready, we can focus on defining the actions that will mutate this state. We have two operations that will be invoked from the UI:
- Setting the github-ID
- Invoking the async operation to fetch the repos
Both of these have been incorporated in the _GithubStore
class:
abstract class _GithubStore with Store {
// ...
@action
Future<List<Repository>> fetchRepos() async {
repositories = [];
final future = client.repositories.listUserRepositories(user).toList();
fetchReposFuture = ObservableFuture(future);
return repositories = await future;
}
@action
void setUser(String text) {
fetchReposFuture = emptyResponse;
user = text;
}
}
The interesting thing about fetchRepos()
is that it is an async-action. MobX is smart enough to wrap the mutations inside an action-construct. This ensures there are no stray mutations once the async operation completes.
And with that we are ready with our observable state and actions. Let's get on with the Flutter UI.
Tackle the UI
The top-level widget is a StatefulWidget
that holds the instance of the GithubStore
and assembles all of the other observer-Widgets.
class GithubExample extends StatefulWidget {
const GithubExample();
@override
GithubExampleState createState() => GithubExampleState();
}
class GithubExampleState extends State<GithubExample> {
final GithubStore store = GithubStore();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Github Repos'),
),
body: Column(
children: <Widget>[
UserInput(store),
ShowError(store),
LoadingIndicator(store),
RepositoryListView(store)
],
));
}
Most of the widgets in the Column
should be self-explanatory. The simplest of these is the LoadingIndicator
, a StatelessWidget
, that tracks when the fetch operation is in progress. The Observer
is from the package that tracks the use of observables within its builder-function and rebuilds on change. Notice we rely on the fetchReposFuture
field, an ObservableFuture
, to show the loading indicator when the status
is pending.
LoadingIndicator
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator(this.store);
final GithubStore store;
@override
Widget build(BuildContext context) => Observer(
builder: (_) => store.fetchReposFuture.status == FutureStatus.pending
? const LinearProgressIndicator()
: Container());
}
RepositoryListView
The list-view for repositories also shows the state for an empty list of repos. If there is at least one repo, it will build the ListView
and render the repositories. Notice the use of the computed field hasResults
, which simplifies the overall logic of the component.
class RepositoryListView extends StatelessWidget {
const RepositoryListView(this.store);
final GithubStore store;
@override
Widget build(BuildContext context) => Expanded(
child: Observer(
builder: (_) {
if (!store.hasResults) {
return Container();
}
if (store.repositories.isEmpty) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('We could not find any repos for user: '),
Text(
store.user,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
);
}
return ListView.builder(
itemCount: store.repositories.length,
itemBuilder: (_, int index) {
final repo = store.repositories[index];
return ListTile(
title: Row(
children: <Widget>[
Text(
repo.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(' (${repo.stargazersCount} ⭐️)'),
],
),
subtitle: Text(repo.description ?? ''),
);
});
},
),
);
}
In Action
Below you can see the example working for various use cases.
info
The full source code can be seen here