Flutter Starter: Guide to building a Hacker News App

Flutter Starter: Guide to building a Hacker News App

Are you confused about how to get started with Flutter? Use Flutter Starter to create a functional Hacker News App in no time! Flutter Starter is a kit that enables coders to build production-ready apps. It has API configurations, state management, UI style guides and configurations for Flutter projects.

Firstly, use the template from Github. Proceed to the examples folder, go to the hacker-news folder and run it. Now, let us start with the API SDK. Using the rest method, insert the API links in api_constants.dart:

Map<String, String> apiConstants = {
  "hacker_news": "https://hacker-news.firebaseio.com/v0",
  "auth": "https://reqres.in/api"
};

In main.dart, we need 2 functions fetchTopIds and fecthItems. fetchTopIds will get all the top IDs for news and fetchItems will take the ID and give us the content for that news ID.

Both of them are get methods so we will call get from restapihandler as shown below:

static fetchTopId() async {
    final response = await RestApiHandlerData.getData(
        '${apiConstants["hacker_news"]}/topstories.json');
    return response;
  }

static fetchItems(int id) async {
    final response = await RestApiHandlerData.getData(
        '${apiConstants["hacker_news"]}/item/$id.json');
    return response;
  }

The API SDK is now a black box from which we will just call the methods fetchTopId and fetchItems, which will return a JSON response.

Now that we have the data, it needs to be called, parsed and stored. Proceed to the shared folder which contains blocs, models and resources.

Resources Use a provider as shown below:

class NewsApiProvider implements Source {
  Future<List<int>> fetchTopId() async {
    final response = await ApiSdk.fetchTopId();
    final ids = response;
    return ids.cast<int>();
  }

  Future<NewsItemModel> fetchItems(int id) async {
    final response = await ApiSdk.fetchItems(id);
    final parsedJson = response;
    return NewsItemModel.fromJson(parsedJson);
  }
}

Then, create a repository in which we will call the API SDK functions to get the data. This will be parsed in the models folder.

The news_item_model.dart file in the models folder contains the following code:

import 'dart:convert';

class NewsItemModel {
  final int id;
  final bool deleted;
  final String type;
  final String by;
  final int time;
  final String text;
  final bool dead;
  final int parent;
  final List<dynamic> kids;
  final String url;
  final int score;
  final String title;
  final int descendants;

  NewsItemModel.fromJson(Map<String, dynamic> parsedJson)
      : id = parsedJson['id'],
        deleted = parsedJson['deleted'] ?? false,
        type = parsedJson['type'],
        by = parsedJson['by'] ?? '',
        time = parsedJson['time'],
        text = parsedJson['text'] ?? '',
        dead = parsedJson['dead'] ?? false,
        parent = parsedJson['parent'],
        kids = parsedJson['kids'] ?? [],
        url = parsedJson['url'],
        score = parsedJson['score'],
        title = parsedJson['title'],
        descendants = parsedJson['descendants'] ?? 0;

  NewsItemModel.fromDb(Map<String, dynamic> parsedJson)
      : id = parsedJson['id'],
        deleted = parsedJson['deleted'] == 1,
        type = parsedJson['type'],
        by = parsedJson['by'],
        time = parsedJson['time'],
        text = parsedJson['text'],
        dead = parsedJson['dead'] == 1,
        parent = parsedJson['parent'],
        kids = jsonDecode(parsedJson['kids']),
        url = parsedJson['url'],
        score = parsedJson['score'],
        title = parsedJson['title'],
        descendants = parsedJson['descendants'];

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'id': id,
      'type': type,
      'by': by,
      'time': time,
      'text': text,
      'parent': parent,
      'url': url,
      'score': score,
      'title': title,
      'descendants': descendants,
      'deleted': deleted ? 1 : 0,
      'dead': dead ? 1 : 0,
      'kids': jsonEncode(kids),
    };
  }
}

To parse the JSON data, go to the repository folder and open news-repository.dart. We will call the function to parse it like this:

Future<List<int>> fetchTopId() {
    return sources[1].fetchTopId();
  }

  Future<NewsItemModel> fetchItem(int id) async {
    NewsItemModel item;
    var source;

    for (source in sources) {
      item = await source.fetchItems(id);
      if (item != null) {
        break;
      }
    }

    for (var cache in caches) {
      if (cache != source) {
        cache.addItem(item);
      }
    }

    return item;
  }

The data has been parsed using models and resources. Lastly, we will create blocs.

For this example, we used rxdart. In stories_bloc.dart, we have used the repository to access data as shown below:

_itemTransformer() {
    return ScanStreamTransformer(
      (Map<int, Future<NewsItemModel>> cache, int id, index) {
        cache[id] = _repository.fetchItem(id);
        return cache;
      },
      <int, Future<NewsItemModel>>{},
    );
  }

With help of providers and rxdart, we will send the data to the main app. In the main app, List widget needs to be wrapped with the provider widget:

body: StoriesProvider(child: NewsList()),

Now in NewsList widget, make the following changes:

class NewsList extends StatelessWidget {
  Widget build(BuildContext context) {
    final storiesBloc = StoriesProvider.of(context);
    storiesBloc.fetchTopIds();
    return topIdList(storiesBloc);
  }
}

The stories bloc is accessed by first we accessing the instance and then calling the fetch function.

Finally, use the l StreamBuilder widget to build the UI:

import 'package:app/src/widgets/news_list_tiles.dart';
import 'package:app/src/widgets/refresh.dart';
import 'package:flutter/material.dart';
import 'package:shared/main.dart';

class NewsList extends StatelessWidget {
  Widget build(BuildContext context) {
    final storiesBloc = StoriesProvider.of(context);
    storiesBloc.fetchTopIds();
    return topIdList(storiesBloc);
  }

  Widget topIdList(StoriesBloc storiesBloc) {
    return StreamBuilder(
      stream: storiesBloc.topId,
      builder: (BuildContext context, AsyncSnapshot<List<int>> snapShot) {
        if (!snapShot.hasData) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }

        return Refresh(
          child: ListView.builder(
            itemCount: snapShot.data.length,
            itemBuilder: (BuildContext context, int index) {
              storiesBloc.fetchItem(snapShot.data[index]);
              return NewsListTile(itemId: snapShot.data[index]);
            },
          ),
        );
      },
    );
  }
}

Your app should look like this:

Hacker_News (2).gif Congratulations! You've now created your very own Hacker News App. If you would like to learn more about Flutter Starter, please visit our official documentation.

This article was written by Sumant Raj and edited by Kavya V.

Flutter-Starter-Banner1.png