</>
CodeLearn Pro
Start Learning Free β†’

Flutter β€” Complete Beginner Tutorial

6 modules Β· 20 examples Β· Click β–Ά Run App to see the UI preview

✦ Intermediate← Overview

πŸ’‘ How to use this tutorial

Read each explanation, study the code, then click β–Ά Run App to see the UI preview.

Module 01

Dart & Flutter Basics

Flutter uses the Dart programming language. Before building UIs, you need to understand Dart basics β€” variables, types, functions and control flow. Dart is easy to learn, especially if you know any other language. Every Flutter app starts with a main() function that calls runApp() with your root widget.

Dart Variables and Types

Dart is a strongly typed language. You can declare variables with a specific type (int, String, bool, double) or use var and let Dart figure it out. The final keyword means a variable can only be set once. const means the value is known at compile time and never changes.

main.dartDart / Flutter
void main() {
  // Explicit types
  int age = 20;
  double price = 9.99;
  String name = "Alice";
  bool isStudent = true;

  // var β€” Dart infers the type
  var city = "Cape Town";    // inferred as String
  var score = 95;            // inferred as int

  // final β€” set once, can't change
  final String country = "South Africa";

  // const β€” compile-time constant
  const double pi = 3.14159;

  // String interpolation with $
  print("Hello, $name!");
  print("Age: $age, City: $city");
  print("Price: R$price");
  print("Country: $country, Pi: $pi");
}
β–Ά Click Run App to see the UI

In Dart, String with a capital S is the type. The $ symbol inside a string lets you embed a variable directly β€” this is called string interpolation and is much cleaner than concatenating strings with +.

Dart Functions

Functions in Dart are declared with a return type, a name, and parameters. If the function body is a single expression, you can use the => arrow shorthand. Dart also supports optional parameters (in square brackets) and named parameters (in curly braces).

main.dartDart / Flutter
// Basic function with return type
String greet(String name) {
  return "Hello, $name!";
}

// Arrow shorthand β€” same thing in one line
String greetShort(String name) => "Hi, $name!";

// Function with optional parameter
String introduce(String name, [int? age]) {
  if (age != null) {
    return "$name is $age years old.";
  }
  return "Name: $name";
}

// Function with named parameters
String buildProfile({
  required String name,
  String role = "Student",
}) {
  return "$name β€” $role";
}

void main() {
  print(greet("Alice"));
  print(greetShort("Bob"));
  print(introduce("Carol", 22));
  print(introduce("Dave"));
  print(buildProfile(name: "Eve", role: "Developer"));
  print(buildProfile(name: "Frank"));
}
β–Ά Click Run App to see the UI

Named parameters (with curly braces {}) are very common in Flutter. Widget constructors use named parameters for everything β€” child: , style: , padding: , etc. That is why you will write things like Text("hello", style: TextStyle(...)).

Your First Flutter App

Every Flutter app starts with the same skeleton: import flutter/material.dart, write a main() function that calls runApp(), then create a StatelessWidget that returns a MaterialApp. The Scaffold widget gives you the standard app structure β€” AppBar at the top, body in the middle.

main.dartDart / Flutter
import 'package:flutter/material.dart';

// Entry point β€” every Flutter app starts here
void main() {
  runApp(const MyApp());
}

// Root widget β€” the app itself
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My First App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
        ),
      ),
      home: const HomePage(),
    );
  }
}

// The first screen
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Welcome to Flutter!'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      body: const Center(
        child: Text(
          'Hello, World! πŸ‘‹',
          style: TextStyle(fontSize: 28),
        ),
      ),
    );
  }
}
β–Ά Click Run App to see the UI

StatelessWidget is for UI that never changes after being built. Every class that extends StatelessWidget must override the build() method which returns a Widget. The BuildContext parameter tells Flutter where this widget sits in the widget tree.

Module 02

Core Widgets

In Flutter, everything is a widget β€” text, buttons, images, even padding and layout are widgets. Think of widgets like LEGO bricks: small individual pieces you snap together to build anything. The most important widgets to learn first are Text, Container, Image, Icon, SizedBox, and Padding. Almost every screen you build will use all of these.

Text Widget β€” Displaying Words

Text is the simplest and most used widget. Pass a string and optionally a TextStyle to control size, weight, color and more. The style parameter takes a TextStyle object where you set fontSize, fontWeight, color, and other properties.

main.dartDart / Flutter
Text(
  'Hello, Flutter!',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

// Multiple lines
Text(
  'Line one\nLine two\nLine three',
  textAlign: TextAlign.center,
  style: TextStyle(
    fontSize: 16,
    color: Colors.black87,
    height: 1.8,   // line height
  ),
)

// Overflow handling
Text(
  'This is a very long text that will be truncated...',
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
  style: TextStyle(fontSize: 14),
)

// Rich text with mixed styles
RichText(
  text: TextSpan(
    children: [
      TextSpan(text: 'Hello ',
        style: TextStyle(color: Colors.black, fontSize: 18)),
      TextSpan(text: 'Flutter',
        style: TextStyle(color: Colors.blue,
          fontWeight: FontWeight.bold, fontSize: 18)),
      TextSpan(text: '!',
        style: TextStyle(color: Colors.black, fontSize: 18)),
    ],
  ),
)
β–Ά Click Run App to see the UI

Colors.black87 means 87% opacity black β€” a slightly softer black that is easier to read on white backgrounds. Flutter has many predefined colours in the Colors class. For custom colours use Color(0xFF1A73E8) where 0xFF is full opacity and 1A73E8 is the hex code.

Container β€” The Swiss Army Widget

Container is one of the most versatile widgets. It can have a set width and height, padding (space inside), margin (space outside), a colour, a border, rounded corners, and a shadow. When you need a box that looks a specific way, Container is usually your answer.

main.dartDart / Flutter
// Basic coloured box
Container(
  width: 200,
  height: 100,
  color: Colors.blue,
  child: const Text(
    'Inside container',
    style: TextStyle(color: Colors.white),
  ),
)

// With decoration (more powerful than color alone)
Container(
  width: 300,
  height: 120,
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.symmetric(vertical: 12),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    border: Border.all(color: Colors.blue, width: 2),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 12,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  child: const Text(
    'Styled card',
    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  ),
)
β–Ά Click Run App to see the UI

You cannot use both color and decoration on the same Container β€” it will throw an error. If you need both, put the color inside the BoxDecoration: decoration: BoxDecoration(color: Colors.white, ...). This is a very common mistake for beginners.

Buttons and Icons

Flutter has three main button types: ElevatedButton (raised, for primary actions), OutlinedButton (bordered, for secondary actions), and TextButton (plain, for low-emphasis actions). Every button takes an onPressed callback and a child widget. Icon adds any Material icon.

main.dartDart / Flutter
// Primary action button
ElevatedButton(
  onPressed: () {
    print('Button tapped!');
  },
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    foregroundColor: Colors.white,
    padding: const EdgeInsets.symmetric(
      horizontal: 32, vertical: 14),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  child: const Text('Start Learning'),
)

// Secondary button
OutlinedButton(
  onPressed: () {},
  child: const Text('View All'),
)

// Icon button
IconButton(
  icon: const Icon(Icons.favorite, color: Colors.red),
  onPressed: () {},
)

// Button with icon + text
ElevatedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.play_arrow),
  label: const Text('Run Code'),
)
β–Ά Click Run App to see the UI

Setting onPressed: null disables the button and makes it grey automatically β€” Flutter handles this for you. This is useful when you want to prevent the user from clicking before they have filled in a form.

Module 03

Layouts & Styling

Flutter's layout system is based on the same ideas as Flexbox in CSS. Row arranges widgets horizontally. Column arranges them vertically. Both have mainAxisAlignment (along their main direction) and crossAxisAlignment (the perpendicular direction). For scrollable content, wrap a Column in a ListView. Stack lets you layer widgets on top of each other.

Row and Column β€” The Core Layout Widgets

Row lays children out horizontally (left to right). Column lays them out vertically (top to bottom). The mainAxisAlignment controls how children are distributed along the main axis. The crossAxisAlignment controls alignment on the perpendicular axis.

main.dartDart / Flutter
// Row β€” horizontal layout
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    const Text('Left',
      style: TextStyle(fontWeight: FontWeight.bold)),
    const Text('Center'),
    const Text('Right',
      style: TextStyle(color: Colors.blue)),
  ],
)

// Column β€” vertical layout
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    const Text('First item',
      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
    const SizedBox(height: 8),   // vertical spacer
    const Text('Second item',
      style: TextStyle(color: Colors.grey)),
    const SizedBox(height: 8),
    const Text('Third item'),
  ],
)

// Nested layout β€” profile card
Row(
  children: [
    CircleAvatar(
      radius: 28,
      backgroundColor: Colors.blue.shade100,
      child: const Text('A', style: TextStyle(fontSize: 22)),
    ),
    const SizedBox(width: 16),
    const Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Alice Smith',
          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
        Text('Flutter Developer',
          style: TextStyle(color: Colors.grey, fontSize: 13)),
      ],
    ),
  ],
)
β–Ά Click Run App to see the UI

SizedBox is the simplest way to add space between widgets. SizedBox(height: 16) adds 16 pixels of vertical space in a Column. SizedBox(width: 16) adds horizontal space in a Row. It is cleaner and more semantic than using padding for spacing.

ListView β€” Scrollable Lists

ListView is how you display a scrollable list of items. ListView.builder is the performance-optimised version β€” it only builds the items that are currently on screen. This is important when you have hundreds or thousands of items. Always use ListView.builder for dynamic lists.

main.dartDart / Flutter
// Simple ListView
ListView(
  children: const [
    ListTile(
      leading: Icon(Icons.code, color: Colors.blue),
      title: Text('Python'),
      subtitle: Text('Beginner friendly'),
      trailing: Icon(Icons.arrow_forward_ios, size: 16),
    ),
    Divider(),
    ListTile(
      leading: Icon(Icons.web, color: Colors.orange),
      title: Text('JavaScript'),
      subtitle: Text('The language of the web'),
      trailing: Icon(Icons.arrow_forward_ios, size: 16),
    ),
  ],
)

// ListView.builder β€” for long/dynamic lists
final List<String> languages = [
  'Python', 'JavaScript', 'Flutter', 'SQL', 'Rust', 'Go'
];

ListView.builder(
  itemCount: languages.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(
        child: Text('${index + 1}'),
      ),
      title: Text(languages[index]),
      onTap: () => print('Tapped ${languages[index]}'),
    );
  },
)
β–Ά Click Run App to see the UI

ListTile is a pre-built widget that gives you the standard "leading icon + title + subtitle + trailing" list item layout. It saves a lot of time. For custom list items, just use a Container or Card with a Row inside.

Module 04

Interaction & State

State is data that can change over time β€” like a counter, a typed text input, or a toggle. When state changes, Flutter rebuilds the relevant widgets automatically. StatefulWidget is used when your widget needs to store and change state. You call setState() to tell Flutter that something changed and the UI needs to update.

StatefulWidget β€” Counter App

A StatefulWidget has two classes: the widget class itself and a State class. The State class holds the data and the build method. When you want to change the UI, update the data inside a setState() call. Flutter will then call build() again with the new data.

main.dartDart / Flutter
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  // This is the state β€” it can change
  int _counter = 0;
  String _message = "Press a button!";

  void _increment() {
    setState(() {   // ← tells Flutter to rebuild
      _counter++;
      _message = "Count is now $_counter";
    });
  }

  void _decrement() {
    setState(() {
      _counter--;
      _message = "Count went down to $_counter";
    });
  }

  void _reset() {
    setState(() {
      _counter = 0;
      _message = "Counter reset!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_message,
              style: const TextStyle(color: Colors.grey)),
            const SizedBox(height: 20),
            Text('$_counter',
              style: const TextStyle(
                fontSize: 72, fontWeight: FontWeight.bold)),
            const SizedBox(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrement,
                  child: const Text('βˆ’')),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: _reset,
                  child: const Text('Reset')),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: _increment,
                  child: const Text('+')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
β–Ά Click Run App to see the UI

Always update state INSIDE the setState() callback. If you change a variable outside setState() Flutter will not know to rebuild the widget and your UI will not update. Think of setState() as the signal that says "hey Flutter, something changed, please rebuild!".

TextField β€” Getting User Input

TextField lets users type text. A TextEditingController connects the text field to your code β€” you can read the typed value with controller.text. Always dispose() the controller when the widget is removed from the screen to free memory.

main.dartDart / Flutter
class InputPage extends StatefulWidget {
  const InputPage({super.key});

  @override
  State<InputPage> createState() => _InputPageState();
}

class _InputPageState extends State<InputPage> {
  // Controller to read/write the text field value
  final TextEditingController _nameController =
      TextEditingController();
  String _greeting = '';

  @override
  void dispose() {
    _nameController.dispose();  // Free memory
    super.dispose();
  }

  void _generateGreeting() {
    setState(() {
      _greeting = "Hello, ${_nameController.text}! πŸ‘‹";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _nameController,
          decoration: const InputDecoration(
            labelText: 'Enter your name',
            hintText: 'e.g. Alice',
            border: OutlineInputBorder(),
            prefixIcon: Icon(Icons.person),
          ),
        ),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: _generateGreeting,
          child: const Text('Say Hello'),
        ),
        const SizedBox(height: 24),
        if (_greeting.isNotEmpty)
          Text(_greeting,
            style: const TextStyle(fontSize: 22)),
      ],
    );
  }
}
β–Ά Click Run App to see the UI

The InputDecoration is what makes a TextField look polished. labelText floats above the field when focused, hintText shows grey placeholder text. border: OutlineInputBorder() gives it the modern outlined look. prefixIcon adds an icon on the left side.

Module 06

Practical Projects

The best way to cement what you have learned is to build real things. This module shows you two complete mini-projects built with everything from the previous modules. Study the code as a whole β€” notice how widgets nest inside each other, how state is managed, and how navigation connects screens.

Simple Todo List App

A todo list is the classic beginner project. It combines a TextField for input, a ListView to display items, setState to update the list, and Dismissible to swipe-to-delete. Study how all the pieces connect.

main.dartDart / Flutter
class TodoApp extends StatefulWidget {
  const TodoApp({super.key});

  @override
  State<TodoApp> createState() => _TodoAppState();
}

class _TodoAppState extends State<TodoApp> {
  final List<String> _todos = [];
  final TextEditingController _controller =
      TextEditingController();

  void _addTodo() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    setState(() {
      _todos.add(text);
      _controller.clear();
    });
  }

  void _removeTodo(int index) {
    setState(() { _todos.removeAt(index); });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        actions: [
          Chip(label: Text('${_todos.length} items')),
          const SizedBox(width: 8),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Add a new task...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _addTodo(),
                  ),
                ),
                const SizedBox(width: 12),
                ElevatedButton(
                  onPressed: _addTodo,
                  child: const Text('Add'),
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                return ListTile(
                  leading: const Icon(
                    Icons.check_circle_outline,
                    color: Colors.green,
                  ),
                  title: Text(_todos[index]),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete,
                        color: Colors.red),
                    onPressed: () => _removeTodo(index),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
β–Ά Click Run App to see the UI

Expanded inside a Column or Row tells a widget to take up all remaining space. This is how the TextField stretches to fill the row next to the button, and how the ListView fills the remaining screen below the input area.

Profile Card UI

This project focuses purely on UI β€” creating a polished profile card using Container, Column, Row, CircleAvatar and styling. No state needed. This is a great exercise in nesting widgets and using BoxDecoration.

main.dartDart / Flutter
class ProfileCard extends StatelessWidget {
  const ProfileCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(20),
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(24),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.08),
            blurRadius: 20,
            offset: const Offset(0, 8),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          CircleAvatar(
            radius: 48,
            backgroundColor: Colors.blue.shade100,
            child: const Text('A',
              style: TextStyle(fontSize: 40,
                  fontWeight: FontWeight.bold,
                  color: Colors.blue)),
          ),
          const SizedBox(height: 16),
          const Text('Alice Smith',
            style: TextStyle(fontSize: 22,
                fontWeight: FontWeight.bold)),
          const SizedBox(height: 4),
          const Text('Flutter Developer',
            style: TextStyle(color: Colors.grey, fontSize: 15)),
          const SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              _StatItem(label: 'Projects', value: '24'),
              _StatItem(label: 'Stars',    value: '142'),
              _StatItem(label: 'Followers', value: '89'),
            ],
          ),
          const SizedBox(height: 20),
          ElevatedButton(
            onPressed: () {},
            style: ElevatedButton.styleFrom(
              minimumSize: const Size(double.infinity, 48)),
            child: const Text('Follow'),
          ),
        ],
      ),
    );
  }
}

class _StatItem extends StatelessWidget {
  final String label;
  final String value;
  const _StatItem({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text(value,
        style: const TextStyle(
            fontSize: 20, fontWeight: FontWeight.bold)),
      Text(label,
        style: const TextStyle(color: Colors.grey, fontSize: 12)),
    ]);
  }
}
β–Ά Click Run App to see the UI

mainAxisSize: MainAxisSize.min on a Column tells it to be as small as possible β€” just big enough to fit its children. Without this, a Column in a Container stretches to fill all available vertical space, which is usually not what you want for a card.

You finished the Flutter tutorial!

You can now build Flutter apps with Dart, use widgets for UI, manage state with setState, build layouts with Row and Column, and navigate between screens. You are ready to build real apps!