Creating a Flutter Pokedex
Merry Christmas to everyone, as I promised, we are creating our first Flutter application which is, as you know, a Pokedex. Initially, I thought that I should create different parts of this article for us and then provide them all day by day on Medium, but, due to my college admissions, this plan failed and I am now writing this single article which contains the procedure and code to create this whole application. So, without this b**shit talk, let’s get started with the app itself.
Introduction and Theme
We have created the application, and now is the time to make changes to the default Flutter application that we have to make it amazing. As we all know, the Pokedex has the color combination of Red & White, and we are following the same convection.
Starting with the app
To start creating an application, the first thing that we have to do is to just remove all the comments. After that, we have to create a new folder inside the lib folder called screens and this is where all our screens will be stored, here screens mean all the widgets that we can navigate to.
The next thing that we have to do is to create a new folder inside the lib folder called widgets which is the sibling of our newly created screens folder and this will contain all the reusable widgets that we are gonna create.

After all these additions, we have to go for creating a new file in the screens folder. Let’s call it home.dart as this file contains the Home widget. This widget is accessed by the App widget that is created inside main.dart.

This is for the basic setup, but we have to change the code of the Home widget afterward.
Fetching first 250 Pokemon
We have discussed in the previous article that we can fetch data, pokemon for this context from the link given below.
https://pokeapi.co/api/v2/pokemon?limit=250
The link above returns the first 250 Pokemon starting from Bulbasaur to Ho-oh. Initially, we are gonna just fetch these Pokemon and print them. This will help us to check if our API is working correctly.
For fetching data from an API we must have permission from the device and this permission can be obtained from the following statement. We have to paste the bold statement at the correct position as shown in the code below.
<manifest xmlns:android...>
...
<uses-permission android:name="android.permission.INTERNET" />
<application ...
</manifest>
Now that we have permission to access the internet from our application, we can move forward to make a network call from our application. But, for that purpose, we need to install the http package, type in the following command in the terminal to do so.
flutter pub add http

The package is now installed and this is the time to create a new folder inside the lib folder which we can use to store our model classes. This folder must contain the file called poke_model.dart with a model class called Pokemon which stores the Pokemon data to show inside the GridView. Let’s create this folder and the file containing the class too. It will look something like this.

According to our use case, this file should contain the following code. It is useful to create a Pokemon object containing parameters like id, name, and img. This class also contains two methods called fromJson and toJson to parse the JSON data from and to Pokemon object.
class Pokemon {
int id;
String name;
String img; Pokemon.fromJson(Map<String, dynamic> data)
: id = data['id'],
name = data['name'],
img = data['img']; Map<String, dynamic> toJson() => {'id': id, 'name': name, 'img': img};
}
Now, we have to create a new folder inside the lib folder named API and in this folder, we will store all our API classes. Here we are creating a file called pokeapi.dart to keep all our APIs. The code in this file should look something like this.
import 'dart:async';
import 'package:http/http.dart' as http;class PokeAPI {
static Future<http.Response> getPokemon() =>
http.get(Uri.parse("https://pokeapi.co/api/v2/pokemon?limit=250"));
}
This file contains a class called PokeAPI which has a static asynchronous method called getPokemon which returns Future<http.Response> and is a static method. The main objective of this method is to fetch all 250 Pokemons from the URL we saw earlier.
In the home.dart file, we can create a method called getPokemonFromPokeApi which is calling the static asynchronous method getPokemon to get Pokemon in the Home widget. This function looks something like this.
void getPokemonFromPokeApi() async {
PokeAPI.getPokemon().then((response) {
List<Map<String, dynamic>> data =
List.from(json.decode(response.body)['results']); setState(() {
pokemon = data.asMap().entries.map<Pokemon>((element) {
element.value['id'] = element.key + 1;
element.value['img'] = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${element.key + 1}.png";
return Pokemon.fromJson(element.value);
}).toList();
});
});
}
The value that this function is updating is of a type List<Pokemon> which is defined at the top of the state class _HomeState. And the above method is called inside initState function which must be overridden to use. The code for this purpose should look something like this.
class _HomeState extends State<Home> {
List<Pokemon> pokemon = List.empty();
@override
void initState() {
super.initState();
getPokemonFromPokeApi();
} void getPokemonFromPokeApi() async {
.....
Showing data in GridView
To show data in GridView, we must render the GridView on the screen. Among the different types of constructors of the GridView, we are going to use the GridView.count(…) constructor. But, we won’t directly render this widget in home.dart file, we will create a new file inside the widgets folder called pokemon_grid.dart exporting the PokemonGrid widget that looks like this.
import 'package:flutter/material.dart';
import 'package:pokedex/models/poke_model.dart';
import 'package:pokedex/widgets/pokemon_card.dart';class PokemonGrid extends StatefulWidget {
final List<Pokemon> pokemon;
const PokemonGrid({Key? key, required this.pokemon}) : super(key: key);@override
_PokemonGridState createState() => _PokemonGridState();
}class _PokemonGridState extends State<PokemonGrid> {
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final crossAxisCount = (width > 1000)
? 5
: (width > 700)
? 4
: (width > 450)
? 3
: 2;return GridView.count(
padding: const EdgeInsets.all(7),
crossAxisCount: crossAxisCount,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
semanticChildCount: 250,
childAspectRatio: 200 / 244,
physics: const BouncingScrollPhysics(),
children: widget.pokemon
.map(
(pokemon) => PokemonCard(
id: pokemon.id,
name: pokemon.name,
image: pokemon.img,
),
)
.toList(),
);
}
}
The above code is a basic code which is taking an array of Pokemon objects as a parameter and we are looping over it to show a widget called PokemonCard with the required data. You may be wondering about the following two lines.
final width = MediaQuery.of(context).size.width;
final crossAxisCount = (width > 1000)
? 5
: (width > 700)
? 4
: (width > 450)
? 3
: 2;
These lines are just asking for the width of your device and storing the same in the variable called width. Then it is conditionally changing the crossAxisCount of the GridView and this makes our GridView responsive for all screen sizes. The physics property BouncingScrollPhysics is allowing us to give a nice bouncing effect when the user over scrolls.
To show the data in GridView we must have a widget that we can reuse to keep our elements consistent. Thus we have to create a file called pokemon_card.dart which will contain our reusable widget. This widget will contain the image of Pokemon with its name and id. The code for this widget will look something like this.
import 'package:flutter/material.dart';
import 'package:pokedex/widgets/pokemon_card_background.dart';
import 'package:pokedex/widgets/pokemon_card_data.dart';class PokemonCard extends StatelessWidget {
final int id;
final String name;
final String image;const PokemonCard({
Key? key,
required this.id,
required this.name,
required this.image,
}) : super(key: key);BoxDecoration getContainerDecoration() => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: Colors.grey.withOpacity(0.24),
width: 1,
),
);@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(7),
decoration: getContainerDecoration(),
child: Stack(
children: [
PokemonCardBackground(id: id),
PokemonCardData(name: name, image: image),
],
),
);
}
}
The above code is just decorating the Container widget and showing two widgets named PokemonCardBackground and PokemonCardData in a Stack widget so that we can achieve a stacked effect. Let’s see these two widgets.
The widget PokemonCardBackground is created in a file called pokemon_card_background.dart, which is stored in the widgets folder. This file contains a class PokemonCardBackground which returns just a Text widget with some styles. The code for this is given below. The widget only accepts an id as a parameter and renders it in the Stack widget from PokemonCard.
import 'package:flutter/material.dart';class PokemonCardBackground extends StatelessWidget {
final int id;
const PokemonCardBackground({Key? key, required this.id}) : super(key: key);@override
Widget build(BuildContext context) {
return Text(
"$id",
style: TextStyle(
fontSize: 101,
fontWeight: FontWeight.bold,
color: Colors.grey[200],
),
);
}
}
But this is just rendering the numbers on the cards but not the Pokemon name and images so let’s see how we can show this data as well. For this purpose, we have to create another file called pokemon_card_data.dart in the widgets folder. This file contains a class PokemonCardData which returns a column containing an Image, Divider, and Text widget. That is just rendering the image and text with a separator in between them. The code looks like this.
import 'package:flutter/material.dart';class PokemonCardData extends StatelessWidget {
final String image;
final String name;
const PokemonCardData({
Key? key,
required this.name,
required this.image,
}) : super(key: key);@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Image.network(
image,
fit: BoxFit.contain,
),
),
const Divider(),
Text(
"${name[0].toUpperCase()}${name.substring(1)}",
style: const TextStyle(
fontSize: 21,
color: Colors.black87,
),
),
],
);
}
}
If you have followed all the steps I mentioned, then you will see a list containing Pokemon on the screen something like this.

Now that we are done with the basic first page, this is the time to set up our routing so that we can navigate to the details page to see the details of the Pokemon we clicked on. So, let’s go.
Adding navigation to the application
For enabling the navigation in Flutter, we must use the routes provided by the MaterialApp widget. The following code is written in the main.dart file and you just have to enter consider the bold text from the following snippet. You have to remove the line home: const Home()
and add the bolded code from the following code snippet.
import 'package:pokedex/screens/details.dart';
import 'package:pokedex/screens/home.dart';void main() {
runApp(const App());
}class App extends StatelessWidget {
const App({Key? key}) : super(key: key);@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pokedex',
theme: ThemeData(
primarySwatch: Colors.red,
),
initialRoute: "/",
routes: {
"/": (context) => const Home(),
"/details": (context) => const Details(),
},
);
}
}
Now that we have added our routes, this is time to expose them to the user so that he can navigate through the app. In our application, we want the user to be able to navigate to the details screen from the main screen, and for this, we will allow him to click on PokemonCards. This can be achieved using InkWell widget with the following code. Here we are just navigating the user when he clicks the PokemonCard and along with the navigation, we are passing the data too. Following is the code to allow users to click on PokemonCard and navigate to the details screen. The code below is from the file called pokemon_card.dart. We just have to consider the bold lines to add.
import 'package:flutter/material.dart';
import 'package:pokedex/models/pokemon_screen_data.dart';
import 'package:pokedex/widgets/pokemon_card_background.dart';
import 'package:pokedex/widgets/pokemon_card_data.dart';class PokemonCard extends StatelessWidget {
final int id;
final String name;
final String image; const PokemonCard({
Key? key,
required this.id,
required this.name,
required this.image,
}) : super(key: key); BoxDecoration getContainerDecoration() => BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: Colors.grey.withOpacity(0.24),
width: 1,
),
); @override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(7),
enableFeedback: true,
splashColor: Colors.red[50],
onTap: () => {
Navigator.pushNamed(
context,
"/details",
arguments: PokemonScreenData(id, name, image),
)
},
child: Container(
padding: const EdgeInsets.all(7),
decoration: getContainerDecoration(),
child: Stack(
children: [
PokemonCardBackground(id: id),
PokemonCardData(name: name, image: image),
],
),
),
),
);
}
}
Now, there is a problem, we are trying to navigate to the widget called Details and this is not yet created by us. We have to create a file named details.dart with the class Details as our widget and this is the screen of our app thus in the screens folder. This file contains the same data as we have already populated but in a different layout. The code is given below which will help you to get to know, how you can access data passed while navigating.
import 'package:flutter/material.dart';
import 'package:pokedex/models/pokemon_screen_data.dart';
import 'package:pokedex/widgets/detail_back_button.dart';
import 'package:pokedex/widgets/detail_data.dart';
import 'package:pokedex/widgets/detail_image.dart';
import 'package:pokedex/widgets/detail_title.dart';class Details extends StatelessWidget {
const Details({Key? key}) : super(key: key);@override
Widget build(BuildContext context) {
final arguments =
ModalRoute.of(context)!.settings.arguments as PokemonScreenData;return Scaffold(
backgroundColor: Colors.black,
body: SingleChildScrollView(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
DetailImage(image: arguments.image),
DetailTitle(id: arguments.id, name: arguments.name),
DetailData(id: arguments.id),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const DetailBackButton(),
);
}
}
In the above code, the line final arguments =
ModalRoute.of(context)!.settings.arguments as PokemonScreenData; is theone which is getting the data from the navigation parameters.
The widgets DetailImage, DetailTitle, and DetailData are populated here. For these widgets, we are creating files called detail_image.dart, detail_title.dart, and detail_data.dart in the widgets folder respectively. They are doing nothing but showing the given data in the different forms of the layout.
The detail_image.dart looks like this.
import 'package:flutter/material.dart';class DetailImage extends StatelessWidget {
final String image;
const DetailImage({Key? key, required this.image}) : super(key: key);@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(
maxHeight: 500,
),
color: Colors.black,
child: Center(
child: Stack(
children: [
Container(
height: 500,
width: 500,
decoration: const BoxDecoration(
color: Colors.white10,
shape: BoxShape.circle,
),
),
Image.network(
image,
fit: BoxFit.contain,
alignment: Alignment.center,
),
],
),
),
);
}
}
The detail_title.dart looks like this.
import 'package:flutter/material.dart';class DetailTitle extends StatelessWidget {
final int id;
final String name;
const DetailTitle({Key? key, required this.id, required this.name})
: super(key: key);@override
Widget build(BuildContext context) {
return Chip(
backgroundColor: Colors.white,
label: Text(
"${name[0].toUpperCase()}${name.substring(1)}",
style: const TextStyle(
fontSize: 24,
color: Colors.black,
),
),
avatar: CircleAvatar(
child: Text(
id.toString(),
),
),
);
}
}
The detail_data.dart looks like this.
import 'package:flutter/material.dart';class DetailData extends StatelessWidget {
final int id;
const DetailData({Key? key, required this.id}) : super(key: key);@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width,
minHeight: 500,
),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
width: 2,
color: Colors.grey,
),
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(71),
topRight: Radius.circular(71),
),
),
child: Column(
children: [],
),
);
}
}
Except these files, we also have a file called detail_back_button.dart containing a floating action button to go back to the Home widget. This file contains the following code.
import 'package:flutter/material.dart';class DetailBackButton extends StatelessWidget {
const DetailBackButton({Key? key}) : super(key: key);@override
Widget build(BuildContext context) {
return FloatingActionButton.extended(
onPressed: () => Navigator.pop(context),
tooltip: 'Share',
label: const Text(
"Back",
),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
);
}
}
After this navigation setup and the creation of a new screen, we are almost done. Our details screen looks like this now.

Don’t you think that this app is not done yet? The details screen looks blank and we have to make it look good. We will add animations in the whole app and also feed it some extra data to fill in the blank spaces. So, this was the basic application. We will continue this app in the next article. Till then you can see the code on my GitHub. The branch for it is called Part 1. Click the link below.