mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 07:11:51 +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.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
|
@ -3,13 +3,7 @@ package com.x8bit.bitwarden
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import com.x8bit.bitwarden.ui.feature.rootnav.RootNavScreen
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,17 +13,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent { RootNavScreen() }
|
||||||
BitwardenTheme {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
color = MaterialTheme.colorScheme.background,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.app_name),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
androidxCore = "1.10.1"
|
||||||
androidxHiltNavigationCompose = "1.0.0"
|
androidxHiltNavigationCompose = "1.0.0"
|
||||||
androidxLifecycle = "2.6.1"
|
androidxLifecycle = "2.6.1"
|
||||||
androidxNavigation = "2.6.0"
|
androidxNavigation = "2.7.0"
|
||||||
androidxRoom = "2.5.2"
|
androidxRoom = "2.5.2"
|
||||||
detekt = "1.23.1"
|
detekt = "1.23.1"
|
||||||
firebaseBom = "32.2.2"
|
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-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
|
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-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-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
|
||||||
|
|
Loading…
Reference in a new issue