Xcoding with Alfian

Mobile development articles and tutorials

Building GitHub Flutter App — Part 1: Trending Repositories List

Alt text

In this part of building Github Flutter App series, we will build a Github application with features such as listing currently trending repositories and searching repositories by their name using Flutter SDK. In part 1, we will focus on creating the main home screen that lists all trending repositories.

What we will build:

  1. Home Widget is stateful widget that provides the ListView for the repositories
  2. GithubItem Widget is stateless widget that provides the list view item that represents a repo.
  3. API Class is a Dart object used as a networking layer for requesting repositories using the GitHub API
  4. Repo Class is a Dart object used as a model object for a repo.

Home Widget

We are using Flutter MaterialApp Widget as our main Widget and Home Widget will be the home widget for our application. It is a stateful widget that has a set of states such as:

repos: Array of Repo object that represents the repositories from the GitHub API isFetching: boolean value that represents the loading state for the request from GitHub API. We will return an Icon Widget for the view when _isFetching is true error: String value that represents the state for error when fetching the request for the GitHub API. We will return an Text Widget containing the error message if error message exists.

class Home extends StatefulWidget {
  Home({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  List<Repo> _repos = List();
  bool _isFetching = false;
  String _error;

  @override
  void initState() {
    super.initState();
    loadTrendingRepos();
  }

  void loadTrendingRepos() async {
    setState(() {
      _isFetching = true;
      _error = null;
    });

    final repos = await Api.getTrendingRepositories();
    setState(() {
      _isFetching = false;
      if (repos != null) {
        this._repos = repos;
      } else {
        _error = 'Error fetching repos';
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Container(
            margin: EdgeInsets.only(top: 4.0),
            child: Column(
              children: <Widget>[
                Text('Github Repos',
                    style: Theme
                        .of(context)
                        .textTheme
                        .headline
                        .apply(color: Colors.white)),
                Text('Trending',
                    style: Theme
                        .of(context)
                        .textTheme
                        .subhead
                        .apply(color: Colors.white))
              ],
            )),
        centerTitle: true,
        actions: <Widget>[
          IconButton(icon: Icon(Icons.search), onPressed: () {}),
        ],
      ),
      body: buildBody(context),
    );
  }

  Widget buildBody(BuildContext context) {
    if (_isFetching) {
      return Container(
          alignment: Alignment.center, child: Icon(Icons.timelapse));
    } else if (_error != null) {
      return Container(
          alignment: Alignment.center,
          child: Text(
            _error,
            style: Theme.of(context).textTheme.headline,
          ));
    } else {
      return ListView.builder(
          padding: EdgeInsets.symmetric(vertical: 8.0),
          itemCount: _repos.length,
          itemBuilder: (BuildContext context, int index) {
            return GithubItem(_repos[index]);
          });
    }
  }
}

When the initState is invoked, we call loadTrendingRepositories method to set the isFetching state to true and fetch the data from the API Class using await. After the results is received, we check if the results is not null and update the state by assigning the results to the repos property and if the results is null we update the state for the error, _isFetching state is also set to false for both conditions.

Home Widget build method will render the Widget depending on state of isFetching and error. When isFetching is true, a Container Widget with child of Icon Widget with the icon of timelapse will be rendered. Then, if error is not null then a Container Widget with child of Text Widget containing the error message will be rendered. If none of these conditions are meet, then ListView.builder will be used to build the ListView dynamically based on the _repos list length, the itemBuilder function will return the GithubItem Widget and Repo data will be passed to create the class so the data can be displayed.

GithubItem Widget

GithubItem Widget is a stateless widget that represents an Item for the ListView, the widget accepts the Repo object for the constructor to display the repository data.

class GithubItem extends StatelessWidget {
  final Repo repo;
  GithubItem(this.repo);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
          highlightColor: Colors.lightBlueAccent,
          splashColor: Colors.red,
          child: Container(
            padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
            child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text((repo.name != null) ? repo.name : '-',
                      style: Theme.of(context).textTheme.subhead),
                  Padding(
                    padding: EdgeInsets.only(top: 4.0),
                    child: Text(
                        repo.description != null
                            ? repo.description
                            : 'No desription',
                        style: Theme.of(context).textTheme.body1),
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 8.0),
                    child: Row(
                      children: <Widget>[
                        Expanded(
                            child: Text((repo.owner != null) ? repo.owner : '',
                                textAlign: TextAlign.start,
                                style: Theme.of(context).textTheme.caption)),
                        Expanded(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            crossAxisAlignment: CrossAxisAlignment.center,
                            children: <Widget>[
                              Icon(
                                Icons.star,
                                color: Colors.deepOrange,
                              ),
                              Padding(
                                padding: EdgeInsets.only(top: 4.0),
                                child: Text(
                                    (repo.watchersCount != null)
                                        ? '${repo.watchersCount} '
                                        : '0 ',
                                    textAlign: TextAlign.center,
                                    style: Theme.of(context).textTheme.caption),
                              ),
                            ],
                          ),
                        ),
                        Expanded(
                            child: Text(
                                (repo.language != null) ? repo.language : '',
                                textAlign: TextAlign.end,
                                style: Theme.of(context).textTheme.caption)),
                      ],
                    ),
                  ),
                ]),
          )),
    );
  }
}

The container of the widget is a Card Widget that renders a card view like container with elevation and box shadow. InkWell widget is providing the ripple and highlight splash effect when user taps on the card. For the data we use Column Widget to stack the Text Widget containing repo name, Text Widget containing repo description, and Row Widget container containing repo owner name, stargazer count, and repo language vertically along its main axis. Row Widget is used to stack the the child widgets horizontally.

API Class

API class is a networking layer class that provides the interface for getting latest trending repositories data from GitHub API. To build the query we will current datetime and subtract it by 7 days.

To format the date string, we use date_format from Dart Package as a dependency in pubspec. The library allow us to pass the format we want to use to format the date required by the GitHub API. We pass created:> date string as our query, then sort by stars descending, and set maximum repos of 25 per page.

import 'package:github_search/repo.dart';
import 'package:date_format/date_format.dart';

import 'dart:convert' show json, utf8;
import 'dart:io';
import 'dart:async';

class Api {
  static final HttpClient _httpClient = HttpClient();
  static final String _url = "api.github.com";

  static Future<List<Repo>> getTrendingRepositories() async {
    final lastWeek = DateTime.now().subtract(Duration(days: 7));
    final formattedDate = formatDate(lastWeek, [yyyy, '-', mm, '-', dd]);

    final uri = Uri.https(_url, '/search/repositories', {
      'q': 'created:>$formattedDate',
      'sort': 'stars',
      'order': 'desc',
      'page': '0',
      'per_page': '25'
    });

    final jsonResponse = await _getJson(uri);
    if (jsonResponse == null) {
      return null;
    }
    if (jsonResponse['errors'] != null) {
      return null;
    }
    if (jsonResponse['items'] == null) {
      return List();
    }

    return Repo.mapJSONStringToList(jsonResponse['items']);
  }

  static Future<Map<String, dynamic>> _getJson(Uri uri) async {
    try {
      final httpRequest = await _httpClient.getUrl(uri);
      final httpResponse = await httpRequest.close();
      if (httpResponse.statusCode != HttpStatus.OK) {
        return null;
      }

      final responseBody = await httpResponse.transform(utf8.decoder).join();
      return json.decode(responseBody);
    } on Exception catch (e) {
      print('$e');
      return null;
    }
  }
}

After the response has been received, we decode the response json string to Map and then construct the list of Repo object from the Map using the Repo Class static factory method, after that we return the results.

Repo Class

Repo Class is just a plain Dart object that represent the repo from GitHub Api, here we have a static factory method that parse the json Map and map it to the list of Repos.

class Repo {
  final String htmlUrl;
  final int watchersCount;
  final String language;
  final String description;
  final String name;
  final String owner;

  Repo(this.htmlUrl, this.watchersCount, this.language, this.description,
      this.name, this.owner);

  static List<Repo> mapJSONStringToList(List<dynamic> jsonList) {
    return jsonList
        .map((r) => Repo(r['url'], r['watchers_count'], r['language'],
            r['description'], r['name'], r['owner']['login']))
        .toList();
  }
}

Congrats!

We have finished building the home screen for the App that lists all the GitHub trending repositories by using Flutter SDK. To see all the code you can visit the GitHub Repository for this project. Stay tune for the part 2 of the series where we will build the search screen so the user can type to search the repositories, Happy Fluttering!.