-
[Android] "좋아요"한 아이템 SharedPreferences에 저장하기 & 가져오기 & 삭제하기안드로이드 2024. 5. 21. 11:32
✏️ TIL(Today I Learned)
좋아요 버튼 클릭 시, SharedPreferences에 저장이 되고 다시 클리하면 삭제되도록 구현했다.
비디오를 클릭해서 VideoDetailFragment로 전환될 때, 해당 비디오가 SharedPreferences에 있는 지
확인하고 있으면 좋아요 버튼을 빨간색으로 표시하도록 했다.
우선 Repository이다.
VideoRepository 인터페이스는 비디오 데이터를 API와 로컬 저장소에서 가져오고 관리하는 메소드를 정의한다.
VideoRepositoryImpl 클래스는 VideoRepository를 구현한다.
- RemoteDataSource를 통해 YouTube API에서 인기 비디오, 카테고리별 비디오, 검색어 기반 비디오 등을 가져온다.
- SharedPreferences를 사용하여 로컬에 비디오 아이템을 저장하고 관리한다. (좋아요 기능)
좋아요한 비디오들은 리스트형태로 저장했다.
interface VideoRepository { suspend fun searchPopularVideo(): List<PopularVideoItemModel> suspend fun searchVideoByCategory(categoryId: String): List<PopularVideoItemModel> suspend fun takeVideoCategories(): List<CategoryItemModel> suspend fun searchChannelByCategory(channelId: String): List<ChannelItemModel> suspend fun searchVideoByText(text: String): Pair<String, List<VideoItemModel>> suspend fun searchMoreVideoByText(text: String, token: String): Pair<String, List<VideoItemModel>> suspend fun saveVideoItem(videoItemModel: VideoItemModel) suspend fun removeVideoItem(videoItemModel: VideoItemModel) suspend fun getStorageItems(): List<VideoItemModel> fun isVideoLikedInPrefs(videoId: String?): Boolean fun clearPrefs() } class VideoRepositoryImpl( private val remoteDataSource: RemoteDataSource, private val context: Context ) : VideoRepository { //중간 생략 private val pref: SharedPreferences = context.getSharedPreferences(Const.PREFERENCE_NAME, 0) private fun getPrefsItems(): List<VideoItemModel> { val jsonString = pref.getString(Const.LIKED_ITEMS, "") return if (jsonString.isNullOrEmpty()) { emptyList() } else { Gson().fromJson(jsonString, object : TypeToken<List<VideoItemModel>>() {}.type) } } private fun savePrefsItems(items: List<VideoItemModel>) { val jsonString = Gson().toJson(items) pref.edit().putString(Const.LIKED_ITEMS, jsonString).apply() } override suspend fun saveVideoItem(videoItemModel: VideoItemModel) { val likedItems = getPrefsItems().toMutableList() val findItem = likedItems.find { it.videoId == videoItemModel.videoId } if (findItem == null) { likedItems.add(videoItemModel) savePrefsItems(likedItems) } } override suspend fun removeVideoItem(videoItemModel: VideoItemModel) { val likedItems = getPrefsItems().toMutableList() likedItems.removeAll { it.videoId == videoItemModel.videoId } savePrefsItems(likedItems) } override fun clearPrefs() { pref.edit().clear().apply() } override suspend fun getStorageItems(): List<VideoItemModel> { return getPrefsItems() } override fun isVideoLikedInPrefs(videoId: String?): Boolean { val likedItems = getPrefsItems() return likedItems.any { it.videoId == videoId } } }
그 다음, ViewModel이다.
VideoDetailFragment의 로직을 처리하며,
VideoRepository와 상호작용하여 비디오의 저장, 삭제 및 좋아요 상태를 확인한다.
저장 작업 결과를 관찰하기 위해, LiveData인 _saveResult를 사용했다.
SaveUiState는 ViewModel에서 비디오 아이템 저장/삭제 작업의 상태를 관리하고, 이 상태 변화를 UI에 전달한다. (ViewModel과 UI 계층 간의 상태 전달을 용이하게 해준다.)
과정은 아래와 같다.
- _saveResult 라는 MutableLiveData<SaveUiState> 객체를 통해 UI에 상태 변화를 전달
- saveVideoItem, removeVideoItem 메서드에서 비디오 아이템 저장/삭제 작업을 수행한 후,
_saveResult에 SaveUiState.Success를 할당하여 UI에 성공 메시지를 전달 - updateSaveItem 메서드에서 비디오 아이템의 좋아요 상태에 따라
saveVideoItem 또는 removeVideoItem을 호출하여 저장/삭제 작업을 수행 - VideoDetailFragment에서 viewModel.saveResult를 observe하여 UI 업데이트를 수행
(SaveUiState.Success일 때 SnackBar를 표시)
class VideoDetailViewModel ( private val videoRepository: VideoRepository, private val applicationContext: Context ) : ViewModel() { private val _saveResult = MutableLiveData<SaveUiState>() val saveResult: LiveData<SaveUiState> get() = _saveResult private fun saveVideoItem(videoItemModel: VideoItemModel) { viewModelScope.launch { videoRepository.saveVideoItem(videoItemModel.copy(isLiked = true)) _saveResult.value = SaveUiState.Success(applicationContext.getString(R.string.videodetail_snack_save)) } } private fun removeVideoItem(videoItemModel: VideoItemModel) { viewModelScope.launch { videoRepository.removeVideoItem(videoItemModel.copy(isLiked = false)) _saveResult.value = SaveUiState.Success(applicationContext.getString(R.string.videodetail_snack_delete)) } } fun updateSaveItem(videoItemModel: VideoItemModel) { viewModelScope.launch { if (videoItemModel.isLiked) { saveVideoItem(videoItemModel) } else { removeVideoItem(videoItemModel) } } } fun isVideoLikedInPrefs(videoId: String?): Boolean { return videoRepository.isVideoLikedInPrefs(videoId) } fun clearPreferences() { videoRepository.clearPrefs() } } class VideoDetailViewModelFactory( private val videoRepository: VideoRepository, private val applicationContext: Context ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(VideoDetailViewModel::class.java)) { @Suppress("UNCHECKED_CAST") return VideoDetailViewModel(videoRepository, applicationContext) as T } throw IllegalArgumentException("Unknown ViewModel class") } }
SaveUiState는 sealed class로 정의했으며,
비디오 아이템 저장/삭제 작업의 상태를 표현합니다. 세 가지 상태를 가지고 있다.
- Init: 초기 상태
- Success: 저장/삭제 작업이 성공했을 때의 상태로, 성공 메시지를 포함
- Error: 예외가 발생했을 때의 상태로, 예외 객체를 포함
sealed class SaveUiState { object Init : SaveUiState() data class Success(val message: String) : SaveUiState() data class Error(val exception: Throwable) : SaveUiState() }
마지막으로, VideoDetailFragment이다.
선택한 비디오의 상세 정보를 표시하는 UI 프래그먼트이며,
좋아요 버튼을 클릭하면 VideoDetailViewModel의 updateSaveItem를 호출하여 비디오 아이템을 저장/삭제한다.
class VideoDetailFragment : Fragment() { private var _binding: FragmentVideoDetailBinding? = null private val binding get() = _binding!! private val viewModel: VideoDetailViewModel by viewModels { VideoDetailViewModelFactory(VideoRepositoryImpl(RepositoryClient.youtubeService, requireContext()), requireContext().applicationContext ) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentVideoDetailBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initView() } private fun initView() { val item = arguments?.getParcelable<VideoItemModel>(BUNDLE_KEY_FOR_DETAIL_FRAGMENT) ?: arguments?.getParcelable<VideoItemModel>(ShortsFragment.BUNDLE_KEY_FOR_DETAIL_FRAGMENT_FROM_SHORTS) val isLikedInPrefs = viewModel.isVideoLikedInPrefs(item?.videoId) updateLikeButton(item, isLikedInPrefs) viewModel.saveResult.observe(viewLifecycleOwner) { state -> when (state) { is SaveUiState.Success -> { Snackbar.make(binding.root, state.message, Snackbar.LENGTH_SHORT).show() } else -> {} } } with(binding) { ivThumbnail.load(item?.videoThumbnail) tvChannelTitle.text = item?.channelTitle tvTitle.text = item?.videoTitle tvDescription.text = item?.videoDescription ivBack.setOnClickListener { requireActivity().supportFragmentManager.popBackStack() } llLike.setOnClickListener { item?.let { it.isLiked = !it.isLiked viewModel.updateSaveItem(it) updateLikeButton(it, it.isLiked) } } llShare.setOnClickListener { val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, SHARE_URL + item?.videoId) } startActivity(Intent.createChooser(shareIntent, "Share Video Link")) } } } private fun updateLikeButton(item: VideoItemModel?, isLikedInPrefs: Boolean) { binding.ivLike.setImageResource( if (isLikedInPrefs) R.drawable.ic_liked else R.drawable.ic_like ) item?.isLiked = isLikedInPrefs } override fun onDestroyView() { super.onDestroyView() _binding = null } companion object { const val BUNDLE_KEY_FOR_DETAIL_FRAGMENT = "BUNDLE_KEY_FOR_DETAIL_FRAGMENT" fun newInstance(bundle: Bundle): VideoDetailFragment { return VideoDetailFragment().apply { arguments = bundle } } } }
다른 프래그먼트에서의 호출은 아래와 같다.
// SearchResultFragment.kt private fun selectItem(item: VideoItemModel) { val bundle = Bundle().apply { putParcelable(BUNDLE_KEY_FOR_DETAIL_FRAGMENT, item) } val detailFragment = VideoDetailFragment.newInstance(bundle) (requireActivity() as MainActivity).showVideoDetailFragment(detailFragment) } // HomeFragment.kt private fun selectItem(videoItem: PopularVideoItemModel) { val videoItemModel = videoItem.toVideoItemModel() // PopularVideoItemModel을 VideoItemModel로 변환 val bundle = Bundle().apply { putParcelable(BUNDLE_KEY_FOR_DETAIL_FRAGMENT, videoItemModel) // VideoItemModel을 전달 } val detailFragment = VideoDetailFragment.newInstance(bundle) (requireActivity() as MainActivity).showVideoDetailFragment(detailFragment) } // MainActivity.kt fun showVideoDetailFragment(detailFragment: Fragment) { supportFragmentManager.commit { setCustomAnimations( R.anim.zoom_in, R.anim.zoom_out, R.anim.zoom_in, R.anim.zoom_out ) replace(R.id.fl_video_detail, detailFragment) addToBackStack(null) } }
'안드로이드' 카테고리의 다른 글
[Android] Intent.ACTION_SEND를 사용하여 공유하기 (공유 가능한 앱 목록을 보여주기) (0) 2024.05.21 [Android] 프래그먼트 전환 시 애니메이션 효과 적용 (0) 2024.05.21 [Android] 특정 Tab에서만 Toolbar 보이게 하기 (0) 2024.05.21 [Android] Tab Layout 구현 (Tab 선택 시, icon & text 색상 변경) (0) 2024.05.16 [Android] java.net.UnknownServiceException: CLEARTEXT communication to apis.data.go.kr not permitted by network security policy 오류 해결 (0) 2024.05.01