Back

Google Developer Group HK: Flutter Study Group #01

Aug 7, 2026
FlutterOpenAPI

Description: 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.

Prerequisites

Here are the prerequisites before continuing any further during the live coding session:

*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):

  1. Web browser (e.g., Google Chrome, Microsoft Edge, etc.) - Available by default in your development machine
  2. Android - Required to setup Developer Mode (guide)
  3. 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 project
  • title - Sets the title to whatever value you set if you're on web browser
  • Scaffold - 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 r in 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:

  1. Preparing local files for Flutter to use - (sample files provided here)
  2. 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 in assets/images/ folder
  • fonts - 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 flutter on 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:

To install http package, we can run this command:

flutter pub add http

If command had no errors while executing, you should be able to find http 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.