Android - A Cleaner Clean Architecture
https://hmh.engineering/cqrs-pattern-94c27d4b9a68

Android - A Cleaner Clean Architecture

It has been a while now since Clean Architecture was out, and even many of us started embracing hexagonal (ports and adapters) architecture in our projects due to the famous Spotify model and how the squads structure affect the project ... as we need to handle all those dependencies between the squads

And I think we have become used to those architecture patterns, but every pattern has it's pros and cons ... and even if we do not feel those cons right now, as we got used to it in our day to day tasks, there are still other solutions available that we may need to give them a try

Hint: the upcoming topics maybe known very well in server side development, but for Android they maybe new for most of us

Pain Points In Our Daily Tasks

1- Investigating Market crashes

2- Maintaining Dependencies across all layers / Modules of our Architecture, and Testing with Test Doubles / Mocks

3- Moving to Declarative UI pattern (MVI or Compose)

4- Having one-liner Use-Cases or Repositories, classes that does nothing but just call one line from another class ... and most probably such classes exist either to follow some guidelines, or for testing (although the original class can be mocked in tests)

5- Too many coding in General, while Simplicity should be the key to a healthy code base

The Next Big Thing

For approaching those problems while maintaining the pros of the current architectures benefits, we have a combination of patterns that we can use on Mobile side, those patterns collectively together make our lives much easier

please note that this is a personal point of view and still under progress, it will be interesting if you start thinking about those solutions as well

Pipes And Filters Architecture

This Architecture, along with Event Sourcing & CQRS, and Declarative UI through MVI pattern (or Jetpack Compose) ... we can take the Android Development to a whole new level ... will come to each point in details, but as a glimpse of a real life example of such architecture, this is a source code for a real login/signup Activity / Fragment :

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)
    pipeline {
        with filter ::SignUpValidator
        with filter ::SignInValidator
        with filter ::RemoteDataSources
        render<Authentication.Data> { stream, data -> drawUi(stream, data) }
    }
}        

The Architecture consists of 2 parts :

No alt text provided for this image


  • Pipes : which can be streams, like a MutableFlow or a MutableLiveData, or an RxSubject
  • Filters : which are functions that has the same type for it's input and output ... and this can be implemented wither by passing the results of the filter functions to each other (which is not that scalable), or Filters can register them selves to the pipe stream, and modify the events in this stream, so the event reaches the next filter, it will take the last event in the pipe/stream as it's input, and it will publish it's event of the same type to the same stream as an output

If you feel that things started to get complex don't worry, as in such architecture, there is an engine in its core module that is responsible for starting the Pipes and attaching the Filters to it, and start passing the events from one filter to the other ... so this part of the architecture is a small engine isolated from the day to day tasks ... more on this later, but to give you an example of a Filter, it looks like this :

class SignUpValidator : Filter {
    override suspend fun onReceive(stream: PipeLineStream) {
        // observe on stream events, and post result to it
    }
}        

so at the end of the day, all the developer has to do is implement the code in such filters, and assemble them in the Activity or Fragment, without the need to declare any dependencies in the constructors for most cases in the code ... Imagine most of The components of your project are just as simple as this function, more details on how this works later as well

CQRS & Event Sourcing

Since in Android we have an Input events and results from these events, our architecture will be implemented as 2 Pipes, input pipe, and output pipe, and the engine will make the filters get registered on both pipes at the same time

this pattern is similar to CQRS pattern, where Commands are the input events, and Queries are the result events ... and since everything that is going around is modeled in Events now, we can benefit from this by storing those events with there timing and order, and once a user reports a crash or a bug, we can retrieve those events, replay them (the engine should be responsible for this as well), and will see the exact scenario that happened at runtime and fix it ... instead of logging every where in the app to some dashboards, or walking through crashes stack-traces

Before the practical example

So the Architecture itself can be merged with Clean or Hexagonal Architecture, or it can even has each feature in it's own module (you will see how small features will be in a minute), but the important part is that, by having such an engine, our Architecture will eliminate dependencies in most filters, also will eliminate one liners, as there is nothing called repository or use case, everything is just a Filter attached to a Pipeline, and the engine is responsible for passing events around, storing them, switching between input (commands) and results (queries) ... and this will fix the market ambiguity while fixing bugs, and also eliminate many useless code ... if not simplify the components of the code base (everything is a filter function now)

Where to Start

First thing we define our Commands And Queries for our feature, also we define the state of the feature (all the data required for this feature to work) ... as the state is the only place where we can declare our fields for our feature ... this is the Authentication file in our core module :

sealed class Authentication {
    abstract val data: Data

    sealed class Command : Authentication() {
        class SignUp(override val data: Data) : Command()
        class SignIn(override val data: Data) : Command()
    }

    sealed class Query : Authentication() {
        class SignUp(override val data: Data) : Query()
        class SignIn(override val data: Data) : Query()
    }

    data class Data(
        val userName: String? = null,
        val password: String? = null,
        val token: String? = null,
        val error: Throwable? = null
    )
}        

Next we need to implement our use-cases in Filters, like this :

class SignUpValidator : Filter {
    override suspend fun onReceive(stream: PipeLineStream) {
        // handle signUp
    }
}

class SignInValidator : Filter {
    override suspend fun onReceive(stream: PipeLineStream) {
        // handle signIn
    }
}        

Now how will the Filter use the stream parameter

as this is an implementation detail for the engine I have, this can be different in your project, but the stream in the current engine has 4 main methods :

class PipeLineStream(...) {

    
    fun cancel(...) {
        // stops the events flow in the current pipeline, 
        // like in case of error    
    }

    fun postCommand(...) {
        // put a Command event in the Commands pipeline
    }


    fun postQuery(...) {
        // put a Query event in the Queries pipeline
    }


    fun onReceive(handler: (T) -> Unit): Filter {
        // invoke the handler when an Event is observed in 
        // any of the 2 pipelines
    }


}        

So now any filter takes the stream as a parameter, and passes it's handler function to the onReceive(), and it can either respond to the events in the pipeline, it can stop the current pipeline, it can post a command or a query to the pipelines ... or it can do nothing so that the event passes to the next filter by the engine

so the SignUp use-case will look like this :

 class SignUpValidator : Filter {

    override suspend fun onReceive(stream: PipeLineStream) {
       
        // listen on Authentication.Command.SignUp  command/input event
        stream.onReceive<Authentication.Command.SignUp> { command ->
       
            if (isInvalidCredentials(command.data)) {
       
                // stop the events in the pipeline
                stream.cancel() 
       
                // send a query/result event holding error
                stream.postQuery(Authentication.Query.SignUp(error()))
            }
        
            // else the event will pass to the next filter normally        

        }
    }

    ...
}        

as you can see Use-case Filter does not need to depend on repositories any more, which makes it useless to create repositories, and makes testing use-cases as simple as just putting a command in the pipeline, and watching for the event after passing by this filter, without any dependencies of any kind (remember the engine is part of the core module)

same for the final step which is the Data-Source :

class RemoteDataSources(
    private val api: RetrofitService = RetrofitService()
) : Filter {
 
    override suspend fun onReceive(stream: FeatureStream) {
        
        stream.onReceive<Authentication.Command.SignIn> {
        
            stream.postQuery(api.signIn(it.data.userName,it.data.password))
        
        }

        
        stream.onReceive<Authentication.Command.SignUp> {
        
            stream.postQuery(api.signUp(it.data.userName,it.data.password))
        
        }
    }
}        

as Data-Sources module depend on the Domain module, and since repositories are not needed any more, our data sources will be as simple as a Filter triggering Retrofit or Room when it received the command declared in the domain module (Authentication file), and sends a query event with the result State

and the Final step is assembling the pipe line in the Activity/Fragment

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)
 

    // assemble a pipe line 
    pipeline {
 
       // put first filter to receive events
       with filter ::SignUpValidator  
 
       // put next filter to receive events
       with filter ::SignInValidator 
 
       // put next filter to receive events
       with filter ::RemoteDataSources 
 
       // put last step: render the state when receiveing a query
       render<Authentication.Data> { stream, data -> drawUi(stream, data) }
    }
}

private fun drawUi(
    stream: PipeLineStream,
    data: Authentication.Data
) {
    ...

    signInButton.setOnClickListener {

        // send a command with the user action
        stream.postCommand(
            Authentication.Command.SignIn(
                data.copy(
                    userName = userName.text?.toString(),
                    password = password.text?.toString()
                )
            )
        )
    }

    signUpButton.setOnClickListener {
 
        // send a command with the user action 
        stream.postCommand(
            Authentication.Command.SignUp(
                data.copy(
                    userName = userName.text?.toString(),
                    password = password.text?.toString()
                )
            )
        )
    }
}        

Conclusion

Combining Pipes and Filters, CQRS, Event Sourcing, and Declarative UI pattern (like MVI or Compose) together introduced an Engine that handles Concurrency for you, eliminates the need to handle dependency injection across all layers/modules, made the code much more concise and as simple as writing only the business/UI logic, eliminated one-liners as the engine by-pass events if not stopped, and made it easy to re-run the user actions on the market as if the user is there with you

And the most important thing of them all, is that it can be composed with any existing Architecture (like Clean or Hexagonal), or it can have it's own structure as long as it maintains the right dependency rules (the engine is part of the domain, and the domain does not depend on any thing)

The sample code is in this link :

https://github.com/Ahmed-Adel-Ismail/GDG-Helwan-MVI-2021/tree/feature/cqrs/app/src/main/java/com/mvi/sample/cqrs

please note that the code shared in the article is modified for the sake of example, and the whole idea is still in progress, as we need more people to join and work more on the Ideas, to help simplify the Android Projects, without having to make big frameworks and many base classes ... just a small engine in the core module and that's it

* Important notice: Pipes and Filters, Event Sourcing and CQRS are much more than what is shown here, I just took the essence of those patterns, but not the actual patterns

Tanany A.

Android @Robusta | Building Mobile Apps to Solve Real World Problems | Turning Ideas into Engaging Apps | Care about Innovation | Love building new things, thrive in ambiguity and even failure.

8 个月

Hey Eng Ahmed, thanks for sharing this innovative architecture! Do you have any thoughts on using it in a production environment? Specifically, does it live up to the expectation of improving crash handling and reducing boilerplate code in a production app?

赞
回复
Yuliya Maltaki

Развиваю бизнес в компании Law firm "Vetrov and partners"

1 å¹´

Ahmed, ??

赞
回复
Saeed Al-obidi

Software engineer || full-stack web developer @ Youstudy

2 å¹´

?

Araib Shafiq

Project Manager | Creative Lead at Ideofuzion

2 å¹´

I can't wait to try this out as the whole Clean MVI direction currently is 50% boilerplate which just gets on my nerves sometimes ??

Ahmad Hamwi

Mobile Software Engineer II @ noon

2 å¹´

This is actually really interseting! I loved how you're introducing new patterns to the Android developement. I'm a big fan this and I'll be following any new updates on these topics from you.

要查看或添加评论,请登录

Ahmed Adel Ismail的更多文章

  • Sharing data across multiple Mobile Squads - with examples

    Sharing data across multiple Mobile Squads - with examples

    Earlier I shared an article suggesting a solution to a common problem with teams following the "Spotify Model", which…

  • SDD - Squad Driven Design

    SDD - Squad Driven Design

    Working in multiple big teams I've found that we are always trying to apply our known best practices in software, but…

    4 条评论
  • Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Before we start, this article requires basic knowledge about the following topics : Clean Architecture Unit Testing…

    10 条评论
  • Beyond Functional Programming

    Beyond Functional Programming

    In the Android industry, lately functional programming was the all new stuff to learn, RxJava, Kotlin, and the whole…

    7 条评论
  • Dependency Injection in Clean Architecture

    Dependency Injection in Clean Architecture

    After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers…

    18 条评论
  • Meta Programming in Android

    Meta Programming in Android

    Year after year we are getting rid of the boilerplate code that we need to write for small and simple tasks in Android,…

    2 条评论
  • MVI Pattern For Android In 4 Steps

    MVI Pattern For Android In 4 Steps

    Lately I wrote an article about MVI pattern, but as we are facing new problems every day and face more use-cases, we…

    7 条评论
  • Agile - Moving Fast

    Agile - Moving Fast

    We always here about Agile, and think about which methodology do we use, what practices do we have, team velocity…

    1 条评论
  • Kotlin Unit Testing with Mockito

    Kotlin Unit Testing with Mockito

    I've always believed that, if the code is designed to be tested, we wont need any testing framework or library ..

    17 条评论
  • MVI - Model View Intent simplified

    MVI - Model View Intent simplified

    I have been searching for a proper resource to explain the MVI pattern, but every time I get hit with Hannes Dorfmann…

    4 条评论

社区洞察

其他会员也浏览了