What you'll learn: overview

Hello fellow developer! Welcome to Wisemen, we are happy to have you on board! We want to make sure you are soon up to speed with the core of Flutter development and our way of working. Therefore we created this awesome series of CodeLabs for you. These are a constant work in progress, so please let us know if you have any feedback or suggestions!

1. Welcome to our team!

Tools

At Wisemen, we use the following tools daily:

Some other tools that might be useful are:

Flutter Circle

We operate within a circle-based structure, which includes:

1.1 Flutter Setup

Install VS Code

Install VS Code with this link.

Install Xcode

In order to develop iOS apps you will need to install Xcode. You can find the app in the App Store. After downloading you should automatically have an app called "simulator" in which you will be able to open several virtual devices to run and test your Flutter apps.

Install CocoaPods

Installing CocoaPods via gem (as Flutter tutorials suggest) is a bit of a hassle. It's easier to use brew for this. First install Homebrew Next up follow this tutorial to install CocoaPods. Only execute the installation via Homebrew (not gem).

Android Studio

Install Android studio with this link.

Install the Flutter SDK

Follow the steps in the following guide: https://docs.flutter.dev/get-started/install/macos/mobile-ios#install-the-flutter-sdk

Install Flutter extensions

Open VS Code and open the sidebar on the left. Click on the extensions tab and search for the "Flutter" extension or just visit the extension marketplace and download the flutter extension. This will automatically install the dart extension as well.

Also download the following extensions:

Other useful extensions:

Install Mason & Bricks

Later in this codelab you will learn about bricks. For these to work you will need to install Mason. By now you should already have Homebrew installed. You can use brew to install all kind of packages on your mac. To install Mason enter the following commands in the terminal:

brew tap felangel/mason
brew install mason

If this is your first time this will not have any effect, but when updating the bricks you need to clear your cache:

mason cache clear

Get all the bricks from brickhub (make sure to add the -g flag)

mason add wise_starter_project -g
mason add wise_controller -g
mason add wise_feature -g

Click this link to check if you need any additional bricks to install (this guide normally already contains every brick we've created)

Execute the next command to check if you have the latest versions

mason list -g

Verify that all the bricks are from registry.brickhub.dev like below

/Users/your_beautiful_name/.mason-cache/global
├── wise_controller 0.1.0+2 -> registry.brickhub.dev
├── wise_feature 0.1.0+2 -> registry.brickhub.dev
└── wise_starter_project 0.1.0+2 -> registry.brickhub.dev

If this is the case you should be ready to go!

If you need more info on the bricks check our guide for more info.

2. Prerequisites

3. What are you going to build?

In this CodeLab you are going to build a simple Todo app in Flutter. The app will contain a few screens where the user can create and manage tasks. The requirements are written out in Linear TODO: Add Linear. The designs for this app are here: Figma wireframes

Let's get started!

Start off by creating a new private github Repository on your personal account. Name it wiselab-flutter-.

We are going to run our first Brick. A Brick is a code generator that generates code for you. We use Bricks to generate boilerplate code for us. This way we can focus on the important stuff.

Open a terminal and navigate to a project folder on your machine where the project will be stored. Before cloning your repository, use the terminal and execute ‘mason make wise_starter_project'. This will create a new Flutter project with our own custom template project.

Enter your project name: wiselab_flutter_ Note: The name should be in snake_case. Enter the app name: wiselab_flutter The rest of the questions can be entered with the default values. Then press Y to finish.

Then open this project in your IDE.

run the following command: run build_runner build --delete-conflicting-outputs to generate the necessary files.

Open Xcode, click runner, targets -> runner: minimum deployments target -> latest iOS version minus 2 versions.

In the VSCode terminal, open the root folder, go to the ios folder, execute pod install --repo-update to install the necessary pods.

Now you should be able to build your app. 🚀

2.1 Add flavours

Open flavors.dart

Add your base url to the flavors like this:

static String get baseUrl {
  switch (appFlavor) {
    case Flavor.DEVELOPMENT:
      return 'https://onboarding-todo.internal.appwi.se/';
    case Flavor.STAGING:
    case Flavor.QA:
    case Flavor.PRODUCTION:
    case null:
      return 'null';
  }
}

These flavors are used to switch between different environments. The baseUrl is used to make API calls to the correct environment. We use these different environments to test the app in different stages of development.

Add client id and client secrets in the same way:

Now add the following block for the applicationId getter in the flavors.dart file:

static String get applicationId {
  switch (appFlavor) {
    case Flavor.DEVELOPMENT:
      return 'com.wisemen.app.development';
    case Flavor.STAGING:
    case Flavor.QA:
    case Flavor.PRODUCTION:
    default:
      return 'com.wisemen.app.development';
  }
}

Use the terminal or IDE to link your project to GitHub

We recommend to use a GIT GUI like SourceTree or Fork. As backup we will show you how to work with the terminal.

From here on you can choose to use the terminal or the IDE to work with Git.

You may now commit your local changes to the main branch with an ‘init project' commit message.

2.2 Our Branching strategy

We use Trunk-base development. You can find more information about this strategy here.

You can use git by either using the terminal, IDE or a GUI tool like SourceTree or Fork.

Now checkout the main branch and create a new feature branch called feature/setup-theme.

2.3 Theme

2.3.1 Colors

Open ‘Colors' tab in the Figma file. Here you can find the colors that are used in the app.

Open /theme/app_theme.dart in the project and add the colors to the theme. Edit the _AppColors class like this:

class _AppColors {
  static const white = Colors.white;
  static const black = Colors.black;
  static const blackPearl = Color.fromRGBO(27, 33, 45, 1);
  static const sanJuan = Color.fromRGBO(71, 81, 97, 1);
  static const shadowBlue = Color.fromRGBO(120, 135, 160, 1);
  //...
}

Add the rest of the colors yourself.

2.3.2 ColorScheme

delete darkTheme from app_theme.dart and from app.dart.

Edit the lightTheme from app_theme.dart like this:

colorScheme: const ColorScheme.light(
      primary: _AppColors.sanJuan,
      secondary: _AppColors.blackPearl,
      surface: _AppColors.white,
      onSurface: _AppColors.black,
      onPrimary: _AppColors.catskillWhite,
      primaryContainer: _AppColors.shadowBlue,
      onPrimaryContainer: _AppColors.periwinkel,
      tertiary: _AppColors.solitude,
    ),

See if there is any room for improvement in the theme and colors. If you have any questions, don't hesitate to contact your buddy.

Then open theme.dart and add the following code:

import 'package:flutter/material.dart';
import 'app_theme.dart';
import 'styles/styles.dart';
import 'text_styles/app_text_styles.dart';

extension AppThemeColorExtension on BuildContext {
  Color get sanJuan => colorScheme.primary;
  Color get blackPearl => colorScheme.secondary;
  Color get solitude => colorScheme.tertiary;
  Color get shadowBlue => colorScheme.primaryContainer;
  Color get periwinkel => colorScheme.onPrimaryContainer;
  Color get catskillWhite => colorScheme.onPrimary;
  Color get black => colorScheme.onSurface;
  Color get white => colorScheme.surface;

  ColorScheme get colorScheme => Theme.of(this).colorScheme;
}

This extension will make it easier to access the colors in the app from the context.

2.3.3 TextStyles

In the Theme.dart file, add the following code:

extension TextStyleExtension on BuildContext {
  TextStyle get normal => AppStyles.normal;
  TextStyle get title => AppStyles.title;
  TextStyle get appBarTitle => AppStyles.title;
  TextStyle get label => AppStyles.label;
  TextStyle get button => AppStyles.label;
}

This extension will make it easier to access the text styles in the app from the context.

You may now commit these theming changes to the created branch an create a PR to the develop branch. Assign your buddy as a reviewer.

If you need help with creating or resolving your pull request consult our full guide

We use zitadel as our identity provider. For more information about zitadel, please visit zitadel.com. Create a new branch from the feature/setup-theme branch called feature/login.

3.1 Wise Zitadel Login package

Add the wise_zitadel_login package to your pubspec by executing: flutter pub add wise_zitadel_login

Then go to your main.dart file and replace the initCore function with this:

final sharedPreferences = await SharedPreferences.getInstance();
final container = ProviderContainer(
  overrides: [
    sharedPreferencesProvider.overrideWithValue(sharedPreferences),
    wiseZitadelOptionsProvider.overrideWithValue(
      WiseZitadelOptions(
        baseUrl: F.zitadelBaseUrl,
        bundleId: F.applicationId,
        applicationId: F.zitadelApplicationId,
        organizationId: F.zitadelOrganisationId,
        buttonOptions: WiseZitadelButtonOptions(
          color: (context) => context.sanJuan,
          buttonTextStyle: (context) => context.button.copyWith(
            color: context.catskillWhite,
          ),
        ),
        onLoginSuccess: (router, ref, token) async {
          if (token == null) {
            return;
          }
          await ref.read(protectedClientProvider).setFreshToken(token: token);
          router.replace(const TodosOverviewScreenRoute());
        },
        supportedTypes: [
          ZitadelLoginType(
            buttonText: 'Login with Apple',
            iconSvgString: AppAssets.appleLogo,
            idp: F.zitadelAppleId,
          ),
          ZitadelLoginType(
            buttonText: 'Login with Google',
            iconSvgString: AppAssets.googleLogo,
            idp: F.zitadelGoogleId,
          ),
        ],
      ),
    ),
  ],
);

3.2 Add an empty screen

Add to your shared/views a empty_screen.dart file and add a empty screen like the following:

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

@RoutePage()
class EmptyScreen extends StatelessWidget {
  const EmptyScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Empty Screen'),
      ),
      body: const Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Empty Screen'),
        ],
      ),
    );
  }
}

This view contains a simple scaffold with an app bar and a text widget. This is a good starting point for a new screen.

Reminder to add the empty_screen.dart to the views.dart barrel file in the same folder.

3.3 Add the route

Open the app_router.dart file and add the following routes to the AutoRouter:

AdaptiveRoute(
  page: EmptyScreenRoute.page,
),
CustomRoute(
  page: WiseLoginScreenRoute.page,
  transitionsBuilder: TransitionsBuilders.noTransition,
),

3.4 Navigation after login

Open the auth_guard.dart and add the following case to the switch statement:

case AuthenticationStatus.unauthenticated:
  resolver.redirect(
    WiseLoginScreenRoute(),
  );
case AuthenticationStatus.authenticated:
  resolver.redirect(
    const EmptyScreenRoute(),
  );

Create a new branch from the feature/login branch called feature/todo-overview.

First off we'll run our Feature Brick. In your terminal, run: mason make wise_feature. Enter the feature's name: todos-overview We'll be using all modules, so press enter to confirm. This will create a new feature, next up we'll run our build runner again.

This will be the main screen of our app. So we need to make sure this is where the user ends up after logging in or opening the app. Replace all the references to the EmptyScreenRoute in the auth_guard.dart and login_navigation_manager_impl with the TodosOverviewScreenRoute. After doing this, you may hot restart and end up in the TodosOverviewScreen.

4.1 Networking

In the network folder, create a new folder called services. In this folder, create a new file called todo_service.dart. This file will contain the service that will handle all the networking for the todos.

Add the following code to the file:

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../clients/protected_client.dart';

class TodosService {
  TodosService(this.ref);
  final Ref ref;

  Future<dynamic> getTodos({
    required int limit,
    required int offset,
  }) async {
    try {
      return await ref.read(protectedClientProvider).wGet(
        'api/v1/todos',
      );
    } catch (error) {
      rethrow;
    }
  }
}

Let's explain what's happening here:

Create your dto model by creating a folder network/dto/todos and create a file called todo.dart:

import 'package:drift/drift.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:onboarding_todo_app/database/database.dart';

part 'todo_dto.g.dart';

@JsonSerializable()
class TodoDTO {
  final String uuid;
  final String createdAt;
  //...

  const TodoDTO({
    required this.uuid,
    required this.createdAt,
    //...
  });

  factory TodoDTO.fromJson(Map<String, dynamic> json) => _$TodoDTOFromJson(json);

  Map<String, dynamic> toJson() => _$TodoDTOToJson(this);
}

Add the necessary fields from the api documentation.

4.2 Local storage

We will be using drift for our local storage. Drift is a simple and efficient way to store data locally in your app using SQLite. First off create a table by creating a new folder called tables with a file called todos_table.dart.

Add the following code to the file:

import 'package:drift/drift.dart';

@DataClassName('TodoObject')
class TodosTable extends Table {
  TextColumn get uuid => text()();
  TextColumn get title => text()();
  //...

  @override
  Set<Column> get primaryKey => {uuid};
}

Make sure to add the necessary fields.

Now you can create an extension function on your todo dto model to map it to the table model (repositories/dto_mapper):

extension TodoDTOMapper on TodoDTO {
  TodosTableCompanion toCompanion() {
    return TodosTableCompanion(
      uuid: Value(uuid),
      title: Value(title),
      //...
    );
  }
}

4.2.1 Dao

A DAO (Data Access Object) is a class that provides an abstract interface to some type of database or other persistence mechanism.

import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:onboarding_todo_app/network/dto/todos/todo_dto.dart';

import '../database.dart';
import '../tables/tables.dart';

part 'todos_dao.g.dart';

@DriftAccessor(tables: [TodosTable])
class TodosDao extends DatabaseAccessor<Database> with _$TodosDaoMixin {
  TodosDao(super.attachedDatabase);

  Future<void> insertTodos({
    required Iterable<TodosTableCompanion> todos,
  }) async {
    await batch(
      (batch) => batch.insertAllOnConflictUpdate(
        todosTable,
        todos,
      ),
    );
    await _deleteTodos(todos: todos);
  }

  Stream<List<TodoObject>> getTodos({
    required int limit,
    required int offset,
  }) {
    return (select(todosTable)
          ..limit(
            limit,
            offset: offset,
          ))
        .get();
  }

  Future<void> _deleteTodos({
    required Iterable<TodosTableCompanion> todos,
  }) async {
    await (delete(todosTable)
          ..where(
            (tbl) => tbl.uuid.isNotIn(todos.map((e) => e.uuid.value).toList()),
          ))
        .go();
  }
}

final todosDaoProvider = Provider.autoDispose(
  (ref) => TodosDao(ref.read(databaseServiceProvider)),
);

4.3 Repository

A repository is a class that abstracts the data layer from the rest of the app. It is responsible for fetching data from the network, store it in the local storage and providing it to the rest of the app.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:onboarding_todo_app/features/todos_overview/todos_overview.dart';

class TodosRepositoryImpl implements TodosOverviewRepository {
  TodosRepositoryImpl(this.ref);
  final Ref ref;
}

final todosRepositoryProvider = Provider.autoDispose(
  (ref) => TodosRepositoryImpl(ref),
);

This repository needs to be accessible to our features, without them knowing it exists. We will use Riverpod (provider) to provide the repository to the rest of the app. We'll achieve this by adding this to feature_init_util.dart.

TodosOverviewFeature.init(
  navigationManager: todosNavigationManagerProvider,
  repository: todosRepositoryProvider,
);

4.4 Data

This is the feature model. It contains the data that will be displayed in the UI. Check the documentation and what fields you need in the design to create the model.

class Todo {
  final String uuid;
  //...

  Todo({
    required this.uuid,
    //...
  });
}

If we look at the designs, what do we need? We need a list of todos, paginated. Pagination is a way to split up a large list of items into smaller chunks. This way we can load the data in parts, which is more efficient. We need the data from the api and store them into our local database.

Place this in the todos_overview_repository.dart file.

Future<void> getPaginatedTodos({
    required int limit,
    required int offset,
  });
  Future<List<Todo>> getTodos({
    required int limit,
    required int offset,
  });

This will give an issue on the TodosRepositoryImpl class, because it doesn't implement the method. Click command + . on the class name and select ‘implement missing members'. This will add the method to the class.

4.4.1 Pagination Model

Create a new file in the shared/utils folder called pagination_model.dart and add the following code:

class Paginated<T> {
  final List<T> items;
  final Map<String, int> meta;

  Paginated({
    required this.items,
    required this.meta,
  });

  factory Paginated.fromJson(
    Map<String, dynamic> json,
    T Function(Map<String, dynamic>) fromJson,
  ) {
    return Paginated(
      items: (json['items'] as List).map((e) => fromJson(e)).toList(),
      meta: Map<String, String?>.from(json['meta']),
    );
  }
}

This model will be used to parse the paginated data from the api. It contains a list of items and a map with meta data. The meta data can contain information like the total number of items, the current page, etc.

Go back to the TodosRepositoryImpl class and implement the following functions:

@override
Future<void> getPaginatedTodos({required int limit, required int offset}) async {
  try {
    final todos = await ref.read(todosServiceProvider).getTodos(
          limit: limit,
          offset: offset,
        );
    final paginatedTodos = Paginated<TodoDTO>.fromJson(todos, TodoDTO.fromJson);
    await ref.read(todosDaoProvider).insertTodos(
          todoDTOs: paginatedTodos.items.map((todo) => todo.toCompanion()),
        );
  } catch (error) {
    rethrow;
  }
}

Some explanation for the code above: This function has return type of void because this calls the data from the api and writes it to the local database. It doesn't return anything to the UI. It requires 2 parameters: limit and offset. These are used to paginate the data. The function reads the todos from the api, parses them to a Paginated object and writes them to the local database.

 @override
Future<List<Todo>> getTodos({required int limit, required int offset}) async {
  return (await ref.read(todosDaoProvider).getTodos(
            limit: limit,
            offset: offset,
          ))
      .map((e) => e.toModel())
      .toList();
}

This function has a return type of List because this function returns the data to the UI. It requires 2 parameters: limit and offset. These are used to paginate the data. The function reads the todos from the local database and maps them to a Todo object.

We still need to add the map function. Add the following code to repositories/table_mappers/todos_table_mapper.dart. This code will map the table model to the feature model.

extension TodosTableMapper on TodoObject {
  Todo toModel() => Todo(
        uuid: uuid,
        title: title,
        description: description,
        completed: isCompleted,
        deadline: deadline,
        createdAt: createdAt,
        updatedAt: updatedAt,
      );
}

4.5 Page Controller

We need to add a page controller to the feature. This controller will handle the pagination logic. Open the todos_overview_providers.dart file.

import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:onboarding_todo_app/features/shared/shared.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:talker_flutter/talker_flutter.dart';

import '../todos_overview.dart';

part 'todos_overview_providers.g.dart';

class TodosOverviewProviders {
  static var todosPageController = todosPageControllerProvider;
}

@riverpod
// ignore: unsupported_provider_value
class TodosPageController extends _$TodosPageController {
  static const _pageSize = AppConstants.pageSize;
  @override
  PagingController<int, Todo> build() {
    return PagingController<int, Todo>(firstPageKey: 0)..addPageRequestListener(_fetchNewPage);
  }

  Future<void> _fetchNewPage(
    int pageKey,
  ) async {
    try {
      await ref
          .read(TodosOverviewFeature.todosOverviewRepository)
          .getPaginatedTodos(
            limit: _pageSize,
            offset: pageKey * _pageSize,
          )
          .catchError(
        (error) {
          TalkerLogger().error(error);
        },
      );

      final items = await ref.read(TodosOverviewFeature.todosOverviewRepository).getTodos(
            limit: _pageSize,
            offset: pageKey * _pageSize,
          );

      final isLastPage = items.length < _pageSize;
      if (isLastPage) {
        state.appendLastPage(items);
      } else {
        final nextPageKey = pageKey + 1;
        state.appendPage(items, nextPageKey);
      }
    } catch (error) {
      state.error = error;
    }
    ref.notifyListeners();
  }
}

First and foremost we need a build function that returns a PagingController. This controller will handle the pagination logic. The int is the page key and the Todo is the data type that will be paginated. In the constructor the first page key is passed. We also add a listener to the controller that listens for page requests.

We create a _fetchNewPage function that takes a page key as a parameter. We have a try-catch block that fetches the paginated data from the api. It has a silently catch block that logs the error. This is done so when the api calls fail, the app doesn't crash and still queries the local database. When it's done calling the api, it fetches the todos from the local database. Followed by a check if it's the last page. If it is, it appends the last page to the state. If it's not, it appends the page to the state and increments the page key.

The ref.notifyListeners() is called to notify the listeners that the state has changed.

4.6 UI

Let's create the UI for the todos overview screen. Open the todos_overview_screen.dart file.

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:onboarding_todo_app/generated/l10n.dart';
import 'package:onboarding_todo_app/theme/theme.dart';
import 'package:wise_nav_bar/wise_nav_bar.dart';
import 'package:wisewidgetslibrary/wise_widgets_library.dart';

import '../todos_overview.dart';

@RoutePage()
class TodosOverviewScreen extends ConsumerWidget {
  const TodosOverviewScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          PlatformSliverAppBar.text(
            title: S.of(context).myTodos,
          ),
          SliverPadding(
            padding: padH16.copyWith(
              bottom: Sizes.p24,
            ),
            sliver: PagedSliverList<int, Todo>.separated(
              pagingController: ref.watch(
                TodosOverviewProviders.todosPageController,
              ),
              separatorBuilder: (context, index) => gapH10,
              builderDelegate: PagedChildBuilderDelegate<Todo>(
                // TODO: Styling
                itemBuilder: (context, item, index) => ListTile(
                  title: Text(item.title),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Start by switching the StateLessWidget to a ConsumerWidget. The consumer widget is a widget that can read providers and listen to changes. Change the body with a CustomScrollView. This is a scrollable view that allows you to create custom scroll effects. Add a PlatformSliverAppBar with the title ‘My Todos'. This is a custom app bar that adapts to both iOS and Android styles. Slivers are scrollable areas that can be composed to create custom scroll effects. Add a SliverPadding with padding and a PagedSliverList. This is a sliver list that fetches data from the PagingController and displays it in a list. The PagedSliverList has a separatorBuilder that adds a gap between the items. The builderDelegate is a delegate that builds the list items. The itemBuilder builds the list items.

You can see that the background color does not match with the design. Open the AppTheme and add the following code:

scaffoldBackgroundColor: _AppColors.catskillWhite.white,

This will only apply on Android. For iOS, we need to add the background color to the screen. Open the todos_overview_screen.dart and add the following line in the scaffold and the app bar:

backgroundColor: context.catskillWhite,

Make sure to commit these changes. Create a PR and assign your buddy as a reviewer.

Create a new branch from the feature/todo-overview branch called feature/add-todo. First off add an floatingActionButton to the TodosOverviewScreen. This button will navigate to the AddTodoScreen.

floatingActionButton: FloatingActionButton.small(
        backgroundColor: context.sanJuan,
        elevation: 0,
        onPressed: ref.read(TodosOverviewFeature.todosOverviewNavigationManager).navigateToAddTodo,
        shape: const RoundedRectangleBorder(
          borderRadius: rad8,
        ),
        child: Icon(
          Icons.add_rounded,
          color: context.white,
        ),
      )

5.1 AddTodoScreen

Run the feature Brick. In your terminal, run: mason make wise_feature. Now you should be able to create a new feature called add_todo.

Don't hesitate to ask your buddy for help if you're stuck.

5.2 navigation

Next, add a navigateToAddTodo inside the TodosOverviewNavigationManager class and implement it in the impl class.

If the add todo feature is ready, create a PR and assign your buddy as a reviewer.

Before creating a pull request make sure to go over our code conventions and try to improve your code where needed.

Follow the steps in the create a pull request section of our guide

💡 Tip: When using a simulator, use cmd + r to start and stop a screen recording

6 Feature: Styling overview

Create a new branch from the feature/add-todo branch called feature/styling-overview.

Style the overview according to the design. Make sure to use the correct colors, fonts, and paddings.

Don't forget to create a PR and assign your buddy as a reviewer.

7 Feature: Edit todo

Now you should be able to implement the following feature: editing a todo. Create a new branch from the feature/styling-overview branch called feature/edit-todo. Think ahead about a strategy to implement this feature. Discuss it with your buddy before you start.

After finishing the feature, you are able to finish up the whole application.

Good luck!