androidkotlinmvvmandroid-jetpack-composestate

Updating UiState of a screen from a viewModel of another screen Kotlin


In my Kotlin app I have 3 screens:

  1. QueryScreen
    • has one field where the user enters a query, and a button for search action. When pressed, it fetches data from the Google Books API as a list of books.
  2. BookListScreen
    • needs to show that list of books and when the user selects one book, it performs another API request to get details of that specific book.
  3. BookDetailsScreen
    • needs to show details of the book fetched by BookListScreen.

Currently, I tried to approach this problem by having UiState and ViewModel classes for each of the screens. The data in my app needs to flow like this: QueryScreen fetches data from the server and updates BookListUiState to reflect those changes. BookListViewModel sees the data and updates the Ui accordingly for the BookListScreen. User taps on a book then another request is performed which updates BookDetailsUiState. I need to use manual DI because I am learning Kotlin and I want to work with the basics.

My questions for this problem are:

data class QueryScreenState(
    val query: String = "",
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

data class BookListScreenState(
    val books: List<Book> = emptyList(),
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

data class BookDetailsScreenState(
    val book: Book? = null,
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

Also, I am using a repository

sealed interface BookshelfResult<out T> {
    data class Success<T>(val data: T) : BookshelfResult<T>
    data class Error(val exception: Exception) : BookshelfResult<Nothing>
}

interface BookshelfRepository {
    suspend fun getBooks(query: String): BookshelfResult<List<Book>>
    suspend fun getBook(id: String): BookshelfResult<Book>
}

class DefaultBookshelfRepository(
    private val bookshelfApiService: BookshelfApiService
): BookshelfRepository {
    override suspend fun getBooks(query: String): BookshelfResult<List<Book>> {
        return try {
            val res = bookshelfApiService.getBooks(query)
            if(res.isSuccessful) {
                val books = res.body()?.items ?: emptyList()
                BookshelfResult.Success(books)
            } else {
                BookshelfResult.Error(HttpException(res))
            }
        } catch (e: IOException) {
            BookshelfResult.Error(e)
        } catch (e: HttpException) {
            BookshelfResult.Error(e)
        }
    }

    override suspend fun getBook(id: String): BookshelfResult<Book> {
        return try {
            val res = bookshelfApiService.getBook(id)
            if (res.isSuccessful) {
                val book = res.body()
                if (book != null) {
                    BookshelfResult.Success(book)
                } else {
                    BookshelfResult.Error(Exception("Book not found"))
                }
            } else {
                BookshelfResult.Error(HttpException(res))
            }
        } catch (e: IOException) {
            BookshelfResult.Error(e)
        } catch (e: HttpException) {
            BookshelfResult.Error(e)
        }
    }
}

I am a beginner, and all of your advice will be helpful. This problem is from the Android Development with Kotlin official course: https://developer.android.com/codelabs/basic-android-kotlin-compose-bookshelf#0


Solution

    1. Do I need 3 separate UiStates and ViewModel for each screen?

      Yes, three separate UiStates needs.

      ViewModel for each screen? Its depends on the your modules. if you separate each feature as individual that case each screen needs separate view model.

      otherwise you can define multiple Uistate in single view model

      val topicUiState: StateFlow<TopicUiState> = topicUiState(
          topicId = topicId,
          userDataRepository = userDataRepository,
          topicsRepository = topicsRepository,
      ).stateIn(
              scope = viewModelScope,
              started = SharingStarted.WhileSubscribed(5_000),
              initialValue = TopicUiState.Loading,
          )
      
      val newsUiState: StateFlow<NewsUiState> = newsUiState(
          topicId = topicId,
          userDataRepository = userDataRepository,
          userNewsResourceRepository = userNewsResourceRepository,
      ).stateIn(
              scope = viewModelScope,
              started = SharingStarted.WhileSubscribed(5_000),
              initialValue = NewsUiState.Loading,
          )
      

      then you can use your screen like this

        val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle()
        val newsUiState: NewsUiState by viewModel.newsUiState.collectAsStateWithLifecycle()
      
    2. What is the best approach for my case?

      The following Google samples demonstrate good app architecture. Go explore them to see this guidance in practice samples