Clean architecture flutter: A Flutter package that makes it easy and intuitive to implement Uncle Bob's Clean Architecture in Flutter. This package provides basic classes that are tuned to work with Flutter and are designed according to the Clean Architecture.
MIT License
A Flutter package that makes it easy and intuitive to implement Uncle Bob's Clean Architecture in Flutter. This package provides basic classes that are tuned to work with Flutter and are designed according to the Clean Architecture.
Add this to your package's pubspec.yaml file:
dependencies:
flutter_clean_architecture: ^6.0.1
You can install packages from the command line:
with Flutter:
$ flutter packages get
Alternatively, your editor might support flutter packages get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
It is architecture based on the book and blog by Uncle Bob. It is a combination of concepts taken from the Onion Architecture and other architectures. The main focus of the architecture is separation of concerns and scalability. It consists of four main modules: App
, Domain
, Data
, and Device
.
Source code dependencies only point inwards. This means inward modules are neither aware of nor dependent on outer modules. However, outer modules are both aware of and dependent on inner modules. Outer modules represent the mechanisms by which the business rules and policies (inner modules) operate. The more you move inward, the more abstraction is present. The outer you move the more concrete implementations are present. Inner modules are not aware of any classes, functions, names, libraries, etc.. present in the outer modules. They simply represent rules and are completely independent from the implementations.
The Domain
module defines the business logic of the application. It is a module that is independent from the development platform i.e. it is written purely in the programming language and does not contain any elements from the platform. In the case of Flutter
, Domain
would be written purely in Dart
without any Flutter
elements. The reason for that is that Domain
should only be concerned with the business logic of the application, not with the implementation details. This also allows for easy migration between platforms, should any issues arise.
Domain
is made up of several things.
Login
usecase expects a Repository
that has login
functionalityUsecases
from outer layersDomain
represents the inner-most layer. Therefore, it the most abstract layer in the architecture.
App
is the layer outside Domain
. App
crosses the boundaries of the layers to communicate with Domain
. However, the Dependency Rule is never violated. Using polymorphism
, App
communicates with Domain
using inherited class: classes that implement or extend the Repositories
present in the Domain
layer. Since polymorphism
is used, the Repositories
passed to Domain
still adhere to the Dependency Rule since as far as Domain
is concerned, they are abstract. The implementation is hidden behind the polymorphism
.
Since App
is the presentation layer of the application, it is the most framework-dependent layer, as it contains the UI and the event handlers of the UI. For every page in the application, App
defines at least 3 classes: a Controller
, a Presenter
, and a View
.
View
builds the page's UI, styles it, and depends on the Controller
to handle its events. The View
has-a Controller
.View
is comprised of 2 classes
View
, which would be the root Widget
representing the View
ViewState
with the template specialization of the other class and its Controller
.ViewState
contains the view
getter, which is technically the UI implementationStatefulWidget
contains the State
as per Flutter
StatefulWidget
only serves to pass arguments to the State
from other pages such as a title etc.. It only instantiates the State
object (the ViewState
) and provides it with the Controller
it needs through it's consumer.StatefulWidget
has-a State
object (the ViewState
) which has-a Controller
StatefulWidget
and the State
are represented by a View
and ViewState
of the page.ViewState
class maintains a GlobalKey
that can be used as a key in its scaffold. If used, the Controller
can easily access it via getState()
in order to show snackbars and other dialogs. This is helpful but optional.ViewState
has-a Controller
. The Controller
provides the needed member data of the ViewState
i.e. dynamic data. The Controller
also implements the event-handlers of the ViewState
widgets, but has no access to the Widgets
themselves. The ViewState
uses the Controller
, not the other way around. When the ViewState
calls a handler from the Controller
, refreshUI()
can be called to update the view.Controller
extends the Controller
abstract class, which implements WidgetsBindingObserver
. Every Controller
class is responsible for handling lifecycle events for the View
and can override:
Controller
has to implement initListeners() that initializes the listeners for the Presenter
for consistency.Controller
has-a Presenter
. The Controller
will pass the Repository
to the Presenter
, which it communicate later with the Usecase
. The Controller
will specify what listeners the Presenter
should call for all success and error events as mentioned previously. Only the Controller
is allowed to obtain instances of a Repository
from the Data
or Device
module in the outermost layer.Controller
has access to the ViewState
and can refresh the ControlledWidgets
via refreshUI()
.Controller
has-a Presenter
. The Presenter
communicates with the Usecase
as mentioned at the beginning of the App
layer. The Presenter
will have members that are functions, which are optionally set by the Controller
and will be called if set upon the Usecase
sending back data, completing, or erroring.Presenter
is comprised of two classes
Presenter
e.g. LoginPresenter
Controller
Usecase
to be usedObserver<T>
class and the appropriate arguments. E.g. with username
and password
in the case of a LoginPresenter
Observer<T>
Presenter
class. Ideally, this should be an inner class but Dart
does not yet support them.Usecase
Usecase
returns an object, it will be passed to onNext(T)
.onError(e)
.onComplete()
.Presenter
that are set by the Controller
. This way, the event is passed to the Controller
, which can then manipulate data and update the ViewState
Utility
classes (any commonly used functions like timestamp getters etc..)Constants
classes (const
strings for convenience)Navigator
(if needed)Represents the data-layer of the application. The Data
module, which is a part of the outermost layer, is responsible for data retrieval. This can be in the form of API calls to a server, a local database, or even both.
Repository
should implement Repository
from the Domain layer.polymorphism
, these repositories from the data layer can be passed across the boundaries of layers, starting from the View
down to the Usecases
through the Controller
and Presenter
.Entities
with the addition of extra members that might be platform-dependent. For example, in the case of local databases, this can be manifested as an isDeleted
or an isDirty
entry in the local database. Such entries cannot be present in the Entities
as that would violate the Dependency Rule since Domain should not be aware of the implementation.Data
layer will not be necessary as we do not have a local database. Therefore, it is unlikely that we will need extra entries in the Entities
that are platform-dependent.Entity
objects to Models
and vice-versa.Entity
or a Model
and return the other.Models
Utility
classes if neededConstants
classes if neededPart of the outermost layer, Device
communicates directly with the platform i.e. Android and iOS. Device
is responsible for Native functionality such as GPS
and other functionality present within the platform itself like the filesystem. Device
calls all Native APIs.
Repositories
in Data
, Devices
are classes that communicate with a specific functionality in the platform.Repositories
are pass across the boundaries of the layer: using polymorphism between the App
and Domain
layer. That means the Controller
passes it to the Presenter
then the Presenter
passes it polymorphically to the Usecase
, which receives it as an abstract class.Utility
classes if neededConstants
classes if neededlib/
app/ <--- application layer
pages/ <-- pages or screens
login/ <-- some page in the app
login_controller.dart <-- login controller extends `Controller`
login_presenter.dart <-- login presenter extends `Presenter`
login_view.dart <-- login view, 2 classes extend `View` and `ViewState` resp.
widgets/ <-- custom widgets
utils/ <-- utility functions/classes/constants
navigator.dart <-- optional application navigator
data/ <--- data layer
repositories/ <-- repositories (retrieve data, heavy processing etc..)
data_auth_repo.dart <-- example repo: handles all authentication
helpers/ <-- any helpers e.g. http helper
constants.dart <-- constants such as API keys, routes, urls, etc..
device/ <--- device layer
repositories/ <--- repositories that communicate with the platform e.g. GPS
utils/ <--- any utility classes/functions
domain/ <--- domain layer (business and enterprise) PURE DART
entities/ <--- enterprise entities (core classes of the app)
user.dart <-- example entity
manager.dart <-- example entity
usecases/ <--- business processes e.g. Login, Logout, GetUser, etc..
login_usecase.dart <-- example usecase extends `UseCase` or `CompletableUseCase`
repositories/ <--- abstract classes that define functionality for data and device layers
main.dart <--- entry point
Checkout a small example here and a full application built here.
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterPage extends CleanView {
@override
// Dependencies can be injected here
State<StatefulWidget> createState() => CounterState();
}
class CounterState extends CleanViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
@override
Widget get view => MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
key: globalKey, // using the built-in global key of the `View` for the scaffold or any other
// widget provides the controller with a way to access them via getContext(), getState(), getStateKey()
body: Column(
children: <Widget>[
Center(
// show the number of times the button has been clicked
child: ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text(controller.counter.toString());
}
),
),
// you can refresh manually inside the controller
// using refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return MaterialButton(onPressed: controller.increment);
}
),
],
),
),
);
}
To deal with screens on flutter web, you can take advantage of the responsive view state,
that abstracts the main web apps breakpoints (desktop, tablet and mobile) to ease development
for web with flutter_clean_architecture
For example:
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterPage extends CleanView {
@override
// Dependencies can be injected here
State<StatefulWidget> createState() => CounterState();
}
class CounterState extends ResponsiveViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
Widget AppScaffold({Widget child}) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
key: globalKey, // using the built-in global key of the `View` for the scaffold or any other
// widget provides the controller with a way to access them via getContext(), getState(), getStateKey()
body: child
),
);
}
@override
ViewBuilder get mobileView => AppScaffold(
child: Column(
children: <Widget>[
// you can refresh manually inside the controller
// using refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('Counter on mobile view ${controller.counter.toString()}');
}
),
],
)
);
@override
ViewBuilder get tabletBuilder => AppScaffold(
child: Column(
children: <Widget>[
// you can refresh manually inside the controller
// using refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('Counter on tablet view ${controller.counter.toString()}');
}
),
],
)
);
@override
ViewBuilder get desktopBuilder => AppScaffold(
child: Row(
children: <Widget>[
// you can refresh manually inside the controller
// using refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('Counter on desktop view ${controller.counter.toString()}');
}
),
],
)
);
}
In the event that multiple widgets need to use the same Controller
of a certain Page
,
the Controller
can be retrieved inside the children widgets of that page via
FlutterCleanArchitecture.getController<HomeController>(context)
.
For example:
import '../pages/home/home_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class HomePageButton extends StatelessWidget {
final String text;
HomePageButton({@required this.text});
@override
Widget build(BuildContext context) {
// use a common controller assuming HomePageButton is always a child of Home
HomeController controller =
FlutterCleanArchitecture.getController<HomeController>(context);
return GestureDetector(
onTap: controller.buttonPressed,
child: Container(
height: 50.0,
alignment: FractionalOffset.center,
decoration: BoxDecoration(
color: Color.fromRGBO(230, 38, 39, 1.0),
borderRadius: BorderRadius.circular(25.0),
),
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w300,
letterSpacing: 0.4),
),
),
);
}
}
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterController extends Controller {
int counter;
final LoginPresenter presenter;
CounterController() : counter = 0, presenter = LoginPresenter(), super();
void increment() {
counter++;
}
/// Shows a snackbar
void showSnackBar() {
ScaffoldState scaffoldState = getState(); // get the state, in this case, the scaffold
scaffoldState.showSnackBar(SnackBar(content: Text('Hi')));
}
@override
void initListeners() {
// Initialize presenter listeners here
// These will be called upon success, failure, or data retrieval after usecase execution
presenter.loginOnComplete = () => print('Login Successful');
presenter.loginOnError = (e) => print(e);
presenter.loginOnNext = () => print("onNext");
}
void login() {
// pass appropriate credentials here
// assuming you have text fields to retrieve them and whatnot
presenter.login();
}
}
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart'
as clean;
class LoginPresenter extends clean.Presenter {
Function loginOnComplete; // alternatively `void loginOnComplete();`
Function loginOnError;
Function loginOnNext; // not needed in the case of a login presenter
final LoginUseCase loginUseCase;
// dependency injection from controller
LoginPresenter(authenticationRepo): loginUseCase = LoginUseCase(authenticationRepo);
/// login function called by the controller
void login(String email, String password) {
loginUseCase.execute(_LoginUseCaseObserver(this), LoginUseCaseParams(email, password));
}
/// Disposes of the [LoginUseCase] and unsubscribes
@override
void dispose() {
_loginUseCase.dispose();
}
}
/// The [Observer] used to observe the `Stream` of the [LoginUseCase]
class _LoginUseCaseObserver extends clean.Observer<void>{
// The above presenter
// This is not optimal, but it is a workaround due to dart limitations. Dart does
// not support inner classes or anonymous classes.
final LoginPresenter loginPresenter;
_LoginUseCaseObserver(this.loginPresenter);
/// implement if the `Stream` emits a value
// in this case, unnecessary
void onNext(_) {}
/// Login is successful, trigger event in [LoginController]
void onComplete() {
// any cleaning or preparation goes here
assert(loginPresenter.loginOnComplete != null);
loginPresenter.loginOnComplete();
}
/// Login was unsuccessful, trigger event in [LoginController]
void onError(e) {
// any cleaning or preparation goes here
assert(loginPresenter.loginOnError != null);
loginPresenter.loginOnError(e);
}
}
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
// In this case, no parameters were needed. Hence, void. Otherwise, change to appropriate.
class LoginUseCase extends CompletableUseCase<LoginUseCaseParams> {
final AuthenticationRepository _authenticationRepository; // some dependency to be injected
// the functionality is hidden behind this
// abstract class defined in the Domain module
// It should be implemented inside the Data or Device
// module and passed polymorphically.
LoginUseCase(this._authenticationRepository);
@override
// Since the parameter type is void, `_` ignores the parameter. Change according to the type
// used in the template.
Future<Stream<void>> buildUseCaseStream(params) async {
final StreamController controller = StreamController();
try {
// assuming you pass credentials here
await _authenticationRepository.authenticate(email: params.email, password: params.password);
logger.finest('LoginUseCase successful.');
// triggers onComplete
controller.close();
} catch (e) {
print(e);
logger.severe('LoginUseCase unsuccessful.');
// Trigger .onError
controller.addError(e);
}
return controller.stream;
}
}
class LoginUseCaseParams {
final String email;
final String password;
LoginUseCaseParams(this.email, this.password);
}
A usecase can be made to run on a separate isolate using the BackgroundUseCase
class.
Implementing this kind of usecase is a little different than a regular usecase due to the constraints of an isolate.
In order to create a BackgroundUseCase
, simply extend the class and override the buildUseCaseTask
method.
This method should return a UseCaseTask
, which is just a function that has a void return type and takes a
BackgroundUseCaseParameters
parameter. This method should be static and will contain all the code you wish to run
on a separate isolate. This method should communicate with the main isolate using the port
provided in the BackgroundUseCaseParameters
as follows. This example is of a BackgroundUseCase
that performs matrix multiplication.
class MatMulUseCase extends BackgroundUseCase<List<List<double>>, MatMulUseCaseParams> {
// must be overridden
@override
buildUseCaseTask() {
return matmul; // returns the static method that contains the code to be run on an isolate
}
/// This method will be executed on a separate isolate. The [params] contain all the data and the sendPort
/// needed
static void matmul(BackgroundUseCaseParams params) async {
MatMulUseCaseParams matMulParams = params.params as MatMulUseCaseParams;
List<List<double>> result = List<List<double>>.generate(
10, (i) => List<double>.generate(10, (j) => 0));
for (int i = 0; i < matMulParams.mat1.length; i++) {
for (int j = 0; j < matMulParams.mat1.length; j++) {
for (int k = 0; k < matMulParams.mat1.length; k++) {
result[i][j] += matMulParams.mat1[i][k] * matMulParams.mat2[k][j];
}
}
}
// send the result back to the main isolate
// this will be forwarded to the observer listneres
params.port.send(BackgroundUseCaseMessage(data: result));
}
}
Just like a regular [UseCase], a parameter class is recommended for any [BackgroundUseCase]. An example corresponding to the above example would be
class MatMulUseCaseParams {
List<List<double>> mat1;
List<List<double>> mat2;
MatMulUseCaseParams(this.mat1, this.mat2);
MatMulUseCaseParams.random() {
var size = 10;
mat1 = List<List<double>>.generate(size,
(i) => List<double>.generate(size, (j) => i.toDouble() * size + j));
mat2 = List<List<double>>.generate(size,
(i) => List<double>.generate(size, (j) => i.toDouble() * size + j));
}
}
abstract class AuthenticationRepository {
Future<void> register(
{@required String firstName,
@required String lastName,
@required String email,
@required String password});
/// Authenticates a user using his [username] and [password]
Future<void> authenticate(
{@required String email, @required String password});
/// Returns whether the [User] is authenticated.
Future<bool> isAuthenticated();
/// Returns the current authenticated [User].
Future<User> getCurrentUser();
/// Resets the password of a [User]
Future<void> forgotPassword(String email);
/// Logs out the [User]
Future<void> logout();
}
This repository should be implemented in Data layer
class DataAuthenticationRepository extends AuthenticationRepository {
// singleton
static DataAuthenticationRepository _instance = DataAuthenticationRepository._internal();
DataAuthenticationRepository._internal();
factory DataAuthenticationRepository() => _instance;
@override
Future<void> register(
{@required String firstName,
@required String lastName,
@required String email,
@required String password}) {
// TODO: implement
}
/// Authenticates a user using his [username] and [password]
@override
Future<void> authenticate(
{@required String email, @required String password}) {
// TODO: implement
}
/// Returns whether the [User] is authenticated.
@override
Future<bool> isAuthenticated() {
// TODO: implement
}
/// Returns the current authenticated [User].
@override
Future<User> getCurrentUser() {
// TODO: implement
}
/// Resets the password of a [User]
@override
Future<void> forgotPassword(String email) {
// TODO: implement
}
/// Logs out the [User]
@override
Future<void> logout() {
// TODO: implement
}
}
If the repository is platform-related, implement it in the Device layer.
Defined in Domain layer.
class User {
final String name;
final String email;
final String uid;
User(this.name, this.email, this.uid);
}
Checkout a small example here and a full application built here.