Build a Todo App with Flutter(16)
The project structure created by very_good_cli would like:
├── lib├── packages│ ├── local_storage_todos_api│ ├── todos_api│ └── todos_repository└── test
We split the project into multiple packages in order to maintain explicit dependencies for each package with clear boundaries that enforce the single responsibility principle.
The single-responsibility principle (SRP) is a computer programming principle that states that “A module should be responsible to one, and only one, actor.” The term actor refers to a group (consisting of one or more stakeholders or users) that requires a change in the module.
Modularizing the project like this has many benefits including but not limited to:
-
• Easy to reuse packages across multiple projects -
• CI/CD improvements in terms of efficiency (run checks on only the code that has changed) -
• Easy to maintain the packages in isolation with their dedicated test suites, semantic versioning, and release cycle/cadence
This project uses the bloc_lint package to enforce best practices using bloc.
To validate linter errors, run:
$ dart run bloc_tools:bloc lint .
We can also validate with VSCode-based IDEs using the official bloc extension. We can activate the commands by launching the command palette (View -> Command Palette) and running entering the command name or we can right click on the directory in which we’d like to create the bloc/cubit and select the command from the context menu.
Our application consists of three main layers:
-
• data layer -
• domain layer -
• feature layer -
• presentation/UI (widgets) -
• business logic (blocs/cubits)
Layering our application allows us to easily reuse libraries across multiple projects (especially with respect to the data layer).
-
• Each layer has a single responsibility and can be used and tested in isolation, we can keep changes contained to a specific layer in order to minimize the impact on the entire application.
Data Layer
The Data Layer is the lowest layer and is responsible for retrieving raw data from external sources such as a databases, APIs, and more.
Packages in the data layer generally should not depend on any UI and can be reused and even published on pub.dev as a standalone package.
In this example, the data layer consists of the todos_api and local_storage_todos_api packages.
Domain Layer
The Domain Layer combines one or more data providers and applies “business rules” to the data.
Each component in this layer is called a repository and each repository generally manages a single domain.
-
• Packages in the Repository Layer should generally only interact with the Data Layer.
In this example, the repository layer consists of the todos_repository package.
Feature Layer
The Feature Layer contains all of the application-specific features and use cases. Each feature generally consists of some UI and business logic.
-
• Features should generally be independent of other features so that they can easily be added/removed without impacting the rest of the codebase. -
• Within each feature, the state of the feature along with any business logic is managed by blocs.
Blocs interact with zero or more repositories.
-
• Blocs react to events and emit states which trigger changes in the UI. -
• Widgets within each feature should generally only depend on the corresponding bloc and render UI based on the current state.
The UI can notify the bloc of user input via events. In this example, the application will consist of the home, todos_overview, stats, and edit_todos features.
Data Layer
The data layer is the lowest layer in the application and consists of raw data providers.
Packages in data layer are primarily concerned with where/how data is coming from. In this case the data layer will consist of the TodosApi, which is an interface, and the LocalStorageTodosApi, which is an implementation of the TodosApi backed by shared_preferences.
The todos_api package will export a generic interface for interacting/managing todos, next we’ll implement the TodosApi using shared_preferences.
Having an abstraction will make it easy to support other implementations without having to change any other part of the application.
For example, we can later add a FirestoreTodosApi, which uses cloud_firestore instead of shared_preferences, with minimal code changes to the rest of the application.
# packages/todos_api/pubspec.yamlname: todos_apidescription: The interface and models for an API providing access to todos.version: 0.1.0+1publish_to: noneenvironment: sdk: ^3.11.0dev_dependencies: mocktail: ^1.0.5 test: ^1.31.0 very_good_analysis:^10.2.0
Todo Model
The Todo model is part of the todos_api package. This is because the TodosApi defines APIs that return/acceptTodo objects.
The Todo model is a Dart representation of the raw Todo object that will be stored/retrieved.
part'todo.g.dart';@immutable@JsonSerializable()classTodoextendsEquatable{ Todo({required this.title,String? id,this.description = '',this.isCompleted = false, }) : assert( id == null || id.isNotEmpty,'id must either be null or not empty', ), id = id ?? const Uuid().v4(); final String id; final String title; final String description; final bool isCompleted; Todo copyWith({String? id,String? title,String? description,bool? isCompleted, }) {return Todo( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, isCompleted: isCompleted ?? this.isCompleted, ); } static Todo fromJson(JsonMap json) => _$TodoFromJson(json); JsonMap toJson() => _$TodoToJson(this); @override List<Object> get props => [id, title, description, isCompleted];}
The Todo model uses json_serializable to handle the json (de)serialization.
json_map.dart provides a typedef for code checking and linting.
/// The type definition for a JSON-serializable [Map].typedef JsonMap = Map<String, dynamic>;
The model of the Todo is defined in todos_api/models/todo.dart and is exported by package:todos_api/todos_api.dart.
The Todo model and the TodosApi are exported via barrel files, We import it in lib/src/todos_api.dart with a reference to the package barrel file: import 'package:todos_api/todos_api.dart';.
Update the barrel files to resolve any remaining import errors:
export'json_map.dart';export'todo.dart';
夜雨聆风