feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용
Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제. Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
This commit is contained in:
@@ -6,6 +6,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
|
import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
|
||||||
|
|
||||||
class CharacterGalleryAdapter(
|
class CharacterGalleryAdapter(
|
||||||
@@ -19,6 +20,7 @@ class CharacterGalleryAdapter(
|
|||||||
fun bind(item: CharacterImageListItemResponse) {
|
fun bind(item: CharacterImageListItemResponse) {
|
||||||
Glide.with(binding.ivImage)
|
Glide.with(binding.ivImage)
|
||||||
.load(item.imageUrl)
|
.load(item.imageUrl)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||||
.into(binding.ivImage)
|
.into(binding.ivImage)
|
||||||
|
|
||||||
if (item.isOwned) {
|
if (item.isOwned) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
|
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
|
||||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@@ -21,7 +22,9 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
FragmentCharacterGalleryBinding::inflate
|
FragmentCharacterGalleryBinding::inflate
|
||||||
) {
|
) {
|
||||||
private val viewModel: CharacterGalleryViewModel by viewModel()
|
private val viewModel: CharacterGalleryViewModel by viewModel()
|
||||||
|
|
||||||
private lateinit var adapter: CharacterGalleryAdapter
|
private lateinit var adapter: CharacterGalleryAdapter
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
private val characterId: Long by lazy {
|
private val characterId: Long by lazy {
|
||||||
arguments?.getLong("arg_character_id")
|
arguments?.getLong("arg_character_id")
|
||||||
@@ -30,8 +33,11 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
observeState()
|
observeState()
|
||||||
|
|
||||||
viewModel.loadInitial(characterId)
|
viewModel.loadInitial(characterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +76,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로딩 다이얼로그 표시/해제
|
||||||
|
if (state.isLoading) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
hideLoadingDialog()
|
||||||
|
}
|
||||||
|
|
||||||
if (state.items.isNotEmpty() && !state.isLoading) {
|
if (state.items.isNotEmpty() && !state.isLoading) {
|
||||||
binding.rvGallery.visibility = View.VISIBLE
|
binding.rvGallery.visibility = View.VISIBLE
|
||||||
binding.clRatio.visibility = View.VISIBLE
|
binding.clRatio.visibility = View.VISIBLE
|
||||||
@@ -117,4 +130,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
viewModel.purchaseImage(item.id, position)
|
viewModel.purchaseImage(item.id, position)
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hideLoadingDialog() {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
hideLoadingDialog()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ class CharacterGalleryViewModel(
|
|||||||
isLastPage = false
|
isLastPage = false
|
||||||
isRequesting = false
|
isRequesting = false
|
||||||
accumulatedItems.clear()
|
accumulatedItems.clear()
|
||||||
_uiState.value = UiState(isLoading = true)
|
|
||||||
request(page = currentPage)
|
request(page = currentPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +50,7 @@ class CharacterGalleryViewModel(
|
|||||||
private fun request(page: Int) {
|
private fun request(page: Int) {
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
isRequesting = true
|
isRequesting = true
|
||||||
|
_uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository.getCharacterImageList(
|
repository.getCharacterImageList(
|
||||||
token = token,
|
token = token,
|
||||||
@@ -63,6 +63,9 @@ class CharacterGalleryViewModel(
|
|||||||
.subscribe({ response ->
|
.subscribe({ response ->
|
||||||
val success = response.success
|
val success = response.success
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
||||||
|
isRequesting = false
|
||||||
|
|
||||||
if (success && data != null) {
|
if (success && data != null) {
|
||||||
// 누적 처리
|
// 누적 처리
|
||||||
val newItems = data.items
|
val newItems = data.items
|
||||||
@@ -88,18 +91,17 @@ class CharacterGalleryViewModel(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_uiState.value = _uiState.value?.copy(
|
_uiState.value = _uiState.value?.copy(
|
||||||
isLoading = false,
|
isLoading = isRequesting || isPurchasing,
|
||||||
error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
|
error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isRequesting = false
|
|
||||||
}, { throwable ->
|
}, { throwable ->
|
||||||
|
isRequesting = false
|
||||||
Logger.e(throwable, throwable.message ?: "")
|
Logger.e(throwable, throwable.message ?: "")
|
||||||
_uiState.value = _uiState.value?.copy(
|
_uiState.value = _uiState.value?.copy(
|
||||||
isLoading = false,
|
isLoading = isRequesting || isPurchasing,
|
||||||
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||||
)
|
)
|
||||||
isRequesting = false
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -112,50 +114,58 @@ class CharacterGalleryViewModel(
|
|||||||
|
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
isPurchasing = true
|
isPurchasing = true
|
||||||
|
_uiState.value = _uiState.value?.copy(
|
||||||
|
isLoading = isRequesting || isPurchasing,
|
||||||
|
)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository.purchaseCharacterImage(token = token, imageId = imageId)
|
repository.purchaseCharacterImage(token = token, imageId = imageId)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ response ->
|
.subscribe(
|
||||||
val success = response.success
|
{ response ->
|
||||||
val data = response.data
|
val success = response.success
|
||||||
if (success && data != null) {
|
val data = response.data
|
||||||
// 응답 imageUrl로 교체, 소유 상태 true로 변경
|
isPurchasing = false
|
||||||
val updated = target.copy(
|
if (success && data != null) {
|
||||||
imageUrl = data.imageUrl,
|
// 응답 imageUrl로 교체, 소유 상태 true로 변경
|
||||||
isOwned = true
|
val updated = target.copy(
|
||||||
)
|
imageUrl = data.imageUrl,
|
||||||
accumulatedItems[position] = updated
|
isOwned = true
|
||||||
|
)
|
||||||
|
accumulatedItems[position] = updated
|
||||||
|
|
||||||
val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong()
|
val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong()
|
||||||
val ownedBefore = _uiState.value?.ownedCount
|
val ownedBefore = _uiState.value?.ownedCount
|
||||||
?: accumulatedItems.count { it.isOwned }.toLong()
|
?: accumulatedItems.count { it.isOwned }.toLong()
|
||||||
val ownedAfter = ownedBefore + 1
|
val ownedAfter = ownedBefore + 1
|
||||||
val ratio = if (total > 0) {
|
val ratio = if (total > 0) {
|
||||||
ownedAfter.toFloat() / total.toFloat()
|
ownedAfter.toFloat() / total.toFloat()
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.value = _uiState.value?.copy(
|
||||||
|
ownedCount = ownedAfter,
|
||||||
|
ratio = ratio,
|
||||||
|
items = accumulatedItems.toList(),
|
||||||
|
isLoading = isRequesting || isPurchasing,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
0f
|
_uiState.value = _uiState.value?.copy(
|
||||||
|
error = response.message ?: "구매에 실패했습니다."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
isPurchasing = false
|
||||||
|
Logger.e(throwable, throwable.message ?: "")
|
||||||
_uiState.value = _uiState.value?.copy(
|
_uiState.value = _uiState.value?.copy(
|
||||||
ownedCount = ownedAfter,
|
isLoading = isRequesting || isPurchasing,
|
||||||
ratio = ratio,
|
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||||
items = accumulatedItems.toList(),
|
|
||||||
error = null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
_uiState.value = _uiState.value?.copy(
|
|
||||||
error = response.message ?: "구매에 실패했습니다."
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isPurchasing = false
|
)
|
||||||
}, { throwable ->
|
|
||||||
Logger.e(throwable, throwable.message ?: "")
|
|
||||||
_uiState.value = _uiState.value?.copy(
|
|
||||||
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
|
||||||
)
|
|
||||||
isPurchasing = false
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user