Intro
- 오늘도 개발개발중에 MVVM으로도 뇌용량이 꽉찬 시점에서 계속해서 새로운것들이 들어오고있다..! interface를 사용해 Data를 주고받는것이다.
Event Interface만들기
- 네이밍을 어떻게 명명해야할지몰라 이렇게 불렀다. 이것을 설명하려면 우선 기존 ViewModel에서 데이터를 어떤식으로 처리하는지 살펴봐야한다.
class MyPageViewModel(application: Application) : AndroidViewModel(application) {
private var _list = MutableLiveData<List<ContentData>>()
val list get() = _list
fun removeItem(position: Int? = null, item: ContentData) {
fun searchIndex(item: ContentData): Int {
val currentList = list.value.orEmpty().toMutableList()
val selectedItem = currentList.find { it.id == item.id }
return selectedItem?.let { currentList.indexOf(it) } ?: return -1
}
val currentList = list.value.orEmpty().toMutableList()
val currentPosition = position ?: searchIndex(item)
if (currentPosition == -1) {
return
}
currentList.removeAt(currentPosition)
_list.value = currentList
}
fun updateMyPageItems(items: List<ContentData>) {
_list.value = items
}
fun updateMyPageSharedPrefs(items: List<ContentData>) {
val appContext = getApplication<Application>().applicationContext
for (i in items.indices) {
if (items[i].isLike) {
Utils.saveMyPageContent(appContext, items[i])
} else {
Utils.removeMyPageContent(appContext, items[i])
}
}
}
}
- 현재 내가 개발중인 프로젝트에서는 RecyclerView+ListAdapter를 사용중이다. 그러므로 데이터역시 여러개를 화면에 띄워주게된다. 그러니 ViewModel에서 LiveData역시 List형태를 사용하고있다.
- 현재 프로젝트는 2개의 Fragment와 1개의 Activity로 화면을 구성중이다. Fragment끼리 통신을 위해 SharedViewModel도 구현해주었다.
SharedViewModel
- 안드로이드에서 제공하는 AAC-ViewModel을 사용하는 방법은 총 3가지정도있다.
- Activity에서만 사용하는경우
- Fragment에서만 사용하는경우
- Activity를 기준으로 Fragment에서 공유해서 사용해야하는 경우
- 이중 SharedViewModel은 세번째경우에 해당한다.
- SharedViewModel을 사용하는 이유는 당연히도 Fragment끼리 데이터를 공유하고싶어서이다. 기존 ViewModel의 경우 각각 Fragment에서 선언했을때 하나의 instance로 공유할 수가없다. 그래서 Fragment를 거쳐서 통신해야하는등의 불편한 부분이 있는데 SharedViewModel의 경우 Activity의 Lifecycle을 따른다. 따라서 같은 Activity위에 있는 Fragment끼리는 하나의 SharedViewModel instance를 공유하게되어 데이터를 주고받는게 가능한 것이다.
- 여기에서 오늘 정리할 Event Interface만들기가 등장한다! 두둥!
class MainSharedViewModel: ViewModel(){
private val _searchedEvent: MutableLiveData<MainSharedEventForSearch> = MutableLiveData()
val searchedEvent get() = _searchedEvent
private val _myPageEvent: MutableLiveData<MainSharedEventForMyPage> = MutableLiveData()
val myPageEvent get() = _myPageEvent
fun updateSearchedItem(item: ContentData) {
_searchedEvent.value = MainSharedEventForSearch.UpdateSearcedContent(item)
}
fun updateMyPAgeItems(items: List<ContentData>?) {
items?.filter {
it.isLike
}?.also {
_myPageEvent.value = MainSharedEventForMyPage.UpdateMyPageItem(it)
}
}
}
sealed interface MainSharedEventForSearch {
data class UpdateSearcedContent(
val item: ContentData
): MainSharedEventForSearch
}
sealed interface MainSharedEventForMyPage {
data class UpdateMyPageItem(
val items: List<ContentData>
):MainSharedEventForMyPage
}
Event Interface만들기
- 요런 인터페이스를 만드는 이유는 데이터를 메모리에 계속 띄워놓고싶지 않아서이다. 이벤트는 한번 받아서 처리하면 그걸로 땡이니까.(이건 내 주관적인 견해이고 실제 내부에서도 이런식으로 돌아가는지는 모른다..! 다만 오늘 튜터님에게 들었던 이야기를 정리해놓았으니 일단은 그런것으로 알고 넘어갔다 ㅎㅎ)
- 만드는 것은 간단하다. Interface를 선언하고 내부에 주고받고싶은 데이터클래스를 정의한다. 그리고나서 해당 이벤트를 observe하면 끗! 자세한건 아래 코드를 참고하시길..!(initViewModel 부분에 선언되어있다.)
package bootcamp.sparta.nb_deepen_assignment.ui.search
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import bootcamp.sparta.nb_deepen_assignment.databinding.SearchFragmentBinding
import bootcamp.sparta.nb_deepen_assignment.model.ContentData
import bootcamp.sparta.nb_deepen_assignment.repository.ContentRepository
import bootcamp.sparta.nb_deepen_assignment.ui.main.MainActivity
import bootcamp.sparta.nb_deepen_assignment.ui.main.MainSharedEventForMyPage
import bootcamp.sparta.nb_deepen_assignment.ui.main.MainSharedEventForSearch
import bootcamp.sparta.nb_deepen_assignment.ui.main.MainSharedViewModel
import bootcamp.sparta.nb_deepen_assignment.utils.Utils
import bootcamp.sparta.nb_deepen_assignment.utils.Utils.saveMyPageContent
import bootcamp.sparta.nb_deepen_assignment.viewmodel.SearchViewModel
import bootcamp.sparta.nb_deepen_assignment.viewmodel.MainViewModelFactory
class SearchFragment() : Fragment() {
companion object {
fun newInstance() = SearchFragment()
}
private var _binding: SearchFragmentBinding? = null
private val binding get() = _binding!!
private val adapter by lazy {
SearchListAdapter(
onLikeClickListener = { position, item ->
modifySearchItem(position, item)
}
)
}
private val repository by lazy {
ContentRepository()
}
private val viewModel: SearchViewModel by viewModels {
MainViewModelFactory(repository)
}
private val sharedViewModel: MainSharedViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = SearchFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
initViewModel()
}
private fun initView() = with(binding) {
recyclerView.adapter = adapter
searchView.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(p0: String?): Boolean {
if (p0 != null) {
viewModel.resultImageAndVideo(p0)
}
return false
}
override fun onQueryTextChange(p0: String?): Boolean {
return true
}
})
}
private fun initViewModel() = with(viewModel) {
list.observe(viewLifecycleOwner) {
adapter.submitList(it)
sharedViewModel.updateMyPAgeItems(it)
}
with(sharedViewModel) {
searchedEvent.observe(viewLifecycleOwner) { event ->
when (event) {
is MainSharedEventForSearch.UpdateSearcedContent -> {
viewModel.modifySearchItem(
item = event.item
)
}
}
}
}
}
private fun modifySearchItem(position: Int, item: ContentData) {
viewModel.modifySearchItem(position, item)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
Outro
- 새로운 기술을 배운다는건 새로운 패러다임을 받아들인다는게 아닐까라는 생각이든다. 안드로이드는 시작하자마자 callback처리를 먼저 배운다. button 클릭을 처리하려면 callback을 써야하기 때문이다. EventListener를 사용해 이벤트를 등록하고 액션이 일어날때 이벤트가 동작하는 방식역시 "그냥 이렇게 써야돼"라고 받아들이고 외워서 사용할 수도있지만 조금씩 실력이 쌓여간다는건 이런것을 이해하고 문제를 해결하는데 유연하게 사용할 수 있는것 아닐까 조심스럽게 정리해본다..