diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/bindingadapter/BindingAdapters.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/bindingadapter/BindingAdapters.kt index 971e95b47..2d288c706 100644 --- a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/bindingadapter/BindingAdapters.kt +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/bindingadapter/BindingAdapters.kt @@ -1,10 +1,10 @@ package com.on.staccato.presentation.bindingadapter -import android.net.Uri import android.view.View import android.view.ViewGroup import android.widget.ScrollView import androidx.databinding.BindingAdapter +import com.on.staccato.presentation.memorycreation.ThumbnailUiModel import com.on.staccato.presentation.timeline.model.TimelineUiModel @BindingAdapter("visibleOrGone") @@ -24,26 +24,20 @@ fun ScrollView.setScrollToBottom(isScrollable: Boolean) { } } -@BindingAdapter(value = ["visibilityByEmptyThumbnailUri", "visibilityByEmptyThumbnailUrl"]) -fun View.setThumbnailVisibility( - thumbnailUri: Uri?, - thumbnailUrl: String?, -) { +@BindingAdapter(value = ["visibilityByEmptyThumbnail"]) +fun View.setThumbnailVisibility(thumbnail: ThumbnailUiModel) { visibility = - if (thumbnailUri == null && thumbnailUrl == null) { + if (thumbnail.uri == null && thumbnail.url == null) { View.VISIBLE } else { View.GONE } } -@BindingAdapter(value = ["loadingVisibilityByThumbnailUri", "visibilityByEmptyThumbnailUrl"]) -fun View.setThumbnailLoadingVisibility( - thumbnailUri: Uri?, - thumbnailUrl: String?, -) { +@BindingAdapter(value = ["loadingVisibilityByThumbnail"]) +fun View.setThumbnailLoadingVisibility(thumbnail: ThumbnailUiModel) { visibility = - if (thumbnailUri != null && thumbnailUrl == null) { + if (thumbnail.uri != null && thumbnail.url == null) { View.VISIBLE } else { View.GONE diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/MemoryCreationActivity.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/MemoryCreationActivity.kt index b8370d7e0..c45343526 100644 --- a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/MemoryCreationActivity.kt +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/MemoryCreationActivity.kt @@ -63,14 +63,12 @@ class MemoryCreationActivity : override fun onImageDeletionClicked() { currentSnackBar?.dismiss() - viewModel.setThumbnailUri(null) - viewModel.setThumbnailUrl(null) + viewModel.clearThumbnail() } override fun onUrisSelected(vararg uris: Uri) { currentSnackBar?.dismiss() viewModel.createThumbnailUrl(this, uris.first()) - viewModel.setThumbnailUri(uris.first()) } private fun buildDateRangePicker() = diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/ThumbnailUiModel.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/ThumbnailUiModel.kt new file mode 100644 index 000000000..69e166368 --- /dev/null +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/ThumbnailUiModel.kt @@ -0,0 +1,14 @@ +package com.on.staccato.presentation.memorycreation + +import android.net.Uri + +data class ThumbnailUiModel( + val uri: Uri? = null, + val url: String? = null, +) { + fun updateUrl(newUrl: String?): ThumbnailUiModel = this.copy(url = newUrl) + + fun isEqualUri(newUri: Uri?): Boolean = uri == newUri + + fun clear() = this.copy(uri = null, url = null) +} diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/viewmodel/MemoryCreationViewModel.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/viewmodel/MemoryCreationViewModel.kt index b6d1c7f46..519080cee 100644 --- a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/viewmodel/MemoryCreationViewModel.kt +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memorycreation/viewmodel/MemoryCreationViewModel.kt @@ -21,12 +21,16 @@ import com.on.staccato.presentation.common.MutableSingleLiveData import com.on.staccato.presentation.common.SingleLiveData import com.on.staccato.presentation.memorycreation.DateConverter.convertLongToLocalDate import com.on.staccato.presentation.memorycreation.MemoryCreationError +import com.on.staccato.presentation.memorycreation.ThumbnailUiModel import com.on.staccato.presentation.util.convertMemoryUriToFile import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject +private typealias ThumbnailUri = Uri + @HiltViewModel class MemoryCreationViewModel @Inject @@ -36,6 +40,7 @@ class MemoryCreationViewModel ) : ViewModel() { val title = ObservableField() val description = ObservableField() + val isPeriodActive = MutableLiveData(false) private val _startDate = MutableLiveData(null) val startDate: LiveData get() = _startDate @@ -46,11 +51,8 @@ class MemoryCreationViewModel private val _createdMemoryId = MutableLiveData() val createdMemoryId: LiveData get() = _createdMemoryId - private val _thumbnailUri = MutableLiveData(null) - val thumbnailUri: LiveData get() = _thumbnailUri - - private val _thumbnailUrl = MutableLiveData(null) - val thumbnailUrl: LiveData get() = _thumbnailUrl + private val _thumbnail = MutableLiveData(ThumbnailUiModel()) + val thumbnail: LiveData get() = _thumbnail private val _isPosting = MutableLiveData(false) val isPosting: LiveData get() = _isPosting @@ -58,39 +60,25 @@ class MemoryCreationViewModel private val _isPhotoPosting = MutableLiveData(false) val isPhotoPosting: LiveData get() = _isPhotoPosting - val isPeriodActive = MutableLiveData(false) - private val _errorMessage = MutableLiveData() val errorMessage: LiveData get() = _errorMessage private val _error = MutableSingleLiveData() val error: SingleLiveData get() = _error + private val thumbnailJobs = mutableMapOf() + fun createThumbnailUrl( context: Context, - thumbnailUri: Uri, + uri: Uri, ) { - _thumbnailUri.value = thumbnailUri _isPhotoPosting.value = true - val thumbnailFile = convertMemoryUriToFile(context, thumbnailUri, name = MEMORY_FILE_NAME) - viewModelScope.launch { - val result: ResponseResult = - imageRepository.convertImageFileToUrl(thumbnailFile) - result.onSuccess(::setThumbnailUrl) - .onServerError(::handlePhotoError) - .onException { e, message -> - handlePhotoException(e, message, thumbnailUri) - } - } - } - - fun setThumbnailUri(thumbnailUri: Uri?) { - _thumbnailUri.value = thumbnailUri + setThumbnailUri(uri) + registerThumbnailJob(context, uri) } - fun setThumbnailUrl(imageResponse: ImageResponse?) { - _thumbnailUrl.value = imageResponse?.imageUrl - _isPhotoPosting.value = false + fun clearThumbnail() { + _thumbnail.value = thumbnail.value?.clear() } fun setMemoryPeriod( @@ -114,13 +102,57 @@ class MemoryCreationViewModel } } + private fun setThumbnailUri(uri: Uri?) { + val currentJob = thumbnailJobs[_thumbnail.value?.uri] + if (isNewUri(uri) && currentJob?.isActive == true) { + currentJob.cancel() + } + _thumbnail.value = ThumbnailUiModel(uri = uri, url = null) + } + + private fun isNewUri(uri: Uri?): Boolean = _thumbnail.value?.isEqualUri(uri) == false + + private fun registerThumbnailJob( + context: Context, + uri: Uri, + ) { + val job = createFetchingThumbnailJob(context, uri) + job.invokeOnCompletion { + thumbnailJobs.remove(uri) + } + thumbnailJobs[uri] = job + } + + private fun createFetchingThumbnailJob( + context: Context, + uri: Uri, + ): Job { + val thumbnailFile = convertMemoryUriToFile(context, uri, name = MEMORY_FILE_NAME) + return viewModelScope.launch { + val result: ResponseResult = + imageRepository.convertImageFileToUrl(thumbnailFile) + result + .onSuccess(::setThumbnailUrl) + .onServerError(::handlePhotoError) + .onException { e, message -> + handlePhotoException(e, message, uri) + } + } + } + + private fun setThumbnailUrl(imageResponse: ImageResponse) { + val newUrl = imageResponse.imageUrl + _thumbnail.value = _thumbnail.value?.updateUrl(newUrl) + _isPhotoPosting.value = false + } + private fun setCreatedMemoryId(memoryCreationResponse: MemoryCreationResponse) { _createdMemoryId.value = memoryCreationResponse.memoryId } private fun makeNewMemory() = NewMemory( - memoryThumbnailUrl = thumbnailUrl.value, + memoryThumbnailUrl = _thumbnail.value?.url, memoryTitle = title.get() ?: throw IllegalArgumentException(), startAt = getDateByPeriodSetting(startDate), endAt = getDateByPeriodSetting(endDate), @@ -145,9 +177,11 @@ class MemoryCreationViewModel private fun handlePhotoException( e: Throwable, message: String, - thumbnailUri: Uri, + uri: Uri, ) { - _error.setValue(MemoryCreationError.Thumbnail(message, thumbnailUri)) + if (thumbnailJobs[uri]?.isActive == true) { + _error.setValue(MemoryCreationError.Thumbnail(message, uri)) + } } private fun handleCreateServerError( diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/MemoryUpdateActivity.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/MemoryUpdateActivity.kt index 62e8db679..310c155a2 100644 --- a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/MemoryUpdateActivity.kt +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/MemoryUpdateActivity.kt @@ -65,13 +65,11 @@ class MemoryUpdateActivity : override fun onPhotoDeletionClicked() { currentSnackBar?.dismiss() - viewModel.setThumbnailUri(null) - viewModel.setThumbnailUrl(null) + viewModel.clearThumbnail() } override fun onUrisSelected(vararg uris: Uri) { currentSnackBar?.dismiss() - viewModel.setThumbnailUri(uris.first()) viewModel.createThumbnailUrl(this, uris.first()) } diff --git a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/viewmodel/MemoryUpdateViewModel.kt b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/viewmodel/MemoryUpdateViewModel.kt index 28e70ad59..069384766 100644 --- a/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/viewmodel/MemoryUpdateViewModel.kt +++ b/android/Staccato_AN/app/src/main/java/com/on/staccato/presentation/memoryupdate/viewmodel/MemoryUpdateViewModel.kt @@ -20,13 +20,17 @@ import com.on.staccato.domain.repository.MemoryRepository import com.on.staccato.presentation.common.MutableSingleLiveData import com.on.staccato.presentation.common.SingleLiveData import com.on.staccato.presentation.memorycreation.DateConverter.convertLongToLocalDate +import com.on.staccato.presentation.memorycreation.ThumbnailUiModel import com.on.staccato.presentation.memoryupdate.MemoryUpdateError import com.on.staccato.presentation.util.convertMemoryUriToFile import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject +private typealias ThumbnailUri = Uri + @HiltViewModel class MemoryUpdateViewModel @Inject @@ -37,11 +41,8 @@ class MemoryUpdateViewModel private val _memory = MutableLiveData() val memory: LiveData get() = _memory - private val _thumbnailUri = MutableLiveData(null) - val thumbnailUri: LiveData get() = _thumbnailUri - - private val _thumbnailUrl = MutableLiveData(null) - val thumbnailUrl: LiveData get() = _thumbnailUrl + private val _thumbnail = MutableLiveData(ThumbnailUiModel()) + val thumbnail: LiveData get() = _thumbnail val title = ObservableField() val description = ObservableField() @@ -71,8 +72,10 @@ class MemoryUpdateViewModel private val _error = MutableSingleLiveData() val error: SingleLiveData get() = _error - fun fetchMemory(memoryId: Long) { - fetchMemoryId(memoryId) + private val thumbnailJobs = mutableMapOf() + + fun fetchMemory(id: Long) { + memoryId = id viewModelScope.launch { val result = memoryRepository.getMemory(memoryId) result @@ -82,39 +85,6 @@ class MemoryUpdateViewModel } } - private fun fetchMemoryId(id: Long) { - memoryId = id - } - - fun createThumbnailUrl( - context: Context, - thumbnailUri: Uri, - ) { - _thumbnailUrl.value = null - _thumbnailUri.value = thumbnailUri - _isPhotoPosting.value = true - val thumbnailFile = convertMemoryUriToFile(context, thumbnailUri, name = MEMORY_FILE_NAME) - viewModelScope.launch { - val result: ResponseResult = - imageRepository.convertImageFileToUrl(thumbnailFile) - result.onSuccess(::setThumbnailUrl) - .onServerError(::handlePhotoError) - .onException { e, message -> - handlePhotoException(e, message, thumbnailUri) - } - } - } - - fun setThumbnailUri(thumbnailUri: Uri?) { - _thumbnailUri.value = thumbnailUri - _thumbnailUrl.value = null - } - - fun setThumbnailUrl(imageResponse: ImageResponse?) { - _thumbnailUrl.value = imageResponse?.imageUrl - _isPhotoPosting.value = false - } - fun updateMemory() { viewModelScope.launch { val newMemory: NewMemory = makeNewMemory() @@ -134,8 +104,21 @@ class MemoryUpdateViewModel _endDate.value = convertLongToLocalDate(endAt) } + fun createThumbnailUrl( + context: Context, + uri: Uri, + ) { + _isPhotoPosting.value = true + setThumbnailUri(uri) + registerThumbnailJob(context, uri) + } + + fun clearThumbnail() { + _thumbnail.value = thumbnail.value?.clear() + } + private fun initializeMemory(memory: Memory) { - _thumbnailUrl.value = memory.memoryThumbnailUrl + _thumbnail.value = _thumbnail.value?.updateUrl(memory.memoryThumbnailUrl) title.set(memory.memoryTitle) description.set(memory.description) _startDate.value = memory.startAt @@ -149,7 +132,7 @@ class MemoryUpdateViewModel private fun makeNewMemory() = NewMemory( - memoryThumbnailUrl = thumbnailUrl.value, + memoryThumbnailUrl = _thumbnail.value?.url, memoryTitle = title.get() ?: throw IllegalArgumentException(), startAt = getDateByPeriodSetting(startDate), endAt = getDateByPeriodSetting(endDate), @@ -169,6 +152,49 @@ class MemoryUpdateViewModel _isUpdateSuccess.setValue(true) } + private fun setThumbnailUri(uri: Uri?) { + val currentJob = thumbnailJobs[_thumbnail.value?.uri] + if (isNewUri(uri) && currentJob?.isActive == true) { + currentJob.cancel() + } + _thumbnail.value = ThumbnailUiModel(uri = uri, url = null) + } + + private fun isNewUri(uri: Uri?): Boolean = _thumbnail.value?.isEqualUri(uri) == false + + private fun registerThumbnailJob( + context: Context, + uri: Uri, + ) { + val thumbnailJob = createFetchingThumbnailJob(context, uri) + thumbnailJob.invokeOnCompletion { + thumbnailJobs.remove(uri) + } + thumbnailJobs[uri] = thumbnailJob + } + + private fun createFetchingThumbnailJob( + context: Context, + uri: Uri, + ): Job { + val thumbnailFile = convertMemoryUriToFile(context, uri, name = MEMORY_FILE_NAME) + return viewModelScope.launch { + val result: ResponseResult = + imageRepository.convertImageFileToUrl(thumbnailFile) + result.onSuccess(::setThumbnailUrl) + .onServerError(::handlePhotoError) + .onException { e, message -> + handlePhotoException(e, message, uri) + } + } + } + + private fun setThumbnailUrl(imageResponse: ImageResponse) { + val newUrl = imageResponse.imageUrl + _thumbnail.value = _thumbnail.value?.updateUrl(newUrl) + _isPhotoPosting.value = false + } + private fun handlePhotoError( status: Status, message: String, @@ -181,7 +207,9 @@ class MemoryUpdateViewModel message: String, uri: Uri, ) { - _error.setValue(MemoryUpdateError.Thumbnail(message, uri)) + if (thumbnailJobs[uri]?.isActive == true) { + _error.setValue(MemoryUpdateError.Thumbnail(message, uri)) + } } private fun handleInitializeMemoryError( diff --git a/android/Staccato_AN/app/src/main/res/layout/activity_memory_creation.xml b/android/Staccato_AN/app/src/main/res/layout/activity_memory_creation.xml index 21a7b40e7..fffef2455 100644 --- a/android/Staccato_AN/app/src/main/res/layout/activity_memory_creation.xml +++ b/android/Staccato_AN/app/src/main/res/layout/activity_memory_creation.xml @@ -58,8 +58,8 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="500dp" - bind:coilRoundedCornerImageUri="@{viewModel.thumbnailUri}" - bind:coilRoundedCornerImageUrl="@{viewModel.thumbnailUrl}" + bind:coilRoundedCornerImageUri="@{viewModel.thumbnail.uri}" + bind:coilRoundedCornerImageUrl="@{viewModel.thumbnail.url}" bind:coilRoundedCornerPlaceHolder="@{@drawable/shape_all_gray1_8dp}" bind:coilRoundingRadius="@{12f}" /> @@ -71,7 +71,7 @@ android:onClick="@{() -> handler.onImageDeletionClicked()}" android:padding="12dp" android:src="@drawable/ic_delete" - android:visibility="@{viewModel.thumbnailUrl == null ? View.GONE : View.VISIBLE }" + android:visibility="@{viewModel.thumbnail.url == null ? View.GONE : View.VISIBLE }" app:layout_constraintEnd_toEndOf="@id/iv_memory_creation_photo_attach" app:layout_constraintTop_toTopOf="@id/iv_memory_creation_photo_attach" tools:visibility="visible" /> @@ -85,8 +85,7 @@ app:layout_constraintEnd_toEndOf="@id/iv_memory_creation_photo_attach" app:layout_constraintStart_toStartOf="@id/iv_memory_creation_photo_attach" app:layout_constraintTop_toTopOf="@id/iv_memory_creation_photo_attach" - bind:visibilityByEmptyThumbnailUri="@{viewModel.thumbnailUri}" - bind:visibilityByEmptyThumbnailUrl="@{viewModel.thumbnailUrl}" /> + bind:visibilityByEmptyThumbnail="@{viewModel.thumbnail}" /> @@ -71,7 +71,7 @@ android:onClick="@{() -> handler.onPhotoDeletionClicked()}" android:padding="12dp" android:src="@drawable/ic_delete" - android:visibility="@{viewModel.thumbnailUrl == null ? View.GONE : View.VISIBLE }" + android:visibility="@{viewModel.thumbnail.url == null ? View.GONE : View.VISIBLE }" app:layout_constraintEnd_toEndOf="@id/iv_memory_update_photo_attach" app:layout_constraintTop_toTopOf="@id/iv_memory_update_photo_attach" tools:visibility="visible" /> @@ -85,8 +85,7 @@ app:layout_constraintEnd_toEndOf="@id/iv_memory_update_photo_attach" app:layout_constraintStart_toStartOf="@id/iv_memory_update_photo_attach" app:layout_constraintTop_toTopOf="@id/iv_memory_update_photo_attach" - bind:visibilityByEmptyThumbnailUri="@{viewModel.thumbnailUri}" - bind:visibilityByEmptyThumbnailUrl="@{viewModel.thumbnailUrl}" + bind:visibilityByEmptyThumbnail="@{viewModel.thumbnail}" tools:visibility="visible" />