Why should your android app speed matter?
I made a tweet some weeks back about how some mobile apps take long to load up. Asides the poor user experience, this can affect your bottom line.
In this article, I’ll show you how we upped Cowrywise android app speed. The app has tons of modules (Savings, Investments, Circles, Stash, Profile, etc). Hence, there’s a lot of data flows at any point in time. Given that, it won’t be fair to have a user wait for all pages to load before they make a specific action.
Android app speed starts with the SSOT (Single Source of Truth)
Simply put, the SSOT refers to having accurate data in one place. That way, any needed data is simply referenced from that source. This works better than having your data scattered around multiple places.
On the Cowrywise app, We always display the data from the app’s local database which lives on the user’s phone. Once the user logs in, We fetch recent (and most important) data in the background with the WorkManager API.
That way, the load on the app’s interface thread is reduced. In turn, the user is able to switch between screens as fast as possible. Once the user gets to the page they are looking for, data required for that page would have already been fetched. We also offload certain one-off tasks to WorkManager.
The app fetches what is in the local database while displaying progress indicators/placeholders where necessary.
How we handle state changes
Using LiveData and Coroutines, We’re able to keep track of the loading, success and error states in the app. Data for UI is wrapped with a Resource class which has 4 states.
//Some Code Omitted for brevity
//Similar to this https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
)
class Success<T>(data: T) : Resource<T>(data)
class Failed<T> : Resource<T>()
class Loading<T>(data: T? = null) : Resource<T>(data)
class Error<T>(message: String?, data: T? = null) : Resource<T>(data, message)
fun <T, A> resourceLiveData(databaseCall: suspend () -> LiveData<T>,
networkCall: suspend () -> Response<A>,
saveResourceCall: suspend (A) -> Unit) =
liveData<Resource<out T>>(Dispatchers.IO) {
val source: LiveData<Resource<out T>> = databaseCall.invoke().map { Success(it) }
val disposable = emitSource(source.map { Loading(it.data) })
try {
val response = networkCall.invoke()
if (response.isSuccessful) {
disposable.dispose()
saveResourceCall.invoke(response.body()!!)
emitSource(databaseCall.invoke().map { Success(it) })
} else {
var msg: String? = "Error occurred. Please try again later"
try {
msg = extractMessageFromJson(response.errorBody()!!.string())
} catch (e: IOException) {
e.printStackTrace()
}
emit(Error(msg))
emitSource(source)
}
} catch (e: IOException) {
emit(Failed())
emitSource(source)
Timber.e(e)
} catch (e: Exception) {
emit(Error(e.message))
emitSource(source)
Timber.e(e.message)
e.printStackTrace()
}
}
/**
* The code is used in the repository like this
* fun fetchAllPlans(userId: String) = resourceLiveData(
* { plansDao.getAllPlans(userId) },
* { client.getUserPlans(userId) },
* { plansDao.insertAll(it.results) }
* )
*
* The ViewModel calls this function.
*/
In the Fragment, we can then observe different states and update the UI respectively.
class AllPlansFragment : Fragment(R.layout.fragment_plans), PlanClickListener {
private val plansViewModel by activityViewModels<PlansViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Some Code Omitted for brevity
plansViewModel.plans.observe(viewLifecycleOwner, Observer {
when (it) {
is Success -> {
swipeContainer.isRefreshing = false
setupPlans(it.data!!)
}
is Loading -> {
if (!it.data.isNullOrEmpty()) {
swipeContainer.isRefreshing = true
setupPlans(it.data)
}else{
swipeContainer.isRefreshing = false
no_data_layout.visibility = View.GONE
loading_layout.visibility = View.VISIBLE
}
}
is Error -> {
loading_layout.visibility = View.GONE
swipeContainer.isRefreshing = false
it.message?.let { errorMessage ->
showErrorSnackBar(errorMessage)
}
}
is Failed -> {
swipeContainer.isRefreshing = false
loading_layout.visibility = View.GONE
childFragmentManager.showNoInternetDialog()
}
}
})
}
}
Placeholders and your android app speed
Sometimes your android app speed can be an illusion. When a task is going to take a while, there’s a need to inform the user without blocking interaction with other parts of the app. To do this, many make use of progress indicators and progress Dialogs. While these are good, they pose two problems:
- The user has no expectation of what is to come
- They are boring and plain
The combination of both problems can mess with the user’s mind. Yes, the process is taking time but it can feel longer with those. On the other hand, using skeleton screens, a mind game is played. As we move on, you’ll see how we have applied this approach.
Skeleton screens vs progress indicators
Loading appears faster to the user because the focus has shifted from loading progress to the content that is being loaded. Some examples are shown below
Still on Placeholders: Spinners vs Progress Dialogs
We also got rid of 95% of all ProgressDialogs in the Cowrywise android app (iOS also). Then, we replaced them with progress indicators as recommended by Google:
The login screen is an example of this. We show the progress indicator on the button. That way, the user can still interact with the app despite the loading process.
Got questions for me? You can drop a comment or ask me on twitter. If you’d love to read more about my team’s work at Cowrywise, check out how we managed to keep the android app size small here.