Please, first, checkout Developer: Principles. These Android developer principles are the "implementation details" of developer principles on the Android world.
The following advices are very opinionated. Before saying "bullshit", please read the why below in the Details section. One of the main ability of developer is to understand the why. (This advice apply to myself of course. Every tool I'm criticising exist because of a why that I consider less important than the alternative. But for sure, you can disagree).
The goal of this document is more to start a reflection than make you think twice of your code.
Keep in mind that these rules are in a context of a long term support of applications / libraries. These are related to my needs as a developer.
Here are best practices as an Android developer.
# | Principles |
---|---|
1 | Do not use Fragment -> use Activity and Views |
2 | Do not use GSON -> use JsonObject |
3 | Do not use reactive programmation -> use classic programmation |
4 | Do not use Retrofit and OkHttp with asynchronous API -> use synchronous OkHttp or Ktor |
5 | Do not use coroutine and suspend -> use synchronous methods on a worker thread |
6 | Do not use lib for Dep. Inj -> use hand made Dep.Inj |
7 | Do not use MVVM and Google View Model -> use MVP |
8 | Do not use Jetpack Compose -> use xml |
9 | Do not use one gradle modle -> use multiple gradle modules with samples |
10 | Do no use Toolbar and BottomNavigationView -> use your own custom view |
11 | Do not use Navigation component -> use your own navigation |
12 | Split resources by feature matching your packages |
13 | Prefix resource ids and layout by feature name, prefix layout ids by layout name |
14 | Do the view binding yourself |
15 | Do not make variable name, class name or package name impact the apk (when possible) |
16 | Do not write a BaseActivity (composition over inheritance) |
17 | Do not code Row/Cell inflation and binding in RecyclerView adapter -> do custom view |
18 | Do not forward Row/Cell listeners via the RecyclerView adapter -> do stand alone Row/Cell with dedicated MVP |
Why?
- Because it make less easy to understand what you are doing
- Memory leaks are more common with fragment
- Because it's leaking with "Android viewModel" like discussed on this medium
- Because it does not bring notion that Activity and View does not support
- Fragment were first introduced for Honeycomb (3.0) with the goal of make UI for tablet. With fragments, the idea was to propose screen split in two part with, on the left side a recyclerView with menus, and on the right side the content. "Fragment" concept was bringing Activity like lifecycle more close to the view. But fragment is not the only way to split your UI into smallest part. From the ground up of Android,
Views
are designed like that.
- Because it's heavy
- Because Moshi is better
- Because the AOSP already bring a json parser
"Gson are like fragment", JW's joke about point 1. and 2.
Reactive programmation is a pradigm that impact the whole project. If done correclty, you cannot isolate the "rectivity" to a small part of the code (why? consistency).
So, with that in mind, why do I think "reactive programmation" is not the way to go
- Because one of the main principle, is to keep the code easy to read, understandable most people (juniors...). On board new developer is key! Reactive programmation is not one isolated feature, that change the whole codebase.
- Because a lot of libraries and third parties will not use reactive programmation
Keep in mind the first rule of developer: keep it simple, easy to read in order to easily onboard new developpers.
Asynchronous API with callback or listener for example are great for single use. I mean, when you need to do multiple asynchronous task, the trap of asynchronous method is the callback hell. Imagine a simple network call that download a .zip
. Imagine you want to unzip the zip just after the download. With synchronous API for the network and the unzip, you will be able to code with less callback:
workerThreadHandler.post {
val zipContent = try {
val zipFile = networkManager.downloadZipFile()
zipManager.unzip(zipFile)
} catch (e: NeworkException) {
null
} catch (e: UnzipException) {
null
}
mainThreadHandler.post {
// Update the UI / Rest of the app with "zipContent"
}
}
Perso, I do not use Retrofit abstraction to do myself the header / domain / parsing management.
suspend
is part of kotlin language. The problem with the suspend
keywork is that it's spread accross your project.
Why does design pattern for the "view" exist?
a.
To split the "view" from the "logic" codeb.
To split the "platform specific" code from the "logic" code
Why the a.
- To unit test the "logic" part of the code
- To have the "logic" part of the code working with "any" UI
- Because UI is more subject to change, and we do not want to break the logic with small UI changes (Uncle Bob argument)
- To avoid 3000 line long Activity (was the state once, on one of the project I was working on)
Why the b.
- To be able to run the logic code on your computer (or on a CI)
- To be able to produce "similar code" whatever the platform (I mean, code with similar concepts)
So, with that, why do I think the Google way of MVVM is not the way to go
- Because "ViewModel", everywhere else, is a model crafted specificly to meet the View needs
- Because one of the main principle, is to keep the code easy to read, understandable most people (juniors, non pure Android dev...). Keep in mind that Android is using jvm code (kotlin and java), so a lot of the code an app is platform agnostic.
Because Jetpack Compose cannot be isolated to the view. UI should not drive your architecture. Avoid strong dependency is key to have robust code.
// WIP
On Android, all the resources are accessible by the whole code (resources are not feature private on the same gradle module). To avoid conflict, prefix your resources. This rules is even more important for resources in shared gradle modules / on libraries.
I remember when I was coding on Unity, libraries that have "app_name" resources ^^
. That may force your client to use "replace" in xml, this situation could be avoidwith prefix.
With the rule 13. every id is unique so could be long. View binding done by Android can allow you to have field directly accessible with res id as field name. Avoid that.
Resource ids are lowercase with _
, fields should use cameCase.
Here how to do that:
// On Activity
private val version: TextView by bind(R.id.settings_activity_version)
private fun <T : View> Activity.bind(@IdRes res: Int): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) { findViewById(res) }
}
// On CustomView
private val view = inflate(context, R.layout.section_bar_view, this)
private val galleryIconOn: ImageView = bind(R.id.section_bar_view_gallery_icon_on)
@Suppress("SameParameterValue")
private fun <T : View> bind(@IdRes id: Int): T {
@Suppress("RemoveExplicitTypeArguments")
return view.findViewById<T>(id)
}
15. Do not make variable name, class name or package name having an impact on the binary when possible
- When you use GSON without annotation for example, the field name of the class are use as JSON keys
- When you have an enum and use
ColorEnum.WHITE.name
- When you use java reflect
When possible, you should avoid having your code depending on itself (exception for the java reflect, sometimes, no other choice)
Why?
Because be able to refacto without any fear is what's make the codebase robust. You should be able to change variable and classe names without breaking the product. As a developer, it is fair to assume we can rename a classes whithout breaking the product.
Other articles and projects on Mercandj.