Data management

When using Activities or Fragments, you will usually want to store some data. For instance, the user checked some options in the previous activity, and you want to keep track of it.

It's even more needed as many scenarios lead to an Activity being destroyed, so you may have to save data to load it back, for instance, when the user rotates the screen.

This usually involves a few classes:

  • ViewModel 🏠: a class to store data in a model
  • Bundle πŸŽ’: a class to store data in a model
  • LiveData<T> πŸ“©: update the model when the view changes; update the view when the model changes

A model may be temporary (a variable), or persistent (a database, a file...). A database may be local (SQLite) or remote (an API...).


ViewModel

We commonly use the ViewModel 🏠 class to handle data.

A ViewModel is created when the application is started, and destroyed when the application is destroyed (ViewModel#onCleared()).

When loading back an activity, you can use the view model to configure the view with the data you stored in it.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'

To define a basic blank model:

class EmptyViewModel : ViewModel() {
    // implement your ViewModel here
    // e.g. methods to store data/query an API...
    // πŸ”₯ the simplest usage, stock data in variables
    var count = 0
}

The next step is to load the view model inside an activity or a fragment. The code is different for Activities and Fragments.

Activity

This import is unneeded if you added the one below for fragments.

implementation "androidx.activity:activity-ktx:1.6.1"
class MainActivity : AppCompatActivity() {
    private val viewModel: EmptyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        // ex: init the view with count
        val count = viewModel.count
    }
}

Fragment

This will import the function for use both in Fragments and Activities.

implementation "androidx.fragment:fragment-ktx:1.5.5"
class BlankFragment : Fragment() {
    private val viewModel: EmptyViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // ex: init the view with count
        val count = viewModel.count
    }
}

ViewModel sharing

Any viewModel is not shared between activities or fragments. For instance, two activities with the code for activities will create two separate viewModel instances.

For fragments, it's possible to create an activity-scoped viewModel, which is shared between all fragments of an activity:

class BlankFragment : Fragment() {
-    private val viewModel: EmptyViewModel by viewModels()
+    private val viewModel: EmptyViewModel by activityViewModels()
}

Other code samples

Alternative: ViewModelProvider
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: BlankViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        // init the view with count
        viewModel = ViewModelProvider(this)[BlankViewModel::class.java]
        val count = viewModel.count
    }   
}
Pass arguments to your ViewModel

The example below is with an Integer.

class XXXViewModel(v: Integer) : ViewModel() {
    // ...
}
class XXXViewModelFactory(private val v: Integer) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return XXXViewModel(v) as T
    }
}
- private val viewModel: XXXViewModel by viewModels()
+ private val viewModel: XXXViewModel by viewModels {
+    XXXViewModelFactory(10)
+}

LiveData

A LiveData πŸ“© is a life-cycle aware observable variable.

Observable means we can execute some code when the variable's value changes. Life-cycle aware means it doesn't check if the variable changed when the application is not either Started or Resumed.

implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'

βœ… They are used to update the view when the model changes.

A LiveData wraps a variable and adds to it its logic. For instance, LiveData<Int> wrap a value of type Int.

The value can be null, so we have to use null-safe operators. (!!, ?, ?:...).

val count : LiveData<Int> = MutableLiveData<Int>(0)
if (count.value!! == 0) {} // true

Each time value is assigned (modifying value won't trigger observers), observers will be called. In an Activity:

viewModel.count.observe(this) { newCount -> /* ... */ }
viewModel.count.observe(this) { /* it == newCount */ }

⚠️ Use viewLifecycleOwner instead of this in a Fragment.

πŸš€ With Data binding, you can avoid using observers to update views.

LiveData and MutableLiveData

The value attribute of a LiveData<T> cannot be modified, while we can for a MutableLiveData<T>. We usually use MutableLiveData<T> internally, and only expose a LiveData<T> to other classes.

class MainViewModel : ViewModel() {
    // use a backing field
    // count is unmodifiable from the "outside"
    var _count = MutableLiveData(0)
    val count : LiveData<Int> = _count

    // only allow count to be increased
    fun increaseCount() {
        // assign value to trigger observers
        _count.value = _count.value!!.inc()
    }
}

LiveData Code Samples

When a button is clicked, we update the ViewModel
val myButton = ...
myButton.setOnClickListener {
    viewModel.increaseCount()
}
When the ViewModel is updated, we update the View
val countTextView = ...
viewModel.count.observe(viewLifecycleOwner) {
    // it is the newValue for "count"
    // here, we update a TextView
    countTextView.text = it.toString()
}
Handle Loading or Errors using LiveData
private val _state = MutableLiveData<LoadingState>()
val state : LiveData<LoadingState>  = _state

enum class LoadingState {
    LOADING, SUCCESS, FAILED
}

The views will observe this state, and show an appropriate message to the user such as a Toast for errors.

Inside the ViewModel, when doing API requests, we set the state:

// set the state before the request
_state.value = LoadingState.LOADING
// ... An API request
// set the state according to the result
_state.value =
    if (...) LoadingState.SUCCESS
    else LoadingState.FAILED
πŸ“– Transformations

Transformations will create a "fake" LiveData from another LiveData. For instance, to convert a string to a date.

// Ex: here we simply return "count" as a "String"
// When _count is changed, we execute the code below.
// The last line is the newValue of our LiveData<String>
val count: LiveData<String> = Transformations.map(_count) {
    it.toString()
}

In the code above to update the view, we would write:

- countTextView.text = it.toString()
+ countTextView.text = it
πŸ“– Mediator

A mediator is a LiveData that is bound to multiple LiveData. When one LiveData is updated, then the mediator is updated.

➑️Increasing "a" or "b" will update "count" with the sum of both.

private val a = MutableLiveData(0)
private val b = MutableLiveData(0)
private val mediator = MediatorLiveData<Int>()
val count: LiveData<Int> = mediator

init {
    mediator.addSource(a) { mediator.value = it + b.value!! }
    mediator.addSource(b) { mediator.value = it + a.value!! }
}

fun increaseCount() {
    if ((1..10).random() > 5){
         a.value = a.value!! + 10
    } else {
        b.value = b.value!! + 100
    }
}

See also Transformations with multiple arguments.


πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

Another alternative to ViewModel is using a Bundle πŸŽ’, but they are limited in size, so they're not convenient to use.

val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
if (preferences.getBoolean("key", false)) {}
Flows

A flow is canceled when the app goes to the background/rotating the screen (see here a patch). We could also patch that by giving a timeout to "asLiveData", but by doing that, the flow will continue to be run while the app is in the background, until the timeout that is.

// implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
val myLiveData : LiveData<Int> = flow {
    while (true) {
        val data : Int = 0 /* fetch from the api some data */
        emit(data) // send
        delay(60000) // wait 60 seconds
    }
}.asLiveData()

public val tasksStateFlow = MutableStateFlow<List<Task>>(emptyList())