Articles

Flutter Tutorial for Beginners: Build Your First App

This Flutter tutorial will teach you how to build your first Flutter app from scratch. The Flutter app we’ll build is a habit tracker with the following features:

  • add new habits
  • mark habits as completed or incomplete
  • save your progress locally

You’ll also learn to use BLoC (Business Logic Component) for clean state management and SharedPreferences for saving data on the device. If you don’t have an inadequate knowledge of BLoC or state management, make sure to check our tutorial on BLoC.

By the end, you’ll have a working Flutter app that demonstrates real-world architecture and business logic.

  • This course will introduce learners to the Flutter framework with interactive lessons on basic app components.
    • Beginner Friendly.
      1 hour
  • Learn how to build iOS applications with Swift and SwiftUI and publish them to Apples' App Store.
    • Includes 7 Courses
    • With Certificate
    • Beginner Friendly.
      13 hours

Set up your flutter app

First, open your terminal and create a new Flutter project using this command:

flutter create habit_tracker_app

Note: Make sure to run flutter doctor before creating the project to check if everything is up to date and working.

Once the project is created, open it in VS Code or Android Studio.

Now, organize the lib folder for better structure in your Flutter app. Inside lib, create the following subfolders:

Flutter app project structure

The above structure separates logic, data models, UI, and storage, making it easier to maintain and extend.

Next, open pubspec.yaml and add these dependencies under dependencies:

flutter_bloc: ^8.1.3
shared_preferences: ^2.2.2

Then run:

flutter pub get

These packages help manage state (flutter_bloc) and store data locally (shared_preferences).

Creating the habit model for Flutter app

Before writing logic or UI, define what a habit looks like. Each habit needs a name, a completion status, and a streak count. Let’s create a new file named habit_model.dart inside the models folder and add this code:

class Habit {
final String name;
bool isCompleted;
int streak;
Habit({
required this.name,
this.isCompleted = false,
this.streak = 0,
});
Map<String, dynamic> toMap() => {
'name': name,
'isCompleted': isCompleted,
'streak': streak,
};
factory Habit.fromMap(Map<String, dynamic> map) {
return Habit(
name: map['name'],
isCompleted: map['isCompleted'],
streak: map['streak'],
);
}
}

What this code does is:

  • name stores the habit title.
  • isCompleted tracks if the habit was done for the day.
  • streak keeps track of consecutive completions.
  • toMap() and fromMap() allow saving and loading the object easily as JSON.

When the app saves data, it will convert each habit to a map, and when it loads, it will rebuild them back into Dart objects.

Implementing the habit storage in our Flutter app

Apps need to remember habits between sessions. To handle that, we’ll use SharedPreferences, which stores data locally on the device in key-value pairs.

Let’s now create a new file named habit_storage.dart inside the services folder and add this code in the file:

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/habit_model.dart';
class HabitStorage {
final SharedPreferences _prefs;
HabitStorage(this._prefs);
Future<List<Habit>> loadHabits() async {
final data = _prefs.getString('habits');
if (data == null) return [];
final decoded = jsonDecode(data) as List;
return decoded.map((e) => Habit.fromMap(e)).toList();
}
Future<void> saveHabits(List<Habit> habits) async {
final encoded = jsonEncode(habits.map((e) => e.toMap()).toList());
await _prefs.setString('habits', encoded);
}
}

This service separates data handling from the UI or logic layers. It helps us:

  • saves the list of habits as a JSON string,
  • loads it when the app starts,
  • ensures the rest of the code doesn’t need to know how the data is stored.

The HabitStorage class is a prime example of the Separation of Concerns principle. It effectively creates a clear boundary, isolating the specific how of data storage from the rest of the application.

Your user interface (widgets) and business logic (such as a BLoC or Provider) focus only on what they need: a list of habits. This class takes care of the how: BLoC manages data storage using SharedPreferences, serializes data to JSON, and utilizes the specific 'habits' key.

The main advantage of this design is maintainability. Because the app’s logic is decoupled from the storage implementation, you can change how data is stored in the future without affecting the rest of your application.

For instance, if you decide to transition from SharedPreferences to a more complex storage solution like an SQLite database or a cloud-based service like Firebase, you would only need to modify the logic inside the HabitStorage class. The rest of your application code, which simply calls loadHabits() and saveHabits(), would remain unchanged.

Furthermore, it allows us to mock this class during testing easily. We can create a fake storage class that returns a pre-defined list of habits, enabling us to write unit test for our business logic quickly and reliably, without needing to interact with the device’s actual storage.

To understand this graphically let’s see how the Saving of habits work:

How saving the habits works in flutter app

Let’s see how the Loading of habits works:

How loading the habits works in flutter app

Creating the bloc logic Flutter state management

The BLoC pattern manages the flow of data between UI and storage. It listens for events (user actions) and updates the state (data shown on screen).

Inside blocs folder, create three files:

  • habit_event.dart
  • habit_state.dart
  • habit_bloc.dart

These will define how the BLoC behaves.

We will now begin writing the BLoC logic, and our first task is to work on the event. This is how our event logic looks like which we’ll implement inside the habit_event.dart file:

part of 'habit_bloc.dart';
abstract class HabitEvent {}
class LoadHabits extends HabitEvent {}
class AddHabit extends HabitEvent {
final String name;
AddHabit(this.name);
}
class ToggleHabit extends HabitEvent {
final int index;
ToggleHabit(this.index);
}

These events represent user interactions like loading, adding, or toggling a habit. The habit_event.dart file defines all the possible inputs for our HabitBloc.

We have structured it around the abstract class HabitEvent {}, which serves as a common blueprint. By having all our specific events extend this base class, we ensure that our BLoC can recognize them in a type-safe manner.

The events themselves are simple classes. Like LoadHabits, they function as signals that carry no data. Others, such as AddHabit and ToggleHabit, are more complex as they act as packages that contain a payload.

  • AddHabit includes the name of the new habit.
  • ToggleHabit carries the index of the habit to be modified.

This structure provides our HabitBloc with all the necessary information to handle each request effectively. Next, we’ll work on the Habit State.

Our next task is to write the logic for the state which we’ll implement inside the habit_state.dart file like:

part of 'habit_bloc.dart';
class HabitState {
final List<Habit> habits;
HabitState(this.habits);
}

In this code, the state holds the current list of habits and updates when changes occur.

In the end, we’ll implement the habit_bloc.dart file:

import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/habit_model.dart';
import '../services/habit_storage.dart';
part 'habit_event.dart';
part 'habit_state.dart';
class HabitBloc extends Bloc<HabitEvent, HabitState> {
final HabitStorage storage;
HabitBloc(this.storage) : super(HabitState([])) {
on<LoadHabits>((event, emit) async {
final loadedHabits = await storage.loadHabits();
emit(HabitState(loadedHabits));
});
on<AddHabit>((event, emit) async {
final newHabit = Habit(name: event.name);
final updatedList = List<Habit>.from(state.habits)..add(newHabit);
await storage.saveHabits(updatedList);
emit(HabitState(updatedList));
});
on<ToggleHabit>((event, emit) async {
final updatedList = List<Habit>.from(state.habits);
final habit = updatedList[event.index];
updatedList[event.index] = Habit(
name: habit.name,
isCompleted: !habit.isCompleted,
streak: habit.streak,
);
await storage.saveHabits(updatedList);
emit(HabitState(updatedList));
});
}
}

What this code does is:

  • Handles user actions through events.
  • Uses immutable states (List.from) so UI updates correctly.
  • Automatically saves and loads data using HabitStorage.

When a user toggles a checkbox, a ToggleHabit event is fired, and the BLoC updates the data before rebuilding the UI.

Building the home screen

The home screen displays the habit list and allows user to add new ones. Let’s start implementing it by creating a new file home_screen.dart in the screens folder and write the following code:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/habit_bloc.dart';
import 'add_habit_dialog.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Habit Tracker")),
body: BlocBuilder<HabitBloc, HabitState>(
builder: (context, state) {
if (state.habits.isEmpty) {
return Center(
child: Text(
"No habits yet. Add one!",
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
);
}
return ListView.builder(
itemCount: state.habits.length,
itemBuilder: (context, index) {
final habit = state.habits[index];
return ListTile(
title: Text(habit.name),
trailing: Checkbox(
value: habit.isCompleted,
onChanged: (_) {
context.read<HabitBloc>().add(ToggleHabit(index));
},
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (_) => AddHabitDialog(),
),
child: Icon(Icons.add),
),
);
}
}

What we have done in this code:

  • Display habits dynamically using BlocBuilder.
  • Update instantly when habits change.
  • Show a message when no habits are added.
  • Opens a dialog when the floating action button is pressed.

After saving, open your emulator to test your Flutter app. When you run the app, this screen will display your habits and allow toggling them.

Adding the habit dialog to your Flutter app

Dialogs help collect user input neatly. Here, the dialog allows users to type and add a new habit. Create add_habit_dialog.dart file inside the screens folder and write the following code:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/habit_bloc.dart';
class AddHabitDialog extends StatefulWidget {
@override
_AddHabitDialogState createState() => _AddHabitDialogState();
}
class _AddHabitDialogState extends State<AddHabitDialog> {
final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Add Habit"),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(hintText: "Enter habit name"),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text("Cancel"),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
context.read<HabitBloc>().add(AddHabit(controller.text));
Navigator.pop(context);
}
},
child: Text("Add"),
),
],
);
}
}

Let’s see how this works:

When the users tap “Add” button in our app, a new AddHabit event is sent to the BLoC, which adds it to the list and updates the UI automatically.

Here’s how we have implemented the onPress logic in sequence for the + button:

  1. if (controller.text.isNotEmpty): Performs a quick validation to ensure the user isn’t adding an empty habit.
  2. context.read<HabitBloc>(): Finds the HabitBloc instance from the widget tree using the BuildContext.
  3. .add(AddHabit(controller.text)): Creates a new AddHabit event, packages the user’s input text inside it, and dispatches it to the BLoC.
  4. Navigator.pop(context): Closes the dialog once the event is sent.

You can test it by pressing the + button on the home screen, a dialog will appear where you can enter a new habit.

Running the Flutter app

Now let’s tie everything together in main.dart file and run our app:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'blocs/habit_bloc.dart';
import 'screens/home_screen.dart';
import 'services/habit_storage.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
final storage = HabitStorage(prefs);
runApp(
BlocProvider(
create: (_) => HabitBloc(storage)..add(LoadHabits()),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Habit Tracker',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
),
),
);
}

Let’s see what this code does:

  • Initializes Flutter and SharedPreferences.
  • Creates the BLoC and injects it into the widget tree.
  • Loads previously saved habits automatically on startup.

Now run the app in your emulator by running the following command:

flutter run

You’ll see your habit tracker ready to use add habits, mark them complete, and close/reopen the app to see persistent data.

Compiling the flutter app

Conclusion

Congratulations on completing this Flutter tutorial! You’ve successfully built your first Flutter app and learned essential Flutter app development skills, including:

  • Structuring a Flutter app with proper project organization
  • Managing state with BLoC (Business Logic Component)
  • Saving data locally using SharedPreferences
  • Building responsive UIs with Flutter widgets

This project demonstrated how even simple concepts, such as habits, can teach powerful lessons about app structure, state management, and persistence in Flutter. With this feel free to play arround the code base and create new features, happy coding!

Frequently asked questions

1. Is Flutter easy to learn?

Yes, Flutter is considered beginner-friendly, especially if you have basic programming knowledge. The hot reload feature lets you see changes instantly, making the learning process faster and more interactive.

2. What is Flutter used for?

Flutter is used for building mobile apps (Android and iOS), web applications, and desktop applications from a single codebase. It’s popular for creating high-performance, visually appealing apps quickly.

3. What language is used in Flutter?

Flutter uses the Dart programming language, developed by Google. Dart is easy to learn, especially if you know JavaScript, Java, or C++.

4. Is Flutter difficult to learn?

Flutter has a moderate learning curve. Beginners can build simple apps quickly, but mastering advanced concepts like state management takes practice. Following structured tutorials helps accelerate learning.

5. Can I learn Flutter without Dart?

No, Flutter requires Dart programming language knowledge. However, Dart is easy to pick up alongside Flutter, and you can learn both together through hands-on projects like this tutorial.

Codecademy Team

'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'

Meet the full team

Learn more on Codecademy

  • This course will introduce learners to the Flutter framework with interactive lessons on basic app components.
    • Beginner Friendly.
      1 hour
  • Learn how to build iOS applications with Swift and SwiftUI and publish them to Apples' App Store.
    • Includes 7 Courses
    • With Certificate
    • Beginner Friendly.
      13 hours
  • Learn how to use Swift and SwiftUI to build iOS applications.
    • Includes 26 Courses
    • With Certificate
    • Beginner Friendly.
      40 hours