FYI There are no 500 DON'ts in here, just random 8 things I was thinking about. The 500 is a reference to this Simpsons episode.
I decided to do a small post about some of the most common mistakes I have seen during my career as an Android engineer.
You will see a little bit of everything here, Kotlin, Android SDK, commonly used third-party libraries, etc. 😉
1️⃣ .let isn't a replacement for if != null
I have seen many engineers misinterpret what the .let
function is for and use it like:
data class EspressoMachine(
val amountCoffee: Int,
val hasWater: Boolean
)
fun testFoo(espressoMachine: EspressoMachine?) {
var canPrepEspresso = false
espressoMachine?.let {
canPrepEspresso = it.hasWater && it.amountCoffee > 0
}
if (canPrepEspresso) {
// Do something else
}
}
The let
scoping function or the let
operator (call it whatever you want, potato-poteto) it lets you run an algorithm on a variable, it is designed to transform a variable into something else.
The correct usage would be something like
fun testFoo(espressoMachine: EspressoMachine?) {
val canPrepEspresso = espressoMachine?.let { it.hasWater && it.amountCoffee > 0 }
}
Simple as that, again, let
isn't a replacement for the if
statement. Also, please do not end up in these kind of situations
fun testFoo() {
foo?.let {
boo?.let {
// Just go with a if (foo != null && boo != null)
}
}
}
So Donnie, please don't abuse the .let
None of the scoped-functions are meant to be a replacement for if != null
The same thing applies to any other scoped function, apply
, run
, let
, etc.
For example, the apply
lets you apply changes upon an object. For example, if you have something like this 👇
fun testApply(): Intent {
val someIntent = Intent()
someIntent.putExtra("foo", 123)
return someIntent
}
You can easily change it to something a bit more readable, like:
fun testApply() = Intent().apply {
putExtra("foo", 123)
}
So Donnie, please don't go over the top with the scoped functions and the elvis operator.
2️⃣ Using RecyclerView instead of LinearLayout
I often see engineers try to create a dashboard-like UI using a RecyclerView
, where each item is a "section" of the dashboard. This does not scale well and it is not the use case for the RecyclerView
. It is far easier, and more scalable to just have a LinearLayout
with a bunch of Fragments
👇
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:name="com.something.company.SectionOneFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<fragment
android:name="com.something.company.SectionTwoFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<fragment
android:name="com.something.company.SectionThreeFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
If you try to implement this using a RecyclerView
, you will have to worry about:
The recycling of the
ViewHolders
. If one of these "sections" contains anEditText
you will be forced to keep track of that value in theAdapter
.The sheer amount of callbacks to communicate back to the UI.
So Donnie, if every item in the Adapter
is going to be different, don't go with a RecyclerView
use a LinearLayout
instead.
3️⃣ Mutable and immutable data
I have seen this with people who are moving away from Java. In Kotlin you have two types of variables, read-only variables (val
), and re-assignable variables (var
). You also have mutable (for example ArrayList<String>
) data, and immutable data (for example Array<String>
) data.
Let's try to picture a RecyclerView.Adapter<VH>
and think about the data structure we are going to use to model the list.
If we go with a read-only mutable data structure, we can update the items one way only 👇
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val myList = mutableListOf<String>()
fun updateList(newList: List<String>) {
myList.apply {
clear()
addAll(newList)
}
notifyDataSetChanged()
}
// ...
If we go with a re-assignable variable and immutable data, there's also only one way to update the list 👇
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var myList = listOf<String>()
fun updateList(newList: List<String>) {
myList = newList
notifyDataSetChanged()
}
But, if we go with both, a var
variable and also use mutable data, it kinda defeats the purpose 👇
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var myList = mutableListOf<String>()
fun updateList(newList: ArrayList<String>) {
myList = newList
notifyDataSetChanged()
}
And also notice that now I'm tied to use ArrayList
🫠
So Donnie, please use either val
with mutable data or var
with immutable data.
4️⃣ Too much lateinit var
I find that way too many times engineers rely a bit too much on the lateinit
operator, it doesn't always guarantee that your variable will be initialized, it is just re-introducing NullPointerException
as an UninitializedPropertyAccessException
exception. I find it much more safe to go with either using lazy
initialization or just initializing the var
variable with something. For example 👇
private lateinit var myRecyclerViewAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
myRecyclerViewAdapter = MyAdapter()
myRecyclerView.adapter = myRecyclerViewAdapter
}
Instead you could
private val myRecyclerViewAdapter by lazy {
MyAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
myRecyclerView.adapter = myRecyclerViewAdapter
}
5️⃣ Checking if context != null
It might come as a surprise to some, but there is a whole requiresSomething()
API out there for Fragments
and Activities
so you don't have to null-check things that are late initialized, such as the context. For example:
If you are doing something like
class TestFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
someButton.setOnClickListener {
if (context != null) {
startActivity(Intent(context, SomeActivity::class.java))
}
}
}
}
You can instead do something like
class TestFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
someButton.setOnClickListener {
startActivity(Intent(requireContext(), SomeActivity::class.java))
}
}
}
Like this you have the requireActivity()
to get the attached Activity
or the requireView()
to get the root view, quite useful to prompt Snackbars
. Do keep in mind that if the context hasn't been initialized, you will get an exception.
So Donnie, no need to null-check everything here.
6️⃣ Observables, Callbacks, and where to use them
There are two widely-use mechanisms to communicate data between components, Callback interfaces and Observables:
Callback
It might be a callback interface like this 👇
data class EspressoMachine(
val callback: EspressoMachine.Callback
) {
interface Callback {
fun onCoffeeReady(isTooHot: Boolean)
}
}
fun testCallback() {
val espressoMachine = EspressoMachine(object : EspressoMachine.Callback {
override fun onCoffeeReady(isTooHot: Boolean) {
// Do something when ready
}
})
}
or you might use high-order functions, like this 👇
data class EspressoMachine(
private var callback: ((Boolean) -> Unit)? = null
) {
interface Callback {
fun onCoffeeReady(isTooHot: Boolean)
}
fun setOnCoffeeReady(func: ((Boolean) -> Unit)) {
callback = func
}
}
fun testCallback() {
val espressoMachine = EspressoMachine()
espressoMachine.setOnCoffeeReady {
// Do something
}
}
This is the kind of API that we see in the Android SDK. For example, when you want to set an onclick event 👇
fun testCallback(view: View) {
// () -> Unit
view.setOnClickListener {
}
// (View, MotionEvent) -> Unit
view.setOnTouchListener { v, event ->
true
}
// (View, Boolean) -> Unit
view.setOnFocusChangeListener { v, hasFocus ->
}
}
So let me ask you something; it is a good practice to keep code homogeneous, right? If you approach a code base where they are using ViewBinding
you are not going to start using Butterknife
or Kotlin Synthetics
, right? – Okay, following that same way of thinking, if you create a custom view, you should keep things homogeneous with the Android SDK, right? That's the idea here.
Observables
So we have a quite a few "observables" out there, RxJava
, Kotlin Flow
, LiveData
, MutableLiveData
, the SingleLiveEvent
ugly cousin, the (we-got-it-right-this-time) StateFlow
and ShareFlow
– all these observables address different use cases, some people might argue that you could use them all in the same project, if you use them to address specific use cases, and some people might tell you to just pick one. I'm kinda in the middle. I differentiate between two different use cases:
1# Use Case: ViewModel <> UI comms
You want to communicate data from your ViewModels
back to your Activity
, Fragment
or Composable
, right? You have a few options here:
LiveData / MutableLiveData / SingleLiveEvent: The "old" Android solution, I don't see anything bad about going with this option, but keep in mind that you will have to deal with the SingleLiveEvent issue, a swept-under-the-rug Google solution.
StateFlow / ShareFlow: It would be the solution that Google recommends nowadays, and it addresses the SingleLiveEvent issue AFAIK.
2# Use Case: Exposing an API for UseCases / Interactors / Repositories
However you structure your app, I'd like to think that we should not do things like querying the backend API or fetching things from the DB, straight in the ViewModel. Usually, most engineers, create a middle layer, call them Repositories, UseCases, Interactors, whatever you want – basically a wrapper to fetch or submit data. What comes next is to figure out what kind of API these components are going to expose. We have a few solutions out there:
RxJava: You have rx.Observable or rx.Single depending on if your stream is going to emit one (a GET request, for example) or many (actively listening for DB changes) objects. It has a
.zip
,.contact
,.merge
API so you can combine streams any way you want. Quite flexible – I did use it a lot before moving to Kotlin Coroutines and Flows.Kotlin Flow: You have
Flow
for when your stream is emitting multiple items, and you can use simplesuspendable
functions for when you expect a one-time response.
This is a very-brief and utterly-simplified version of the kind of data bus solutions we have out there, and I haven't talked about Kotlin Channles and what's the use case for that. Hopefully by now you can dig that there are two types of "observables" the ones used to communicate data from the ViewModels
back to the UI, and the one used to expose data from the Repositories
/ UseCases
/ Interactors
out to the ViewModels
So, Donnie please –
- Don't use
Callback
interfaces inViewModels
. - Don't use
LiveData
as callbacks for custom views. - Don't use
RxJava
observables to expose data from theViewModels
7️⃣ Re-using UI
The ways in which you can re-use UI on Android are: Fragments
, Custom Views, the <includes>
tag, and now also
Composables
. Each of these has a different use case.
If you want to re-use a completely static piece of UI, then you can go with the
<includes>
tag. For example, a banner with hardcoded text, or aToolbar
that's the same everywhere.If you want to re-use a piece of UI that has functionality but the functionality is independent of your business, then you can go with a Custom View. For example, a carousel of
ImageViews
, anything that makes you think "oh, I could push this up to GitHub as a lib" can probably be done as a Custom View.If you want to re-use a piece of UI that has functionality and that functionality is closely related to your business, then go with a
Fragment
. For example, you prompt a screen during onboarding so the user can invite people to download your app, and you also want to prompt the same screen within the app.
So Donnie, don't re use UI using ViewHolders
or anything else.
8️⃣ Incorrect usage of LiveData + Coroutines
I have seen some people do things like:
sealed class State {
data class Loading(
val isLoading: Boolean
): State()
data class DataSuccessfullyFetched(
val someData: Data
): State()
object ErrorFetchingData : State()
}
val state = MutableLiveData<State>() // Or SingleLiveEvent
viewModelScope.launch(Dispatcher.IO) {
state.postValue(State.Loading(true))
val response = someRepository.fetchData()
if (response != null) {
state.postValue(State.DataSuccessfullyFetched(response))
} else {
state.postValue(State.ErrorFetchingData)
}
state.postValue(State.Loading(false))
}
We want the state
property to go through the State.Loading(true)
state, then either State.DataSuccessfullyFetched(response)
or State.ErrorFetchingData
and finally State.Loading(false)
, right?
Well, what's going to happen is that the state
property is just going to "broadcast" the last set value, State.Loading(false)
.
The issue here is that you shouldn't be using postValue
– "but if I use setValue
instead I get a background thread error message", that's correct, because you shouldn't be using the Dispatcher.IO
at all, the only thing that needs to run on a "background thread" is the fetchData()
suspendable function, which you created as:
suspendable fun fetchData() = withContext(Dispatcher.IO) {
...
}
What we should actually be doing is 👇
...
viewModelScope.launch {
state.value = State.Loading(true)
val response = someRepository.fetchData()
if (response != null) {
state.value = State.DataSuccessfullyFetched(response)
} else {
state.value = State.ErrorFetchingData
}
state.value = State.Loading(false)
}
So Donnie, using Dispatcher.IO
doesn't make things magically run in the background thread, also just adding the suspendable
keyword to a function doesn't make that function run in the background either. There is no reason why we should use the postValue
in the ViewModel
Sorry, I lied, there are no 500 don'ts 😜 – Many of the things I pointed out here are up to personal criteria, you can use a knife as a fork if you want, you would probably still be able to pick up the food, no one can stop you. You might also want to use your underwear as a hat – and some people might even think you are cool! – but I'm not sure people will feel totally comfortable around you 🤷.
Anywho, hope you liked it! 😉
Top comments (3)
Great article, great The Simpsons fan here too! Could I get your bless to perform a similar article but for Flutter? "The 500 DON'Ts about Flutter"
Phew, I thought there really we're going to be 500 don'ts 😂 Nice article
haha thanks! 😊