diff --git a/app/src/main/java/com/lateinit/rightweight/data/mediator/SharedRoutineRemoteMediator.kt b/app/src/main/java/com/lateinit/rightweight/data/mediator/SharedRoutineRemoteMediator.kt index b757f3d7..7204f77f 100644 --- a/app/src/main/java/com/lateinit/rightweight/data/mediator/SharedRoutineRemoteMediator.kt +++ b/app/src/main/java/com/lateinit/rightweight/data/mediator/SharedRoutineRemoteMediator.kt @@ -20,8 +20,6 @@ import com.lateinit.rightweight.data.model.remote.StructuredQueryData import com.lateinit.rightweight.data.remote.model.IntValue import com.lateinit.rightweight.data.remote.model.TimeStampValue import kotlinx.coroutines.flow.first -import retrofit2.HttpException -import java.io.IOException @OptIn(ExperimentalPagingApi::class) class SharedRoutineRemoteMediator( @@ -41,96 +39,90 @@ class SharedRoutineRemoteMediator( loadType: LoadType, state: PagingState ): MediatorResult { - try { - var endOfPaginationReached = false + var endOfPaginationReached = false - var pagingFlag = when (loadType) { - LoadType.REFRESH -> "$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG" - LoadType.PREPEND -> { - endOfPaginationReached = true - return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) - } - LoadType.APPEND -> { - val flag = appPreferencesDataStore.sharedRoutinePagingFlag.first() - flag.ifEmpty { "$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG" } - } + var pagingFlag = when (loadType) { + LoadType.REFRESH -> "$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG" + LoadType.PREPEND -> { + endOfPaginationReached = true + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } + LoadType.APPEND -> { + val flag = appPreferencesDataStore.sharedRoutinePagingFlag.first() + flag.ifEmpty { "$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG" } } + } - val splitedPagingFlag = pagingFlag.split("/") - var modifiedDateFlag = splitedPagingFlag[0] - var sharedCountFlag = splitedPagingFlag[1] + val splitedPagingFlag = pagingFlag.split("/") + var modifiedDateFlag = splitedPagingFlag[0] + var sharedCountFlag = splitedPagingFlag[1] - when (sortType) { - SharedRoutineSortType.MODIFIED_DATE_FIRST -> { - runQueryBody = RunQueryBody( - StructuredQueryData( - from = FromData("shared_routine"), - orderBy = listOf( - OrderByData(FiledReferenceData("modified_date"), "DESCENDING"), - OrderByData(FiledReferenceData("shared_count.count"), "DESCENDING") - ), - limit = 10, - startAt = StartAtData( - listOf( - TimeStampValue(modifiedDateFlag), - IntValue(sharedCountFlag) - ) + when (sortType) { + SharedRoutineSortType.MODIFIED_DATE_FIRST -> { + runQueryBody = RunQueryBody( + StructuredQueryData( + from = FromData("shared_routine"), + orderBy = listOf( + OrderByData(FiledReferenceData("modified_date"), "DESCENDING"), + OrderByData(FiledReferenceData("shared_count.count"), "DESCENDING") + ), + limit = 10, + startAt = StartAtData( + listOf( + TimeStampValue(modifiedDateFlag), + IntValue(sharedCountFlag) ) ) ) - } - SharedRoutineSortType.SHARED_COUNT_FIRST -> { - runQueryBody = RunQueryBody( - StructuredQueryData( - FromData("shared_routine"), - orderBy = listOf( - OrderByData(FiledReferenceData("shared_count.count"), "DESCENDING"), - OrderByData(FiledReferenceData("modified_date"), "DESCENDING") - ), - limit = 10, - startAt = StartAtData( - listOf( - IntValue(sharedCountFlag), - TimeStampValue(modifiedDateFlag) - ) + ) + } + SharedRoutineSortType.SHARED_COUNT_FIRST -> { + runQueryBody = RunQueryBody( + StructuredQueryData( + FromData("shared_routine"), + orderBy = listOf( + OrderByData(FiledReferenceData("shared_count.count"), "DESCENDING"), + OrderByData(FiledReferenceData("modified_date"), "DESCENDING") + ), + limit = 10, + startAt = StartAtData( + listOf( + IntValue(sharedCountFlag), + TimeStampValue(modifiedDateFlag) ) ) ) - } + ) } + } - val documentResponses = api.getSharedRoutines( - runQueryBody - ) + val documentResponses = api.getSharedRoutines( + runQueryBody + ) - db.withTransaction { - if (loadType == LoadType.REFRESH) { - appPreferencesDataStore.saveSharedRoutinePagingFlag("$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG") - db.sharedRoutineDao().removeAllSharedRoutines() - } + db.withTransaction { + if (loadType == LoadType.REFRESH) { + appPreferencesDataStore.saveSharedRoutinePagingFlag("$INIT_MODIFIED_DATE_FLAG/$INIT_SHARED_COUNT_FLAG") + db.sharedRoutineDao().removeAllSharedRoutines() + } - documentResponses.forEach { documentResponse -> - if (documentResponse.document != null) { - db.sharedRoutineDao() - .insertSharedRoutine(documentResponse.document.toSharedRoutine()) - modifiedDateFlag = - documentResponse.document.fields.modifiedDate.value - sharedCountFlag = - documentResponse.document.fields.sharedCount.value.remoteData.count.value - pagingFlag = "$modifiedDateFlag/$sharedCountFlag" - } else { - endOfPaginationReached = true - } + documentResponses.forEach { documentResponse -> + if (documentResponse.document != null) { + db.sharedRoutineDao() + .insertSharedRoutine(documentResponse.document.toSharedRoutine()) + modifiedDateFlag = + documentResponse.document.fields.modifiedDate.value + sharedCountFlag = + documentResponse.document.fields.sharedCount.value.remoteData.count.value + pagingFlag = "$modifiedDateFlag/$sharedCountFlag" + } else { + endOfPaginationReached = true } - appPreferencesDataStore.saveSharedRoutinePagingFlag(pagingFlag) } - - return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) - } catch (e: IOException) { - return MediatorResult.Error(e) - } catch (e: HttpException) { - return MediatorResult.Error(e) + appPreferencesDataStore.saveSharedRoutinePagingFlag(pagingFlag) } + + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } companion object { diff --git a/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineFragment.kt b/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineFragment.kt index 92eeae18..5a8aa5d8 100644 --- a/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineFragment.kt +++ b/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineFragment.kt @@ -50,7 +50,15 @@ class SharedRoutineFragment : Fragment(), SharedRoutineClickHandler { is LatestSharedRoutineUiState.Success -> { sharedRoutinePagingAdapter.submitData(uiState.sharedRoutines) } - is LatestSharedRoutineUiState.Error -> Exception() + is LatestSharedRoutineUiState.Error -> { + Snackbar.make( + binding.root, + R.string.wrong_connection, + Snackbar.LENGTH_LONG + ).apply { + anchorView = binding.guideLineBottom + }.show() + } } } } diff --git a/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineViewModel.kt b/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineViewModel.kt index 181f57bf..baa8cc22 100644 --- a/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineViewModel.kt +++ b/app/src/main/java/com/lateinit/rightweight/ui/share/SharedRoutineViewModel.kt @@ -7,34 +7,61 @@ import androidx.paging.cachedIn import androidx.paging.map import com.lateinit.rightweight.data.mapper.local.toSharedRoutineSortType import com.lateinit.rightweight.data.repository.SharedRoutineRepository +import com.lateinit.rightweight.ui.login.NetworkState import com.lateinit.rightweight.ui.mapper.toSharedRoutineUiModel import com.lateinit.rightweight.ui.model.shared.SharedRoutineSortTypeUiModel import com.lateinit.rightweight.ui.model.shared.SharedRoutineUiModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import retrofit2.HttpException +import java.net.SocketException +import java.net.UnknownHostException import javax.inject.Inject @HiltViewModel class SharedRoutineViewModel @Inject constructor( private val sharedRoutineRepository: SharedRoutineRepository ) : ViewModel() { - private val _uiState = MutableStateFlow(LatestSharedRoutineUiState.Success(PagingData.empty())) + private val _uiState = + MutableStateFlow(LatestSharedRoutineUiState.Success(PagingData.empty())) val uiState: StateFlow = _uiState + val networkExceptionHandler = CoroutineExceptionHandler { _, throwable -> + when (throwable) { + is SocketException -> sendNetworkResultEvent(NetworkState.BAD_INTERNET) + is HttpException -> sendNetworkResultEvent(NetworkState.PARSE_ERROR) + is UnknownHostException -> sendNetworkResultEvent(NetworkState.WRONG_CONNECTION) + else -> sendNetworkResultEvent(NetworkState.OTHER_ERROR) + } + } + init { + getSharedRoutinesByPaging() + } + + private fun sendNetworkResultEvent(state: NetworkState) { viewModelScope.launch { - sharedRoutineRepository.getSharedRoutinesByPaging().cachedIn(this).collect{ sharedRoutinePagingData -> - val sharedRoutines = sharedRoutinePagingData.map { sharedRoutine -> - sharedRoutine.toSharedRoutineUiModel() + _uiState.value = LatestSharedRoutineUiState.Error(state) + } + } + + private fun getSharedRoutinesByPaging() { + viewModelScope.launch(networkExceptionHandler) { + sharedRoutineRepository.getSharedRoutinesByPaging().cachedIn(this) + .collect { sharedRoutinePagingData -> + val sharedRoutines = sharedRoutinePagingData.map { sharedRoutine -> + sharedRoutine.toSharedRoutineUiModel() + } + _uiState.value = LatestSharedRoutineUiState.Success(sharedRoutines) } - _uiState.value = LatestSharedRoutineUiState.Success(sharedRoutines) - } } } - fun setSharedRoutineSortType(sortTypeUiModel: SharedRoutineSortTypeUiModel){ + fun setSharedRoutineSortType(sortTypeUiModel: SharedRoutineSortTypeUiModel) { + getSharedRoutinesByPaging() viewModelScope.launch { sharedRoutineRepository.setSharedRoutineSortType(sortTypeUiModel.toSharedRoutineSortType()) } @@ -46,5 +73,5 @@ sealed class LatestSharedRoutineUiState { data class Success(val sharedRoutines: PagingData) : LatestSharedRoutineUiState() - data class Error(val exception: Throwable) : LatestSharedRoutineUiState() -} \ No newline at end of file + data class Error(val state: NetworkState) : LatestSharedRoutineUiState() +} diff --git a/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailFragment.kt b/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailFragment.kt index 364ad5dc..eab5e1d8 100644 --- a/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailFragment.kt +++ b/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailFragment.kt @@ -9,6 +9,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar +import com.lateinit.rightweight.R import com.lateinit.rightweight.databinding.FragmentSharedRoutineDetailBinding import com.lateinit.rightweight.ui.model.routine.DayUiModel import com.lateinit.rightweight.ui.routine.detail.DetailExerciseAdapter @@ -48,6 +50,7 @@ class SharedRoutineDetailFragment : Fragment() { setRoutineDayAdapter() setExerciseAdapter() setSharedRoutineDetailCollect() + setButtonRoutineImportOnClickListener() handleNavigationEvent() } @@ -80,21 +83,36 @@ class SharedRoutineDetailFragment : Fragment() { binding.sharedRoutineUiModel = uiState.sharedRoutineUiModel routineDayAdapter.submitList(uiState.dayUiModels) setCurrentDayPositionObserve(uiState.dayUiModels) - - binding.buttonRoutineImport.setOnClickListener { - viewModel.importSharedRoutineToMyRoutines( - uiState.sharedRoutineUiModel, - uiState.dayUiModels - ) - - } } - is LatestSharedRoutineDetailUiState.Error -> Exception() + is LatestSharedRoutineDetailUiState.Error -> { + Snackbar.make( + binding.root, + R.string.wrong_connection, + Snackbar.LENGTH_LONG + ).apply { + anchorView = binding.guideLineBottom + }.show() + findNavController().navigateUp() + } } } } } + private fun setButtonRoutineImportOnClickListener() { + binding.buttonRoutineImport.setOnClickListener { + if (viewModel.importSharedRoutineToMyRoutines().not()) { + Snackbar.make( + binding.root, + R.string.none_routine, + Snackbar.LENGTH_LONG + ).apply { + anchorView = binding.guideLineBottom + }.show() + } + } + } + private fun setCurrentDayPositionObserve(dayUiModels: List) { viewModel.currentDayPosition.observe(viewLifecycleOwner) { if (dayUiModels.size > it) { @@ -106,7 +124,7 @@ class SharedRoutineDetailFragment : Fragment() { private fun handleNavigationEvent() { viewLifecycleOwner.collectOnLifecycle { - viewModel.navigationEvent.collect { + viewModel.navigationEvent.collect { setFragmentResult("routineCopy", bundleOf()) findNavController().navigateUp() } diff --git a/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailViewModel.kt b/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailViewModel.kt index 508e10ca..25b13efa 100644 --- a/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailViewModel.kt +++ b/app/src/main/java/com/lateinit/rightweight/ui/share/detail/SharedRoutineDetailViewModel.kt @@ -14,12 +14,14 @@ import com.lateinit.rightweight.data.mapper.local.toRoutine import com.lateinit.rightweight.data.repository.RoutineRepository import com.lateinit.rightweight.data.repository.SharedRoutineRepository import com.lateinit.rightweight.data.repository.UserRepository +import com.lateinit.rightweight.ui.login.NetworkState import com.lateinit.rightweight.ui.mapper.toDayUiModel import com.lateinit.rightweight.ui.mapper.toSharedRoutineUiModel import com.lateinit.rightweight.ui.model.routine.DayUiModel import com.lateinit.rightweight.ui.model.shared.SharedRoutineUiModel import com.lateinit.rightweight.util.FIRST_DAY_POSITION import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -27,6 +29,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import retrofit2.HttpException +import java.net.SocketException +import java.net.UnknownHostException import java.util.UUID import javax.inject.Inject @@ -39,8 +44,8 @@ class SharedRoutineDetailViewModel @Inject constructor( val userInfo = userRepository.getUser().stateIn(viewModelScope, SharingStarted.Eagerly, null) - private val _uiState = MutableStateFlow( - LatestSharedRoutineDetailUiState.Success(null, mutableListOf()) + private val _uiState = MutableStateFlow( + LatestSharedRoutineDetailUiState.Success(null, emptyList()) ) val uiState: StateFlow = _uiState @@ -50,9 +55,22 @@ class SharedRoutineDetailViewModel @Inject constructor( private val _navigationEvent = MutableSharedFlow() val navigationEvent = _navigationEvent.asSharedFlow() + val networkExceptionHandler = CoroutineExceptionHandler { _, throwable -> + when (throwable) { + is SocketException -> sendNetworkResultEvent(NetworkState.BAD_INTERNET) + is HttpException -> sendNetworkResultEvent(NetworkState.PARSE_ERROR) + is UnknownHostException -> sendNetworkResultEvent(NetworkState.WRONG_CONNECTION) + else -> sendNetworkResultEvent(NetworkState.OTHER_ERROR) + } + } + + private fun sendNetworkResultEvent(state: NetworkState) { + _uiState.value = LatestSharedRoutineDetailUiState.Error(state) + } + fun getSharedRoutineDetail(routineId: String) { - viewModelScope.launch { + viewModelScope.launch(networkExceptionHandler) { sharedRoutineRepository.getSharedRoutineDetail(routineId) .collect { sharedRoutineWithDays -> @@ -67,30 +85,34 @@ class SharedRoutineDetailViewModel @Inject constructor( sharedRoutineWithDay.exercises ) } - ) - if (_uiState.value.dayUiModels.isNotEmpty()) { - initClickedDay() + ).apply { + if (this.dayUiModels.isNotEmpty()) { + initClickedDay(this) + } } } } } - private fun initClickedDay() { - val originDayUiModels = _uiState.value.dayUiModels.toMutableList() + private fun initClickedDay(successUiState: LatestSharedRoutineDetailUiState.Success) { + val originDayUiModels = successUiState.dayUiModels.toMutableList() originDayUiModels[FIRST_DAY_POSITION] = originDayUiModels[FIRST_DAY_POSITION].copy(selected = true) _currentDayPosition.value = FIRST_DAY_POSITION _uiState.value = LatestSharedRoutineDetailUiState.Success( - _uiState.value.sharedRoutineUiModel, + successUiState.sharedRoutineUiModel, originDayUiModels ) } fun clickDay(dayPosition: Int) { - if (_currentDayPosition.value == dayPosition) return + if (_currentDayPosition.value == dayPosition + || _uiState.value is LatestSharedRoutineDetailUiState.Error + ) return - val originDayUiModels = _uiState.value.dayUiModels.toMutableList() + val successUiState = _uiState.value as LatestSharedRoutineDetailUiState.Success + val originDayUiModels = successUiState.dayUiModels.toMutableList() val lastSelectedPosition = _currentDayPosition.value ?: return originDayUiModels[lastSelectedPosition] = @@ -99,14 +121,17 @@ class SharedRoutineDetailViewModel @Inject constructor( _currentDayPosition.value = dayPosition _uiState.value = LatestSharedRoutineDetailUiState.Success( - _uiState.value.sharedRoutineUiModel, + successUiState.sharedRoutineUiModel, originDayUiModels ) } fun clickExercise(exercisePosition: Int) { + if (_uiState.value is LatestSharedRoutineDetailUiState.Error) return + + val successUiState = _uiState.value as LatestSharedRoutineDetailUiState.Success val nowDayPosition = _currentDayPosition.value ?: return - val originDayUiModels = _uiState.value.dayUiModels.toMutableList() + val originDayUiModels = successUiState.dayUiModels.toMutableList() val originExerciseUiModels = originDayUiModels[nowDayPosition].exercises.toMutableList() val exerciseUiModel = originExerciseUiModels[exercisePosition] @@ -119,19 +144,24 @@ class SharedRoutineDetailViewModel @Inject constructor( _currentDayPosition.value = _currentDayPosition.value _uiState.value = LatestSharedRoutineDetailUiState.Success( - _uiState.value.sharedRoutineUiModel, + successUiState.sharedRoutineUiModel, originDayUiModels ) } - fun importSharedRoutineToMyRoutines( - sharedRoutineUiModel: SharedRoutineUiModel?, - dayUiModels: List - ) { - if (sharedRoutineUiModel != null) { - viewModelScope.launch { - val sharedRoutineId = sharedRoutineUiModel.routineId - val routine = sharedRoutineUiModel.toRoutine( + fun importSharedRoutineToMyRoutines(): Boolean { + if (_uiState.value is LatestSharedRoutineDetailUiState.Error) { + return false + } + + val successUiState = _uiState.value as LatestSharedRoutineDetailUiState.Success + + if (successUiState.sharedRoutineUiModel == null) { + return false + } else { + viewModelScope.launch(networkExceptionHandler) { + val sharedRoutineId = successUiState.sharedRoutineUiModel.routineId + val routine = successUiState.sharedRoutineUiModel.toRoutine( createUUID(), userInfo.value?.displayName ?: "", routineRepository.getHigherRoutineOrder()?.plus(1) ?: 0 @@ -140,7 +170,7 @@ class SharedRoutineDetailViewModel @Inject constructor( val exercises = mutableListOf() val exerciseSets = mutableListOf() - dayUiModels.forEach { dayUiModel -> + successUiState.dayUiModels.forEach { dayUiModel -> val dayId = createUUID() days.add(dayUiModel.toDayWithNewIds(routine.routineId, dayId)) dayUiModel.exercises.forEach { exerciseUiModel -> @@ -148,14 +178,20 @@ class SharedRoutineDetailViewModel @Inject constructor( exercises.add(exerciseUiModel.toExerciseWithNewIds(dayId, exerciseId)) exerciseUiModel.exerciseSets.forEach { exerciseSetUiModel -> val exerciseSetId = createUUID() - exerciseSets.add(exerciseSetUiModel.toExerciseSetWithNewIds(exerciseId, exerciseSetId)) + exerciseSets.add( + exerciseSetUiModel.toExerciseSetWithNewIds( + exerciseId, + exerciseSetId + ) + ) } } } routineRepository.insertRoutine(routine, days, exercises, exerciseSets) - sharedRoutineRepository.increaseSharedCount(sharedRoutineId) _navigationEvent.emit(routine.routineId) + sharedRoutineRepository.increaseSharedCount(sharedRoutineId) } + return true } } @@ -171,6 +207,6 @@ sealed class LatestSharedRoutineDetailUiState { val dayUiModels: List ) : LatestSharedRoutineDetailUiState() - data class Error(val exception: Throwable) : LatestSharedRoutineDetailUiState() + data class Error(val state: NetworkState) : LatestSharedRoutineDetailUiState() } diff --git a/app/src/main/res/layout/fragment_shared_routine_detail.xml b/app/src/main/res/layout/fragment_shared_routine_detail.xml index dc08c22f..0dbb8f69 100644 --- a/app/src/main/res/layout/fragment_shared_routine_detail.xml +++ b/app/src/main/res/layout/fragment_shared_routine_detail.xml @@ -172,5 +172,13 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 322afa9e..6e10a754 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,6 +67,7 @@ stopwatch start and pause 프로필 이미지 인터넷 연결을 확인해주세요. + 가져올 루틴이 없습니다. 루틴이 저장되었습니다. 루틴명을 입력해주세요. 루틴 설명을 입력해주세요.