Learn Flutter by creating your first Flutter app!

Learn Flutter by creating your first Flutter app!

In this article, I'm going to introduce you to your first Flutter app. We will explore the parts of a Flutter project, their roles, and we'll cover some fundamental concepts of state, including the differences between StatelessWidgets and StatefulWidgets.

If you haven't installed Flutter yet, here are some step-by-step videos that will guide you through the process of installing Flutter on Mac, Windows, and Linux.

🎥
Video version available on YouTube

Creating a basic Flutter app

Let's start by creating a basic application. You can do it from the menus of your favorite code editor, although I always prefer to do it from the terminal:

flutter create flutter_test_app

This command will create a basic counting app that, when executed, allows us to increment a numerical value by clicking on a button:

The goal of this sample code is to give you a first introduction to Flutter. We are going to go little by little to understand this template that is already given to us.

The pubspec.yaml Configuration File

The first thing we are going to look at is the pubspec.yaml file:

This file contains essential application metadata, enumerates all dependencies, and includes various configuration settings. If you open it, you will find comments for each section that explain their purpose. However, for clarity, we'll remove these comments to keep the file straightforward and briefly overview each segment:

name: flutter_test_app
description: "You can add a description for your project here."
publish_to: 'none'
version: 1.0.0+1

environment:
   sdk: '>=3.2.6 <4.0.0'

Let's clarify the meaning of each parameter:

  • name: This represents the project's name. It serves as an "internal" identifier and is not the name presented to your users.

  • description: This field allows you to provide a brief outline of your project's purpose.

  • publish_to: This setting is primarily relevant for package development. Since this article focuses on basic concepts, we'll leave it unchanged.

  • version: Here, you can specify your project's version using semantic versioning followed by an additional integer. This practice enables version control directly from this file, which then applies across platform-specific projects. The trailing integer typically corresponds to the versionCode in Android projects.

  • environment: This specifies the compatible Dart SDK version range for your application. If in the future you want to use new versions of the Dart SDK you might be interested in modifying it, but for now, we leave it as is.

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

The next part focuses on the application's dependencies, which are categorized into two types: those incorporated into the app's final package (dependencies) and those used during development but not included in the final app package (dev_dependencies). To grasp this distinction, let's examine the flutter_lints package as an example. This package aids in static code analysis, which is performed locally on your machine. There's no need for it to be part of the final app package distributed to users.

After adding flutter_lints to your dev_dependencies and running flutter pub get, you can analyze your code according to the lint rules specified by flutter_lints using the command flutter analyze. This command checks your code for issues based on the linting rules defined by the package.

For finding new packages, Flutter developers often visit https://pub.dev. Say you want to do a network request to a remote server using the http package, you can simply add it to your dependencies and then execute flutter pub get to fetch the package and make it ready to use.

Alternatively, you can use the command flutter pub add http to not only download but also automatically add the http package to your dependencies. To add a package to dev_dependencies, you would use flutter pub add --dev package_name.

Experimenting with these methods can help you determine the most comfortable way to manage packages in your Flutter application.

flutter:
  uses-material-design: true

Towards the end of the file, there's a section labeled "flutter," featuring the uses-material-design: true setting. This particular setting informs Flutter that our application will utilize the Material Design style, providing a suite of visual, interaction, and motion design guidelines developed by Google.

As you advance more into Flutter development, you'll encounter a range of additional configurations that can be applied within this file to further personalize themes and other aspects of your app.

Additionally, it's worth mentioning the pubspec.lock file, a crucial component of Flutter projects. This file is automatically generated by Flutter when you run commands like flutter pub get or flutter pub add. Its primary purpose is to record the exact versions of each dependency used in your project at the time these commands are executed. This ensures that your project remains consistent and stable, even if dependencies are updated in the future. By tracking these versions, the pubspec.lock file helps to prevent the "it works on my machine" problem by ensuring that every developer working on the project uses the same versions of dependencies, thus minimizing conflicts and compatibility issues.

Platform-Specific Projects in Flutter

Within a Flutter project, beyond the pubspec.yaml and pubspec.lock files, you'll notice several directories named after platforms. These directories are android, ios, macos, linux, and windows. These aren't just folders; they're complete native projects for their respective platforms.

Flutter's strength lies in its ability to provide a multi-platform development framework, hiding the complexities of platform-specific implementation details. Yet, the existence of these platform-specific projects is crucial for Flutter to operate seamlessly across different environments.

There are instances when you'll need to dip into native development within these directories. This is often the case when certain functionality can only be achieved with platform-specific code. It's in these scenarios that modifications to the native parts of a project become necessary.

It's important to note that you're not required to maintain all these directories if your app doesn't target all supported platforms. For instance, if your focus is solely on Android and iOS, you can safely remove the macos, linux, and windows directories.

Conversely, should you decide to expand your app's availability to additional platforms not initially included in your project, Flutter simplifies this expansion. Using the flutter create command with the --platforms option, you can add the necessary platform projects. For example, if you start with a project only supporting Android and iOS and later decide to include support for macOS, Linux, and Windows, you can execute flutter create --platforms=macos,linux,windows . This command creates the required directories for the new platforms to be supported.

Your app's source code: the lib directory

We have already seen the main files and directories of a Flutter project, although it is true that I have not listed them all, for now I have described the most relevant ones that you should know from the beginning. Now let's jump to the lib directory, the place where all the Dart code that makes up your application resides.

When you create a new project, Flutter automatically generates the main.dart file within the lib folder. This file contains the source code responsible for the counter app that you see if you run the project. If you enter, you will see some comments that explain each section. As before, we are going to eliminate these comments and we are going to explain each part little by little:

import 'package:flutter/material.dart';

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

The first line introduces an import of material.dart. This import is essential because, by default, we use Material widgets to construct the user interface.

Following that, we encounter the main() method. Every Dart application, Flutter included, requires an entry point, which is provided by the main() function. Within this function, we call runApp(), enabling the application to launch. We pass it an instance of MyApp, which is the next widget we come across in the file:

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

This snippet introduces the MyApp widget, serving as the foundational element of your application. It's at the top of the widget hierarchy, essentially acting as the root from which all other widgets will branch out. Flutter apps are structured as a vast tree of widgets, where each widget may be a parent or a child to others. Here, MyApp stands as the initial node in this interconnected structure.

MyApp is defined as a class that extends StatelessWidget. StatelessWidgets are characterized by their lack of internal state—they don't manage any data that changes over time. Consequently, a StatelessWidget does not rebuild itself in response to internal data changes. Further details on this will be provided as we progress.

Every StatelessWidget must implement the Widget build(BuildContext context) method. This method is where the app's user interface is constructed. In this example, we create a MaterialApp widget within this method. MaterialApp facilitates the development of an app following Material Design guidelines, including aspects like the app's title and theme.

The home attribute specifies the widget that will be displayed when the app starts. Here, it's set to MyHomePage, which is the widget that comes next in the file:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

MyHomePage is a widget similar to MyApp, but it inherits from StatefulWidget instead of StatelessWidget. This distinction introduces two related classes: MyHomePage itself, which sets up the widget, and _MyHomePageState, a class that manages the widget's state, extending State.

The reason _MyHomePageState starts with the underscore symbol (_) is to indicate that this class must be private within this file.

In the state class, we're obliged to implement the Widget build(BuildContext context) method again. However, this time it's within the state class, where we define a widget tree that composes the counter interface:

  • Initially, a Scaffold widget lays out the basic structure of our screen, considering elements like system navigation bars.

  • An AppBar acts as the top navigation bar, where we specify a title and modify the background color using themes.

  • The Scaffold's body comprises a Center widget, which ensures its content is centered on the screen. Inside the Center, we place a Column for vertical arrangement of widgets. This column contains two Text widgets: one displays a static message, and the other shows the dynamic _counter value, styled with the current theme.

  • The floatingActionButton property of the Scaffold employs a FloatingActionButton widget. Positioned at the bottom right, this button is tasked with increasing the counter each time it's pressed.

Understanding State Management in Flutter

Now that we have seen roughly everything that is in the main.dart file, let's understand in a simple way how Flutter manages the state.

As I said before, the _MyHomePageState class is responsible for managing the state of the MyHomePage widget. In this case, we have an application with a numerical value that increments when a button is pressed. This is the state. Specifically, the variable that is defined at the beginning of the class:

  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

When we invoke the _incrementCounter() method, it calls the setState method and within it the value of the _counter variable is incremented. By calling the setState method we are updating the state, telling Flutter that it has changed and causing the build method to run again, but this time with the updated state. Later, in the second Widget of type Text, the _counter variable is read to display the value on the screen.

This is a very simplified explanation of state management in Flutter, let's perform an experiment, add the following line just before build returns the Scaffold:

  @override
  Widget build(BuildContext context) {

    print('State refresh'); // <-- Add this new line

    return Scaffold(

Now run the application again and look at the output log. You will see the String "State refresh" printed when you run the application, but also when you modify the value of the counter.

As you can see, this is the simplest way to manage the state of your application in Flutter. In addition, you have also been able to observe the role of the build method and its importance when composing the interface based on state changes.

Your first app in Flutter: key concepts

If this is your first contact with Flutter, it is possible that at this point you are a little saturated with so much information, don't worry, I am going to list the key points that we have seen throughout the article.

  • A Flutter project contains a pubspec.yaml file. It defines several configuration parameters as well as the list of packages that are used by the project.

  • A Flutter project can contain several subdirectories named after a platform such as android or ios, these are the native projects that Flutter uses to run on each platform.

  • Inside the lib directory we find the source code of the project, this is where we will write our files in Dart code to create our application.

  • At its core, a Flutter app is structured as an extensive hierarchy of widgets. Widgets can be of two types: StatefulWidget, which holds state (variables that can change over time), and StatelessWidget, which does not hold state.

  • We can alter the state of a StatefulWidget by using the setState method.

These are broadly the main basic points that you should know if you are getting started in developing applications with Flutter.

I hope this article has been useful to you. Do not hesitate to follow this blog and my YouTube channel if you want to continue learning about application development with Flutter, as well as stay up to date with news and other topics of interest about this magnificent framework.

Thanks for reading this far, happy coding!

Did you find this article valuable?

Support David Serrano by becoming a sponsor. Any amount is appreciated!