Our Day-to-Day Guide to Productivity for Android developers — Part 1

leboncoin tech
leboncoin tech Blog
8 min readJun 16, 2020

By Christian Kula (Android developer)

The ever growing leboncoin’s user base requires us to be more effective in order to be able to deliver new features quickly. Tools, frameworks and languages are constantly evolving so it’s important for us to choose wisely in order to improve our development workflow.

In this series of articles, I’ll show you some tips & tricks we use every day at leboncoin as Android developers.

This series is definitely not the an Ultimate Guide but rather a collection of handy little things that make our everyday life easier.

You may know some, you may not know others. In any case, feel free to share them or add yours in comment!

Programming tips

Ok, let’s talk about some Android pattern code.

ActivityLifecycleCallbacks

This very handy interface was introduced in API 14 and lets you know of any Activities’ lifecycle events in your app. It becomes very useful when you need to perform the same action in every Activity but don’t want to (or can’t) implement a BaseActivity that will be extended by every other Activity.

Having a BaseActivity is still a bad idea, though. Remember : Composition over Inheritance

This way, you avoid having several layers of abstraction that are difficult to maintain and you’re also able to compose easily with interfaces instead of concrete implementations.

You just need to register an ActivityLifecycleCallbacks in the onCreate() method of your app’s Application class.

class MyApplication : Application(), ActivityLifecycleCallbacks {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(this)
}
override fun onActivityResumed(activity: Activity) {
// Do something on Resume
}
override fun onActivityPaused(activity: Activity) {
// Do something on Pause
}
// other callbacks omitted for clarity
}

Also, your ActivityLifecycleCallbacks doesn’t need to know about concrete Activities if you make them implement interfaces.

class NotificationSubscriberLifecycleCallbacks (
private val notificationManager: NotificationManager
) : ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
if (activity is NotificationSubscriber)) {
notificationManager.subscribe(activity)
}
}
}

In this example, any Activity that implements the NotificationSubscriber interface will be subscribed to the NotificationManager. NotificationSubscriberLifecycleCallbacks doesn’t know about any concrete Activity implementation.

More information

Exposing LiveData

If you use Android Architecture Components in your application, and you’ve embraced the MVVM pattern, you certainly use LiveData to push data from your ViewModel (VM) to the View.

Be careful how you expose LiveData from your VM to the View : never expose a MutableLiveData to your View.

Your View shouldn’t be able to post any value to the LiveDatas it listens to. Keep the MutableLiveData inside your VM private and expose a public regular LiveData.

DON’T

val someLiveData = MutableLiveData<String>()

DO

private val _someLiveData = MutableLiveData<String>()val someLiveData: LiveData<String> = _someLiveData

PERFECT

val someLiveData: LiveData<String> get() = _someLiveData

We use a custom getter here to ensure that every time someLiveData is accessed, it’s the _someLiveData reference that is returned. If we would use the classic assignation form (without get()), someLiveData would have whatever the value of _someLiveData was at assignation time and someLiveData would not reflect the correct value of _someLiveData if the latter would happen to change.

Usage of VM in Fragments

Fragments have a very peculiar lifecycle and it’s not easy figuring out where to correctly initialize ViewModel and observe LiveDatas.

A ViewModel for a Fragment should be initialized as soon as possible in its related Fragment’s lifecycle, so in onCreate().

Then, any needed LiveData observations should be declared in onViewCreated() so that any modifying UI events could be applied correctly.

class SomeFragment : Fragment() {
private lateinit var someViewModel: SomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
someViewModel = ViewModelProviders.of(this).get(SomeViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
someViewModel.someLiveData.observe(viewLifecycleOwner, Observer {
/ / Do something here
})
}
}

RxJava

Even though we use RxJava a lot in our codebase, its steep learning curve makes it not the most easy thing to pick up, especially for our junior colleagues.

just() & fromCallable { }

Since most Observables are created by other libraries (Room, Retrofit etc.), we sometimes forget how to properly create one ourselves.

Often, when you need to create a reactive stream that just emits a value, you would use the just() operator. It creates an Observable that will emit the value as soon as someone subscribes to it and then completes.

In most cases, this is the desired behavior but let’s say that you create a Single that emits a value with the just() operator and before subscribing the value changes. Also, you want the Single to emit the most recent value.

Your Single will emit the initial value and not the updated one because the just() operator creates a Single that will emit the given value at instantiation.

var someValue = “someValueval someValueSingle = Single.just(someValue) // Single instantiationsomeValue = “somethingElsesomeValueSingle.subscribe() // will emit “someValue” and not “somethingElse

To prevent this, you need to use the fromCallable operator. It takes a lambda that will be executed when the Observable is subscribed to. This way, it ensures that your Observable will emit the most updated value.

var someValue = “someValueval someValueSingle = Single.fromCallable { someValue }someValue = “somethingElsesomeValueSingle.subscribe() // will emit “somethingElse

Another mistake that I see often is to make any potentially long operations and then emit the result using the just() operator. You miss one of RxJava’s strengths that is to easily off-load operations to a different Thread.

Wrap your long operation with fromCallable { } and provide a fitting scheduler for your operation.

DON’T

fun doSomething(): Single<String> {
val result = getResultThatTakesTime() // will block here util getResultThatTakesTime() returns a value
return Single.just(result) // and then returns the Single of the result
}

DO

fun doSomething(): Single<String> {
return Single.fromCallable { getResultThatTakesTime() }
.subscribeOn(Schedulers.computation())
}

Also, always specify a Scheduler (with subscribeOn()) when creating a new Observable.

By default, Observables operate on the Thread where they have been subscribed to so if you create and subscribe to an Observable on the Main Thread, you may block the Main Thread.

More info
just : https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#just
fromCallable : https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#fromcallable

The cache() operator

The cache() operator is used to store the emitted values or Exception by the Observable and then replays it to the late Observers. Quite useful when you don’t want to execute costly operations multiple times.

Unfortunately, this operator is often not correctly used or understood. If you want to cache the result of your Observable, you absolutely need to keep a reference to the Observable instance and then serve it to other subscribers.

DON’T

fun doSomethingCached(): Single<String> {                         return Single.fromCallable { someExpensiveOperation() }               .subscribeOn(Schedulers.computation()).cache()                       }

// You create a new instance of Single that will execute someExpensiveOperation() every time the method doSomethingCached() is invoked. However, if you keep the reference to the returned Single, its result will be cached.

DO

private val cachedSomethingSingle: Single<String> = Single.fromCallable { someExpensiveOperation() }.subscribeOn(Schedulers.computation()).cache()fun doSomethingCached(): Single<String> = cachedSomethingSingle

// Or, thanks to Kotlin, you could also expose directly the Observable through a public field

val cachedSomethingSingle: Single<String> = Single.fromCallable { someExpensiveOperation() }.subscribeOn(Schedulers.computation()).cache()

More information

Kotlin

Object expressions and declarations

Object expressions and declarations are very powerful Kotlin features. You can create an object class or interface implementation without having to define a new subclass for it.

But did you know that you can also make an anonymous class implement an interface?

val someObject = object : SomeClass(), SomeInterface {override fun doSomething() = Unitoverride fun doSomeInterfaceThing() = Unit}

This way, someObject is an instance of SomeClass and SomeInterface, and you didn’t have to define a new subclass for it.

Let’s go one step further : you can add fields to your anonymous object if needed. The compiler is smart enough to let you do this:

val someObject = object : SomeClass(), SomeInterface {var x = 0var y = “someValue”override fun doSomething() = Unitoverride fun doSomeInterfaceThing() = Unit}someObject.doSomething()someObject.doSomeInterfaceThing()println(someObject.x + someObject.y)

You can even define a completely anonymous object that only holds values without implementing anything. It’s useful in the case you want to keep some values logically together with clear variable names without having to create a class or using a Pair, Triple etc.

val location = object {val longitude = 48.8566val latitude = 2.3522}val location2 = Pair(48.8566, 2.3522)// location.longitude/latitude is clearer than location2.first/second

Using a Pair today may seem good enough but think of the person coming after you (or even yourself in a couple of weeks!) that will have to figure out what location.first/second refers to. Be explicit whenever possible (or at least use destructuring declaration where applicable, see below)!

Keep in mind that there are some limitations :

Note that anonymous objects can be used as types only in local and private declarations. If you use an anonymous object as a return type of a public function or the type of a public property, the actual type of that function or property will be the declared supertype of the anonymous object, or Any if you didn’t declare any supertype. Members added in the anonymous object will not be accessible.

More information

Pair/Triple manipulation

As seen above, I recommend using an anonymous object with explicit field names rather than a Pair/Triple. But if you want to keep using them, here’s some nice tricks.

fun <F, S> Pair<F, S>.named(block: (F, S) -> Unit): Unit = block(first, second)val location = Pair(48.8566, 2.3522)location.named { longitude, latitude ->}// You can achieve the same effect without using an extension// Note the usage of parenthesislocation.let { (longitude, latitude) ->}val (longitude, latitude) = location

The 2 last forms are called destructuring declarations : you destructurize an Object as its components.

Destructuring declaration works with all data classes, that’s why it works with Pair/Triple.

More information

Exhaustive when extension

This one became quickly popular when the guys at Plaid used it in their codebase.

val <T> T.exhaustive: Tget() = this

The point of having this extension is to force you to handle all cases in a when block.

If you use when as a statement, the compiler doesn’t require you to handle all the cases. However, when used as an expression, the compiler forces you to handle all the cases. And that’s what this extension does : turn a when statement into a when expression.

It’s particularly useful when used with sealed classes or enums. By using exhaustive, you make it explicit that all cases must be handled and if someone adds a new value, your code won’t compile unless that case is handled.

Also, unless you have a really specific use case, you should always explicitly define every case and not rely on the else case because it can lead to unexpected behavior if new values are added in the future.

With exhaustive, it ensures that you won’t have unexpected behavior in case of unhandled cases. However, using exhaustive won’t help you (and is kinda pointless) if you have already an else case because all the cases are handled and thus satisfies the compiler.

sealed class Customer {object Regular : Customer()object Gold : Customer()object Diamond : Customer()}// will compile but if customer is a Diamond customer, nothing will happenwhen (customer) {Customer.Regular -> println(“Hello Regular customer!”)Customer.Gold -> println(“Hello Gold customer!”)}
// won’t compile unless ‘Diamond’ case is handled
when (customer) {Customer.Regular -> println(“Hello Regular customer!”)Customer.Gold -> println(“Hello Gold customer!”)}.exhaustive

More information

The second part of this series will be related to Git and Code Editor tips and tricks. ✌️

--

--