Skip to main content

GitHub Repos

In this example, we will use the pub package to fetch the repositories for a user. This will also show the use of an ObservableFuture with an Observer.

The full source code can be seen

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

pub 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 = [];


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 a Future and gives you a way to react to the status 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

ObservableFuture<List<Repository>> fetchReposFuture = emptyResponse;


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 an emptyResponse to avoid making null checks.
  • The @computed field hasResults 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 {
// ...


Future<List<Repository>> fetchRepos() async {
repositories = [];
final future = client.repositories.listUserRepositories(user).toList();
fetchReposFuture = ObservableFuture(future);

return repositories = await future;
}


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();


GithubExampleState createState() => GithubExampleState();
}

class GithubExampleState extends State<GithubExample> {
final GithubStore store = GithubStore();


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

pub 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;


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;


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.

The full source code can be seen