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.
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"
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)
}
}
}
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)
}
}
}
}
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()
}
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()
}
}
}
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
}
}
Top comments (6)
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.
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.
Can you share your view model?
I don't have exact implementation in hand. But, here is the rough layout (minus repository / data source)
but your using HiltViewModel in compose function
how this is possible?
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.