RxLifecycle

JitPack version number

A small library that allows smooth integration between the Android Fragment, Activity and ViewModel lifecycle and RxJava/RxKotlin.

View the API documentation.

Including the library

First add the following to your project level gradle repositories:

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}

Then add the RxLifecycle dependency in the module level build.gradle:

dependencies {
	implementation 'uk.co.conjure:RxLifecycle:1.0.0-alpha03'
}

Code Sample

Given an MVVM architecture app we need to consider the lifecycle of both the ViewModel and the View. The RxLifecycle library provides tools for both.

RxViewModel

Often you want to store the information your view model provides and replay it to late subscribers. A typical pattern for doing this is to use replay and refCount like so:

interface Model {
	fun getUserInfo(): Observable<UserInfo>
}

class ProfileViewModel(private val model: Model): ViewModel() {
	val userName: Observable<String> = model
		.getUserInfo()
		.map { it.userName }
		.replay(1)
		.refCount()
}

refCount will ensure that the upstream stops being observed as soon as there are no more subscribers, which is good because it tidies things up for us automatically, but it also presents a problem. Often a view model only has one observer at a time (the view) so when the view is destroyed temporarily (e.g. during a configuration change) the upstream stops being observed and the value stored by replay(1) will be forgotten.

We can get around this problem by adding our own subscriber to the observable, so that the view isn’t the only subscriber, like so:

class ProfileViewModel(private val model: Model): ViewModel() {

	private val keepAlive = CompositeDisposable()

	val userName: Observable<String> = model
		.getUserInfo()
		.map { it.userName }
		.replay(1)
		.refCount()

	init {
		keepAlive.add(userName.subscribe())
	}

	override fun onCleared() {
		keepAlive.dispose()
		super.onCleared()
	}
}

The RxLifecycle library simplifies this code for us by providing the RxViewModel base class. RxViewModel stores this CompositeDisposable and provides the hot extension function which tells RxViewModel to replay the last value to late subscribers and keep hold of a subscription to the upstream until onCleared is called. Now our code is much cleaner:

class ProfileViewModel(private val model: Model): RxViewModel() {
	val userName: Observable<String> = model
		.getUserInfo()
		.map { it.userName }
		.hot()
}

(The hot extension function is also available for Flowable, Single, Completable and Maybe)

RxView

You can use the RxView class to safely wrap view logic so that it is only executed during the view lifecycle. It can also be helpful to keep a clear distinction between the responsibilities of a Fragment or Activity (such as app navigation or launching an Intent) and the view layer of an MVVM app (presenting data from and sending events to the view model).

For example a fragment may define its view like so:

class LoginFragment : Fragment() {
    private lateinit var loginView: LoginView
    private val loginViewModel: LoginViewModelImpl by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        loginView = LoginView().apply {
            viewModel = loginViewModel
            registerBinding(
                FragmentLoginBinding.inflate(inflater, container, false),
                this@LoginFragment
            )
        }
        return loginView.requireBinding().root
    }
}

(Notice we don’t need to store a reference to the binding and set it to null when onDestroyView is called. RxView will clean up this memory leak for us.)

Now we can define our view logic as follows:

class LoginView : RxView<FragmentLoginBinding>() {

    lateinit var viewModel: LoginViewModel

    override fun onStart() {
        super.onStart()

        binding.etEmail.bind(viewModel.email)
        binding.etPassword.bind(viewModel.password)
        binding.btnLogin.bind(viewModel.loginClick, viewModel.isLoginButtonEnabled)

        viewModel.isLoading.whileStarted(this, { loading ->
            if (loading) {
                binding.etEmail.isEnabled = false
                binding.etPassword.isEnabled = false
                binding.pbLoading.visibility = View.VISIBLE
            } else {
                binding.etEmail.isEnabled = true
                binding.etPassword.isEnabled = true
                binding.pbLoading.visibility = View.GONE
            }
        })

        binding.tvOpenTerms.setOnClickListener {
            viewModel.showTermsClick.onNext(Unit)
        }
    }
}

(This example is taken directly from the example app.)

RxView will call onStart for us when the Fragment view lifecycle reaches the STARTED state. If we constructed our RxView using an Activity it would call on start when the Activity reached the STARTED state. Either way we can rely on the binding to have been initialized here so we don’t have to worry about nullability.

We can also avoid having to store a CompositeDisposable in our RxView by using a few handy functions:

We provide the whileStarted and whileCreated extension functions for all of Observable, Flowable, Single, Completable and Maybe. These functions accept a LifecycleOwner and an onNext callback. (You can also optionally provide other callbacks for onError, onStart, onComplete etc.). whileCreated and whileStarted will subscribe to the upstream when the lifecycle reaches the CREATED or STARTED state and dispose the subscription automatically once it reaches the DESTROYED state. Notice that RxView is a LifecycleOwner and the lifecycle is the same as the fragments viewLifecycleOwner so we can pass this to whileStarted in the above example.

For common tasks like observing an EditText or Button we provide the bind extension functions. These accept an Observer to send events to, and an optional Observable to receive updates back from, the view model. These subscriptions are disposed for you automatically when the RxView reaches the DESTROYED state.

Conjure Logo

Copyright ©2023 Conjure Ltd.
All product names and trademarks are the property of their respective owners. Reg. England & Wales 6897070.