DEV Community

Cover image for The 500 DON'Ts about Android Development
Agustín Tomas Larghi
Agustín Tomas Larghi

Posted on • Edited on

The 500 DON'Ts about Android Development

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 } 
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

You can easily change it to something a bit more readable, like:

fun testApply() = Intent().apply {
    putExtra("foo", 123)
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 an EditText you will be forced to keep track of that value in the Adapter.

  • 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()
    }
    // ...
Enter fullscreen mode Exit fullscreen mode

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()
    }
Enter fullscreen mode Exit fullscreen mode

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()
    }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Instead you could

private val myRecyclerViewAdapter by lazy {
    MyAdapter()
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // ...
    myRecyclerView.adapter = myRecyclerViewAdapter
}
Enter fullscreen mode Exit fullscreen mode

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))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 -> 

    }
}
Enter fullscreen mode Exit fullscreen mode

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 simple suspendable 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 in ViewModels.
  • Don't use LiveData as callbacks for custom views.
  • Don't use RxJava observables to expose data from the ViewModels

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 a Toolbar 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))
}
Enter fullscreen mode Exit fullscreen mode

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) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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


Image description

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)

Collapse
 
enzoftware profile image
Enzo Lizama Paredes

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"

Image description

Collapse
 
erdo profile image
Eric Donovan

Phew, I thought there really we're going to be 500 don'ts 😂 Nice article

Collapse
 
4gus71n profile image
Agustín Tomas Larghi

haha thanks! 😊