[TIL] Android 앱 개발 심화 : 이미지 검색 어플 구현하기 3

지혜·2024년 1월 30일

Android_TIL

목록 보기
48/70

✏240130 화요일 TIL(Today I learned) 오늘 배운 것

📖ViewModel을 활용하여 보관함 기능 만들기 1

[SharedViewModel.kt]

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class SharedViewModel : ViewModel() {
    private val _likedDataList = MutableLiveData<List<KakaoImageData>>()
    val likedDataList: LiveData<List<KakaoImageData>> get() = _likedDataList

    fun filterDataList(dataList: MutableList<KakaoImageData>) {
        val likedList = dataList.filter { it.isliked }
        _likedDataList.value = likedList.toMutableList()
        Log.d("뷰모델검사","${likedDataList.value}")
        Log.d("뷰모델사이즈검사","${likedDataList.value?.count()}")
    }

    fun getFilterDataList(): MutableList<KakaoImageData> = likedDataList.value?.toMutableList() ?: mutableListOf()

}
  • 보관함에는 SearchFragment에서 받아온 전체 데이터들 중에 isliked=ture 인 경우만 따로 관리할 것이기 때문에, filter를 사용해서 isliked=ture를 따로 걸러준 값을 담아주었다.
  • 그렇게 담은 값을 getFilterDataList( )로 가져갈 수 있게 했다.
  • [주의] 뷰모델에는 context나 view값이 들어가면 잘 못 만든 뷰모델이라고 배웠는데,, 지금은 의도해서 만든건 아니고.., 여기저기 참고해서 겨우 만든거라..ㅎㅎ 나중에 만약 좀 더 활용하게 되면 유의하라고 적어둔다.

[SearchFragment.kt]

class SearchFragment : Fragment() {
    private lateinit var adapter: SharedAdapter
    private lateinit var searchWord : String
    private var _binding: FragmentSearchBinding? = null
    private val binding get() = _binding!!
    private val sharedViewModel : SharedViewModel by activityViewModels()
    private var dataList = mutableListOf<KakaoImageData>()
    
    ...
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        adapter = SharedAdapter(dataList)
        binding.rvImgData.adapter = adapter
        binding.rvImgData.layoutManager = GridLayoutManager(context, 2)

        loadSearchWord()

        sharedViewModel.likedDataList.observe(viewLifecycleOwner, Observer {
            adapter.setData(dataList)
        })

        searchClick()
        clickLiked()

    }


    private fun getSearchImg(searchWord: String) {
        RetrofitInstance.api.getImgData(query = searchWord).enqueue(object : Callback<ImageResponse> {
            override fun onResponse(call: Call<ImageResponse>, response: Response<ImageResponse>) {
                Log.d("레트로핏검색어검사", searchWord)
                val body = response.body()
                body?.let {
                    dataList.clear()
                    dataList.addAll(it.documents)
                    adapter.notifyDataSetChanged()
                }
                Log.d("api검사", "$dataList")
            }

            override fun onFailure(call: Call<ImageResponse>, t: Throwable) {
                Log.d("api검사", "네트워크오류/데이터변환오류.")
            }

        })
    }

    private fun searchClick() {
        binding.btnSearch.setOnClickListener {
            searchWord = binding.etSearch.text.toString()
            getSearchImg(searchWord)
            Log.d("검색어검사", "$searchWord")
            hideKeyboard()
            saveSearchWord(searchWord)
        }

    }

    private fun clickLiked() {
        adapter.itemClick = object : SharedAdapter.ItemClick {
            override fun onClick(view: View, position: Int) {
                val data = dataList.getOrNull(position) ?: return
                data.isliked = !data.isliked
                sharedViewModel.filterDataList(dataList)
                adapter.notifyItemChanged(position)
            }
        }
    }
    
    ...
  • private val sharedViewModel : SharedViewModel by activityViewModels()를 통해 뷰모델을 사용할 수 있도록 연결해줬다.

  • [트러블슈팅] 처음 검색한 후, 두번째 검색어를 넣으면 데이터가 바뀌지 않는 오류가 생겼다. 처음에는 searchWord가 안바뀌는게 문제인가 싶어서(아마 로그를 찍어봤을 때 이것도 문제였던게 맞는 것 같다.)
    => 클래스변수로 private lateinit var searchWord : String빼서 공유했는데도 데이터가 안바뀌어서 확인해보니까 api로 들어오는 값은 바뀌는 것 같길래 dataList에서 못받는 것 같아서 찾아보니까
    => 새로운 값이 들어올 때 dataList를 한 번 비워줘야, 거기에 인덱스 0 번부터 새로운 값이 들어올 수 있는 거였다. 그래서 요청 후에 새로운 응답을 datalist에 add 하기전에 dataList.clear()를 통해 데이터를 비운 후 새 값을 담아서 리사이클러뷰에 0번부터 나올 수 있도록 했다.

  • onViewCreated에 LiveData의 변화를 감지해줄 sharedViewModel.likedDataList.observe를 걸어준다. observe안에는 submitList()를 넣어줘야하지만, 아직 일반 어댑터라서 setData함수를 새로 만들어서 데이터의 변화를 감지하도록 만들었다.

    //SharedAdapter 클래스
    fun setData(newDataList: MutableList<KakaoImageData>) {
           kakaoItemData = newDataList
           notifyDataSetChanged()
       }

    +보관함 기능을 만들면서 Adapter를 공유해서 사용하기 때문에 SearchAdapter에서 SharedAdapter로 이름을 바꿔주었다.

  • 아이템을 클릭하면, 좋아요로 넘어가야하기 때문에 clickLiked 함수를 만들었다. 리사이클러뷰 아이템 클릭은 어댑터에서 인터페이스를 통해 가능하도록 만들었다. 그래서, 클릭하면 dataList가 IndexOutOfBoundsException 때문에 강제 종료 당하지 않도록 dataList.getOrNull(position) ?: return를 data에 담아주고, data의 isliked값이 반전 될 수 있도록 data.isliked = !data.isliked를 설정해줬다. 그 다음에 sharedViewModel.filterDataList(dataList)로 isliked가 true인 값들을 거를 수있도록 값을 넘기고, 변경 사항을 바로 알 수 있도록 adapter.notifyItemChanged(position)를 걸어주었다.

[StorageFragment.kt]

class StorageFragment : Fragment() {

    private val sharedViewModel: SharedViewModel by activityViewModels()
    private lateinit var adapter: SharedAdapter

    private var _binding: FragmentStorageBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = FragmentStorageBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val likedData = sharedViewModel.getFilterDataList()
        adapter = SharedAdapter(likedData ?: mutableListOf())
        binding.rvLikedData.adapter = adapter
        binding.rvLikedData.layoutManager = GridLayoutManager(context, 2)


        sharedViewModel.likedDataList.observe(viewLifecycleOwner, Observer {
            adapter.setData(it.toMutableList())
        })

        adapter.itemClick = object : SharedAdapter.ItemClick {
            override fun onClick(view: View, position: Int) {

                if (position < likedData.size) { // 인덱스가 유효한 범위 내에 있는지 확인
                    likedData[position].isliked = false
                    likedData.removeAt(position)
                    sharedViewModel.filterDataList(likedData)
                    adapter.notifyDataSetChanged()
                }
                Log.d("보관함인덱스검사", "${likedData.size},$position")
                
            }
        }

    }


    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}
  • StorageFragment에도 마찬가지로 private val sharedViewModel: SharedViewModel by activityViewModels()를 통해 뷰모델을 연결해준다.

  • 변화를 감지할 observe도 만들어준다.

  • StorageFragment도 SharedAdapter를 사용하기 때문에, 그 안에 있는 ItemClick 인터페이스를 통해서 onClickd 메소드를 override하여 클릭이벤트를 만들었다. 보관함페이지에는 isliked= true인 상태의 데이터만 있기 때문에, islked=false로 만들어주고, likedData에서 해당 인덱스값을 삭제해주고, sharedViewModel을 통해서 공유해주고 adapter에도 변경 사실을 알려준다.


✏오늘의 문제점과 내일 할 일, 느낀 점

  • StorageFragment에서 ShareViewModel의 likedDataList.value가 바뀌면 다시 인식을 안해준다.. ... 그래서 IndexOutOfBoundsException폭탄을 맞았다.ㅎㅎ 지금 생각하기엔. 일단 어댑터가 리스트어댑터가 아닌게 문제라고 생각해서 내일은 리스터어댑터로 SharedAdapter를 변경해볼 생각이다.

  • 이걸 다하면 얼추 필수과제가 끝난 것이므로, 보관한 이미지 리스트가 앱 재시작 후 다시 보일 수 있도록 SharedPreference에 저장하는 것을 해볼 것이다.

  • 최신정렬도 어렵지 않게 할 수 있을 것 같다.

  • 스크롤 최상단 이동 플로팅 액션버튼은.. 애플마켓때 만들었던 것 같은데,, 거기서 따올 수 있으면 간단하게 따오기로..

  • 목요일 오전 제출이라서 목요일 오전은 깃허브에 정리하기 바쁘고,, 벌써 내일 하기엔 빠듯한 계획인 것 같은데, 아주아주만약에 시간이 남는다면, 검색어입력창을 clear시켜주는 아이콘도 하나 걸어볼 생각이다.

  • 벌써 내일이면 수요일인데, 어제 뷰모델로 시간만 덜 버렸어도 좀 더 이것저것 도전할 수 있지 않았을까 하는 생각이든다. 그래도 어제 버린 시간 덕분에 오늘 이만큼 할 수 있었던걸까? 싶기도하다 .. 어제 오늘 이렇게 했어도 솔직히 좀 긴가민가하다.. 팀프로젝트때 뭘 시킬지.. 너무 걱정된다.. 어쨌든 이번에 할 일을 잘 마무리하고.. 나중일은 나중에 생각해야겠다!

profile
파이팅!

0개의 댓글