How to buildOctober 11, 20269 min read
Flutter + DartStep 2 of 6

Flutter and Dart: your first app

In short

A Flutter app is a tree of Dart widgets: runApp receives the root widget, MaterialApp provides theming and navigation, and a screen is composed from Scaffold, Column, Row, Expanded and other layout widgets. Widgets without mutable data extend StatelessWidget, while data that changes with user actions lives in the State of a StatefulWidget and is updated with setState. The Dart you need to start is null safety (String?), classes with named parameters and async/await.

TutorialLevel: BeginnerFlutter & DartPractice: ~1.5–3 h

After this you will be able to

  • write a Dart class with final fields, named parameters and a nullable String? field, plus an async function with Future and await
  • create a project with flutter create and check it with flutter analyze and flutter test
  • compose a screen from Scaffold, Card, Column, Row, Expanded and ListView without overflow on a narrow screen
  • choose between StatelessWidget and StatefulWidget and update the screen with setState

Verified with

Flutter 3.47.7 (stable)Dart 3.13.5flutter_lints 6.0.0

Checked against docs: October 11, 2026

Contents
  1. 01What we will build and what you need
  2. 02Dart in 15 minutes: types, null safety, classes, async
  3. 03Everything is a widget: the widget tree and build
  4. 04Layout: Column, Row, Padding, Expanded, ListView
  5. 05StatefulWidget and setState: the booking button
  6. 06Hot reload, tests and checking the result
  7. 07Common mistakes and exercises

What we will build and what you need

Flutter is Google’s UI framework: one codebase builds for Android, iOS, the web and desktop. You write Flutter apps in Dart. In this lesson we build a service card screen — the same one as in the Expo lesson, so the two approaches are easy to compare: a title, a description, a duration, a “Premium” badge and a “Book now” button that changes after a tap.

The lesson is for people who already understand functions and variables in at least one language and know how a mobile app differs from a website. All code was checked on Flutter 3.47.7 and Dart 3.13.5 (the current stable release at the time of checking): flutter analyze reports no issues, two widget tests pass and flutter build web succeeds. The code was not run on a physical phone — the verification log says so.

  1. Install Flutter

    The official quick start uses VS Code (or another Code OSS-based editor): install Git and the Flutter extension, then run Flutter: New Project and choose Download SDK — the editor downloads the SDK and offers to add it to PATH.

  2. Check the environment

    Run flutter doctor. A warning about a missing Android Studio can be ignored if you start by running the app in a browser.

  3. Create a project

    flutter create service_card creates a project with the starter counter, platform folders, pubspec.yaml (dependencies) and the flutter_lints linter already configured.

  4. Run it

    The docs suggest running your first app in Chrome: in VS Code use Flutter: Select Device, then F5; from a terminal use flutter run -d chrome.

bash
flutter doctor
flutter create service_card
cd service_card
flutter run -d chrome
Terminal commands. Project names use lowercase_with_underscores.

Dart in 15 minutes: types, null safety, classes, async

Dart is statically typed with type inference: var year = 1977 gets the type int from its value. A variable that is never reassigned is declared with final, and a compile-time constant with const. Four topics matter most for Flutter: null safety, classes with named parameters, enums and asynchrony.

Null safety means a variable cannot be null by default. If a value may be missing, you add ? to the type: String?. The Dart docs stress that since Dart 3 (May 2023) null safety is mandatory and sound — the analyzer will not let you access a member of a possibly-null value until you check it. After a check such as if (city == null) return …, Dart promotes the type to String on its own.

lib/models/service.dart
/// Service level: an enum is a fixed set of values.
enum ServiceLevel { standard, premium }

class Service {
  const Service({
    required this.title,
    required this.durationMinutes,
    this.description,
    this.level = ServiceLevel.standard,
  });

  final String title;
  final int durationMinutes;
  final String? description; // may be absent — type String?
  final ServiceLevel level;

  // A getter is a computed property, called without parentheses.
  String get durationLabel => '$durationMinutes min';

  // ?? supplies a fallback value when the left side is null.
  String get descriptionOrPlaceholder =>
      description ?? 'Description coming soon';

  bool get isPremium => level == ServiceLevel.premium;
}

/// Simulates a server request: returns the service list after 300 ms.
Future<List<Service>> fetchServices() async {
  await Future<void>.delayed(const Duration(milliseconds: 300));
  return const [
    Service(
      title: 'Facial cleansing',
      durationMinutes: 60,
      description: 'Ultrasonic cleansing and a mask for your skin type.',
      level: ServiceLevel.premium,
    ),
    Service(title: 'Back massage', durationMinutes: 45),
  ];
}
The service model. The file does not import Flutter — it is plain Dart and can be run and tested on its own.

final / const

What it does
Immutable field / compile-time value
In the example
all Service fields, const Service(…)

{required this.title}

What it does
Required named constructor parameter
In the example
Service(title: …, durationMinutes: …)

String?

What it does
A type that allows null
In the example
description

??

What it does
Fallback value instead of null
In the example
descriptionOrPlaceholder

get

What it does
Computed property
In the example
durationLabel, isPremium

enum

What it does
Fixed set of values
In the example
ServiceLevel

Future<T> + async/await

What it does
A result that arrives later
In the example
fetchServices()

A Future is the promise of a result from an asynchronous operation: a server request, a file read, a timer. Inside a function marked async you can await the value without blocking the program; errors are caught with an ordinary try/catch. Below is a console script that uses the model. You run it with dart run — no Flutter needed.

bin/basics.dart
// A console script: print is fine here, so we disable the lint rule.
// ignore_for_file: avoid_print

import 'package:service_card/models/service.dart';

// A named parameter in curly braces; String? means it can be null.
String greet(String name, {String? city}) {
  if (city == null) return 'Hello, $name!';
  // After the check above, Dart knows city is a String, not String?.
  return 'Hello, $name from ${city.toUpperCase()}!';
}

Future<void> main() async {
  print(greet('Aruzhan'));
  print(greet('Aruzhan', city: 'Almaty'));

  final services = await fetchServices(); // waits without blocking
  for (final s in services) {
    print('${s.title} — ${s.durationLabel}. ${s.descriptionOrPlaceholder}');
  }

  final premiumCount = services.where((s) => s.isPremium).length;
  print('Premium services: $premiumCount');
}
A console script in the project’s bin/ folder: functions, named parameters, type promotion after a null check and await.
terminal
$ dart run bin/basics.dart
Hello, Aruzhan!
Hello, Aruzhan from ALMATY!
Facial cleansing — 60 min. Ultrasonic cleansing and a mask for your skin type.
Back massage — 45 min. Description coming soon
Premium services: 1
Actual script output in the verification project.

Everything is a widget: the widget tree and build

The Flutter docs put the core idea this way: almost everything in Flutter is a widget, even layout models. Text, padding, a row, a card, the theme and the app itself are widgets nested inside each other. The result is a tree: every widget has a parent, and children are passed through child (one) or children (a list).

  1. 01ServiceApp
  2. 02MaterialApp
  3. 03Scaffold
  4. 04ListView
  5. 05ServiceCard
One branch of our screen’s widget tree, from the app root to the card.

A widget is a description of the UI, not the UI itself. The build method returns a fresh description whenever the framework needs to repaint part of the screen, so build must be fast and free of side effects. A widget without mutable data extends StatelessWidget: all its fields are final, and its constructor can be const so Flutter can reuse the instance.

lib/main.dart
import 'package:flutter/material.dart';

import 'service_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Service card',
      // The whole Material 3 palette is generated from one seed color.
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: const ServiceScreen(),
    );
  }
}
Entry point: runApp receives the root widget.

MaterialApp brings in Material Design: navigation, theming, typography. Material 3 is on by default in Flutter — ThemeData.useMaterial3 is true, and the API docs warn that the flag will eventually be deprecated, leaving only Material 3. ColorScheme.fromSeed generates the whole palette — primary, secondary, surface colors and the “on” colors used on top of them — from a single seed color.

Layout: Column, Row, Padding, Expanded, ListView

Column lays out children top to bottom, Row left to right. Each has a main axis (along which children flow) and a cross axis: for a Row the main axis is horizontal, for a Column it is vertical. Alignment is set with mainAxisAlignment and crossAxisAlignment. Padding adds space around a single child, SizedBox is an empty box of fixed size, and Card is a Material surface with rounded corners and elevation.

lib/service_screen.dart
import 'package:flutter/material.dart';

import 'models/service.dart';

const demoService = Service(
  title: 'Facial cleansing',
  durationMinutes: 60,
  description: 'Ultrasonic cleansing and a mask for your skin type.',
  level: ServiceLevel.premium,
);

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Service')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: const [ServiceCard(service: demoService)],
      ),
    );
  }
}

class ServiceCard extends StatelessWidget {
  const ServiceCard({super.key, required this.service});

  final Service service;

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

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                // Expanded gives the title all the free space in the row.
                Expanded(
                  child: Text(service.title, style: textTheme.titleLarge),
                ),
                if (service.isPremium) const Chip(label: Text('Premium')),
              ],
            ),
            const SizedBox(height: 8),
            Text(service.descriptionOrPlaceholder, style: textTheme.bodyMedium),
            const SizedBox(height: 16),
            Row(
              children: [
                const Icon(Icons.schedule, size: 20),
                const SizedBox(width: 4),
                // Expanded rather than Spacer: on a narrow screen the text shrinks
                // and the button stays at the right edge.
                Expanded(child: Text(service.durationLabel)),
                const BookButton(),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class BookButton extends StatefulWidget {
  const BookButton({super.key});

  @override
  State<BookButton> createState() => _BookButtonState();
}

class _BookButtonState extends State<BookButton> {
  bool _booked = false;

  @override
  Widget build(BuildContext context) {
    if (_booked) {
      return OutlinedButton.icon(
        onPressed: () => setState(() => _booked = false),
        icon: const Icon(Icons.check),
        label: const Text('Booked'),
      );
    }
    return FilledButton(
      onPressed: () => setState(() => _booked = true),
      child: const Text('Book now'),
    );
  }
}
The screen, the card and the booking button. ServiceCard is a StatelessWidget, BookButton is a StatefulWidget (covered in the next section).

Column / Row

Use it when
Children stacked vertically / in a row
Keep in mind
No scrolling; extra content overflows

Expanded

Use it when
A child of Row/Column needs the remaining space
Keep in mind
flex sets the share, default 1

Spacer

Use it when
Flexible empty space
Keep in mind
Does not shrink its siblings

Padding / SizedBox

Use it when
Space around / an empty gap
Keep in mind
Insets use EdgeInsets

ListView

Use it when
Content may not fit vertically
Keep in mind
Scrolls on its own

Scaffold

Use it when
Material screen skeleton
Keep in mind
appBar, body, floatingActionButton

When content does not fit, Flutter draws a yellow and black striped bar along the edge and reports A RenderFlex overflowed by … pixels. We hit this during verification: the first version of the bottom row had the text, a Spacer and the button, and the test with a 320-logical-pixel screen failed with A RenderFlex overflowed by 19 pixels on the right. Spacer only takes free space; when there is none, it shrinks nobody. The fix is to wrap whatever can shrink — the duration text — in Expanded. The service title is the same story: without Expanded, a long title in a Row runs off the screen.

StatefulWidget and setState: the booking button

The card does not need to remember anything: it receives a Service and draws it. The booking button, however, has to remember whether it was pressed. Such a widget extends StatefulWidget. It consists of two classes: the widget itself (immutable configuration) and a State object that outlives individual build calls and holds mutable data — in our case the _booked field.

To change data and update the screen, you change it inside setState(() { … }). The docs call this call critical: it tells the framework that the state has changed and the widget should be redrawn. If you simply assign _booked = true without setState, the value changes but the screen stays the same.

StatelessWidget

  • All fields are final; data comes in through the constructor
  • Rebuilds when the parent passes new data
  • A card, text, an icon, a screen title

StatefulWidget

  • Mutable data lives in the State object
  • Rebuilds after setState
  • A toggle button, a text field, a counter

The docs give a rule for deciding who owns the state: if it is user data (whether a checkbox is checked, a slider position), the parent should manage it; if it is purely visual (an animation), the widget itself; when in doubt, start with the parent. In a real app the “booked” flag must reach the server and other screens, so sooner or later it moves up the tree. That is the topic of the next lesson on state and navigation.

Hot reload, tests and checking the result

Hot reload injects changed code into the running app and rebuilds the widget tree while preserving state. Change seedColor or the button label, save the file — the screen updates and a pressed button stays pressed. It works only in debug mode.

Hot reload

How
Save the file in the IDE or press r in the flutter run terminal
What happens
New code, state preserved; main() and initState() are not re-run

Hot restart

How
R in the terminal or the IDE button
What happens
The app restarts, state is reset

Full restart

How
Stop and run flutter run again
What happens
Needed after native code changes (Kotlin, Swift)

Hot reload will not pick up changes to initializers of global variables and static fields, or turning an enum into a regular class — you need a hot restart. Checking the screen by eye is handy, but a widget test pins the behavior down more reliably: it runs the widget without a device, finds elements by text and taps buttons.

test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:service_card/main.dart';
import 'package:service_card/models/service.dart';
import 'package:service_card/service_screen.dart';

void main() {
  testWidgets('card shows the service, button changes state', (tester) async {
    await tester.pumpWidget(const ServiceApp());

    expect(find.text('Facial cleansing'), findsOneWidget);
    expect(find.text('60 min'), findsOneWidget);
    expect(find.text('Premium'), findsOneWidget);

    await tester.tap(find.text('Book now'));
    await tester.pump(); // rebuild after setState

    expect(find.text('Booked'), findsOneWidget);
    expect(find.text('Book now'), findsNothing);
  });

  testWidgets('a long title on a narrow screen does not overflow', (
    tester,
  ) async {
    tester.view.physicalSize = const Size(320, 640);
    tester.view.devicePixelRatio = 1.0;
    addTearDown(tester.view.reset);

    const service = Service(
      title: 'A very long service title that does not fit on one line',
      durationMinutes: 90,
      level: ServiceLevel.premium,
    );
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(body: ServiceCard(service: service)),
      ),
    );

    expect(tester.takeException(), isNull); // no overflow error
    expect(find.text('Description coming soon'), findsOneWidget);
  });
}
Two widget tests: the button behavior and no overflow on a narrow 320×640 screen.
terminal
$ flutter analyze
Analyzing service_card...
No issues found! (ran in 9.7s)

$ flutter test
00:00 +0: card shows the service, button changes state
00:00 +1: a long title on a narrow screen does not overflow
00:00 +2: All tests passed!
Actual result in the verification project (Flutter 3.47.7).
  • flutter analyze prints No issues found!
  • flutter test prints All tests passed!
  • in the app, tapping “Book now” turns the button into “Booked”
  • no yellow and black overflow bar on a narrow screen

Common mistakes and exercises

Yellow and black bar, RenderFlex overflowed

Cause
Children of a Row/Column do not fit
Fix
Expanded for the flexible child, or a ListView instead of a Column

Tapping changes nothing on screen

Cause
A field was changed without setState
Fix
Change data inside setState(() { … })

The property 'length' can't be unconditionally accessed

Cause
Accessing a value of type String?
Fix
Check for null, use ?? or ?.

Lint avoid_print

Cause
print in app code
Fix
Use debugPrint for debugging; in a console script disable the rule with an ignore_for_file comment

Change not visible after hot reload

Cause
Code in main(), initState() or a global variable initializer
Fix
Do a hot restart (R)

Next step: the lesson on state and navigation in Flutter — how to lift state up, share it between screens with ChangeNotifier and provider, and move from a list to a details screen while passing parameters.

How this was verified5 · passed1 · not runchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.