DEV Community

Mahendran
Mahendran

Posted on • Edited on

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

Top comments (6)

Collapse
 
karangupta2388 profile image
karangupta2388 • Edited

Why is LaunchEffect even needed inside the composable? We are already using viewModelScope.launch to fetch the book which should prevent any leaks right?
A response would help as I am confused between viewModelScope.launch vs LaunchEffect.

Collapse
 
mahendranv profile image
Mahendran

LaunchEffect is a trigger to load content upon first composition (provided proper key).

viewModelScope.launch will scope the coroutine to retained-activity (i-e survives config changes). It is independent of the whether we use traditional xml UI or compose.

Collapse
 
jamescodingnow profile image
James

Can you share your view model?

Collapse
 
mahendranv profile image
Mahendran • Edited

Updated the post to include repo and viewmodel implementation.

I don't have exact implementation in hand. But, here is the rough layout (minus repository / data source)

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.launch

class Book {
    var name: String = ""
}

interface BookListViewModel {

    var books: LiveData<List<Book>>

    fun fetchBooks()

}

class BookListViewModelImpl : BookListViewModel {
    private val _books = MutableLiveData<List<Book>>()
    override var books: LiveData<List<Book>>
        get() = _books
        set(value) { // no-op
        }

    override fun fetchBooks() {
        viewModelScope.launch {
            // _books.value = someLongOperationInRepo()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kururu95 profile image
kururu

but your using HiltViewModel in compose function

how this is possible?

Thread Thread
 
mahendranv profile image
Mahendran

It is supported. Just like the viewModel() delegate. Please check this documentation: developer.android.com/develop/ui/c...

Oddly it is placed under hilt-navigation.