I think I got navigation working?

This commit is contained in:
Z. Charles Dziura 2024-10-31 23:48:14 -04:00
parent db7264c3b9
commit dd0cc9ba2f
10 changed files with 153 additions and 45 deletions

View file

@ -4,36 +4,71 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import ing.bikeshedengineer.debtpirate.theme.DebtPirateTheme
import ing.bikeshedengineer.debtpirate.auth.presentation.login.LoginScreen import ing.bikeshedengineer.debtpirate.auth.presentation.login.LoginScreen
import ing.bikeshedengineer.debtpirate.auth.presentation.register.RegistrationScreen import ing.bikeshedengineer.debtpirate.auth.presentation.register.RegistrationScreen
import kotlinx.serialization.Serializable import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.NavigationAction
@Serializable import ing.bikeshedengineer.debtpirate.navigation.Navigator
object LoginRoute import ing.bikeshedengineer.debtpirate.theme.DebtPirateTheme
import kotlinx.coroutines.Dispatchers
@Serializable import kotlinx.coroutines.flow.Flow
object RegistrationRoute import kotlinx.coroutines.withContext
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@Inject
lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
ObserveAsEvents(navigator.navigationActions) { action ->
when (action) {
is NavigationAction.Navigate -> {
navController.navigate(action.destination) {
action.navOptions(this)
}
}
NavigationAction.NavigateUp -> {
navController.navigateUp()
}
}
}
DebtPirateTheme { DebtPirateTheme {
NavHost(navController = navController, startDestination = LoginRoute) { NavHost(navController = navController, startDestination = Destination.AuthGraph) {
composable<LoginRoute>() { LoginScreen(navController = navController) } navigation<Destination.AuthGraph>(startDestination = Destination.LoginScreen) {
composable<RegistrationRoute>() { RegistrationScreen() } composable<Destination.LoginScreen>() { LoginScreen(navController = navController) }
composable<Destination.RegistrationScreen>() { RegistrationScreen() }
}
} }
} }
} }
} }
}
@Composable
private fun <T> ObserveAsEvents(flow: Flow<T>, onEvent: (T) -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner.lifecycle) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
withContext(Dispatchers.Main.immediate) {
flow.collect(onEvent)
}
}
}
} }

View file

@ -3,31 +3,31 @@ package ing.bikeshedengineer.debtpirate.auth.di
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.auth.domain.repository.AuthRepositoryImpl import ing.bikeshedengineer.debtpirate.auth.domain.repository.AuthRepositoryImpl
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase
import retrofit2.Retrofit import retrofit2.Retrofit
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(ViewModelComponent::class)
object AuthDiModule { object AuthDiModule {
@Provides @Provides
@Singleton @ViewModelScoped
fun provideAuthRepository(httpClient: Retrofit): AuthRepository { fun provideAuthRepository(httpClient: Retrofit): AuthRepository {
return AuthRepositoryImpl(httpClient) return AuthRepositoryImpl(httpClient)
} }
@Provides @Provides
@Singleton @ViewModelScoped
fun provideSubmitLoginCredentialsUseCase(authRepository: AuthRepository): SubmitLoginCredentialsUseCase { fun provideSubmitLoginCredentialsUseCase(authRepository: AuthRepository): SubmitLoginCredentialsUseCase {
return SubmitLoginCredentialsUseCase(authRepository) return SubmitLoginCredentialsUseCase(authRepository)
} }
@Provides @Provides
@Singleton @ViewModelScoped
fun provideValidateLoginCredentialsUseCase() = ValidateLoginCredentialsUseCase() fun provideValidateLoginCredentialsUseCase() = ValidateLoginCredentialsUseCase()
} }

View file

@ -3,10 +3,10 @@ package ing.bikeshedengineer.debtpirate.auth.domain.repository
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import ing.bikeshedengineer.debtpirate.auth.data.remote.endpoint.AuthEndpoint import ing.bikeshedengineer.debtpirate.auth.data.remote.endpoint.AuthEndpoint
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse import ing.bikeshedengineer.debtpirate.auth.data.remote.model.AuthLoginResponse
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import retrofit2.Retrofit import retrofit2.Retrofit

View file

@ -1,6 +0,0 @@
package ing.bikeshedengineer.debtpirate.auth.navigation
sealed class AuthNavigationEvent {
object NavigateToRegistrationScreen : AuthNavigationEvent()
object NavigateToLoginScreen : AuthNavigationEvent()
}

View file

@ -25,7 +25,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -42,8 +41,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import ing.bikeshedengineer.debtpirate.R import ing.bikeshedengineer.debtpirate.R
import ing.bikeshedengineer.debtpirate.app.host.RegistrationRoute
import ing.bikeshedengineer.debtpirate.auth.navigation.AuthNavigationEvent
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
@ -51,17 +48,17 @@ fun LoginScreen(
navController: NavController, navController: NavController,
viewModel: LoginScreenViewModel = hiltViewModel<LoginScreenViewModel>() viewModel: LoginScreenViewModel = hiltViewModel<LoginScreenViewModel>()
) { ) {
LaunchedEffect(viewModel.navigationEvent) { // LaunchedEffect(viewModel.navigationEvent) {
viewModel.navigationEvent.collect { event -> // viewModel.navigationEvent.collect { event ->
when (event) { // when (event) {
is AuthNavigationEvent.NavigateToRegistrationScreen -> { // is AuthNavigationEvent.NavigateToRegistrationScreen -> {
navController.navigate(RegistrationRoute) // navController.navigate(RegistrationRoute)
} // }
//
else -> {} // else -> {}
} // }
} // }
} // }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View file

@ -5,13 +5,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import ing.bikeshedengineer.debtpirate.PrefsDataStore import ing.bikeshedengineer.debtpirate.PrefsDataStore
import ing.bikeshedengineer.debtpirate.app.host.RegistrationRoute
import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository import ing.bikeshedengineer.debtpirate.auth.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.LoginCredentialsValidationResult import ing.bikeshedengineer.debtpirate.auth.domain.usecase.LoginCredentialsValidationResult
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.auth.domain.usecase.SubmitLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.auth.domain.usecase.ValidateLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.auth.navigation.AuthNavigationEvent
import ing.bikeshedengineer.debtpirate.domain.model.Token import ing.bikeshedengineer.debtpirate.domain.model.Token
import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,6 +37,7 @@ enum class InvalidReason {
@HiltViewModel @HiltViewModel
class LoginScreenViewModel @Inject constructor( class LoginScreenViewModel @Inject constructor(
private val navigator: Navigator,
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val prefsStore: DataStore<PrefsDataStore>, private val prefsStore: DataStore<PrefsDataStore>,
private val submitLoginCredentials: SubmitLoginCredentialsUseCase, private val submitLoginCredentials: SubmitLoginCredentialsUseCase,
@ -44,9 +45,6 @@ class LoginScreenViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
// private val storeLoginData = StoreLoginDataUseCase(dataStore) // private val storeLoginData = StoreLoginDataUseCase(dataStore)
private val _navigationEvent = MutableStateFlow<AuthNavigationEvent?>(null)
val navigationEvent = _navigationEvent.asStateFlow()
private val _emailAddress = MutableStateFlow("") private val _emailAddress = MutableStateFlow("")
val emailAddress = _emailAddress.asStateFlow() val emailAddress = _emailAddress.asStateFlow()
@ -101,6 +99,8 @@ class LoginScreenViewModel @Inject constructor(
} }
fun onRegisterButtonClick() { fun onRegisterButtonClick() {
_navigationEvent.value = AuthNavigationEvent.NavigateToRegistrationScreen viewModelScope.launch {
navigator.navigate(destination = Destination.RegistrationScreen)
}
} }
} }

View file

@ -0,0 +1,19 @@
package ing.bikeshedengineer.debtpirate.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator
import ing.bikeshedengineer.debtpirate.navigation.NavigatorImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NavigatorModule {
@Provides
@Singleton
fun provideNavigator(): Navigator = NavigatorImpl(startDestination = Destination.AuthGraph)
}

View file

@ -0,0 +1,16 @@
package ing.bikeshedengineer.debtpirate.navigation
import kotlinx.serialization.Serializable
sealed interface Destination {
/* Auth Destinations */
@Serializable
data object AuthGraph : Destination
@Serializable
data object LoginScreen : Destination
@Serializable
data object RegistrationScreen : Destination
}

View file

@ -0,0 +1,13 @@
package ing.bikeshedengineer.debtpirate.navigation
import androidx.navigation.NavOptionsBuilder
sealed interface NavigationAction {
data class Navigate(
val destination: Destination,
val navOptions: NavOptionsBuilder.() -> Unit = {}
) : NavigationAction
data object NavigateUp : NavigationAction
}

View file

@ -0,0 +1,34 @@
package ing.bikeshedengineer.debtpirate.navigation
import androidx.navigation.NavOptionsBuilder
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
interface Navigator {
val startDestination: Destination
val navigationActions: Flow<NavigationAction>
suspend fun navigate(destination: Destination, navOptions: NavOptionsBuilder.() -> Unit = {})
suspend fun navigateUp()
}
class NavigatorImpl(
override val startDestination: Destination
) : Navigator {
private val _navigationActions = Channel<NavigationAction>()
override val navigationActions = _navigationActions.receiveAsFlow()
override suspend fun navigate(
destination: Destination,
navOptions: NavOptionsBuilder.() -> Unit
) {
_navigationActions.send(NavigationAction.Navigate(destination, navOptions))
}
override suspend fun navigateUp() {
_navigationActions.send(NavigationAction.NavigateUp)
}
}