ShadyBoukhary / flutter_clean_architecture

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.
https://pub.dartlang.org/packages/flutter_clean_architecture
MIT License
688 stars 173 forks source link
clean-architecture clean-code dart dart2 dartlang design-pattern flutter flutter-package hacktoberfest uncle-bob

flutter_clean_architecture Package

CI

Overview

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.

Installation

1. Depend on It

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_clean_architecture: ^6.0.1

2. Install it

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.

3. Import it

Now in your Dart code, you can use:

import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';

Flutter Clean Architecture Primer

Introduction

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.

The Dependency Rule

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.

Layers

Domain

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.

Contents of Domain

Domain is made up of several things.

Domain represents the inner-most layer. Therefore, it the most abstract layer in the architecture.

App

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.

Contents of App

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.

Data

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.

Contents of Data

Device

Part 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.

Contents of Data

Folder structure

lib/
    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

Example Code

Checkout a small example here and a full application built here.

View and ControlledWidgetBuilder

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);
                }
              ),
          ],
        ),
      ),
    );
}
Responsive view state

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()}');
                }
              ),
            ],
          )
      );
}
Widgets with Common Controller

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),
        ),
      ),
    );
  }
}

Controller

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();
  }
}

Presenter

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);
  }
}

UseCase

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);
}
Background UseCase

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));
  }
}

Repository in Domain


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.

Entity

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.

Authors

Shady Boukhary Rafael Monteiro