... This README is in WIP...
I am not the owner of the truth, this repo code, and README.MD texts are based on my knowledge and views, feel free to disagree, open issues, or just say that I am wrong.
The internet is a place full of very critical people who think that understand how software should be developed, I am no different, I think I know, and to show it, I am playing around with this project. I would love to receive some constructive feedback about this project.
My goal is just to learn and maybe share some knowledge/views while also being a bit cranky.
This project is meant to try to show a pluggable architecture in an Android app, and you will hate it!
Lots of interfaces are created with that goal in mind, yeah, that is right, lots of interfaces, interfaces everywhere, for everything! And I mean it, even for your model classes!
Well, if you just develop software because you want to deliver something for today, and don't need to worry about the next 6 months or 2 years, just move on and close this browser tab. Don't waste your time reading me.
If you think that software can be developed in a way that would make it easier to keep working for years, even though parts of it need to be changed from time to time and also while those changes happen, stick around.
One thing that you may be sure of is, technologies will change with time, and making your code "pluggable" will allow adoption of those new shiny techs with relative ease.
Four years ago we were sure that RxJava was here to stay, three years ago LiveData was here to stay, and now it is coroutine Flows...
"Ahh, but that is just one exception, it won't change again..."
- findViewById -> Android Annotations -> Android Extensions -> Data Binding -> ViewBinding -> Compose
- GSON -> Moshi -> Kotlin Serialization(1)
- URLConnection -> Volley -> OkHttp -> Ktor(1)
- Direct access to the Http client -> Retrofit -> KtorFit(1)
- God Activity -> God Fragments -> Presenters -> ViewModels
- ContentValues -> DbFlow -> Room/ Sqldelight(1)
- Manual DI -> Dagger2 -> Dagger Android -> Hilt -> Koin(1)(2)
(1) Multiplatform libraries as a differential over other options have been a cause for migrations
(2) I like pure Dagger better, you just have more freedom.
Have I made my point clear? All those libraries are implementation details inside your app. If you plan to make any migration of those or others, it is wise to limit your changes to isolated parts of your app and possibly keep the old and new versions working at the same time. To limit the risk of one implementation impacting another, you should keep all implementations while you ensure the new implementation does not bring regressions.
Back to this project, I am implementing this project using some ideas borrowed from Hexagonal Arch, but I didn't like naming everything ports and adapters, so I kept the names that I am used to. If I understood the basic idea of Hexagonal Arch, each port is an interface, and each adapter is an implementation to that interface, so you can swap implementations as needed.
If you dig around this project code, you will notice that the feature code is split into multiple modules, most of them easily
recognizable
by their names, but there is this abstraction
module that you may find a bit odd.
abstraction
modules are meant to be the blueprint of your feature, so, only interfaces are allowed there, and most
feature modules
should depend on it.
"But on the arch XYZ that means we are exposing too much to all layers!"
As I said before "blueprint for the feature", the emphasis here is on feature, with an abstraction
you can replace any layer (or all layers) of your feature
with another implementation and it should just work.
Also, if you want, you could split the abstraction
module into smaller modules by layer. I am too old and too tired for that.
In some other archs, you may relate abstraction
with the domain
layer, we can have a domain
in this project,
but it will only host implementations for UseCases, the domain
is stripped of its powers in favor of having a strong
isolation in between behavior definition and implementations.
You totally can have a pluggable arch without having an abstraction
module, just make every communication in between layers (and sometimes inside the layer) an interface and you have it, but do you implement that?
Well, with this sample project, I am trying to enforce it.
Until now, what I noticed is that many times in Android development we forget about some basic SOLID principles... Yeah
I know
I know
that SOLID
Do you remember what are they?
Do you know how to identify them or the breakage of them in an Android project?
What are the consequences of breaking those principles?
For instance,
You see a Response implementation in the UI layer, with some parsing annotations, like @SerializedName
for GSON and @Parcelize
for Parcelable for Android state restoration in the same class definition.
This means that this same model implementation is being used in multiple layers of the application with different responsibilities, more, you are exposing implementation details of one layer to another.
The UI doesn't need to know about how to parse this model as JSON, the data layer does not need (should not) know about
this model's capabilities of being parcelized, and no layer in between those two (data and UI) should know about any of
it.
"Why? Why not?"
Well, I for sure don't think too much about the "Single Responsibility Principle", but I do think that something stinks because tomorrow you will migrate from Gson to Kotlin Serialization in the project, and at that moment you will have two options.
- Go all in removing the Gson annotations and adding the new ones.
- Keep both annotations in the same model.
In 1, you may break something in production and there is no way to switch back to the old implementation. In 2, you may be able to switch between implementations but two sprints later when you decide to remove the old annotations you need to edit all those models again bringing the possibility of breaking something again.
If the models you pass around your layers were interfaces, they would be implementation details free (hopefully).
Your Response model would implement that model interface, your Parcelable model as well, each on its layer, and every layer in between would see only the interface.
With that we have a 3rd option for the migration above, you can keep the old models with GSON annotations in a package and the new models with Kotlin Serialization in another package. That allows switching between them and also an easy removal when the moment comes.
... This readme is still in WIP, come back later...
- OMDB API KEY here, place it in
api.properties
apiKey=YOUR_API_KEY
- GitHub Personal Access Token, get it in here, place it on
github.properties
github.user=YOUR_GITHUB_USER
github.token=YOUR_GITHUB_TOKEN