Flutter provides a set of rich and concise APIs that allows us to target multiple platform with single codebase. However targeting multiple platforms is a tough job on its own as front-end ecosystem is pretty much fragmented and can derail the whole project if not done right. So we’ll be addressing two major hurdle that needs to be overcome when developing an enterprise application with flutter. As an end goal we want our resource v/s time graph to look like below to ensure that we are on the right track.
Problem 1 : Responsiveness is not enough
Although flutter ships responsiveness straight out of the box and can adapt to different screen sizes easily, it is still not enough as different platform may take different path of evolution. In other words, designers might want to give different user experience on different platform on the basis of screen sizes. For example the billing and shipping step during a typical checkout process can be merged into a single screen while targeting platform with bigger screen sizes. Similarly, it can also be splitted into separate screens while targeting mobile platform with smaller screen sizes.
Problem 2 : Business logic needs to be composable
As different platform can evolve differently, we might end up with different UI per platform for same feature. So we want reuse the business logic in such a way that we can compose the existing one to create bigger business logic. For instance, referring to above example of billing and shipping process, we would require individual business logic for handling billing and shipping tasks when the process is splitted. Similarly, when the process is merged as a single feature we want to compose the existing business logic instead of writing it from scratch. This allows us to reuse the already tested business logic and integrate with minimum effort and time.
Solution : Adopting Domain Driven Design
Domain Driven Design is set of principles that pushes domain i.e. business logic/policies at the core and rest of implementation detail layered on top of it on different level. There are different variants of this approach such as Onion Architecture, Ports and Adapters, Hexagonal Architecture and Clean Architecture. Although the overall intention is the same, we’ll be considering clean architecture for demonstration as it is fairly renowned in mobile development community.
The layer depicts the different level at which a component may reside upon. This allows outer layer to depend on the inner layer and hold “Has-A” relationship. This relationship compliments Inversion of Control (IoC)as outer layer are more kind of implementation detail and inner layer are more kind of higher level policies. So, if any inner layer wants to communicate with outer layer, we define an abstract type in inner layer and enforce outer layer to implement it, thus maintaining IoC (Example is shown in the “Application Layer” section).
Similarly, multiple component may also reside in same level such as Presentation Layer and Data Layer in the above diagram. This indicates that the following components can directly depend on the same inner layer i.e Application Layer.
It consists of group of related plain classes called Entity(classes defined by types and primitives offered by its language specification only with no references to any particular library/frameworks) that communicate with each other using public method calls.
Most of the time while developing mobile app, all core business operation are carried out on server side. So we don’t have to work on this layer that often. However for illustration, let us consider we are developing a Loan App, where we calculate the EMI for the loan taken. So, the core formula to calculate EMI is expressed as the behaviour of Entity in Domain Layer.
In this layer we abstract the underlying data source such as remote server, file-system or database. The benefit of abstracting data source is that change in underlying data persistence mechanism doesn’t affect behaviour of our application as it gets decoupled.
Presentation Layer :
In presentation layer, we add a component called “Presenter” that exposes the states which the widget tree depends upon and also public APIs (methods) to receive event calls from UI.
The “Widget Tree Layer” resembles the Widget/UI tree that is controlled by the framework.
Application Layer :
This layer is responsible for orchestrating all related component to fulfil an interaction either expressed by user or in response to some other events. Hence it is also called usecase/interactor. More concisely it expresses “What” intent rather than “How” intent. For example :
“We want to post user information when user presses a submit button”
So the usecase/interactor will know what to post on the response of button tap event but doesn’t know exactly how to post to remote server. For this it delegates the responsibility to corresponding layer that abstracts underlying data source for example a Data Access Object or Repository or any other similar design pattern.
Notice the arrow direction in above figure, the black arrow represents “Has-A” relationship. Out of all arrow direction, the one from Application Layer to Data Layer violates IoC as the inner layer is directly referencing the outer layer. So to solve this we introduce an abstract type and maintain IoC as below.
Hence, the introduction of “Abstract Type” in between Data Layer and Application Layer maintains the IoC. Here, “Is-A” refers to inheritance relationship and “Uses”/”Has-A” refers to composition relationship.
This high level overview gives a structural glimpse of our application. Now we proceed to address the two problems that were stated in the beginning.
Solution to “Responsiveness is not enough”
To address this problem, we will maintain separate UI+Presentation Layer according to our need. For instance, let us consider the following cases with their corresponding separations:
Case 1 : Grouping according to screen sizes
Separations : Mobile, Desktop, Web
Case 2: Grouping according to platform
Separations : Android, Ios, MacOS, Windows, Linux, Web
Although managing separate UI+Presentation for targeted platform seems to be a tedious task at first but in terms of flexibility and co-oping with hard requirement change, it definitely yields reward later on as non-UI responsibilities are segregated to other layers.
Solution to “Business logic needs to be composable”
To address this problem, we want the Application Layer to implicitly support composability. In other words, we want the use-case/interactors in this layer to be composable just like different lego pieces can be put together to make a complex lego structure.
The easiest way to achieve this in dart is to express our use-case in terms of Streams due to its interoperability with RxDart. RxDart puts Streams on steroid and provides useful utilities function on top of it to make our use case composable.
So, lets see how it is constructed for the sample use case from the beginning of the article “Billing and Shipping process” with requirement specification as below:
“For mobile, breakdown the process into two step on separate screens while on web implement it as a single process on single screen.”
The above diagram is suffice enough to define our required use-cases. For me, I always tend not to leak responsibility to other layers and avoid duplication of business logic as a rule of thumb when writing use-cases.
The base use-case is expressed as a generic class that is parameterised on type P and R where it denotes the Param and Result respectively. It also defines an abstract method “buildUseCase” which its subclass must override. For simplicity of the example, the execute method helps to invoke the corresponding use-case from the presentation layer (This method is intended to compliment the presentation layer and its behaviour can be adjusted accordingly).
Here, the composited “PostBillingAndShippingInfoUseCase” use-case is executed from the presentation layer of web platform where as “PostBillingInfoUseCase” and “PostShippingInfoUseCase” are executed from their corresponding individual presentation layer of mobile platform.
Now lets further extend the use-case with some hypothetical requirement added later in time and stated as below:
“The billing and shipping address information needs to be identical.”
In other words, after posting billing and shipping information, we need to validate the address provided in different format mentioned in both and allow user to proceed only if we get success response from the server.
To meet our requirement, we will be reusing the existing one to derive a new use-case instead of modifying it and respect Open-Closed principle.
So lets add new “ValidateBillingShippingAddressUseCase”, “PostShippingAndValidateBillingShippingAddressUseCase” and “PostBillingShippingInfoAndValidateAddressUseCase” use-cases. The following UML class diagram depicts the mentioned changes.
Here, the composited “PostBillingShippingInfoAndValidateAddressUseCase” use-case is executed from the presentation layer of web platform where as “PostShippingAndValidationBillingShippingAddress” is executed from presentation layer of shipping screen of mobile platform.
A complex use case may have nested level of composition. Then we resort to dependency injection library to decouple the use-case creational logic from presentation layer and is left out as an exercise. :)
As they say the only thing that is constant in software development is “change” and so far we’ve only emphasised on the structure of the application. In my next writing we’ll cover how this way of structuring our application promotes Test Driven Development.
A stripped down version of source code with no aesthetics is available on github highlighting the above mentioned ideas.