The app displays a list of comics by consuming the marvel api. On clicking any list item it opens the details screen (which can be collapsed). The list view has a EditText inside a TextInputLayout from material design which makes it stay on top while the list is scrolled. This EditText is used for searching the comics by title name.
It uses code generation libraries like Dagger2, Databinding, Room. So please kindly gradle-sync the project first and build it once. Else you might find unknown symbol references in the code base. If required, "invalidate caches and restart" using the 'File' menu option in the Android studio.
- Kotlin
- Rxjava2
- RxRelay
- Dagger2 (Dependency injection)
- Coroutines (extension)
- Glide (Image library)
- Navigation (Android architecture component from jetpak)
- DataBinding (Android architecture component from jetpak)
- ViewModel (Android architecture component from jetpak)
- LiveDate (Android architecture component from jetpak)
- PagedList (Android architecture component from jetpak)
- Room (SQLite backed DB for persistence and the automatic PagedList DataSource support using paging library's BoundaryCallback)
- Retrofit2 (For service calls)
- okHttp (For Network layer, intercepting http logs and intercepting to add apiKey to query parameters for every service call, we make)
- Espresso (AndroidJUnit4ClassRunner for UITests)
- JUnit4 (For unit tests)
- Instrumentation tests for the RoomDB Dao classes
- Automatic Master-detail layout switching for tablets and large screen devices
The project has been split into 9 modules, listed below from top-down order according to the application flow:
-
app - Android app module which has a SplashActivity and the UI tests (espresso) and the necessary, It is the module which has application class and provides the dependency injection for all the other modules using Dagger2. The android test module mocks the "Dagger-Modules": Remote, Local and Domain and overrides the retrofit fit service to provide the data from a fake service, "FakeComicsService". It also implements a NetworkStateIdlingResource to trigger the testing only after the network state is LOADED and the UI is in an idle testable state.
-
feature-home - Android Library module. This is the feature module and contains all the UI logic+layouts for the two screens (list and detail).
It consumes the ViewModel from the 'presentation' module and listens to the various network states that the 'data' module emits and reacts to them by updating the UI appropriately. It listens to the LiveData constructed from the DataSourceFactory emitted by the local Room DB layer. The paged list is then used to update the adapter backing the recycler view. There are two views and two bindings are generated using the data binding library to bind the UI layouts with the Fragments. Additionally, because a recycler view is used, there is an ItemViewBinding generated for the item views in the recycler view. You can find these classes in the appropriately named packages in this module. Actually, the DataSourcefactory is generated by the Room sql query based the search text entered from UI layer and passes via several layers before reaching the view model where it is converted to the LiveData<PagedList. Each layer has its own model class to reduce strong coupling between layers and support additional transformations in each layer. Example, in 'localdata' module calls the model as ComicsLocal which has annotated fields for providing meta info for the RoomDB, where as the 'domain' or the 'feature' layer does not need those annotations so they use their own format. The network layer has a lot of other models to map the response received from the api call and transforms to a stripped down version just appropriate for the next layer above ('data') to consume. If you can notice carefully, there are two different bindings bigImageUrl and imageUrl used for the Imageviews in the details screen and the list view screen respectively. This enables us to download and maintain two different image sizes for the two screens so that the list view uses a smaller icon size for the images while the full screen details screen uses a bigger image. The detail view is designed using the CoordinatorLayout, NestedScrollview(body) and CollapsableToolBarLayout(header), so that it can be scrolled up to fade and collapse. The 'feature-home' module also uses the navigation component for the navigation of the screens. It uses the databinding to bind the data it got from the viewmodel to the actual view, reducing much of the boiler plate code. It also handles the Tablet and Phone specific layouts.
-
presentation - Android library module. This module has the ViewModel component which survives the lifecycle changes and provides the UI layer ('feature-home' module) with the fresh PagedList for items via LiveData to bind with the UI. It uses coroutines to do the room operations in bgscope. It also maintains the state for the progress bar layout which the UI layer uses to bind using the data binding component. It also has unit tests to cover the working for the Livedata that the UI layer relies on. When the search action is triggered by the UI, the viewmodel uses the use case from the 'domain' layer to fetch the new DataSourceFactory for the search string. Along with datasource it also fetches the BoundaryCallback implementation from the 'data' module in the same call to the 'domain' usecase. The data source together with the boundary callback together is then used to create a LivedData for paged list which will be observed by the 'feature-home' layer.
-
domain - Kotlin library module. This is the core of the app containing the use cases for the app. If necessary we can have several domain modules for each feature for scaling the team according the business or organization need. This layer has the non android specific implementation and defines contracts for the layer above('presentation') and below ('data') It calls the 'data' module layer to fetch or trigger the reactive flow of the data from the below layer(data) to the top(UI). It does not use any android specific api except for DataSourceFactory type used as a return type from the paging library. It uses only RxJava so it is away from any platform dependencies hence following the CLEAN architecture principle. It has Junit tests for all the usecases provided in this layer.
-
data - Kotlin library module. This is an abstraction following the Repository pattern. It hides the actual source of the data. It encapsulates the Local and Remote data sources from the layers above. It defines the contract for the local and remote repositories. It does not use any android API. It is a pure kotlin module. This layer provides the implementation of the BoundaryCallback class required for triggering the fetch of the new data from the PagedListAdapter. But it has no reference to the android specific apis and does its job using the local and remote data source abstractions defined as interfaces for the respective layer to abide and implement. It has unit tests covering all the methods exposed by this layer.
-
localdata - Android library module. This layer implements the LocalRepository interface contract defined in the 'data' module layer. It uses RoomDB API provided by the android for persistence. It provides the CRUD operations via Dao's and Entity definitions that the Room compiler understands. It has the instrumentation tests using in-memory db and unit tests required for the API exposed in this layer. It supports the app to run in offline.
-
remotedata - Kotlin module. This layer implements the RemoteRepository interface of the 'data' module layer. It uses Retrofit api and the okHttp as the client for the API calls. The comics endpoint of the marvel is used for the serving the comics list. The API keys(public and private) are provided in the gradle.properties file and are available as BuildConfig defined in the 'app' module's gradle file. Because this is a non-android pure kotlin module, the properties required for the retrofit are provided via Dagger from the 'app' module via dependency injection. This module has two versions of fetching the list as the marvel api does not allow searching with empty key. If a non-empty search key is available it uses the "titleStartsWith" query param of the comics endpoint for fetching the results else it makes a call without the 'titleStartsWith' query param. It has tests covering all the APIs this module expose.
-
utils - Kotlin library module. This is a small module for helper classes. It provides the BehaviorRelay wrapper singleton for tracking the network state and errors in the network across the modules (android and non-android).
The project has been structured with scalability in mind. The structure can be expanded. The feature-home module has its own layouts in the res folder and this abstraction is good for not separation of concerns. We can even use the feature of dynamic-feature-modules to provide the code on demand and for reducing the initial apk size. We can have several such presentation, feature and domain modules for having separation of concerns and even be moved to separate repos and consumed as a library file by other teams, so that the build time can be significantly reduced. It is even simple to AB test features and then enable a module to be available for the public.
-
android-utils - Android library module. It provides the connectivity state monitoring Livedata which monitors the change of network availability. This live data is used in the 'feature-home' module to detect the change in the network state and act accordingly. The ConnectivityMonitorLiveData is a singleton injected via dagger to the Activity in the 'feature-home' modules.
The NetworkStateRelay in the 'domain' layer is a domain level abstraction of the network states that the application should handle. It is implemented using the RxRelay. It is injected via dagger. The ComicsListFeatureFragment in the 'feature' module uses the state to change the UI. The relay is basically pushed from 2 places:
- ComicsListFeatureActivity - sets the initial state to EMPTY and then pushes the CONNECTED/ DISCONNECTED state based on the connection availability.
- ComicsListBoundaryCallback - sets the LOADING/LOADED/ERROR state based on the service API call status.
- app (includes the UI test for the feature module and dagger dependency injection modules and the application component)
- feature-home (contains the UI for the feature)
- presentation (contains the viewmodel. It has the unit test for the comicsListSource livedata that emits the pagelist of the comics entities)
- android-utils (contains the connetivity state change helper)
- localdata ( contains the roomdb. It has both instrumentation test and the unit tests)
- domain (contains the usecases. It has the unit tests covering all the use cases)
- data ( It is an implentation of the repository pattern. It supplies data to the domain with out revealing the source of the data. It has unit tests.)
- remotedata ( It is the service layer implemented using the retrofit and okhttp library. The okhttp has apikey injection interceptor and the http logging interceptor. It has unit tests for all the apis it exposes to the data layer)
- utils (Contains the utility functions and the Mapper interface which is used in the other layers to convert the models from one layer specific type to another)
I have used the RXJava and RxRelay to communicate between the android and non-android modules.
The versions of all the external libraries used are maintained in the versions.gradle file in the root of the project. So we can fiddle with the various library versions, and also the minSdk, targetSdk and compileSdk versions easily.
The app starts with a splash activity in the 'app' module and after a delay launches the ComicsListFeatureActivity in the 'feature' module. The ComicsListFeatureActivity sets the domain level network state to EMPTY via the NetworkStateRelay which got injected via Dagger from the 'domain' module. The activity then sets listeners to the connectivity changes to communicate to the other systems via the NetworkStateRelay. It then uses the res boolean value isTablet to choose between the landscape (master-detail style ) layout or the portrait (single fragment at a time). It uses the viewmodel from the 'presentation' layer to see if there is a selected comics from the list to populate the details fragment in the master-detail layout if the device is a tablet. It uses the navHostFragment of the navigation component to deal with these fragment transactions.
The ComicsListFeatureFragment listens to the network state changes and as it receives the EMPTY state change triggered by the ComicsListFeatureActivity, it loads the search with the current query set. If no query is set it uses the default query which is "Avengers".
The query is sent to viewmodel in the 'presentation' module, the viewmodel uses the GetComicsListAction usecase from the 'domain' module. The GetComicsListAction uses the ComicsRepository defined in the 'data' module to get the DatasourceFactory and the BoundaryCallback necessary for generating the LiveData of Pagelist of ComicsEntity to be displayed in the list view. The BoundaryCallback is defined in the 'data' module itself, where as the DatasourceFactory from the Room DB defined in the 'localdata' module. Each query generates a new DatasourceFactory.
Once the LiveData<PageList is received via the comicListSource in the viewmodel, the ComicsListFeatureFragment tries to populate the Recyclerview's adapter to render the view. Now there are two cases either there is no data immediately or the end of the data is reached. The BoundaryCallback handles these 2 cases via the onZeroitemLoaded and the onItemAtEndLoaded. Both cases triggers an API call action which is performed via ComicsService defined in the 'remotedata' layer. The BoundaryCallback is in the 'data' module which is a repository abstraction layer. It sets the NetworkStateRelay to LOADING state so that the ComicsListFeatureFragment in the 'feature' module can display the progressbar. The ComicsListFeatureFragment handles this by setting the isLoading Observable in the viewmodel which is binded to the progressbar view in the layout via databinding. Once the service call completes the control comes back to the 'data' module which updates the result in the room db in 'localdb' module. And then the network state is set to LOADED state so the ui layer ('feature' module) can stop the progressbar. At the same time, the updating of the result in the room db triggers an event in the Datasource listened by the viewmodel via the livedata and communicated to the observer in the ComicsListFeatureFragment with the new paged list. The UI updates the recycler view adapter and the list is shown. When the user clicks on an any item the appropriate viewholder's onClick listener is triggered. The data binding calls the onCicked method defined in the ComicsDataBindingModel class which is the binding responsible for loading the data and handle events for the particular viewholder. The onClick listener first updates the currentComics livedata in the viewmodel. The onClick uses the navigation component to perform the navigation in the single fragment layout. If it is a tablet there is no navigation performed. The ComicsDetailFragment observes for changes to the currentComics livedata from the viewmodel. So it updates it's view using the data binding.
NOTE: THE RELEASE SIGNING KEY HAS BEEN ADDED JUST FOR THE SAKE OF COMPLETION AND DEMONSTRATION. BECAUSE PROGAURD RULES ARE APPLIED ONLY ON THE RELEASE FLAVOR. THE SIGNING KEY SHOULD BE HIDDEN AND KEPT SECRET FROM OTHERS IN A SECURED PLACE AND ACCESSED VIA CI/CD PROCESS.
-keep class com.sunragav.indiecampers.remotedata.models.Comic { <fields>; }
-keep class com.sunragav.indiecampers.remotedata.models.DataContainer { <fields>; }
-keep class com.sunragav.indiecampers.remotedata.models.DataWrapper { <fields>; }
-keep class com.sunragav.indiecampers.remotedata.models.Image { <fields>; }
-keepnames class com.sunragav.indiecampers.localdata.models.ComicsLocal { <fields>; }
-keep class * extends androidx.room.RoomDatabase
-dontwarn androidx.room.paging.**
The keep rules are respected by the progaurd and the below image shows how the localdata and remotedata module's POJO models are retained without obfuscation:
The app without optimizations ( shrinking and progaurd rules) APK size is 4.6 MB and the download size is 4.1 MB
The app after the applicaton of necessary progaurd rules the APK size is 2 MB and the download size is 1.5 MB
By the way, because I have used Android studio 3.5.3 for the development, the shrinking+obfuscation using progaurd rules are directly done using the R8 compiler to output the dex. There is no intermediate optimized java byte code generated like it was previously when using D8 compiler.
I hope you understand my effort. Please feel free to reach out to me for any questions. My email id is [email protected]. Mobile: +49 15127928882 Linkedin: https://www.linkedin.com/in/sunragav/