This is a Technical Challenge for Cabify. Product specifications are detailed here: CHALLENGE.md
(You can directly download the apk from releases)
I have solved this challenge with a very simple design, but with some important details in a Product level.
The solution is based on 3 screens:
- Store: A list of Products with their discount label, including a cart updated in real time with prices and discounts.
- Product Detail: You can acces from Store, Cart and Checkout. It provides more information about the product, with discount details and you cand add or remove cart product from here.
- Checkout: A list with cart products and suggestions to add products.
Some screenshots:
As you can check in the third screenshot, I'm showing suggestions to add a new product to the cart. The algorithm is based in the following:
- Every product in cart that have a discount, could be a suggestion to reach the discount rule.
- From all the possible suggestions, it takes one or another based on different criteria:
GetSuggestionByMinValueUseCaseImpl
: It takes the one with lower cost for the user.GetSuggestionByMinObjectsUseCaseImpl
: It takes the one with less objects needed to add to cart.
I'm not sure which algorithm is gonna work better, so I've "created" and A/B testing based on user segments:
SUG_A
:GetSuggestionByMinValueUseCaseImpl
SUG_B
:GetSuggestionByMinObjectsUseCaseImpl
You need to change manually your segment in UserRepository (It could be done from Firebase or server in a Production environment)
Some behaviors have been simplified for this Tech test, for example:
NetworkDiscountDatasource
: should get information about the discounts from the server.UserRepositoryImpl
: should get information about the user segments from a datasource (server, firebase, whatever)- Testing: I did not test for the whole project, I have only tested some parts as an example.
This project has been developed following CLEAN and SOLID principles.
- Having a clear separation between layers, as you can see in Modules explanation.
- Decoupling code using Dependency Injection.
- Using a composable pattern for navigation.
In order to have a good separation between layers (trying to impress you with a fantastic example of overengineering), I have divided the different layers and features into modules, I'm not gonna explain each module, but all of them are connected with buildSrc
plugin. I think that's a super cool way of creating modules with a clear build.gradle
, and syncrhonize project variables and dependencies.
In my opinion one of the best architecture for using compose, and having a state that is the source of thruth for the UI.
BaseViewModel
Is a good example of the way I use viewModels in a MVI architecture.
The future and the present of modern Android development. With the adventages of using a declarative approach. I feel very confident with compose, I think it greatly reduces development times, and it allow to reuse views in a super easy way.
Being honest, that's new for me. In other projects I'm using compose, but always wrapped in fragments, with their own navigation and lifecycle. This Tech-test is the first time I have the opportunity of experiment with navigation between compose views. My requirements for this research was:
- Easy navigation between modules.
- Navigation as decoupled as posible.
- MVI-friendly.
I'm very happy with the solution I've found:
NavigationManager
hasMutableStateFlow<NavigationCommand>
listened fromMainActivity
, and every call will be forwarded to navigation to other view using Jetpack Compose Navigation
I want to explain a bit about the libraries I used in this project and how:
Dependency Inversion: Hilt
Becoming the standard way of handling with coupling in Android. I previously used Koin but Hilt is having more and more support from the community.
Database: JetPack DataStore
Yes, I know I've used a Preferences library as a BBDD, I would do it with Room, but I'm bored of using Room, so I have taken the opportunity of using JetPack DataStore as the first time.
Async Flow: Flow & Coroutines
The standard way of handling async calls and make a reactive app.
Image Loader: Coil
Designed in Kotlin for Kotlin with good support for Compose. I used to use Glide but have been switching to Coil.
Serialization: Gson
One of the most popular JSON libraries for Android. But probably is time to move to Moshi. I did not use it for this test because I want to evaluate the differences in depth.
Network: Retrofit
One of the most used libraries for network calls, I feel very confident with this library, and I use it in all my projects.
Functional Programming Arrow
I did not use this library for this tech test because the benefits of functional programing are more noticeable in medium and long term projects. Arrow is one of my favorite libraries to make kotlin something more functional.
Testing Mock: Mockk
I don't know why but I have a slight preference for Mockk over Mockito.