Using ViewModel-LiveData with Jetpack Compose

mahendranv

Mahendran

Posted on September 12, 2022

Using ViewModel-LiveData with Jetpack Compose

In this post I want to cover on where/how to make API calls on a Jetpack compose screen. In an essence the traditional UI system and and compose differs on where do we invoke the remote/async API vs how the data delivered to us. Following swim-lane diagram explains the overview of the data flow.

Compose-ViewModel

As we can see in the diagram, the ViewModel and inner layers don't differ. In other words, if you're using ViewModel with Android UI, only the UI classes will change and rest of the layers can be kept as it is.


Implementation

In compose, LiveData is consumed as state. To do so, add this dependency in build.gradle.



// https://maven.google.com/web/index.html?q=livedata#androidx.compose.runtime:runtime-livedata
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"


Enter fullscreen mode Exit fullscreen mode

One-off call

In the composable function, observe the data as state using LiveData#observeAsState extension.

Next, make API call using LaunchedEffect - for one time call, use Unit or any constant as key.

For UI, as usual - skim through the data and construct the UI. This example shows listing books.



@Composable
fun BooksScreen(
    viewModel: BookListViewModel = hiltViewModel<BookListViewModelImpl>()
) {
    // State
    val books = viewModel.books.observeAsState()

    // API call
    LaunchedEffect(key1 = Unit) {
        viewModel.fetchBooks()
    }

    // UI
    LazyColumn(modifier = modifier) {
        items(books) {
            // List item composable
            BookListItem(book = it)
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

User triggered - API calls

In case you want to execute the LaunchedEffect block again - such as force refresh, use a variable and conditionally update the key1 value. Remember every time when the key change, it'll invoke the API. So, keep in mind to not assign the value in render logic and put it behind user action.



@Composable
fun BooksScreen(
    viewModel: BookListViewModel = hiltViewModel<BookListViewModelImpl>()
) {
    // State
    val books = viewModel.books.observeAsState()
    var refreshCount by remember { mutableStateOf(1) }

    // API call
    LaunchedEffect(key1 = refreshCount) {
        viewModel.fetchBooks()
    }

    // UI
    Column() {
        IconButton(onClick = {
                        refreshCounter++
                   }) {
                        Icon(Icons.Outlined.Refresh, "Refresh")
                   }
        LazyColumn(modifier = modifier) {
            items(books) {
                // List item composable
                BookListItem(book = it)
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

ViewModel implementation

ViewModel is an interface contract which exposes data through LiveData and has helper functions to carry out actions.



interface BookListViewModel {
    // Data
    val books: LiveData<List<Book>>
    // Operations
    fun fetchBooks()
}


Enter fullscreen mode Exit fullscreen mode

The consuming classes shall refer the interface and the actual implementation will be an Android ViewModel. Wiring of this implementation to UI classes will be taken care by dependency injection.

Internally, the viewmodel implementation overrides the data variables to provide actual data. As for the operations, the viewModelScope ensures the API call lives within viewmodel's lifetime, and launches the remote operation.

Remember viewModelScope still executes in Main thread. Offloading the task to IO happens in repository layer.



class BookListViewModelImpl(private val repo: BooksRepository) : BookListViewModel {
    private val _books = MutableLiveData<List<Book>>()
    override val books: LiveData<List<Book>>
        get() = _books

    override fun fetchBooks() {
        viewModelScope.launch {
            _books.value = repo.fetchBooks()
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Repo implementation

Repo executes a long running operation. In kotlin world, it is a suspend function runs in IO dispatcher context.



class BooksRepository {

    suspend fun fetchBooks() : List<Book> = withContext(Dispatchers.IO) {
        // Some API call
        // Parser logic
        val books = listOf<Book>()
        books
    }
}


Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
mahendranv
Mahendran

Posted on September 12, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related