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