19
Adding a detail data fetching screen in Flutter
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.
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 {
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Anime app',
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(item: 0, title: ''),
},
);
}
}
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);
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Same as before
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Anime app')),
body: Center(
// Same
)
)
}
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.
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);
_DetailPageState createState() => _DetailPageState();
}
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;
void initState() {
super.initState();
episodes = fetchEpisodes(widget.item);
}
Widget build(BuildContext context) {
}
}
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']);
}
}
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');
}
}
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,
)),
);
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:
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,
),
),
);
},
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
}
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
}
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 {
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Anime app',
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(item: 0, title: ''),
},
);
}
}
More maintainable, right?
If you are looking for the complete code for any references, you can find the code on this GitHub repo.
19