Adding a detail data fetching screen in Flutter

Chris Bongers - Jul 24 '21 - - Dev Community

In today's article, I want to show you a combination of a couple of elements to create a functional app in Flutter.

The basis of this application will be the Anime app in Flutter we built a couple of days ago.
But then we'll use the option to send data to a new screen to get a detailed episode list per anime series.
Lastly, we top it off by moving these pages to their widgets in Flutter.

Making the result look like this example below.

Adding a detail data fetching screen in Flutter

Architectural changes

Before we get started, if you do want to follow along, you should download the Anime app from GitHub.

The first code we need to change is our main application. We want this to be a routed application.

Change the AnimeApp class to reflect this.

class AnimeApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Anime app',
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(),
        '/detail': (context) => DetailPage(item: 0, title: ''),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

By doing this, we create an app that contains multiple routes. The initial route is set to our HomePage, which was our previous main application.

The detailed route is added and comes with two parameters being the item and the title. We use these to render the top bar and fetch the details for this show.

Now let's change the old anime app class to be the new home page.

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

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
    // Same as before

    @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Anime app')),
      body: Center(
          // Same
      )
   )
}
Enter fullscreen mode Exit fullscreen mode

Nothing major changed here. I just changed the class names to be more representing of the widget they render.
Also, we can return the Scaffold and not the whole material app in the body since we have that in our anime app widget now.

Retrieving data on the detail page

With the structure fixed, we can make a new widget that will be our detailed page.

class DetailPage extends StatefulWidget {
  final int item;
  final String title;
  DetailPage({Key? key, required this.item, required this.title})
      : super(key: key);

  @override
  _DetailPageState createState() => _DetailPageState();
}
Enter fullscreen mode Exit fullscreen mode

As you can see, this looks a little bit different than the home page widget, as it contains two variables that we can pass to it.

The state looks pretty similar to the homepage state, but it uses a different future.

class _DetailPageState extends State<DetailPage> {
  late Future<List<Episode>> episodes;

  @override
  void initState() {
    super.initState();
    episodes = fetchEpisodes(widget.item);
  }

  @override
  Widget build(BuildContext context) {

  }
}
Enter fullscreen mode Exit fullscreen mode

Some minor things to note here is that we use the same idea to retrieve data using a future.
However, we now pass a variable to this function that will fetch the data, the id of this series we clicked on.
We retrieve this data by using the widget.{variable} call.

Let's quickly go ahead and make the Episode class, just as we made the Show class in the previous article.

class Episode {
  final int episodeId;
  final String title;

  Episode({required this.episodeId, required this.title});

  factory Episode.fromJson(Map<String, dynamic> json) {
    return Episode(episodeId: json['episode_id'], title: json['title']);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, an episode will have an id and title, which is all we need.

The future also looks pretty similar to what we've seen before:

Future<List<Episode>> fetchEpisodes(id) async {
  final response = await http
      .get(Uri.parse('https://api.jikan.moe/v3/anime/${id}/episodes/1'));

  if (response.statusCode == 200) {
    var episodesJson = jsonDecode(response.body)['episodes'] as List;
    return episodesJson.map((episode) => Episode.fromJson(episode)).toList();
  } else {
    throw Exception('Failed to load episodes');
  }
}
Enter fullscreen mode Exit fullscreen mode

Here you can see that we merge the id of the series in the URL we are fetching.

By this point, our state can call the API and retrieve the data into the episodes variable.

Let's look at how the widget will be styled.

return Scaffold(
  appBar: AppBar(title: Text(widget.title)),
  body: Center(
      child: FutureBuilder(
    builder: (context, AsyncSnapshot<List<Episode>> snapshot) {
      if (snapshot.hasData) {
        return Center(
          child: ListView.separated(
            padding: const EdgeInsets.all(8),
            itemCount: snapshot.data!.length,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                leading: CircleAvatar(
                    child: Text('${snapshot.data![index].episodeId}')),
                title: Text(snapshot.data![index].title),
              );
            },
            separatorBuilder: (BuildContext context, int index) =>
                const Divider(),
          ),
        );
      } else if (snapshot.hasError) {
        return Center(child: Text('Something went wrong :('));
      }
      return CircularProgressIndicator();
    },
    future: episodes,
  )),
);
Enter fullscreen mode Exit fullscreen mode

Again, this code looks pretty similar to what we did on the home page. However, it renders slightly different.

Note how we use the widget.title to get the parameter title and set this as the AppBar title text.

This will also create a list view based on the future builder.
Inside the list, it will check if the future has data and return that.
If it has an error, we also display that to the end-user.
And while it's loading, we show a loader.

In the list, we render a list tile, with a circle avatar containing the number of this episode and the title of the episode. Which will look like this:

ListTile circle avatar in Flutter

To navigate to this page, we must add a tap listener to our home page items.

Inside the list tile, add the following function.

onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) =>
            DetailPage(
          item: snapshot.data![index].malId,
          title: snapshot.data![index].title,
        ),
      ),
    );
},
Enter fullscreen mode Exit fullscreen mode

Moving page widgets to their own file in Flutter

We have our functional application ready. However, our main.dart file is getting quite big.

So let's go ahead and move some data to their own files.

I've created a folder called screens inside the lib folder.
In there create these two files:

  • home.dart
  • detail.dart

Now we can move everything related to the homepage into that home.dart file.

That includes all these items: (Note I've minimized the functions)

import 'package:flutter/material.dart';
import 'package:flutter_app/screens/detail.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class HomePage extends StatefulWidget {
  // Widget code
}

class _HomePageState extends State<HomePage> {
  // State code
}

class Show {
  // Class code
}

Future<List<Show>> fetchShows() async {
  // Future code
}
Enter fullscreen mode Exit fullscreen mode

And the same can be done for the detail page. Place that into the detail.dart file.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class DetailPage extends StatefulWidget {
  // Widget code
}

class _DetailPageState extends State<DetailPage> {
  // State code
}

class Episode {
  // Class code
}

Future<List<Episode>> fetchEpisodes(id) async {
  // Future code
}
Enter fullscreen mode Exit fullscreen mode

Our main.dart file now should look like this:

import 'package:flutter/material.dart';
import 'package:flutter_app/screens/home.dart';
import 'package:flutter_app/screens/detail.dart';

void main() async {
  runApp(AnimeApp());
}

class AnimeApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Anime app',
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(),
        '/detail': (context) => DetailPage(item: 0, title: ''),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

More maintainable, right?
If you are looking for the complete code for any references, you can find the code on this GitHub repo.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .