What you will build

You do not need to know how to create a Flutter page before starting. This module builds one with you, one small piece at a time. By the end, you will have a developer_profile project with:

  • an application configured with MaterialApp and ThemeData;
  • a DeveloperProfilePage in its own file;
  • a name, specialty, location, and contact button;
  • a layout that remains usable in a narrow window;
  • visible feedback when the button is pressed;
  • an automated test and a README with run instructions.

Do not paste the entire final solution at once. Run the app at every checkpoint. If something breaks, you will know which small change to inspect.

1. Flutter’s mental model

Flutter is a cross-platform UI toolkit. You write a declarative interface description in Dart, and the framework turns it into persistent layout and rendering structures. On native platforms, drawing code is compiled and Flutter’s engine produces the pixels, currently using Impeller on supported platforms.

If you come from .NET MAUI, use the comparison as a starting point, not as a literal translation:

.NET MAUIFlutter
C# and the .NET runtimeDart and the Dart runtime
XAML or C#Declarative composition in Dart
Platform controls or handlersFlutter’s widget set and engine
Bindings and observable propertiesState-driven rebuilding
ContentPageA composition commonly rooted in Scaffold

Three structures explain what happens below your code:

  1. Widget: an immutable configuration you declare, such as Text or Padding.
  2. Element: a persistent instance that connects a widget to its location in the tree. BuildContext represents that location.
  3. RenderObject: participates in layout, painting, and hit testing when the widget needs to render something.

You will work almost exclusively with widgets at first. Knowing about the other structures prevents the mistaken idea that every object is destroyed and completely redrawn after each build().

DeveloperProfileApp
└── MaterialApp
    └── DeveloperProfilePage
        └── Scaffold
            ├── AppBar
            └── SafeArea
                └── SingleChildScrollView
                    └── Column
                        ├── CircleAvatar
                        ├── Text
                        └── FilledButton

Each level solves one responsibility. Flutter favors composition: instead of configuring dozens of properties on one control, you combine focused widgets.

2. Set up and diagnose the environment

Install Flutter using the official guide for your platform. For this first exercise, web with Chrome is often the shortest path. Android also requires Android Studio, the SDK, licenses, and a device or emulator; Windows desktop requires Visual Studio’s C++ toolchain.

Open PowerShell, Terminal, or your editor’s integrated terminal and run:

flutter --version
flutter doctor -v
flutter devices
Understand the command flutter doctor -v

flutter doctor does not test your application. It diagnoses the development environment: it locates the Flutter SDK and checks known toolchains such as Android SDK, Xcode on macOS, browsers, IDEs, and devices. The -v option shows paths and versions, which helps reveal two installed SDKs or a terminal/IDE mismatch.

means success, ! needs attention, and marks failure. Interpret each result against your chosen target: missing Xcode does not block web or Android on Windows; a broken Android toolchain does block an Android build.

The command does not fix everything for you. It gives you evidence from which to decide what to install or configure.

Use this table to decide whether you can continue:

I want to run onMinimum evidence
ChromeChrome appears in flutter devices
Physical Android devicedevice appears in flutter devices and Android toolchain has no blocking error
Android emulatoremulator is running and appears in flutter devices
WindowsWindows appears in flutter devices and the Visual Studio toolchain is valid

If no device appears

  1. Confirm the target is open: start Chrome or the emulator, or connect the phone.
  2. Run flutter devices again.
  3. For physical Android, enable USB debugging and accept the authorization shown on the phone.
  4. Return to flutter doctor -v and fix only the item related to your chosen target.

Checkpoint 1: do not continue until you can point to an identifier returned by flutter devices, such as chrome, windows, or a phone ID.

3. Create and open the project

First choose a workspace folder. Do not create this project inside another Flutter project. Navigate to that folder in the terminal and run:

flutter create developer_profile
cd developer_profile
flutter devices
flutter run -d chrome

If you selected a different device, replace chrome with the ID shown by flutter devices.

What just happened? flutter create developer_profile

This command created the developer_profile directory, the Dart project, pubspec.yaml, an initial test, and integrations for the platforms enabled by your SDK. It also resolved the initial dependencies.

The name uses snake_case because it also becomes the Dart package name. In a real product, options such as organization and platforms should be decided before generation; see flutter create --help.

What should you expect? flutter run -d chrome

Flutter resolves dependencies, compiles in development mode, starts the app in Chrome, and keeps the terminal attached to Dart VM Service. The first run usually takes longer.

Keep that terminal open. During the session, r requests Hot Reload, R performs Hot Restart, and q quits.

You should see the counter app created by the template. That screen proves that the environment, project, and device are connected.

Now open the developer_profile folder, not a single file:

  • VS Code: choose File > Open Folder and select developer_profile; or run code . if that command is configured.
  • Android Studio: choose Open and select developer_profile.

Checkpoint 2: the default project is open in the editor and running on the selected device.

4. Recognize the project before editing

You will find a structure similar to this:

developer_profile/
├── android/              Android integration
├── ios/                  iOS integration
├── web/                  web application bootstrap
├── windows/              Windows integration
├── lib/
│   └── main.dart         current application entry point
├── test/
│   └── widget_test.dart  test created by the template
├── pubspec.yaml          metadata, SDK, dependencies, and assets
└── README.md             instructions for other people

Platform folders are not where your Flutter interface lives. In this exercise, you edit only:

  • lib/main.dart;
  • lib/developer_profile_page.dart, which you will create;
  • test/widget_test.dart;
  • README.md.

Three points in main.dart form the startup chain:

  • main() is the Dart process entry point;
  • runApp() gives the root widget to the framework;
  • build() describes the interface for the current configuration and state.

The build() method can run many times. It should be fast and free of side effects: it describes widgets; it is not the place to write files, call an API, or modify a database.

5. Checkpoint 3 — create the application shell

Open lib/main.dart, delete the template content, and paste:

import 'package:flutter/material.dart';

import 'developer_profile_page.dart';

void main() {
  runApp(const DeveloperProfileApp());
}

class DeveloperProfileApp extends StatelessWidget {
  const DeveloperProfileApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Developer profile',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const DeveloperProfilePage(),
    );
  }
}

The editor will report an error for developer_profile_page.dart and DeveloperProfilePage. That is expected: the imported file does not exist yet.

Create lib/developer_profile_page.dart. Check the name carefully: lowercase letters, words separated by _, and the .dart extension. Add the first version of the page:

import 'package:flutter/material.dart';

class DeveloperProfilePage extends StatelessWidget {
  const DeveloperProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter & Dart course'),
      ),
      body: const Center(
        child: Text('My first page'),
      ),
    );
  }
}

Save both files. If the app is still running, press r in the terminal. If you quit it, run:

flutter run -d chrome

You should see a top bar that says “Flutter & Dart course” and “My first page” centered below it.

What each widget solved

CodeResponsibility
MaterialAppconfigures navigation, theme, and Material application services
ThemeDatacentralizes shared visual decisions
DeveloperProfilePagerepresents the page you are building
Scaffoldprovides the basic visual structure of a Material screen
AppBaroccupies the top area of the Scaffold
Centerprovides looser constraints and centers its child
Textdescribes visible text

Checkpoint 3: the minimal page compiles and appears. If it does not, do not add the profile yet.

6. Checkpoint 4 — build a responsive profile

Replace only the build() method in DeveloperProfilePage with:

@override
Widget build(BuildContext context) {
  final textTheme = Theme.of(context).textTheme;
  final colors = Theme.of(context).colorScheme;

  return Scaffold(
    appBar: AppBar(
      title: const Text('Flutter & Dart course'),
    ),
    body: SafeArea(
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 520),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                CircleAvatar(
                  radius: 48,
                  backgroundColor: colors.primaryContainer,
                  foregroundColor: colors.onPrimaryContainer,
                  child: const Icon(Icons.person_outline, size: 48),
                ),
                const SizedBox(height: 24),
                Text(
                  'Jordan Smith',
                  textAlign: TextAlign.center,
                  style: textTheme.headlineMedium,
                ),
                const SizedBox(height: 8),
                Text(
                  '.NET developer learning Flutter',
                  textAlign: TextAlign.center,
                  style: textTheme.titleMedium,
                ),
                const SizedBox(height: 8),
                Text(
                  'Austin, United States',
                  textAlign: TextAlign.center,
                  style: textTheme.bodyLarge,
                ),
                const SizedBox(height: 32),
                FilledButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.mail_outline),
                  label: const Text('Get in touch'),
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  );
}

Save and use Hot Reload. The page should show an avatar, name, specialty, location, and button.

The layout includes deliberate safeguards against a beginner’s first overflow:

  • SafeArea avoids system-reserved areas;
  • SingleChildScrollView allows vertical scrolling when height is limited;
  • Padding keeps content away from the edges;
  • ConstrainedBox(maxWidth: 520) prevents an excessively wide column;
  • Column arranges children vertically;
  • crossAxisAlignment: stretch lets the button use the available width.

Remember Flutter’s layout rule: constraints go down, sizes go up, and parents set positions. A child cannot freely pick any size; it must respect the limits it receives.

Why isn’t every line const?

Text('Jordan Smith') receives a style resolved at runtime through Theme.of(context), so that instance cannot be const. SizedBox(height: 24) and Icon(Icons.person_outline) depend only on constant values and can be const.

Checkpoint 4: resize the window until it is narrow. You should not see the yellow-and-black overflow stripe. With very little height, the page should scroll.

7. Checkpoint 5 — turn the button into an action

A button with onPressed: () {} is enabled but does nothing. Replace that callback with:

onPressed: () {
  ScaffoldMessenger.of(context)
    ..hideCurrentSnackBar()
    ..showSnackBar(
      const SnackBar(
        content: Text("Thanks! We'll be in touch soon."),
      ),
    );
},

The callback is the function executed after the click. It is explicit: the code does not invoke the action during build(); it gives the button a function to call later.

The context finds the ScaffoldMessenger available at that location in the tree. The cascade operator .. performs two operations on the same object: it hides an earlier message and shows the new one.

Press “Get in touch.” The message should appear at the bottom of the page.

If onPressed were null, Flutter would display a disabled button. onPressed: () {} enables it without implementing useful behavior.

Checkpoint 5: every click produces visible feedback and no terminal error.

8. Complete page code

After all checkpoints, lib/developer_profile_page.dart should contain:

import 'package:flutter/material.dart';

class DeveloperProfilePage extends StatelessWidget {
  const DeveloperProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final colors = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter & Dart course'),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 520),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  CircleAvatar(
                    radius: 48,
                    backgroundColor: colors.primaryContainer,
                    foregroundColor: colors.onPrimaryContainer,
                    child: const Icon(Icons.person_outline, size: 48),
                  ),
                  const SizedBox(height: 24),
                  Text(
                    'Jordan Smith',
                    textAlign: TextAlign.center,
                    style: textTheme.headlineMedium,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '.NET developer learning Flutter',
                    textAlign: TextAlign.center,
                    style: textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Austin, United States',
                    textAlign: TextAlign.center,
                    style: textTheme.bodyLarge,
                  ),
                  const SizedBox(height: 32),
                  FilledButton.icon(
                    onPressed: () {
                      ScaffoldMessenger.of(context)
                        ..hideCurrentSnackBar()
                        ..showSnackBar(
                          const SnackBar(
                            content: Text("Thanks! We'll be in touch soon."),
                          ),
                        );
                    },
                    icon: const Icon(Icons.mail_outline),
                    label: const Text('Get in touch'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Compare your file with this version only after completing the checkpoints. If the only differences are spaces or line breaks, the formatter will handle them.

9. Try Hot Reload, Hot Restart, and full restart

With the app running:

  1. Replace Jordan Smith with your name, save, and press r. The text should change without losing the session.
  2. Change the seedColor in main.dart from Colors.indigo to Colors.teal and use Hot Reload.
  3. If a change made in an initializer does not appear as expected, press R for Hot Restart.
ActionWhat it preservesWhen to use it
Hot Reload (r)attempts to preserve existing Dart statechanges to widget and method implementations
Hot Restart (R)restarts Dart code and loses in-memory stateinitializers and state that must return to the beginning
Full restartalso restarts the native applicationplugins, permissions, and platform integration

Hot Reload speeds up feedback but does not prove that the app starts correctly from scratch. Before completing the module, quit with q and run the project again.

10. Format and analyze before testing

Open a second terminal at the developer_profile root and run:

dart format lib test
flutter analyze
How to interpret it flutter analyze

The analyzer checks the project against the rules in analysis_options.yaml. It catches invalid references, type problems, and lint violations without running every application path.

No issues found! is the expected result. Read the first error, fix it, and run the command again; one missing parenthesis can create several cascading errors.

11. Write the first guided test

The template test still looks for the counter you deleted. Open test/widget_test.dart, replace everything, and confirm that the package in the import is named developer_profile:

import 'package:developer_profile/main.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('shows the profile and confirms contact', (tester) async {
    await tester.pumpWidget(const DeveloperProfileApp());

    expect(find.text('Jordan Smith'), findsOneWidget);
    expect(find.text('Get in touch'), findsOneWidget);

    await tester.tap(find.text('Get in touch'));
    await tester.pump();

    expect(
      find.text("Thanks! We'll be in touch soon."),
      findsOneWidget,
    );
  });
}

Run:

flutter test

The test mounts the app in a controlled environment, confirms two labels, simulates a tap, and verifies the feedback. It does not replace visual testing in a narrow window; each validation answers a different question.

Checkpoint 6: formatting, analysis, and tests all finish successfully.

12. Document how to run it

Open README.md and record at least:

# Developer Profile

First interface in the Flutter & Dart course.

## Validated environment

- Flutter: paste the output of `flutter --version`
- Target used: Chrome, Android, or Windows

## Run

```bash
flutter pub get
flutter devices
flutter run -d chrome
```

## Validate

```bash
dart format --output=none --set-exit-if-changed lib test
flutter analyze
flutter test
```

Replace chrome with the target you actually used. A README is not decoration: it lets someone else reproduce your environment and validation.

Common errors and how to reason about them

SymptomLikely causeNext action
Target of URI doesn't existdeveloper_profile_page.dart name or path differs from the importcheck the file, extension, and underscores
The name 'DeveloperProfilePage' isn't a classclass is missing or has a different namecompare import, class name, and capitalization
several Expected to find errorsa parenthesis, bracket, or comma is missing before the reported lineinspect the immediately preceding block first
yellow-and-black stripea RenderFlex exceeded available constraintscheck SingleChildScrollView, text, and width
gray, inactive buttononPressed is nullprovide a callback
a change does not appearHot Reload preserved an earlier initializationtry Hot Restart and understand why
No devices foundtarget is not started or authorizedreturn to flutter devices and flutter doctor -v
test still looks for 0 or a + buttonthe original counter test remainsreplace test/widget_test.dart

Avoid “fixing” a problem by copying another complete project. Read the first message, identify the file, and reduce the issue to the last checkpoint that worked.

Independent practice — now personalize it

Only after the guided version works:

  1. Replace the name, specialty, and city with your information.
  2. Choose another seed color in ThemeData; do not scatter fixed colors through the page.
  3. Add a second professional detail with an icon.
  4. Update the test to look for your name.
  5. Run formatting, analysis, and tests again.

Completion criteria

  • A clean checkout starts with flutter pub get and flutter run -d <deviceId>.
  • The page has an AppBar, professional details, and a callback with visible feedback.
  • A narrow window does not overflow and scrolls when needed.
  • The theme lives in ThemeData; the page reads Theme.of(context).
  • dart format, flutter analyze, and flutter test all succeed.
  • The README records the version, target, run command, and validation commands.
  • You can explain every widget you added instead of only reproducing the code.

Check your understanding

  1. What is the difference between a Widget, Element, and RenderObject?
  2. Why shouldn’t build() call an API or modify a database?
  3. What does SingleChildScrollView solve, and what doesn’t it solve?
  4. Why does a Text style obtained from Theme.of(context) prevent that widget from being const?
  5. When is Hot Reload not enough?
  6. What does the automated test prove, and what still needs visual inspection?
Review the expected answers
  1. A Widget is immutable configuration; an Element maintains location and identity in the tree; a RenderObject participates in layout and painting.
  2. build() can run many times and should only describe the interface without side effects.
  3. It allows scrolling when height is limited, but it does not fix every invalid constraint or make every layout automatically responsive.
  4. The style depends on context and the theme available at runtime.
  5. When Dart state, main(), or native integration must be reinitialized; use Hot Restart or a full restart as appropriate.
  6. The test proves the defined content, interaction, and feedback; it does not prove readability, lack of overflow at every size, or visual quality.

Next: Module 2 explains the language behind this code—types, null safety, collections, Futures, Streams, and domain modeling—before the application grows more complex.

Official resources for further study