โ€ข

newsletter

How I build a cross-platform app: zero to release

Cross-platform app development with Flutter and Dart: learn how to setup your project, what packages to choose, and how to use them to implement routing, state management, data storage, and UI.


Sandro Maglione

Sandro Maglione

Software development

How is like to build a mobile app in 2024? ๐Ÿค”

This week I implemented a complete mobile app with Flutter using all the latest packages and features ๐Ÿ’ก

This is how cross-platform mobile app development looks like ๐Ÿ‘‡๐Ÿ‘€


Tech stack

  • Flutter: still the best choice for cross-platform app development, large community, continuous improvements
  • Dart: the engine behind Flutter, Dart keeps getting better as well, and the planned new features have the potential to take it to the next level ๐Ÿš€

Setup

What I like about Flutter is how easy is to install and configure. This is still true today ๐Ÿ’๐Ÿผโ€โ™‚๏ธ

  • Flawless integration with VSCode and continuous updates and bug fixes
  • Simple and clear CLI (flutter and dart commands)
  • Complete devtool for easy debugging and testing

Get started

Goal for this project: use all the newest packages and test them out ๐Ÿ’ก

The building blocks of a Flutter app are:

  • Routing: define and navigate between screens
  • Data models: from plain dart class to Records, sealed, typedef and more
  • State management: keep data and widgets in sync
  • API: business logic, http, storage, the backbone of the app

This is how I put all of these pieces together to make a complete app ๐Ÿ‘‡

There is more ๐Ÿคฉ

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

Implementation

Routing first. There are countless of solutions for routing in Flutter, this time I went with go_router.

My objective is to have type-safe navigation, I used go_router_builder to generate type-safe routes ๐Ÿ› ๏ธ

part 'router.g.dart';

@TypedGoRoute<HomeRoute>(path: '/home')
class HomeRoute extends GoRouteData {
  const HomeRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

@TypedGoRoute<TrackingRoute>(path: '/')
class TrackingRoute extends GoRouteData {
  const TrackingRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const TrackingScreen();
}

Second, data models. Since Dart 3 there are many features to structure data:

  • Plain class
  • Records
  • sealed classes
  • enum
  • typedef
enum Emoji {
  smile,
  rocket,
  dart;

  @override
  String toString() => switch (this) {
        Emoji.smile => "๐Ÿ˜",
        Emoji.rocket => "๐Ÿš€",
        Emoji.dart => "๐ŸŽฏ",
      };
}

typedef Day = ({int day, int month, int year});

class EventModel extends Equatable {
  final int id;
  final int activityId;
  final Day createdAt;
 
  const EventModel({
    required this.id,
    required this.activityId,
    required this.createdAt,
  });
 
  @override
  List<Object?> get props => [id, activityId, createdAt];
}

API: SQLite and Functional Programming

Countless of options also to implement the business logic of the app ๐Ÿค

This time I went with drift and sqlite3 for storage:

  • Model database with SQL and relational tables
  • Map tables to data models
  • Reactive changes on insert, delete, and update โšก๏ธ
@DriftDatabase(tables: [Activity, Event])
class Database extends _$Database {
  Database() : super(_openConnection());

  @override
  int get schemaVersion => 1;
}

I then use functional programming with fpdart to connect widgets and database (type-safe API ๐Ÿช„):

ReaderTaskEither<Database, ApiError, int> addActivity({
  required String name,
  required Emoji emoji,
}) =>
    ReaderTaskEither.Do(
      (_) async {
        final db = await _(ReaderTaskEither.ask());

        return _(
          ReaderTaskEither.fromTaskEither(
            db.query(
              () => db.into(db.activity).insert(
                    ActivityCompanion.insert(
                      name: name,
                      emoji: emoji,
                    ),
                  ),
            ),
          ),
        );
      },
    );

State management: signals ๐Ÿ†•

New entry for state management: signals

signals is easy to implement, less code, many features. Give it a try ๐Ÿค

Complete app

The last step is connecting all together:

  1. Define a new route
  2. Define a new data model
  3. Define a table in the database to store the data
  4. Create the API to read, update, delete data
  5. Get the data from the API and display widgets based on the current state
  6. Update the state on each user interaction
Future<void> onAdd(BuildContext context) =>
    addEvent(activityId: activityModel.id, day: day).match<void>(
      (apiError) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              "Error while adding new event for ${activityModel.name}: $apiError",
            ),
          ),
        );
      },
      (id) {
        print(id); /// Success โœ…
      },
    ).run(getIt.get<Database>());

I wrote a complete step-by-step article on how to build a Flutter app using all the latest packages.

This article covers all the details on how to go from zero to a complete Flutter app ๐Ÿš€

Takeaways

  • Flutter is still the best solution from cross-platform app development
  • Dart is good and it's getting better, give it a try
  • The Flutter ecosystem is full of great packages for most usecases
  • It easy to get started and go from zero to a complete cross-platform app

You can read the full code on the open source repository ๐Ÿ‘‡

Open Source Repository

Some interesting updates and new projects coming in the next weeks (the Effect Days are approaching ๐Ÿ‘€)

See you next ๐Ÿ‘‹

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.