Google Developer Group HK: Flutter Study Group #01
Aug 7, 2026Description: This page is a supplementary document for Google Developer Group Hong Kong (GGDHK) Flutter Study Group session titled Flutter Basics for Web Developers: Stateless vs Stateful Widgets.
Disclaimer: At the of writing this note, I'm fairly new to Flutter development, but has been developing mobile applications with React Native - If there are any mistakes that needs pointing out, feel free to react out at nicholasb1537@gmail.com
Link to the assets used in the Flutter coding session can be accessed here. Includes
assets.zipandpresentation-slide.pdf.
Prerequisites
Here are the prerequisites before continuing any further during the live coding session:
- Flutter installed on your machine (official quickstart guide)
- Mobile device configured* (Android and iOS setup guides)
*Note that if you don't have your mobile device configured yet, you can always run your Flutter app on the web (e.g.,
chrome, etc.) - Which is what I'll be doing!
Creating Your First Flutter Project
Now that you've setup Flutter (at least), let's start by creating a new empty Flutter project with the following command:
flutter create \
--empty \
--platforms ios \
--platforms android \
--platforms web \
--org com.nbenedictcodes \
pokedex
--empty: Tells Flutter to use an empty template instead of the default.--platforms <target>: Specify what platforms to compile for (e.g.,ios,android,web, etc.)--org <reverse_dns>: Unique identifier for your Flutter project (optional) - if you own a domain, see here.
Depending on what your development platform is, you might be prompted to enter a valid development certificate - I'm using MacOS for this workshop, so I'm prompted to enter a certificate like below:
Valid development certificates available (your choice will be saved):
[1] Apple Development: nbenedict@mail-provider.com (XXXXXXXXXX)
Please select a certificate for code signing (or "q" to quit):
After creating your Flutter project, you can open pokedex/ directory (or folder) and see its contents like below*:
pokedex/
├── android/
├── ios/
├── lib/
│ └── main.dart
├── web/
├── README.md
├── analysis_options.yaml
├── pokedex.iml
├── pubspec.lock
└── pubspec.yaml
*File structure above was simplified to prevent showing too many files at once, and this file structure was created with the help of eza
Given this file structure, we only need to focus on the following places for this workshop:
lib/- Where our Dart code lives, our custom Widgets goes here.assets/- This folder doesn't exist yet. We need to create this to store our local assets.pubspec.yaml- Manage third-party dependencies, bundle local assets into your Flutter project, configure linting, etc. More options here.openapi.yml- OpenAPI specification to generate client-side code. We'll talk about this lovely tool below
Running your Flutter Project
Once you reach this section, you have several platform targets on running your Flutter project (ranked by the amount of effort needed to run the Flutter app, from top to bottom):
- Web browser (e.g., Google Chrome, Microsoft Edge, etc.) - Available by default in your development machine
- Android - Required to setup Developer Mode (guide)
- iOS devices - Required to configure a development certificate, trust developer ID, etc. (guide)
You can view available devices to run the Flutter project by running:
flutter devices
Which gives the following results:
Found 3 connected devices:
SM A3760 (mobile) • [SAMSUNG_ID] • android-arm64 • Android 16 (API 36)
macOS (desktop) • macos • darwin-arm64 • macOS 26.5.2 25F84 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.189
Found 1 wirelessly connected device:
Nicholas' iPhone 16 Pro (wireless) (mobile) • [IPHONE_ID] • ios • iOS 26.5.2 23F84
Result above depends on what devices are connected to your machine through either wired/wireless methods.
To run for a certain platform, we'd have to use whatever value that's shown on the second column (e.g., [SAMSUNG_ID], macos, chrome, [IPHONE_ID]) and pass it into -d flag when running flutter run:
flutter run -d chrome
After waiting for Flutter to start up, you should see a new instance of Chrome browser dedicated for your Flutter app.
Creating your first Widget
Now the Flutter app runs, but it looks blank and sad, what do we do next? We add widgets of course. Let's look inside lib/main.dart:
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
We can see that the file has MainApp class inherits StatelessWidget, thus making the child class have to implement build method and its return type is of Widget. Let's start editing this file with the following code, replace L10-L19 with the following snippet:
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Pokédex',
home: Scaffold(
appBar: AppBar(
title: Text('Pokédex', style: TextStyle(fontWeight: FontWeight.w700)),
backgroundColor: Colors.red[400],
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsetsGeometry.all(8),
child: Column(
spacing: 4,
crossAxisAlignment: .stretch,
children: [
Row(
children: [
Expanded(
child: SizedBox(
height: 100,
child: Card(
color: Colors.orange,
child: Center(child: Text("I'm card #1!")),
),
),
),
Expanded(
child: SizedBox(
height: 100,
child: Card(
color: Colors.purple,
child: Center(
child: Text(
"I'm card #2!",
style: TextStyle(color: Colors.white),
),
),
),
),
),
],
),
SizedBox(
height: 100,
child: Card(
color: Colors.blue,
child: Center(
child: Text(
"I'm card #3!",
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
),
),
),
);
}
Whoa! What a big change just to render 3 widgets! Let me try to break it down:
debugShowCheckedModeBanner: false- Removes the Debug banner on the top-right corner of our projecttitle- Sets the title to whatever value you set if you're on web browserScaffold- A top-level container of each screen, exposes easy-to-use properties (e.g.,appBar,body,floatingActionButton, etc.).AppBar- Allows developers to place title of the app on top of the screen and navigation bar at the bottom (docs)SingleChildScrollView- Allows screen to be scrollable when there's too many items in a screen (docs)Column- Whatever children widgets placed in here will go vertically, one after another (docs)Row- Whatever children widgets placed in here will go horizontally, one after another (docs)
There are way more widgets that's used when developing Flutter apps, so do refer to the widget collection by Flutter.
Once the Flutter app is reloaded, we should be able to see the latest changes which looks better than a Hello World! text.
My usual editor doesn't support automatic Hot Reload, so I'd have to keep pressing
rin my terminal :)
Bundling Local Assets into the Project
So our screen doesn't look that empty right now, but what about bundling some assets that needs to be available when there's no network (e.g., fonts, local images, etc.)? we'd have to 2 things:
- Preparing local files for Flutter to use - (sample files provided here)
- Tell Flutter that we have some assets that needs to be bundled together by updating
pubspec.yaml
So after downloading the zip file from the link above, make sure to unzip and move assets/ folder into the project directory. Now we just need to add the following lines to pubspec.yaml#L19:
assets:
- assets/images/
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/poppins/regular.ttf
- asset: assets/fonts/poppins/bold.ttf
weight: 800
- family: Comic Neue
fonts:
- asset: assets/fonts/comic-neue/regular.ttf
- asset: assets/fonts/comic-neue/bold.ttf
weight: 800
assets:- Tells Flutter that we are including all files inassets/images/folderfonts- Tells Flutter of font families that it can use and any variations (e.g., font weight, italic, etc.)
Note the indentation! These sub-items must be placed under
flutteron L18 of the file
Once pubspec.yaml is updated, make sure to stop and re-run the Flutter development server since these assets aren't loaded yet. After that, we will add a new line under lib/main.dart#L14 with the following line:
theme: ThemeData(fontFamily: 'Comic Neue'),
After that, replace L19 within the same file where we pass title property with this:
title: Row(
spacing: 5,
mainAxisAlignment: .center,
children: [
Image(
image: AssetImage('images/pokeball.png'),
width: 25,
height: 25,
),
Text('Pokédex', style: TextStyle(fontWeight: FontWeight.w700)),
],
),
Now the app bar shows a pokeball with Pokédex next to it!
Fetching Data over Network and Creating Structured Data
Now that we some sort of an understanding on how we compose UI using widgets in Flutter, we should start reading up on fetching data over the internet. We'll be using http package to make network requests.
Alongside that, here are the following data sources that we're using for this workshop:
- PokéAPI - Open-source Pokémon data
- Pokémon sprites - Provided by the same entity, but in GitHub
To install http package, we can run this command:
flutter pub add http
Aside from that, install intl too since we'll need that for OpenAPI section later.
flutter pub add intl
If these commands had no errors while executing, you should be able to find bot http and intl inside pubspec.yaml
Once that's done, let's create a Pokemon data model where it holds name and url properties - let's name the file lib/models/pokemon_species.dart, with this content:
class PokemonSpecies {
final String name;
final String url;
const PokemonSpecies({required this.name, required this.url});
factory PokemonSpecies.fromJson(dynamic json) {
return switch (json) {
{'name': String name, 'url': String url} => PokemonSpecies(
name: name,
url: url,
),
_ => throw const FormatException('Failed to load Pokémon.'),
};
}
}
After that, create a file located under lib/components/pokemon_species_catalog.dart, with the following content:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:pokedex/models/pokemon_species.dart';
class PokemonSpeciesCatalog extends StatefulWidget {
const PokemonSpeciesCatalog({super.key});
State<PokemonSpeciesCatalog> createState() => _PokemonCatalog();
}
class _PokemonCatalog extends State<PokemonSpeciesCatalog> {
late Future<List<PokemonSpecies>> futurePokemonSpeciesList;
Future<List<PokemonSpecies>> _fetchPokemonSpeciesList() async {
final response = await http.get(
Uri.parse('https://pokeapi.co/api/v2/pokemon-species/?limit=25'),
headers: {'Accept': 'application/json'},
);
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return (jsonDecode(response.body)['results'] as List<dynamic>)
.map((json) => PokemonSpecies.fromJson(json))
.toList();
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load Pokémon species');
}
}
void initState() {
super.initState();
futurePokemonSpeciesList = _fetchPokemonSpeciesList();
}
Widget build(BuildContext context) {
return FutureBuilder<List<PokemonSpecies>>(
future: futurePokemonSpeciesList,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
List<PokemonSpecies> pokemonSpeciesList = snapshot.requireData;
List<Widget> listTiles = pokemonSpeciesList.map((pokemonSpecies) {
List<String> pokemonUrlParts = pokemonSpecies.url.split('/');
String pokemonSpeciesId =
pokemonUrlParts[pokemonUrlParts.length - 2];
String paddedPokemonSpeciesId = pokemonSpeciesId.padLeft(3, '0');
String pokemonSpriteUrl =
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/$pokemonSpeciesId.png';
return ListTile(
contentPadding: .symmetric(vertical: 5, horizontal: 10),
leading: Container(
padding: .all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
border: BoxBorder.all(color: Colors.black, width: 1),
),
child: CircleAvatar(
backgroundImage: NetworkImage(pokemonSpriteUrl),
),
),
trailing: Icon(Icons.chevron_right),
title: Text('Pokémon #$paddedPokemonSpeciesId'),
);
}).toList();
return Column(children: listTiles);
default:
return Text("Unhandled case");
}
},
);
}
}
Finishing up, you should import PokemonSpeciesCatalog into lib/main.dart and you'll be able to see the first 25 Pokémon entries in the Pokédex.
OpenAPI
Back above in Fetching Data section, I didn't list out all the data source since we have one more that deserves its own section, which is OpenAPI.
OpenAPI is a machine-readable specification which aims to let other developers when doing system integration work so that they know what are the endpoints that they can use. One good part about this specification is that there are many tools that can generate client-to-server interaction, documentation, and so on!
Let's open this OpenAPI specs file by API Evangelist, we can paste the contents there into Swagger Editor to try out the endpoints from PokéAPI. Now, I'd like to introduce a tool called OpenAPI Generator where we can use it to generate dart classes from PokéAPI.
Watch Out! Depending on how you install it, the
openapi-generatorbinary might be named differently in your system.
After following the steps above, we can download the OpenAPI specs file from the link above, name it as openapi.yml and put it into our project directory and run the following command:
openapi-generator generate -i openapi.yml -g dart -o lib/api/
Once you do that, you should be able to replace direct http usage and use the API instance generated by the tool by replacing _fetchPokemonSpeciesList method with the following:
Future<List<NamedAPIResource>> _fetchPokemonSpeciesList() async {
PokmonApi pApi = PokmonApi();
try {
NamedAPIResourceList? pokemonSpeciesList = await pApi.listPokemonSpecies(
// configure these two arguments to change the data returned
limit: 25,
offset: 0,
);
return pokemonSpeciesList!.results;
} catch (e) {
print('An error occurred, $e');
throw Exception(e);
}
}
If you change the arguments when getting the pokemon data, you'd need to use R in the terminal running your Flutter project to perform a hot restart since we're using a state. Difference between r and R shown below:
r- hot reload - re-renders UI without clearing out the app's stateR- hot restart - re-renders UI while clearing out the app's state (forces the app to perform a network call)
After that, we can comment out our custom model PokemonSpecies in lib/models/pokemon_species.dart to remove the errors raised by Flutter about duplicate classes inside our project's codebase if there's any.
Now at this point, I'd like to give you guys a challenge to fix the type errors thrown by flutter inside the same file - the hint is to replace PokemonSpecies with NamedAPIResource!
References
This workshop couldn't have been made without these articles, so the least I can do is to provide these links for you readers to look at!