Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Splash Login Event 연결 #5

Merged
merged 1 commit into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ac.dnd.bookkeeping.android.domain.usecase.authentication

import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import kotlinx.coroutines.delay
import javax.inject.Inject

class IsLoginSucceedUseCase @Inject constructor(
private val authenticationRepository: AuthenticationRepository
) {
suspend operator fun invoke(): Result<Unit> {
// TODO: Implement this
delay(1000L)
return Result.success(Unit)
}
}
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ compose-livedata = "1.5.4"
compose-navi = "2.7.0"
compose-hiltnavi = "1.1.0"
compose-bottomsheet = "1.3.1"
compose-lifecycle = "2.7.0"

[libraries]
# Kotlin
Expand Down Expand Up @@ -109,6 +110,7 @@ compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview", versio
compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-viewmodel" }
compose-navi = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navi" }
compose-hiltnavi = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "compose-hiltnavi" }
compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" }

compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose" }
compose-ui-test = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose" }
Expand All @@ -132,7 +134,8 @@ kotlin = ["kotlin", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "ko
androidx-data = ["androidx-room-runtime", "androidx-room-coroutine", "androidx-paging-runtime"]
androidx-presentation = ["androidx-core", "androidx-appcompat", "androidx-constraintlayout", "androidx-fragment",
"androidx-viewmodel", "androidx-livedata", "androidx-navigation-fragment", "androidx-navigation-ui",
"compose-ui-ui", "compose-ui-preview", "compose-material", "compose-viewmodel", "compose-hiltnavi", "compose-navi"]
"compose-ui-ui", "compose-ui-preview", "compose-material", "compose-viewmodel",
"compose-hiltnavi", "compose-navi", "compose-lifecycle"]
network = ["ktor-core", "ktor-okhttp", "ktor-resources", "ktor-content-negotiation", "ktor-kotlinx-serialization",
"ktor-auth", "kotlinx-serialization"]
logging = ["timber", "sentry"]
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package ac.dnd.bookkeeping.android.presentation.common.util.coroutine

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext

// TODO : 고도화하기
suspend fun <T1, T2> zip(
Expand All @@ -17,3 +24,11 @@ suspend fun <T1, T2> zip(
}
}

fun LifecycleOwner.repeatOnStarted(
context: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
) {
this.lifecycleScope.launch(context) {
repeatOnLifecycle(Lifecycle.State.STARTED, block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,28 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlin.coroutines.CoroutineContext

@Stable
class ApplicationState(
val navController: NavHostController,
val systemUiController: SystemUiController
val systemUiController: SystemUiController,
val coroutineExceptionHandler: (CoroutineContext, Throwable) -> Unit
)

@Composable
fun rememberApplicationState(
navController: NavHostController = rememberNavController(),
systemUiController: SystemUiController = rememberSystemUiController()
systemUiController: SystemUiController = rememberSystemUiController(),
coroutineExceptionHandler: (CoroutineContext, Throwable) -> Unit = { _, _ -> }
) = remember(
navController,
systemUiController
systemUiController,
coroutineExceptionHandler
) {
ApplicationState(
navController = navController,
systemUiController = systemUiController
systemUiController = systemUiController,
coroutineExceptionHandler = coroutineExceptionHandler
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import io.sentry.Sentry

@Composable
fun MainScreen() {
Expand All @@ -21,7 +22,12 @@ fun MainScreen() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val appState = rememberApplicationState()
val appState = rememberApplicationState(
coroutineExceptionHandler = { _, throwable ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙀👍

Sentry.captureException(throwable)
// TODO : Dialog Screen
}
)
ManageSystemUiState(appState = appState)

NavHost(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ac.dnd.bookkeeping.android.presentation.ui.main.splash

import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState
import androidx.core.os.bundleOf
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
Expand All @@ -16,10 +15,9 @@ fun NavGraphBuilder.splashDestination(
defaultValue = SplashConstant.ROUTE_ARGUMENT_ENTRY_POINT_MAIN
}
)
) { backStackEntry ->
) {
SplashScreen(
appState = appState,
arguments = backStackEntry.arguments ?: bundleOf()
appState = appState
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ac.dnd.bookkeeping.android.presentation.ui.main.splash

import ac.dnd.bookkeeping.android.domain.model.error.ServerException

sealed interface SplashEvent {
sealed interface Login : SplashEvent {
data object Success : Login
data class Failure(val exception: ServerException) : Login
data class Error(val exception: Throwable) : Login
}
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,107 @@
package ac.dnd.bookkeeping.android.presentation.ui.main.splash

import ac.dnd.bookkeeping.android.presentation.R
import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve
import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.repeatOnStarted
import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState
import ac.dnd.bookkeeping.android.presentation.ui.main.home.HomeConstant
import ac.dnd.bookkeeping.android.presentation.ui.main.login.LoginConstant
import android.os.Bundle
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.CoroutineExceptionHandler

@Composable
fun SplashScreen(
appState: ApplicationState,
// TODO : savedStateHandle 사용
arguments: Bundle,
viewModel: SplashViewModel = hiltViewModel()
) {
val scope = rememberCoroutineScope()
val entryPoint = arguments.getString(SplashConstant.ROUTE_ARGUMENT_ENTRY_POINT).orEmpty()
LaunchedEffect(Unit) {
scope.launch {
delay(1000L)
when (entryPoint) {
SplashConstant.ROUTE_ARGUMENT_ENTRY_POINT_MAIN -> {
appState.navController.navigate(LoginConstant.ROUTE) {
popUpTo(SplashConstant.ROUTE) {
inclusive = true
}
}
}
}
}
}
val state by viewModel.state.collectAsStateWithLifecycle()

Box(
Observer(
appState = appState,
viewModel = viewModel
)

Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black),
contentAlignment = Alignment.Center
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier.size(100.dp),
painter = painterResource(id = R.drawable.ic_launcher),
contentDescription = ""
)
Text(
text = "Splash Screen",
fontSize = 20.sp,
color = Color.White
color = Color.Black
)
}
}

@Composable
private fun Observer(
appState: ApplicationState,
viewModel: SplashViewModel
) {
val lifecycleOwner = LocalLifecycleOwner.current

fun navigateToLogin() {
appState.navController.navigate(LoginConstant.ROUTE) {
popUpTo(SplashConstant.ROUTE) {
inclusive = true
}
}
}

fun navigateToHome() {
appState.navController.navigate(HomeConstant.ROUTE) {
popUpTo(SplashConstant.ROUTE) {
inclusive = true
}
}
}

fun login(event: SplashEvent.Login) {
when (event) {
SplashEvent.Login.Success -> {
navigateToHome()
}

is SplashEvent.Login.Error -> {
navigateToLogin()
}

is SplashEvent.Login.Failure -> {
// TODO : Dialog Screen
}
}
}
LaunchedEffect(Unit) {
lifecycleOwner.repeatOnStarted(CoroutineExceptionHandler(appState.coroutineExceptionHandler)) {
viewModel.event.eventObserve { event ->
when (event) {
is SplashEvent.Login -> login(event)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ac.dnd.bookkeeping.android.presentation.ui.main.splash

sealed interface SplashState {
data object Init : SplashState
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,53 @@
package ac.dnd.bookkeeping.android.presentation.ui.main.splash

import ac.dnd.bookkeeping.android.domain.model.error.ServerException
import ac.dnd.bookkeeping.android.domain.usecase.authentication.IsLoginSucceedUseCase
import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel
import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow
import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow
import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.asEventFlow
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject

@HiltViewModel
class SplashViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : BaseViewModel()
private val savedStateHandle: SavedStateHandle,
private val isLoginSucceedUseCase: IsLoginSucceedUseCase
) : BaseViewModel() {

private val _state: MutableStateFlow<SplashState> = MutableStateFlow(SplashState.Init)
val state: StateFlow<SplashState> = _state.asStateFlow()

private val _event: MutableEventFlow<SplashEvent> = MutableEventFlow()
val event: EventFlow<SplashEvent> = _event.asEventFlow()

val entryPoint: String by lazy {
savedStateHandle.get<String>(SplashConstant.ROUTE_ARGUMENT_ENTRY_POINT).orEmpty()
}

init {
launch {
login()
}
}

private suspend fun login() {
isLoginSucceedUseCase().onSuccess {
_event.emit(SplashEvent.Login.Success)
}.onFailure { exception ->
when (exception) {
is ServerException -> {
_event.emit(SplashEvent.Login.Failure(exception))
}

else -> {
_event.emit(SplashEvent.Login.Error(exception))
}
}
}
}
}