1. Introduction
What you'll build
Flutter is Google's UI toolkit for building applications for mobile, web, and desktop from a single codebase. In this codelab, you collaborate with Google Antigravity to build a responsive Flutter application that generates random, cool-sounding names:
The application generates word pairings such as "newstay", "lightstream", "mainbrake", or "graypine". The user can request the next name, favorite the current one, and review the list of favorited names on a separate page. The app adapts automatically to different screen sizes.

Your learning journey
This codelab takes you through building the Namer app step by step:
- Set up your Flutter environment - Configure the Flutter SDK and Google Antigravity for your target platform.
- Create a project - Prompt Antigravity to scaffold a new Flutter project with
english_words,provider, andmaterial_ui. - Add a button - Connect a Next button to
MyAppStateto generate new word pairs and experience Stateful Hot Reload. - Make the app prettier - Extract a custom
BigCardwidget styled with the app's MaterialTheme, add screen-reader accessibility, and center the UI. - Add functionality - Track favorited word pairs in
MyAppStateand add a toggleable Like button with a heart icon. - Add navigation rail - Convert
MyHomePageinto aStatefulWidgetwith a responsiveNavigationRaildriven byLayoutBuilder. - Add a new page - Build a scrollable
FavoritesPageusingListViewandListTileto display saved word pairs.
What you'll learn
- How to collaborate with Google Antigravity to iteratively build and customize a responsive Flutter application.
- How the basics of Flutter widgets,
StatelessWidget, andStatefulWidgetwork. - How to create and compose layouts in Flutter using
Column,Row,Center,Expanded, andListView. - How to connect user interactions (such as button presses) to app-wide state with
ChangeNotifierandprovider. - How to achieve a consistent look and feel using
Theme,ColorScheme, andTextThemefrompackage:material_ui/material_ui.dart. - How to make your app responsive to different screen sizes with
LayoutBuilder.
Prerequisites
- No prior Flutter experience is required. Basic familiarity with programming concepts (such as variables, functions, and classes) is helpful.
2. Set up your Flutter environment
Editor
This codelab uses Google Antigravity as your AI-first development environment. In each step, you prompt Antigravity to generate or evolve your app's components, review the key Flutter concepts in the generated code, and optionally explore creative variations.
Choose a development target
Flutter is a multi-platform toolkit. Your app can run on any of the following operating systems:
- iOS
- Android
- Windows
- macOS
- Linux
- web
However, it's common practice to choose a single operating system for which you primarily develop. This is your "development target"—the operating system that your app runs on during development.

For example, suppose you're using a Windows laptop to develop a Flutter app. If you choose Android as your development target, you typically attach an Android device to your Windows laptop with a USB cable (or launch an Android emulator), and your app-in-development runs on that target device. Alternatively, you can choose Windows as the development target, which means your app-in-development runs as a desktop Windows app alongside your editor.
Make your choice before continuing. Remember: you can always run your app on other operating systems later. Having a clear development target in mind makes the next step smoother.
Install Flutter
The most up-to-date instructions on how to install the Flutter SDK are always at docs.flutter.dev.
The instructions on the Flutter website cover installing the SDK itself, development target tools, and editor plugins. For this codelab, you only need to install the following software:
- Flutter SDK
- Google Antigravity with the official Dart and Flutter plugin enabled (available in the Plugin Marketplace under the Build with Google section).
- Compiler software for your chosen development target (for example, Visual Studio to target Windows or Xcode to target macOS or iOS).
Verify your setup
To verify that your environment is set up for your chosen development target, run flutter doctor:
flutter doctor
If there are issues listed in the output that impact your chosen development target, run flutter doctor -v for detailed troubleshooting steps, or reach out to the Flutter community.
In the next step, you'll create your first Flutter project.
3. Create a project
Prompt Antigravity to create the project
- Launch Google Antigravity and open an empty directory where you'd like to build your project (choose a path that doesn't require elevated privileges or contain spaces, such as a folder in your home directory or
C:\src\). - In the Antigravity agent chat panel, send the following prompt to create the Flutter project, add the required packages, and wire up the initial app scaffold:
Create a new empty Flutter project named `namer_app` for a random name generator app. Add the `english_words`, `provider`, and `material_ui` packages. Update `lib/main.dart` (importing `package:english_words/english_words.dart`, `package:material_ui/material_ui.dart`, and `package:provider/provider.dart`) to define `MyApp` (`StatelessWidget` wrapping a `MaterialApp` titled `'Namer App'` with `ColorScheme.fromSeed(seedColor: Colors.deepOrange)` inside a `ChangeNotifierProvider` for `MyAppState`), `MyAppState extends ChangeNotifier` holding `var current = WordPair.random()`, and `MyHomePage` (`StatelessWidget` watching `MyAppState` and returning a `Scaffold` with a `Column` containing `Text('A random idea:')` and `Text(appState.current.asLowerCase)`).
Understanding the generated code
Antigravity runs flutter create --empty namer_app to scaffold the project, adds english_words, provider, and material_ui to pubspec.yaml, and sets up lib/main.dart.
Application entry point and MyApp
At the top of lib/main.dart, the main() function instructs Flutter to run the root widget defined in MyApp:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
The MyApp class extends StatelessWidget. Widgets are the building blocks of every Flutter app—even the app itself is a widget. MyApp provides app-wide state using ChangeNotifierProvider, sets the app title, defines the visual ThemeData using a Material 3 seed color, and sets MyHomePage as the starting home widget.
App-wide state with ChangeNotifier
Next, MyAppState defines the data the application needs to function:
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
- Right now,
MyAppStatecontains a single variable holding the current randomWordPair(from theenglish_wordspackage). MyAppStateextendsChangeNotifier, which means it can notify other widgets whenever its data changes.- Because
MyAppwraps the widget tree in aChangeNotifierProvider, any widget in the app can subscribe toMyAppState.

Building the UI in MyHomePage
Finally, MyHomePage renders the current word pair on screen:
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
- Every widget defines a
build()method that Flutter calls automatically whenever the widget's dependencies change. MyHomePagetracks changes to the app's current state usingcontext.watch.() - Every
build()method returns a widget or a nested tree of widgets. Here, the top-level widget isScaffold, which provides a standard Material layout structure. Columnis a basic layout widget that arranges itschildrenvertically from top to bottom.
Run and test the app
Run the app on your chosen development target to verify everything builds and launches cleanly. Your app displays the initial prompt text and a single random word pair in the upper-left corner:

4. Add a button
In this step, you'll update the header text and add a Next button that generates a new random word pairing whenever pressed.
Prompt Antigravity to add the Next button
Send the following prompt in Antigravity:
In `lib/main.dart`, add a `getNext()` method to `MyAppState` that reassigns `current = WordPair.random()` and calls `notifyListeners()`. In `MyHomePage`, update the first `Text` widget to `'A random AWESOME idea:'` and add an `ElevatedButton` labeled `'Next'` at the bottom of the `Column` whose `onPressed` callback calls `appState.getNext()`.
Understanding the generated code
Mutating state and notifying listeners
In lib/main.dart, MyAppState now includes a getNext() method:
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
The getNext() method reassigns current with a fresh random WordPair and calls notifyListeners() (a method inherited from ChangeNotifier) so that any widget watching MyAppState automatically rebuilds.
Connecting ElevatedButton to MyAppState
In MyHomePage, the Column now includes an ElevatedButton under the two Text widgets:
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
Notice how Flutter code makes heavy use of trailing commas after parameter lists (such as after child: Text('Next'), and ],). Trailing commas streamline adding or reordering parameters and instruct Dart's auto-formatter to place each item on its own line. For more details, see Code formatting.
Run and test the app
Save your changes (or trigger a Hot Reload if your app is already running). Notice how the text updates immediately to A random AWESOME idea: and the Next button appears without losing your current word pair—this is Flutter's Stateful Hot Reload at work.
Click the Next button several times to verify that a new random word pair is generated on every press.
5. Make the app prettier
Right now, the centerpiece of the app—the randomly generated pair of words—is small, off-center, and plain:

In this step, you'll extract the word pair into a dedicated BigCard widget, style it with a rich Material Card and typography from your app's Theme, add screen-reader accessibility, and center the layout in the window:

Prompt Antigravity to style the word card and center the layout
Send the following prompt in Antigravity:
In `lib/main.dart`, extract the word pair display into a `BigCard` `StatelessWidget` that accepts a required `final WordPair pair`. In `BigCard.build`, look up `final theme = Theme.of(context)` and define `style` as `theme.textTheme.displayMedium!.copyWith(color: theme.colorScheme.onPrimary)`, then return a `Card` with `color: theme.colorScheme.primary` wrapping a `Padding` (`EdgeInsets.all(20)`) and a `Text(pair.asLowerCase, style: style, semanticsLabel: "${pair.first} ${pair.second}")`. Finally, update `MyHomePage` to remove the `'A random AWESOME idea:'` text, center the `Column` vertically (`mainAxisAlignment: MainAxisAlignment.center`) inside a `Center` widget, and place a `SizedBox(height: 10)` between `BigCard(pair: pair)` and the `'Next'` `ElevatedButton`.
Understanding the generated code
Extracting BigCard and Composition over Inheritance
Having separate widgets for logical parts of your UI is one of the best ways to manage complexity in Flutter. Rather than passing the entire MyAppState into BigCard, BigCard only accepts the WordPair it needs:
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
}
Styling with Theme, ColorScheme, and TextTheme
Theme.of(context)retrieves the app's currentThemeData.- Setting the card's
colortotheme.colorScheme.primarypaints it with the most prominent color derived from yourColorScheme.fromSeedinMyApp. If you change the seed color inMyApp,BigCardandElevatedButtonsmoothly interpolate to the new palette using Flutter's implicit animations:
theme.textTheme.displayMediumprovides a large typographic style reserved for short, important display text. CallingcopyWith(color: theme.colorScheme.onPrimary)returns a copy of that style using a high-contrast text color designed to be legible on theprimarybackground.
Screen-reader accessibility with semanticsLabel
Flutter exposes text and interactive elements to screen readers such as TalkBack and VoiceOver by default. However, a compound word like cheaphead might confuse a screen reader into pronouncing the ph in the middle as f.

By setting semanticsLabel: "${pair.first} ${pair.second}", Text preserves the visual simplicity of pair.asLowerCase while providing two separate words to screen readers. Learn more on Flutter's Accessibility documentation page.
Centering the UI in MyHomePage
In MyHomePage, the Column is centered both vertically and horizontally:
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
mainAxisAlignment: MainAxisAlignment.centercenters the children along theColumn's vertical (main) axis.- Because a
Columnonly takes as much horizontal width as its children need, wrapping theColumnin aCenterwidget centers it horizontally inside theScaffold. SizedBox(height: 10)adds a 10-pixel vertical gap betweenBigCardand theElevatedButton.
Run and test the app
Hot reload the app and verify that BigCard is centered on screen with the deep-orange primary background and large white text:

6. Add functionality
Whenever the user clicks Next, the previous word pair disappears. In this step, you'll add a favorites list to MyAppState and place a Like button with a heart icon next to the Next button:

Prompt Antigravity to add Favorites state and a Like button
Send the following prompt in Antigravity:
In `lib/main.dart`, add `var favorites = <WordPair>[]` and a `toggleFavorite()` method to `MyAppState` that removes `current` from `favorites` if already present or adds it otherwise, then calls `notifyListeners()`. In `MyHomePage.build`, choose `Icons.favorite` when `appState.favorites.contains(pair)` and `Icons.favorite_border` otherwise, and place an `ElevatedButton.icon` (calling `appState.toggleFavorite()`, with `icon: Icon(icon)` and `label: Text('Like')`), a `SizedBox(width: 10)`, and the `'Next'` `ElevatedButton` inside a horizontal `Row` with `mainAxisSize: MainAxisSize.min`.
Understanding the generated code
Managing favorites in MyAppState
MyAppState now tracks a list of favorited word pairs and exposes a method to toggle the current pair:
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
favoritesis initialized with, using Dart generics to ensure the list can only ever contain[] WordPairobjects.toggleFavorite()removescurrentif it's already infavoritesor adds it otherwise, then callsnotifyListeners()so the UI updates immediately.
Horizontal button layout with Row and ElevatedButton.icon
In MyHomePage.build, the code selects the appropriate heart icon based on whether pair is in appState.favorites, and places both buttons inside a Row:
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
Rowis the horizontal equivalent ofColumn. By default, aRowexpands to fill all available horizontal width and aligns its children to the left. SettingmainAxisSize: MainAxisSize.mintellsRowto shrink-wrap its children so the button pair stays centered underBigCard.ElevatedButton.icon()is a convenient constructor that pairs aniconwith a textlabel.
Run and test the app
Hot reload the app and tap Like on a few generated word pairs. Verify that the heart icon toggles between an outlined heart (Icons.favorite_border) and a solid heart (Icons.favorite):
In the next step, you'll add a navigation rail so users can switch to a dedicated screen to view their saved favorites.
7. Add navigation rail
To let users switch between the word generator and their saved favorites, you'll split MyHomePage into two widgets (MyHomePage and GeneratorPage), convert MyHomePage into a StatefulWidget that tracks the selected navigation index, and wrap the layout in a LayoutBuilder so the NavigationRail automatically expands its labels when the window is at least 600 logical pixels wide:

Prompt Antigravity to add a responsive NavigationRail
Send the following prompt in Antigravity:
In `lib/main.dart`, extract the existing home content (without its `Scaffold`) into a `GeneratorPage` `StatelessWidget`, and convert `MyHomePage` into a `StatefulWidget` whose `_MyHomePageState` tracks `var selectedIndex = 0`. In `_MyHomePageState.build`, select `Widget page` using a `switch (selectedIndex)` that assigns `GeneratorPage()` for `0`, `Placeholder()` for `1`, and throws `UnimplementedError('no widget for $selectedIndex')` by default. Return a `LayoutBuilder` wrapping a `Scaffold` whose body is a `Row` containing a `SafeArea` with a `NavigationRail` (`extended: constraints.maxWidth >= 600`, `'Home'` and `'Favorites'` destinations, `selectedIndex: selectedIndex`, and `onDestinationSelected` updating `selectedIndex` inside `setState`) followed by an `Expanded` `Container` colored with `Theme.of(context).colorScheme.primaryContainer` displaying `page`.
Understanding the generated code
StatelessWidget versus StatefulWidget
Until now, MyAppState covered all of your state needs, so every widget was a StatelessWidget. However, the currently selected tab (selectedIndex) is UI state that only matters to MyHomePage.

Converting MyHomePage into a StatefulWidget creates a companion private state class, _MyHomePageState, which extends State and manages its own mutable selectedIndex variable:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
Selecting the active page and setState
Inside _MyHomePageState.build, a switch statement assigns the active screen to page based on selectedIndex:
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
case 1:
page = Placeholder();
default:
throw UnimplementedError('no widget for $selectedIndex');
}
- Because
FavoritesPageisn't built yet,case 1usesPlaceholder(), a handy built-in widget that draws a crossed rectangle to mark unfinished UI:

- Following the fail-fast principle, the
defaultbranch throws anUnimplementedErrorifselectedIndexis neither0nor1. If you ever add a third destination to the navigation rail and forget to update theswitch, the app immediately exposes the bug during development.
Responsive layout with LayoutBuilder, SafeArea, and Expanded
The build method returns a LayoutBuilder whose callback receives the parent's BoxConstraints whenever the window size or orientation changes:
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
},
);
SafeAreaprevents theNavigationRailbuttons from being obscured by a mobile status bar or hardware notch.extended: constraints.maxWidth >= 600automatically expands the rail to show text labels next to the icons when the window is at least 600 logical pixels wide:

- When the user clicks a destination,
onDestinationSelectedupdatesselectedIndexinside asetState(() { ... })call, which tells Flutter to rebuild_MyHomePageStatewith the new tab selected. Expandedis "greedy": whileSafeAreatakes only the horizontal slice theNavigationRailneeds on the left,Expandedfills all remaining horizontal space with theprimaryContainer-coloredContainerholdingpage.
Run and test the app
Hot reload the app and test the new navigation rail:
- Click Favorites (the heart icon) in the
NavigationRailto switch to thePlaceholderscreen, then click Home to return toGeneratorPage. - Drag the edge of your application window wider and narrower across the 600-pixel threshold to watch the
NavigationRailsmoothly expand and collapse its text labels:
8. Add a new page
In this final construction step, you'll replace the Placeholder widget with a FavoritesPage that displays all the word pairs the user has liked.
Prompt Antigravity to build the FavoritesPage
Send the following prompt in Antigravity:
In `lib/main.dart`, create a `FavoritesPage` `StatelessWidget` that watches `MyAppState`. If `appState.favorites.isEmpty`, return a `Center` widget with `Text('No favorites yet.')`; otherwise return a `ListView` starting with a `Padding` (`EdgeInsets.all(20)`) showing `Text('You have ${appState.favorites.length} favorites:')`, followed by a `ListTile` (`leading: Icon(Icons.favorite)` and `title: Text(pair.asLowerCase)`) for each `pair` in `appState.favorites`. Update `_MyHomePageState` so `case 1` in the `switch (selectedIndex)` statement displays `FavoritesPage()` instead of `Placeholder()`.
Understanding the generated code
Scrollable lists with ListView, ListTile, and collection for
In lib/main.dart, FavoritesPage watches MyAppState and renders either an empty-state message or a scrollable ListView:
class FavoritesPage extends StatelessWidget {
const FavoritesPage({super.key});
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have ${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
- When
appState.favorites.isEmptyistrue, the page returns a centeredNo favorites yet.label so the user gets clear feedback. ListViewis aColumnthat automatically scrolls when its contents exceed the available screen height.- Dart supports collection
forloops directly inside list literals (children: [ ... ]), letting you generate aListTilefor everypairinappState.favoritesdeclaratively. ListTileprovides a standard Material list row with aleadingicon andtitletext.
Run and test the app
Hot reload the app and test the complete flow:
- Switch to the Favorites tab before liking any words to verify the centered
No favorites yet.message appears. - Return to Home, click Next and Like on several word pairs, and switch back to Favorites to see your saved list update automatically:
9. Congratulations
Congratulations! You collaborated with Google Antigravity to take a minimal Flutter scaffold and turn it into a responsive, Material 3 application that generates and saves random word pairs across platforms.

What you've built
- Collaborated with Google Antigravity to scaffold and iteratively evolve a Flutter application.
- Composed Flutter UI layouts using
Scaffold,Column,Row,Center,Padding,SizedBox,SafeArea,Expanded, andListView. - Managed app-wide state with
ChangeNotifierandprovider, and widget-local state withStatefulWidgetandsetState(). - Styled custom widgets with
Theme,ColorScheme, andTextThemeusingpackage:material_ui/material_ui.dart, and addedsemanticsLabelsupport for screen readers. - Built an adaptive
NavigationRaillayout usingLayoutBuilderconstraints.
Next steps
- Experiment further with the app you built in this codelab by prompting Antigravity to add new features, animations, or layouts.
- Explore the code of this advanced version of Namer App to see how you can add animated history lists, gradients, cross-fades, and more:
Learn more
- Continue your Flutter learning journey at flutter.dev/learn.
- Explore Widget catalog and State management in the official Flutter documentation.