diff --git a/app/src/main/java/today/pathos/android/portfolio/core/MainActivity.kt b/app/src/main/java/today/pathos/android/portfolio/core/MainActivity.kt index 10794e4..ce2684e 100644 --- a/app/src/main/java/today/pathos/android/portfolio/core/MainActivity.kt +++ b/app/src/main/java/today/pathos/android/portfolio/core/MainActivity.kt @@ -1,25 +1,41 @@ package today.pathos.android.portfolio.core import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.onEach import today.pathos.android.portfolio.presentation.view.PortfolioApp import today.pathos.android.portfolio.presentation.view.theme.TemplateAndroidTheme +import today.pathos.android.portfolio.presentation.viewmodel.state.MainEffectProvider +import today.pathos.android.portfolio.presentation.viewmodel.state.MainUiEffect +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + + @Inject + lateinit var mainEffectProvider: MainEffectProvider + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val mainEffect by mainEffectProvider.mainEffect.collectAsState() + val actionEffect by mainEffectProvider.actionEffect.collectAsState() + TemplateAndroidTheme { PortfolioApp( windowSizeClass = calculateWindowSizeClass(activity = this), - closeApp = { finish() } + closeApp = { finish() }, + mainEffect = mainEffect, + actionEffect = actionEffect ) } } diff --git a/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkCharacterRepository.kt b/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkCharacterRepository.kt index c37147d..9f1c4d5 100644 --- a/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkCharacterRepository.kt +++ b/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkCharacterRepository.kt @@ -7,7 +7,7 @@ import today.pathos.android.portfolio.data.datasource.remote.dto.res.ResAvatar import today.pathos.android.portfolio.data.datasource.remote.dto.res.ResCharacter import today.pathos.android.portfolio.data.datasource.remote.dto.res.ResEquipment import today.pathos.android.portfolio.data.datasource.remote.dto.res.ResItem -import today.pathos.android.portfolio.data.di.IoDispatcher +import today.pathos.android.portfolio.domain.di.IoDispatcher import today.pathos.android.portfolio.entity.Character import today.pathos.android.portfolio.entity.Equipment import today.pathos.android.portfolio.entity.Item diff --git a/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkFameRepository.kt b/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkFameRepository.kt index 1037639..a101b2d 100644 --- a/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkFameRepository.kt +++ b/data/src/main/java/today/pathos/android/portfolio/data/repository/NetworkFameRepository.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import today.pathos.android.portfolio.data.datasource.remote.NetworkDataSource import today.pathos.android.portfolio.data.datasource.remote.dto.res.ResCharacter -import today.pathos.android.portfolio.data.di.IoDispatcher +import today.pathos.android.portfolio.domain.di.IoDispatcher import today.pathos.android.portfolio.domain.repository.FameRepository import today.pathos.android.portfolio.entity.Character import javax.inject.Inject diff --git a/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstCharacterRepository.kt b/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstCharacterRepository.kt index c221198..0e2aa70 100644 --- a/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstCharacterRepository.kt +++ b/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstCharacterRepository.kt @@ -7,7 +7,7 @@ import today.pathos.android.portfolio.data.datasource.local.db.table.AvatarTbl import today.pathos.android.portfolio.data.datasource.local.db.table.CharacterTbl import today.pathos.android.portfolio.data.datasource.local.db.table.EquipmentTbl import today.pathos.android.portfolio.data.datasource.remote.NetworkDataSource -import today.pathos.android.portfolio.data.di.IoDispatcher +import today.pathos.android.portfolio.domain.di.IoDispatcher import today.pathos.android.portfolio.domain.repository.CharacterRepository import today.pathos.android.portfolio.entity.Avatar import today.pathos.android.portfolio.entity.Character diff --git a/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstFameRepository.kt b/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstFameRepository.kt index 2293bb9..93ecb94 100644 --- a/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstFameRepository.kt +++ b/data/src/main/java/today/pathos/android/portfolio/data/repository/OfflineFirstFameRepository.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.withContext import today.pathos.android.portfolio.data.datasource.local.LocalDataSource import today.pathos.android.portfolio.data.datasource.local.db.table.FameTbl import today.pathos.android.portfolio.data.datasource.remote.NetworkDataSource -import today.pathos.android.portfolio.data.di.IoDispatcher +import today.pathos.android.portfolio.domain.di.IoDispatcher import today.pathos.android.portfolio.domain.repository.FameRepository import today.pathos.android.portfolio.entity.Character import javax.inject.Inject diff --git a/data/src/main/java/today/pathos/android/portfolio/data/di/CoroutinesModule.kt b/domain/src/main/java/today/pathos/android/portfolio/domain/di/CoroutinesModule.kt similarity index 95% rename from data/src/main/java/today/pathos/android/portfolio/data/di/CoroutinesModule.kt rename to domain/src/main/java/today/pathos/android/portfolio/domain/di/CoroutinesModule.kt index 74761a2..7695927 100644 --- a/data/src/main/java/today/pathos/android/portfolio/data/di/CoroutinesModule.kt +++ b/domain/src/main/java/today/pathos/android/portfolio/domain/di/CoroutinesModule.kt @@ -1,4 +1,4 @@ -package today.pathos.android.portfolio.data.di +package today.pathos.android.portfolio.domain.di import dagger.Module import dagger.Provides diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioApp.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioApp.kt index 8d1ff1a..fe59fa3 100644 --- a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioApp.kt +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioApp.kt @@ -1,18 +1,27 @@ package today.pathos.android.portfolio.presentation.view +import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import today.pathos.android.portfolio.presentation.BuildConfig import today.pathos.android.portfolio.presentation.state.PortfolioAppState import today.pathos.android.portfolio.presentation.state.rememberPortfolioAppState +import today.pathos.android.portfolio.presentation.view.dialog.ErrorDialog +import today.pathos.android.portfolio.presentation.view.dialog.LoadingDialog +import today.pathos.android.portfolio.presentation.viewmodel.state.ActionEffect +import today.pathos.android.portfolio.presentation.viewmodel.state.MainUiEffect @Composable fun PortfolioApp( windowSizeClass: WindowSizeClass, closeApp: () -> Unit, + mainEffect: MainUiEffect, + actionEffect: ActionEffect, appState: PortfolioAppState = rememberPortfolioAppState( windowSizeClass = windowSizeClass ), @@ -24,5 +33,33 @@ fun PortfolioApp( closeApp = closeApp ) } + + LaunchedEffect(actionEffect) { + when (actionEffect) { + ActionEffect.Idle -> {} + is ActionEffect.NavigateTo -> appState.navController.navigateTo( + route = actionEffect.postDest.route, + cleanHistory = actionEffect.cleanHistory, + ) + } + } + + when (mainEffect) { + MainUiEffect.Idle -> {} + MainUiEffect.Loading -> { + LoadingDialog(onDismiss = {}) + } + + is MainUiEffect.Error -> { + ErrorDialog( + errorMsg = mainEffect.e.message, + onDismiss = { mainEffect.callback?.invoke(mainEffect.e) } + ) + } + }.also { + if (BuildConfig.DEBUG) { + Log.d("MainEffectProvider", "------mainEffect::$mainEffect") + } + } } } diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioNavHost.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioNavHost.kt index 374de02..829fbc0 100644 --- a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioNavHost.kt +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/PortfolioNavHost.kt @@ -93,13 +93,5 @@ fun PortfolioNavHost( ) { CharacterInfoRoute() } - -// dialog( -// route = OpenStoreDialog.route -// ) { -// OpenStoreRoute( -// onDismiss = { navController.navigateUp() } -// ) -// } } } diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/ErrorDialog.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/ErrorDialog.kt new file mode 100644 index 0000000..92bd55b --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/ErrorDialog.kt @@ -0,0 +1,63 @@ +package today.pathos.android.portfolio.presentation.view.dialog + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.dialog +import androidx.navigation.navArgument +import today.pathos.android.portfolio.presentation.R +import today.pathos.android.portfolio.presentation.state.PortfolioAppState +import today.pathos.android.portfolio.presentation.view.Screens.CharacterInfo +import today.pathos.android.portfolio.presentation.view.Screens.Main +import today.pathos.android.portfolio.presentation.view.Screens.NavigateUp +import today.pathos.android.portfolio.presentation.view.Screens.Splash +import today.pathos.android.portfolio.presentation.view.screen.CharacterInfoRoute +import today.pathos.android.portfolio.presentation.view.screen.MainRoute +import today.pathos.android.portfolio.presentation.view.screen.SplashRoute +import today.pathos.android.portfolio.presentation.view.theme.Typography + +@Composable +fun ErrorDialog( + errorMsg: String?, + onDismiss: () -> Unit = {}, +) { + if (errorMsg != null) { + AlertDialog( + title = { + Text( + text = stringResource(id = R.string.app_name), + style = Typography.titleMedium + ) + }, + text = { + Text( + text = errorMsg, + style = Typography.bodyLarge + ) + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = onDismiss + ) { + Text( + text = stringResource(id = R.string.btn_confirm), + style = Typography.bodyMedium + ) + } + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) + } +} diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/LoadingDialog.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/LoadingDialog.kt new file mode 100644 index 0000000..f363328 --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/view/dialog/LoadingDialog.kt @@ -0,0 +1,39 @@ +package today.pathos.android.portfolio.presentation.view.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoadingDialog( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ), + modifier = modifier + ) { + Text( + text = "Loading", + modifier = Modifier + .background( + color = Color.White, + shape = RoundedCornerShape(8.dp) + ) + .padding(16.dp) + ) + } +} diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/BaseViewModel.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..18d9234 --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/BaseViewModel.kt @@ -0,0 +1,34 @@ +package today.pathos.android.portfolio.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import today.pathos.android.portfolio.presentation.viewmodel.state.MainEffectProvider + +abstract class BaseViewModel( + private val mainEffectProvider: MainEffectProvider, +) : ViewModel() { + + protected fun launchWithMainState( + errorCallback: ((e: Throwable) -> Unit)? = null, + block: suspend CoroutineScope.() -> Unit, + ) { + viewModelScope.launch { + try { + mainEffectProvider.loading() + block() + mainEffectProvider.idle() + } catch (e: Throwable) { + e.printStackTrace() + mainEffectProvider.error(e, errorCallback) + } + } + } + + fun idle() = mainEffectProvider.idle() + + fun loading() = mainEffectProvider.loading() + + fun error(e: Throwable) = mainEffectProvider.error(e) +} diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/CharacterInfoViewModel.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/CharacterInfoViewModel.kt index f8f436b..6332267 100644 --- a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/CharacterInfoViewModel.kt +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/CharacterInfoViewModel.kt @@ -1,38 +1,39 @@ package today.pathos.android.portfolio.presentation.viewmodel import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import today.pathos.android.portfolio.domain.usecase.GetCharacterInfoUseCase import today.pathos.android.portfolio.entity.Character +import today.pathos.android.portfolio.presentation.view.Screens +import today.pathos.android.portfolio.presentation.viewmodel.state.ActionEffect +import today.pathos.android.portfolio.presentation.viewmodel.state.MainEffectProvider import javax.inject.Inject @HiltViewModel class CharacterInfoViewModel @Inject constructor( savedState: SavedStateHandle, + private val mainEffectProvider: MainEffectProvider, private val getCharacterInfoUseCase: GetCharacterInfoUseCase, -) : ViewModel() { +) : BaseViewModel(mainEffectProvider) { private val serverId: String = checkNotNull(savedState["serverId"]) private val characterId: String = checkNotNull(savedState["characterId"]) private val _state = MutableStateFlow(CharacterInfoUiState.EMPTY_STATE) - val state = _state - .onSubscription { initInfo() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = CharacterInfoUiState.EMPTY_STATE - ) + val state = _state.asStateFlow() - private fun initInfo() { - viewModelScope.launch { + init { + launchWithMainState( + errorCallback = { + mainEffectProvider.tryAction( + ActionEffect.NavigateTo( + postDest = Screens.NavigateUp + ) + ) + } + ) { val characterInfo = getCharacterInfoUseCase(serverId, characterId) val armorList = characterInfo.equipment .filter { it.itemType == "방어구" } diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/MainViewModel.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/MainViewModel.kt index a87a85e..d10686f 100644 --- a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/MainViewModel.kt +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/MainViewModel.kt @@ -1,34 +1,25 @@ package today.pathos.android.portfolio.presentation.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import today.pathos.android.portfolio.domain.repository.FameRepository import today.pathos.android.portfolio.entity.Character +import today.pathos.android.portfolio.presentation.viewmodel.state.MainEffectProvider import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( + mainEffectProvider: MainEffectProvider, private val repository: FameRepository, -) : ViewModel() { +) : BaseViewModel(mainEffectProvider) { private val _state = MutableStateFlow(MainUiState.EMPTY_STATE) - val state = _state - .onSubscription { initMain() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = MainUiState.EMPTY_STATE - ) + val state = _state.asStateFlow() - private fun initMain() { - viewModelScope.launch { + init { + launchWithMainState { val fameList = repository.getFameCharacterList() _state.update { diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/ActionEffect.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/ActionEffect.kt new file mode 100644 index 0000000..cff0260 --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/ActionEffect.kt @@ -0,0 +1,8 @@ +package today.pathos.android.portfolio.presentation.viewmodel.state + +import today.pathos.android.portfolio.presentation.view.Screens + +sealed class ActionEffect { + data object Idle : ActionEffect() + class NavigateTo(val postDest: Screens, val cleanHistory: Boolean = false) : ActionEffect() +} diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainEffectProvider.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainEffectProvider.kt new file mode 100644 index 0000000..a9c5bda --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainEffectProvider.kt @@ -0,0 +1,36 @@ +package today.pathos.android.portfolio.presentation.viewmodel.state + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MainEffectProvider @Inject constructor( +) { + private val _mainEffect = MutableStateFlow(MainUiEffect.Idle) + val mainEffect = _mainEffect.asStateFlow() + + private val _actionEffect = MutableStateFlow(ActionEffect.Idle) + val actionEffect = _actionEffect.asStateFlow() + + fun idle() = _mainEffect.tryEmit(MainUiEffect.Idle) + fun loading() = _mainEffect.tryEmit(MainUiEffect.Loading) + fun error( + e: Throwable, + callback: ((e: Throwable) -> Unit)? = null, + ) = _mainEffect.tryEmit( + MainUiEffect.Error(e) { + _mainEffect.tryEmit(MainUiEffect.Idle) + callback?.invoke(it) + } + ) + + fun tryAction(action: ActionEffect) { + _actionEffect.tryEmit(action) + } + + companion object { + const val TAG = "MainEffectProvider" + } +} diff --git a/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainUiEffect.kt b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainUiEffect.kt new file mode 100644 index 0000000..c96ea70 --- /dev/null +++ b/presentation/src/main/java/today/pathos/android/portfolio/presentation/viewmodel/state/MainUiEffect.kt @@ -0,0 +1,7 @@ +package today.pathos.android.portfolio.presentation.viewmodel.state + +sealed class MainUiEffect { + data object Idle : MainUiEffect() + data object Loading : MainUiEffect() + data class Error(val e: Throwable, val callback: ((e: Throwable) -> Unit)? = null) : MainUiEffect() +} diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index dbe2b4a..a42824b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -1,4 +1,7 @@ + Android Portfolio 2024 + + 확인 Lv.%,d 명성치 : %,d <%s>