Skip to content
Permalink
1630f6b35a
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
146 lines (135 sloc) 4.65 KB
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.jetnews.ui
import android.os.Bundle
import androidx.annotation.MainThread
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.example.jetnews.ui.Screen.Article
import com.example.jetnews.ui.Screen.Home
import com.example.jetnews.ui.Screen.Interests
import com.example.jetnews.ui.ScreenName.ARTICLE
import com.example.jetnews.ui.ScreenName.HOME
import com.example.jetnews.ui.ScreenName.INTERESTS
import com.example.jetnews.utils.getMutableStateOf
/**
* Screen names (used for serialization)
*/
enum class ScreenName { HOME, INTERESTS, ARTICLE }
/**
* Class defining the screens we have in the app: home, article details and interests
*/
sealed class Screen(val id: ScreenName) {
object Home : Screen(HOME)
object Interests : Screen(INTERESTS)
data class Article(val postId: String) : Screen(ARTICLE)
}
/**
* Helpers for saving and loading a [Screen] object to a [Bundle].
*
* This allows us to persist navigation across process death, for example caused by a long video
* call.
*/
private const val SIS_SCREEN = "sis_screen"
private const val SIS_NAME = "screen_name"
private const val SIS_POST = "post"
/**
* Convert a screen to a bundle that can be stored in [SavedStateHandle]
*/
private fun Screen.toBundle(): Bundle {
return bundleOf(SIS_NAME to id.name).also {
// add extra keys for various types here
if (this is Article) {
it.putString(SIS_POST, postId)
}
}
}
/**
* Read a bundle stored by [Screen.toBundle] and return desired screen.
*
* @return the parsed [Screen]
* @throws IllegalArgumentException if the bundle could not be parsed
*/
private fun Bundle.toScreen(): Screen {
val screenName = ScreenName.valueOf(getStringOrThrow(SIS_NAME))
return when (screenName) {
HOME -> Home
INTERESTS -> Interests
ARTICLE -> {
val postId = getStringOrThrow(SIS_POST)
Article(postId)
}
}
}
/**
* Throw [IllegalArgumentException] if key is not in bundle.
*
* @see Bundle.getString
*/
private fun Bundle.getStringOrThrow(key: String) =
requireNotNull(getString(key)) { "Missing key '$key' in $this" }
/**
* This is expected to be replaced by the navigation component, but for now handle navigation
* manually.
*
* Instantiate this ViewModel at the scope that is fully-responsible for navigation, which in this
* application is [MainActivity].
*
* This app has simplified navigation; the back stack is always [Home] or [Home, dest] and more
* levels are not allowed. To use a similar pattern with a longer back stack, use a [StateList] to
* hold the back stack state.
*/
class NavigationViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
/**
* Hold the current screen in an observable, restored from savedStateHandle after process
* death.
*
* mutableStateOf is an observable similar to LiveData that's designed to be read by compose. It
* supports observability via property delegate syntax as shown here.
*/
var currentScreen: Screen by savedStateHandle.getMutableStateOf<Screen>(
key = SIS_SCREEN,
default = Home,
save = { it.toBundle() },
restore = { it.toScreen() }
)
private set // limit the writes to only inside this class.
/**
* Go back (always to [Home]).
*
* Returns true if this call caused user-visible navigation. Will always return false
* when [currentScreen] is [Home].
*/
@MainThread
fun onBack(): Boolean {
val wasHandled = currentScreen != Home
currentScreen = Home
return wasHandled
}
/**
* Navigate to requested [Screen].
*
* If the requested screen is not [Home], it will always create a back stack with one element:
* ([Home] -> [screen]). More back entries are not supported in this app.
*/
@MainThread
fun navigateTo(screen: Screen) {
currentScreen = screen
}
}