Anyway, starting on a relatively small and new codebase meant that I could try a few things end to end and have a 100% Kotlin codebase. Here are my takes!
One of the first decisions I made was deciding on using a 2 gradle builds (debug/dev and release) setup. This meant that you could have both builds installed on one device and keep using the production build while working on new features, fixing bugs, or easily compare before an after.
There are also some minor differences between both builds.
Stetho for example is only enabled for debug builds. And
Timber to log warnings and errors as Crashlytics non-fatal crashes in release builds.
Also given that I knew from the start that the codebase would be open sourced it was important to make sure that if someone would like to contribute their barrier to entry would be minimal. They could just clone the project and build it with out worrying about signing or following any special setup steps.
Then it was time to make a few more decisions like whether to use Dagger or a simpler DI framework, RxJava or Kotlin Coroutines, etc…
Dagger has been the most popular Dependency Injection framework for Android for a few years but it is complex, has a decent learning curve to it and it can be too much work to maintain in a small project. A very good Kotlin alternative is Koin. With a few lines of code you could have a very decent DI setup in your project and with ViewModels injection out of the box. It does not support state ViewModels yet but that was not a big issue in my case.
Same goes for RxJava, at the moment it is more capable than Coroutines but again it is more complex and has quite a learning curve it. And like RxJava, Coroutines is supported by libraries like Room and Retrofit.
On the other hand using some Jetpack Components (like AppCompat, Lifecycle, ViewModel, etc…) and Material Components was an obvious choice. Navigation was the one library I was not too sure about but I ended up using it.
Ok, so most of the available online posts about Jetpack Architecture components offer bad advice or are very basic that they do not actually offer much value. There are some great resources though like Google’s own documentations, Medium posts and I/O sessions.
Let us dive into some details, shall we. Starting with ViewModels and config changes.
ViewModels are designed to survive config changes by storing in memory all the data needed to display the associated Fragment or Activity. Given that this data is stored in memory, the system can clear it if say the device was running low on memory. SavedStateViewModel is a possible solution to this issue, only use it to store small bits of data though. Another possible solution is to get the Fragment or Activity to pass in the the initial information the ViewModel need to load its data correctly.
In the above snippet the Fragment passes the initial information the ViewModel requires to load correctly the first time. And the ViewModel checks if the data is different or the same as before and based on that decide to either go fetch the data or not.
Another thing to consider when using ViewModel and LiveData objects is what happens when the Fragment or Activity get re-created. Say the ViewModel made a HTTP request to fetch the data and it timed out or errored. The user would only expects to see this error once but because the Fragment or Activity would get re-created after a config change the LiveData observer would receive that error again. Different possible solution has been discussed by Jose Alcérreca in this Medium post, it is a must-read.
Next is Paging, or RecyclerViews in general. The problem with RecyclerViews is that they require quite a bit of boilerplate code to get something to appear on the screen.
Airbnb’s Epoxy is a great “wrapper” around RecyclerView that simplifies the process of working with them. It also works nicely with Kotlin and PagedList. Using Epoxy is quite simple, you just need an EpoxyController and EpoxyModelWithHolder.
Using Paging with it, is also quite easy, the annoying thing was to find out that you cannot modify anything in the PagedList once created unless it was backed with Room. So if the ViewHolders in the list can be in different states (like bookmarked or not, etc…). Using on-disk Room means that you can add offline support quite easily so that was a nice plus, you can also use in-memory Room.
In the following snippet I join 2 tables, return a DataSource.Factory which is then linked to a PagedList.BoundaryCallback.
The Navigation library was in decent enough state and got the job done but it does not feel like it 100% ready yet. Nested graphs in particular I found to have many issues.
For handling long-running HTTP downloads Android’s DownloadManager is still as relevant as ever, you can easily initiate a download using the following snippet.
Android’s DownloadManager would take care of showing a notification when downloading the file, it will manage retries and it will not be tied to your UI component lifecycle because it is a system service. You obviously need a couple permissions for it to work though.
OK, so for building a media player in an Android app you have 2 options: Android’s MediaPlayer API or ExoPlayer. ExoPlayer is easier to work with and it supports feature currently not supported by MediaPlayer. The main con in using ExoPlayer according to their docs is that:
For audio only playback on some devices, ExoPlayer may consume significantly more battery than MediaPlayer. See the Battery consumption page for details.
To maintain audio playback on UI components config changes a foreground Service is still a requirement. But you can get the ExoPlayer to create a notification and then convert the running Service to a foreground service.
ExoPlayer can also manage the lock screen controllers and allow assistant to play/pause your media.
Checkout the full and up to date code here.
If you enjoyed reading this and found it helpful Buy Me A Coffee, share it or leave me a comment.