Flutter Starter: Guide to building a Hacker News App
Tags: opensource, flutter, dart
Published: 2020-12-22 10:40:44

Are you confused about how to get started with [Flutter](https://flutter.dev/)? Use [Flutter Starter](https://flutter-starter.github.io/?utm_source=Flutter%20Starter&utm_medium=Blog&utm_campaign=Tutorial%20article) 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](https://github.com/GeekyAnts/flutter-starter?utm_source=Flutter%20Starter&utm_medium=Blog&utm_campaign=Tutorial%20article). 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*:

```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:
```dart
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:
```dart
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:
```dart
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:
```dart
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](https://pub.dev/packages/rxdart).
In *stories_bloc.dart*, we have used the *repository* to access data as shown below:
```dart
_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:
```dart
body: StoriesProvider(child: NewsList()),
```

Now in NewsList widget, make the following changes:
```dart
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:
```dart
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1608558960454/VXuzmRulF.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](https://flutter-starter.github.io/?utm_source=Flutter%20Starter&utm_medium=Blog&utm_campaign=Tutorial%20article).

This article was written by [Sumant Raj](https://twitter.com/sumantraj2611?lang=en) and edited by Kavya V.

[![Flutter-Starter-Banner1.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1608558992808/XISs-MptU.png)](https://flutter-starter.github.io/?utm_source=Flutter%20Starter&utm_medium=Blog&utm_campaign=Tutorial%20article)