एक फ्लटर पैकेज जो फ्लटर में अनकल बॉब द्वारा स्वच्छ आर्किटेक्चर को लागू करता है। यह दृश्य, नियंत्रक, प्रस्तुतकर्ता, अवलोकनकर्ता और उपयोग मामले प्रदान करता है।
सार्वजनिक जानकारी से बना चयन मार्गदर्शन। केवल संदर्भ हेतु।
एक फ्लटर पैकेज जो फ्लटर एप्लिकेशन में अंकल बॉब के क्लीन आर्किटेक्चर को लागू करता है, जो दृश्य, नियंत्रक, प्रस्तुतकर्ता, अवलोकनकर्ता और उपयोग केस जैसी अच्छी तरह से परिभाषित लेयर के माध्यम से चिंता के विभाजन को बढ़ावा देता है। यह व्यवसाय तर्क को UI और डेटा लेयर से अलग करके कोड की रखरखाव, परीक्षण और स्केलेबिलिटी को बढ़ाता है। पैकेज एंड्रॉइड, आईओएस, मैकओएस, विंडोज और लिनक्स पर क्रॉस-प्लेटफॉर्म विकास का समर्थन करता है, जिससे यह संरचित कोड संगठन की आवश्यकता वाले बड़े पैमाने के प्रोजेक्ट्स के लिए आदर्श होता है।
उपयोग
बड़े पैमाने के फ्लटर एप्लिकेशन
टेस्ट-ड्रिवन डेवलपमेंट
टीम-आधारित प्रोजेक्ट सहयोग
जटिल व्यापारिक तर्क को बनाए रखना
क्रॉस-प्लेटफॉर्म ऐप विकास
मुख्य विशेषताएँ
चिंता के स्पष्ट विभाजन
निर्मित डेपेंडेंसी इंजेक्शन समर्थन
मॉड्यूलर और पुनः उपयोगी घटक
आधुनिक Flutter अभ्यासों के साथ एकीकरण
UI, व्यापार तर्क, और डेटा के बीच स्पष्ट परतदार संरचना
इनके लिए उपयुक्त
संरचित आर्किटेक्चर की तलाश में विकासकर्ता
कंपनी-ग्रेड ऐप्स बनाने वाली टीमें
उच्च परीक्षण कवरेज वाले प्रोजेक्ट्स
रखरखाव पर ध्यान केंद्रित फ्लटर विकासकर्ता
विकसित हो रहे आवश्यकताओं वाले अनुप्रयोग
विचार योग्य बातें
शुरुआती लोगों के लिए सीखने का ढलान अधिक तीव्र है
अनुशासित कार्यान्वयन की आवश्यकता है
अतिरिक्त बॉयलरप्लेट कोड
सरल ऐप्स के लिए अत्यधिक हो सकता है
पैटर्न के सुसंगत अनुपालन पर निर्भर है
मूल README
संबंधित सोर्स रिपॉज़िटरी के मूल README का स्नैपशॉट। नवीनतम सामग्री के लिए रिपॉज़िटरी देखें।
flutter_clean_architecture Package
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.2.0
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.
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.
Entities
Enterprise-wide business rules
Made up of classes that can contain methods
Business objects of the application
Used application-wide
Least likely to change when something in the application changes
Usecases
Application-specific business rules
Encapsulate all the usecases of the application
Orchestrate the flow of data throughout the app
Should not be affected by any UI changes whatsoever
Might change if the functionality and flow of application change
Repositories
Abstract classes that define the expected functionality of outer layers
Are not aware of outer layers, simply define expected functionality
E.g. The Login usecase expects a Repository that has login functionality
Passed to Usecases from outer layers
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.
View
Represents only the UI of the page. The View builds the page's UI, styles it, and depends on the Controller to handle its events. The Viewhas-aController.
In the case of Flutter
The View is comprised of 2 classes
One that extends View, which would be the root Widget representing the View
One that extends ViewState with the template specialization of the other class and its Controller.
The ViewState contains the view getter, which is technically the UI implementation
StatefulWidget contains the State as per Flutter
The 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.
The StatefulWidgethas-aState object (the ViewState) which has-aController
In summary, both the StatefulWidget and the State are represented by a View and ViewState of the page.
The 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.
Controller
Every ViewStatehas-aController. 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.
Every Controller extends the Controller abstract class, which implements WidgetsBindingObserver. Every Controller class is responsible for handling lifecycle events for the View and can override:
void onInActive()
void onPaused()
void onResumed()
void onDetached()
void onDisposed()
void onReassembled()
void onDidChangeDependencies()
void onInitState()
etc..
Also, every Controllerhas to implement initListeners() that initializes the listeners for the Presenter for consistency.
The Controllerhas-aPresenter. 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.
The Controller has access to the ViewState and can refresh the ControlledWidgets via refreshUI().
Presenter
Every Controllerhas-aPresenter. 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.
The Presenter is comprised of two classes
Presenter e.g. LoginPresenter
Contains the event-handlers set by the Controller
Contains the Usecase to be used
Initializes and executes the usecase with the Observer<T> class and the appropriate arguments. E.g. with username and password in the case of a LoginPresenter
A class that implements Observer<T>
Has reference to the Presenter class. Ideally, this should be an inner class but Dart does not yet support them.
Implements 3 functions
onNext(T)
onComplete()
onError()
These 3 methods represent all possible outputs of the Usecase
If the Usecase returns an object, it will be passed to onNext(T).
If it errors, it will call onError(e).
Once it completes, it will call onComplete().
These methods will then call the corresponding methods of the 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
Extra
Utility classes (any commonly used functions like timestamp getters etc..)
Constants classes (const strings for convenience)
Navigator (if needed)
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
Repositories
Every Repositoryshould implement Repository from the Domain layer.
Using 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.
Retrieve data from databases or other methods.
Responsible for any API calls and high-level data manipulation such as
Registering a user with a database
Uploading data
Downloading data
Handling local storage
Calling an API
Models (not a must depending on the application)
Extensions of 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.
In the case of our application, models in the 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.
Mappers
Map Entity objects to Models and vice-versa.
Static classes with static methods that receive either an Entity or a Model and return the other.
Only necessary in the presence of Models
Extra
Utility classes if needed
Constants classes if needed
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
Devices
Similar to Repositories in Data, Devices are classes that communicate with a specific functionality in the platform.
Passed through the layers the same way 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.
Extra
Utility classes if needed
Constants classes if needed
Usage
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';
classCounterPageextendsCleanView{
@override// Dependencies can be injected hereState<StatefulWidget> createState() => CounterState();
}
classCounterStateextendsCleanViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
@overrideWidgetget 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) {
returnText(controller.counter.toString());
}
),
),
// you can refresh manually inside the controller// using refreshUI()ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
returnMaterialButton(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';
classCounterPageextendsCleanView{
@override// Dependencies can be injected hereState<StatefulWidget> createState() => CounterState();
}
classCounterStateextendsResponsiveViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
WidgetAppScaffold({Widget child}) {
returnMaterialApp(
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
),
);
}
@overrideViewBuilderget mobileView => AppScaffold(
child: Column(
children: <Widget>[
// you can refresh manually inside the controller// using refreshUI()ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
returnText('Counter on mobile view ${controller.counter.toString()}');
}
),
],
)
);
@overrideViewBuilderget tabletBuilder => AppScaffold(
child: Column(
children: <Widget>[
// you can refresh manually inside the controller// using refreshUI()ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
returnText('Counter on tablet view ${controller.counter.toString()}');
}
),
],
)
);
@overrideViewBuilderget desktopBuilder => AppScaffold(
child: Row(
children: <Widget>[
// you can refresh manually inside the controller// using refreshUI()ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
returnText('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).
import'package:flutter_clean_architecture/flutter_clean_architecture.dart';
classCounterControllerextendsController{
int counter;
finalLoginPresenter presenter;
CounterController() : counter = 0, presenter = LoginPresenter(), super();
voidincrement() {
counter++;
}
/// Shows a snackbarvoidshowSnackBar() {
ScaffoldState scaffoldState = getState(); // get the state, in this case, the scaffold
scaffoldState.showSnackBar(SnackBar(content: Text('Hi')));
}
@overridevoidinitListeners() {
// 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");
}
voidlogin() {
// 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;
classLoginPresenterextendsclean.Presenter{
Function loginOnComplete; // alternatively `void loginOnComplete();`Function loginOnError;
Function loginOnNext; // not needed in the case of a login presenterfinalLoginUseCase loginUseCase;
// dependency injection from controllerLoginPresenter(authenticationRepo): loginUseCase = LoginUseCase(authenticationRepo);
/// login function called by the controllervoidlogin(String email, String password) {
loginUseCase.execute(_LoginUseCaseObserver(this), LoginUseCaseParams(email, password));
}
/// Disposes of the [LoginUseCase] and unsubscribes@overridevoiddispose() {
_loginUseCase.dispose();
}
}
/// The [Observer] used to observe the `Stream` of the [LoginUseCase]class_LoginUseCaseObserverextendsclean.Observer<void>{
finalLoginPresenter loginPresenter;
_LoginUseCaseObserver(this.loginPresenter);
/// implement if the `Stream` emits a value// in this case, unnecessaryvoidonNext(_) {}
/// Login is successful, trigger event in [LoginController]voidonComplete() {
// any cleaning or preparation goes hereassert(loginPresenter.loginOnComplete != null);
loginPresenter.loginOnComplete();
}
/// Login was unsuccessful, trigger event in [LoginController]voidonError(e) {
// any cleaning or preparation goes hereassert(loginPresenter.loginOnError != null);
loginPresenter.loginOnError(e);
}
}
Observer shortcuts
Use Observer.fromCallbacks(...) to avoid creating an Observer class while keeping the same execute(...) flow.
import'package:flutter_clean_architecture/flutter_clean_architecture.dart';
// In this case, no parameters were needed. Hence, void. Otherwise, change to appropriate.classLoginUseCaseextendsCompletableUseCase<LoginUseCaseParams> {
finalAuthenticationRepository _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 {
finalStreamController controller = StreamController();
try {
// assuming you pass credentials hereawait _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;
}
}
classLoginUseCaseParams{
finalString email;
finalString password;
LoginUseCaseParams(this.email, this.password);
}
FutureUseCase
For single-shot requests that do not need streams (e.g. login, logout), use FutureUseCase
or CompletableFutureUseCase. The execution flow and Observer<T> callbacks stay the same.
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.
classMatMulUseCaseextendsBackgroundUseCase<List<List<double>>, MatMulUseCaseParams> {
// must be overridden@overridebuildUseCaseTask() {
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 /// neededstaticvoidmatmul(BackgroundUseCaseParams params) async {
MatMulUseCaseParams matMulParams = params.params asMatMulUseCaseParams;
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
abstractclassAuthenticationRepository{
Future<void> register(
{@requiredString firstName,
@requiredString lastName,
@requiredString email,
@requiredString password});
/// Authenticates a user using his [username] and [password]Future<void> authenticate(
{@requiredString email, @requiredString 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
classDataAuthenticationRepositoryextendsAuthenticationRepository{
// singletonstaticDataAuthenticationRepository _instance = DataAuthenticationRepository._internal();
DataAuthenticationRepository._internal();
factoryDataAuthenticationRepository() => _instance;
@overrideFuture<void> register(
{@requiredString firstName,
@requiredString lastName,
@requiredString email,
@requiredString password}) {
// TODO: implement
}
/// Authenticates a user using his [username] and [password]@overrideFuture<void> authenticate(
{@requiredString email, @requiredString password}) {
// TODO: implement
}
/// Returns whether the [User] is authenticated.@overrideFuture<bool> isAuthenticated() {
// TODO: implement
}
/// Returns the current authenticated [User].@overrideFuture<User> getCurrentUser() {
// TODO: implement
}
/// Resets the password of a [User]@overrideFuture<void> forgotPassword(String email) {
// TODO: implement
}
/// Logs out the [User]@overrideFuture<void> logout() {
// TODO: implement
}
}
If the repository is platform-related, implement it in the Device layer.
Checkout a small example here and a full application built here.
CLI Tool
The flutter_clean_architecture package includes a command-line interface (CLI) tool that helps you quickly scaffold Clean Architecture components in your Flutter projects.
Installation
The CLI is automatically available when you add the package to your project:
dependencies:flutter_clean_architecture:^6.1.0
Usage
Run the CLI using Flutter's pub command:
flutter pub run flutter_clean_architecture:cli <command>
Available Commands
Create Default Architecture Structure
Generate the complete Clean Architecture folder structure:
flutter pub run flutter_clean_architecture:cli create
This creates the following structure in your lib/src/ directory: