最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

navigation - Compose resetting state of the screen when going back, but not when going forward - Stack Overflow

programmeradmin2浏览0评论

In my app I have 3 screens: QueryScreen, BookListScreen and BookDetailsScreen. Each has a UiState class which is managed by a ViewModel. Navigation between screens is done using NavHost.

The logic behind my app is this:

  • QueryScreen: I enter a book that I want to search and press search. I call the function searchBooks() from the viewModel which fetches a list of books from a server and navigates to BookListScreen.

  • BookListScreen: Shows the list of books fetched and when user clicks on a book, the function searchBook() from the viewModel is called which fetches more details about that book and navigates to BookDetailsScreen.

  • BookDetailsScreen: Shows details about the book fetched earlier.

When I navigate from first screen onward, I update the state accordingly and my app behaves as it should. The problem I am facing is for the back navigation. Let's say I am in BookDetails and I go back to BookList. That means I should make val book: Book = null in BookDetailsUiState.

The same thing for BookList. When I go back to QueryScreen I should set val books: List<Book> = emptyList() in BookListUiState.

What is the best approach to do this? Let's say I have 20 screens and it will be really tedious having a flag for each screen to keep track whether I am going back or forth. If you can explain me 2 ways of doing this I will be grateful, thank you!

These are my state classes:

    data class QueryUiState(
        val query: String = "",
        val isFetchingBooks: Boolean = false,
        val errorMessage: String? = null
    )
    data class BookListUiState(
        val books: List<Book> = emptyList(),
        val isFetchingBook: Boolean = false,
        val errorMessage: String? = null,
        val selectedBook: Book? = null
    )
    data class BookDetailsUiState(
        val book: Book? = null
    )

This is how I perform navigation between screens:

    object BookshelfDestinations {
        const val QUERY_ROUTE = "query"
        const val BOOK_LIST_ROUTE = "bookList"
        const val BOOK_DETAILS_ROUTE = "bookDetails"
    }
    
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun BookshelfApp(
        appContainer: AppContainer,
        navController: NavHostController = rememberNavController(),
        modifier: Modifier = Modifier
    ) {
        Scaffold { innerPadding ->
            BookshelfNavHost(
                appContainer = appContainer,
                navController = navController,
                modifier = modifier.padding(innerPadding)
            )
        }
    }
    
    @Composable
    fun BookshelfNavHost(
        appContainer: AppContainer,
        navController: NavHostController,
        modifier: Modifier = Modifier
    ) {
        val viewModel: AppViewModel =
            viewModel(factory = AppViewModel.AppViewModelFactory(appContainer.bookshelfRepository))
    
        NavHost(
            navController = navController,
            startDestination = BookshelfDestinations.QUERY_ROUTE,
            modifier = modifier
        ) {
            composable(route = BookshelfDestinations.QUERY_ROUTE) {
                QueryScreen(
                    viewModel = viewModel,
                    onSearch = {
                        navController.navigate(BookshelfDestinations.BOOK_LIST_ROUTE)
                    }
                )
            }
            composable(route = BookshelfDestinations.BOOK_LIST_ROUTE) {
                BookListScreen(
                    viewModel = viewModel,
                    onBookClicked = {
                        navController.navigate(BookshelfDestinations.BOOK_DETAILS_ROUTE)
                    }
                )
            }
            composable(route = BookshelfDestinations.BOOK_DETAILS_ROUTE) {
                BookDetailsScreen(
                    viewModel = viewModel
                )
            }
        }
    }

This is my viewModel class:

class AppViewModel(private val repository: BookshelfRepository) : ViewModel() {
    private val _queryUiState = MutableStateFlow(QueryUiState())
    val queryUiState: StateFlow<QueryUiState> = _queryUiState.asStateFlow()

    private val _bookListUiState = MutableStateFlow(BookListUiState())
    val bookListUiState: StateFlow<BookListUiState> = _bookListUiState.asStateFlow()

    private val _bookDetailsUiState = MutableStateFlow(BookDetailsUiState())
    val bookDetailsUiState: StateFlow<BookDetailsUiState> = _bookDetailsUiState.asStateFlow()

    init {
        Log.d("AppViewModel", "App view model initialized!")
    }

    fun onQueryChanged(query: String) {
        _queryUiState.value = _queryUiState.value.copy(query = query)
    }

    fun onBookSelected(book: Book) {
        _bookListUiState.value = _bookListUiState.value.copy(selectedBook = book)
        searchBook()
    }

    fun searchBooks() {
        viewModelScope.launch {
            _queryUiState.value = _queryUiState.value.copy(isFetchingBooks = true)
            try {
                val result = repository.getBooks(_queryUiState.value.query)
                when(result) {
                    is BookshelfResult.Success -> {
                        _bookListUiState.value = _bookListUiState.value.copy(books = result.data)
                    }
                    is BookshelfResult.Error -> {
                        _queryUiState.value = _queryUiState.value.copy(errorMessage = result.exception.message)
                    }
                }
            } catch (e: Exception) {
                _queryUiState.value = _queryUiState.value.copy(errorMessage = e.message)
            } finally {
                _queryUiState.value = _queryUiState.value.copy(isFetchingBooks = false)
            }
        }
    }

    fun searchBook() {
        viewModelScope.launch {
            _bookListUiState.value = _bookListUiState.value.copy(isFetchingBook = true)
            try {
                val result = repository.getBook(_bookListUiState.value.selectedBook!!.id)
                when(result) {
                    is BookshelfResult.Success -> {
                        _bookDetailsUiState.value = _bookDetailsUiState.value.copy(book = result.data)
                    }
                    is BookshelfResult.Error -> {
                        _bookListUiState.value = _bookListUiState.value.copy(errorMessage = result.exception.message)
                    }
                }
            } catch (e: Exception) {
                _bookListUiState.value = _bookListUiState.value.copy(errorMessage = e.message)
            } finally {
                _bookListUiState.value = _bookListUiState.value.copy(isFetchingBook = false)
            }
        }
    }

    class AppViewModelFactory(private val appRepository: BookshelfRepository) :
        ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(AppViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return AppViewModel(appRepository) as T
            }
            throw IllegalArgumentException("Unknown ViewModel class")
        }
    }
}

In my app I have 3 screens: QueryScreen, BookListScreen and BookDetailsScreen. Each has a UiState class which is managed by a ViewModel. Navigation between screens is done using NavHost.

The logic behind my app is this:

  • QueryScreen: I enter a book that I want to search and press search. I call the function searchBooks() from the viewModel which fetches a list of books from a server and navigates to BookListScreen.

  • BookListScreen: Shows the list of books fetched and when user clicks on a book, the function searchBook() from the viewModel is called which fetches more details about that book and navigates to BookDetailsScreen.

  • BookDetailsScreen: Shows details about the book fetched earlier.

When I navigate from first screen onward, I update the state accordingly and my app behaves as it should. The problem I am facing is for the back navigation. Let's say I am in BookDetails and I go back to BookList. That means I should make val book: Book = null in BookDetailsUiState.

The same thing for BookList. When I go back to QueryScreen I should set val books: List<Book> = emptyList() in BookListUiState.

What is the best approach to do this? Let's say I have 20 screens and it will be really tedious having a flag for each screen to keep track whether I am going back or forth. If you can explain me 2 ways of doing this I will be grateful, thank you!

These are my state classes:

    data class QueryUiState(
        val query: String = "",
        val isFetchingBooks: Boolean = false,
        val errorMessage: String? = null
    )
    data class BookListUiState(
        val books: List<Book> = emptyList(),
        val isFetchingBook: Boolean = false,
        val errorMessage: String? = null,
        val selectedBook: Book? = null
    )
    data class BookDetailsUiState(
        val book: Book? = null
    )

This is how I perform navigation between screens:

    object BookshelfDestinations {
        const val QUERY_ROUTE = "query"
        const val BOOK_LIST_ROUTE = "bookList"
        const val BOOK_DETAILS_ROUTE = "bookDetails"
    }
    
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun BookshelfApp(
        appContainer: AppContainer,
        navController: NavHostController = rememberNavController(),
        modifier: Modifier = Modifier
    ) {
        Scaffold { innerPadding ->
            BookshelfNavHost(
                appContainer = appContainer,
                navController = navController,
                modifier = modifier.padding(innerPadding)
            )
        }
    }
    
    @Composable
    fun BookshelfNavHost(
        appContainer: AppContainer,
        navController: NavHostController,
        modifier: Modifier = Modifier
    ) {
        val viewModel: AppViewModel =
            viewModel(factory = AppViewModel.AppViewModelFactory(appContainer.bookshelfRepository))
    
        NavHost(
            navController = navController,
            startDestination = BookshelfDestinations.QUERY_ROUTE,
            modifier = modifier
        ) {
            composable(route = BookshelfDestinations.QUERY_ROUTE) {
                QueryScreen(
                    viewModel = viewModel,
                    onSearch = {
                        navController.navigate(BookshelfDestinations.BOOK_LIST_ROUTE)
                    }
                )
            }
            composable(route = BookshelfDestinations.BOOK_LIST_ROUTE) {
                BookListScreen(
                    viewModel = viewModel,
                    onBookClicked = {
                        navController.navigate(BookshelfDestinations.BOOK_DETAILS_ROUTE)
                    }
                )
            }
            composable(route = BookshelfDestinations.BOOK_DETAILS_ROUTE) {
                BookDetailsScreen(
                    viewModel = viewModel
                )
            }
        }
    }

This is my viewModel class:

class AppViewModel(private val repository: BookshelfRepository) : ViewModel() {
    private val _queryUiState = MutableStateFlow(QueryUiState())
    val queryUiState: StateFlow<QueryUiState> = _queryUiState.asStateFlow()

    private val _bookListUiState = MutableStateFlow(BookListUiState())
    val bookListUiState: StateFlow<BookListUiState> = _bookListUiState.asStateFlow()

    private val _bookDetailsUiState = MutableStateFlow(BookDetailsUiState())
    val bookDetailsUiState: StateFlow<BookDetailsUiState> = _bookDetailsUiState.asStateFlow()

    init {
        Log.d("AppViewModel", "App view model initialized!")
    }

    fun onQueryChanged(query: String) {
        _queryUiState.value = _queryUiState.value.copy(query = query)
    }

    fun onBookSelected(book: Book) {
        _bookListUiState.value = _bookListUiState.value.copy(selectedBook = book)
        searchBook()
    }

    fun searchBooks() {
        viewModelScope.launch {
            _queryUiState.value = _queryUiState.value.copy(isFetchingBooks = true)
            try {
                val result = repository.getBooks(_queryUiState.value.query)
                when(result) {
                    is BookshelfResult.Success -> {
                        _bookListUiState.value = _bookListUiState.value.copy(books = result.data)
                    }
                    is BookshelfResult.Error -> {
                        _queryUiState.value = _queryUiState.value.copy(errorMessage = result.exception.message)
                    }
                }
            } catch (e: Exception) {
                _queryUiState.value = _queryUiState.value.copy(errorMessage = e.message)
            } finally {
                _queryUiState.value = _queryUiState.value.copy(isFetchingBooks = false)
            }
        }
    }

    fun searchBook() {
        viewModelScope.launch {
            _bookListUiState.value = _bookListUiState.value.copy(isFetchingBook = true)
            try {
                val result = repository.getBook(_bookListUiState.value.selectedBook!!.id)
                when(result) {
                    is BookshelfResult.Success -> {
                        _bookDetailsUiState.value = _bookDetailsUiState.value.copy(book = result.data)
                    }
                    is BookshelfResult.Error -> {
                        _bookListUiState.value = _bookListUiState.value.copy(errorMessage = result.exception.message)
                    }
                }
            } catch (e: Exception) {
                _bookListUiState.value = _bookListUiState.value.copy(errorMessage = e.message)
            } finally {
                _bookListUiState.value = _bookListUiState.value.copy(isFetchingBook = false)
            }
        }
    }

    class AppViewModelFactory(private val appRepository: BookshelfRepository) :
        ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(AppViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return AppViewModel(appRepository) as T
            }
            throw IllegalArgumentException("Unknown ViewModel class")
        }
    }
}
Share Improve this question asked 18 hours ago Andrei RusuAndrei Rusu 153 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

I see two possible approaches here.

Option A

You can try to use a BackHandler to detect back events, and then call a ViewModel function to reset the respective value:

BookDetailsScreen(
    viewModel: AppViewModel,
    onBack: () -> Unit
)  {
    
    BackHandler {
        viewModel.resetBook()
        onBack()
    }
}

And then, in your NavHost do

composable(route = BookshelfDestinations.BOOK_DETAILS_ROUTE) {
    BookDetailsScreen(
        viewModel = viewModel,
        onBack = {
            navController.popBackStack()
        }
    )
}

Option B (recommended)

What I think is more clean however is to choose a proper scope for your ViewModel. Currently, your ViewModel is outside of the NavHost and thus scoped to the Activity. If you had three smaller ViewModels, you can scope them to destinations in the NavGraph and then they will be reset whenever you leave this destination.

You can try code like this:

QueryScreen(
    viewModel: QueryViewModel = viewModel(),
    onSearch = () -> Unit
) {
    //...
}

BookListScreen(
    viewModel: BookListScreen = viewModel(),
    onBookClicked: () -> Unit
) {
    //...
}

BookDetailsScreen(
    viewModel: BookDetailsViewModel = viewModel(),
) {
    //...
}

The viewModel function is a convenience function included in the following dependency:

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")

It automatically generates a ViewModel instance without needing a manual factory class.

Then, you can pass the needed data, like a book ID, from one destination to the next using navigation arguments as described in the official documentation. Also have a look at the navigation documentation.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论