mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 08:55:48 +03:00
BIT-144: Setup root level app navigation (#8)
This commit is contained in:
parent
cd204b9b11
commit
116d48d8ac
9 changed files with 266 additions and 20 deletions
|
@ -68,6 +68,7 @@ dependencies {
|
|||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
|
|
@ -3,13 +3,7 @@ package com.x8bit.bitwarden
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.x8bit.bitwarden.ui.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.feature.rootnav.RootNavScreen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
|
@ -19,17 +13,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
BitwardenTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_name),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
setContent { RootNavScreen() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.x8bit.bitwarden.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Temporary composable that can be used as a navigation placeholder.
|
||||
*/
|
||||
@Composable
|
||||
fun PlaceholderComposable(
|
||||
text: String = "Placeholder Composable"
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package com.x8bit.bitwarden.ui.feature.rootnav
|
||||
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.ui.components.PlaceholderComposable
|
||||
|
||||
/**
|
||||
* Controls root level [NavHost] for the app.
|
||||
*/
|
||||
@Composable
|
||||
fun RootNavScreen(
|
||||
viewModel: RootNavViewModel = viewModel()
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = SplashRoute,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
) {
|
||||
splashDestinations()
|
||||
loginDestinations()
|
||||
}
|
||||
|
||||
// When state changes, navigate to different root navigation state
|
||||
when (state) {
|
||||
RootNavState.Login -> navController.navigateToLoginAsRoot()
|
||||
RootNavState.Splash -> navController.navigateToSplashAsRoot()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The functions below should be moved to their respective feature packages once they exist.
|
||||
*
|
||||
* For an example of how to setup these nav extensions, see NIA project.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO: move to splash package (BIT-147)
|
||||
*/
|
||||
private const val SplashRoute = "splash"
|
||||
|
||||
/**
|
||||
* Add splash destinations to the nav graph.
|
||||
*
|
||||
* TODO: move to splash package (BIT-147)
|
||||
*/
|
||||
private fun NavGraphBuilder.splashDestinations() {
|
||||
composable(SplashRoute) {
|
||||
PlaceholderComposable(text = "Splash")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the splash screen. Note this will only work if splash destination was added
|
||||
* via [splashDestinations].
|
||||
*
|
||||
* TODO: move to splash package (BIT-147)
|
||||
*
|
||||
*/
|
||||
private fun NavController.navigateToSplashAsRoot() {
|
||||
navigate(SplashRoute) {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(graph.id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO move to login package(BIT-146)
|
||||
*/
|
||||
private val LoginRoute = "login"
|
||||
|
||||
/**
|
||||
* Add login destinations to the nav graph.
|
||||
*
|
||||
* TODO: move to login package (BIT-146)
|
||||
*/
|
||||
private fun NavGraphBuilder.loginDestinations() {
|
||||
composable(LoginRoute) {
|
||||
PlaceholderComposable(text = "Login")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the splash screen. Note this will only work if login destination was added
|
||||
* via [loginDestinations].
|
||||
*
|
||||
* TODO: move to login package (BIT-146)
|
||||
*/
|
||||
private fun NavController.navigateToLoginAsRoot() {
|
||||
navigate(LoginRoute) {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(graph.id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.x8bit.bitwarden.ui.feature.rootnav
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Manages root level navigation state of the application.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class RootNavViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<RootNavState>(RootNavState.Splash)
|
||||
val state: StateFlow<RootNavState> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
delay(1000)
|
||||
_state.value = RootNavState.Login
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the root level navigation of the app.
|
||||
*/
|
||||
sealed class RootNavState {
|
||||
/**
|
||||
* Show the login screen.
|
||||
*/
|
||||
data object Login : RootNavState()
|
||||
|
||||
/**
|
||||
* Show the splash screen.
|
||||
*/
|
||||
data object Splash : RootNavState()
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.x8bit.bitwarden.example
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.jupiter.api.extension.AfterAllCallback
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeAllCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
|
||||
/**
|
||||
* JUnit 5 Extension for automatically setting a [testDispatcher] as the "main" dispatcher.
|
||||
*
|
||||
* Note that this may be used as a normal class property with [RegisterExtension] or may be applied
|
||||
* directly to a test class using [ExtendWith].
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherExtension(
|
||||
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
||||
) : AfterAllCallback,
|
||||
AfterEachCallback,
|
||||
BeforeAllCallback,
|
||||
BeforeEachCallback {
|
||||
override fun afterAll(context: ExtensionContext?) {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
override fun beforeAll(context: ExtensionContext?) {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.x8bit.bitwarden.example.ui
|
||||
|
||||
import com.x8bit.bitwarden.example.MainDispatcherExtension
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
|
||||
abstract class BaseViewModelTest {
|
||||
@RegisterExtension
|
||||
protected open val mainDispatcherExtension = MainDispatcherExtension()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.example.ui.feature.rootnav
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.example.ui.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.feature.rootnav.RootNavState
|
||||
import com.x8bit.bitwarden.ui.feature.rootnav.RootNavViewModel
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class RootNavViewModelTests : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be splash`() {
|
||||
val viewModel = RootNavViewModel()
|
||||
assert(viewModel.state.value is RootNavState.Splash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should move from splash to login`() = runTest {
|
||||
val viewModel = RootNavViewModel()
|
||||
viewModel.state.test {
|
||||
assert(awaitItem() is RootNavState.Splash)
|
||||
assert(awaitItem() is RootNavState.Login)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ androidxComposeBom = "2023.06.01"
|
|||
androidxCore = "1.10.1"
|
||||
androidxHiltNavigationCompose = "1.0.0"
|
||||
androidxLifecycle = "2.6.1"
|
||||
androidxNavigation = "2.6.0"
|
||||
androidxNavigation = "2.7.0"
|
||||
androidxRoom = "2.5.2"
|
||||
detekt = "1.23.1"
|
||||
firebaseBom = "32.2.2"
|
||||
|
@ -51,6 +51,7 @@ androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
|||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
|
||||
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
|
||||
androidx-lifecycle-runtime-compose ={ module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
|
||||
|
|
Loading…
Reference in a new issue