FrontEnd/Android

[Android] ViewModel

Satisfaction 2021. 6. 14. 16:30

위와 같이 하나의 액티비티에 존재하는 어떤 sharedData에 대해 

액티비티에 attached된 2개의 fragment가 그것을 사용한다고 가정해 보자.

 

만약 fragment1이 데이터를 변경한다면,

변경한 데이터를 activity가 fragment2에도 반영해 주어야 하고,

반대의 경우에도 마찬가지이다.

 

이것을 코드로 구현하게 되면 코드가 지저분해지면서

가독성을 낮추고, 이는 곧 버그가 발생하기 쉬운 환경이 된다.

 

사실 여기에서 우리는 '데이터가 무엇으로 바뀌는지'에 관심이 있지,

'데이터가 어떻게 변경되는지(how to fetch?)'는 관심이 없다.

 

ViewModel

RxJava나 React를 경험해 본 사람들이라면,

리액티브 프로그래밍 기법을 사용했을 때 위와 같은 불필요한 곳에 신경을 덜 쓸 수 있게 된다는 것을 알 것이다.

 

여전히 sharedData는 액티비티에 존재하지만,

그것을 변경하거나 다른 곳에서 변경된 데이터를 다시 fetch하는 로직을

ViewModel로 위임함으로써 데이터가 필요하다면 해당 ViewModel을 사용하기만 하면 된다

즉 동일한 activity에 attached된 fragment라면,

어떤 fragment든지 손쉽게 sharedData에 접근하거나 업데이트를 수신할 수 있다.

 

ViewModel Example

 

build.gradle

implementation 'androidx.fragment:fragment-ktx:1.3.2'

 

가장 먼저 위 의존성을 추가한다.

 

이제 ViewModel을 사용하기 위해, ViewModel을 상속하여 필요한 데이터들을 MyViewModel 클래스의 필드에 나열한다.

class MyViewModel : ViewModel() {
    //단순히 정수 하나를 저장한다
    val selectedNum= MutableLiveData<Int>()

    fun setLiveData(num: Int) {
        selectedNum.value = num
    }
}

이제 이 MyViewModel을 Activity/Fragment에서 사용해 볼 것이다.

class MyFragmentActivity : AppCompatActivity() {
    lateinit var binding:ActivityMyFragmentBinding
    val myViewModel: MyViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding= ActivityMyFragmentBinding.inflate(layoutInflater)
        setContentView(binding.root)


        supportFragmentManager.beginTransaction().addToBackStack(null).replace(R.id.frameLayout, ImageFragment()).commit()
    }
}
class ImageFragment : Fragment() {
    var binding: FragmentImageBinding? = null
    val mViewModel: MyViewModel by activityViewModels()
    val imgList = arrayListOf<Int>(R.drawable.image_1, R.drawable.image_2, R.drawable.image_3)

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentImageBinding.inflate(layoutInflater, container, false)
        return binding!!.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding!!.apply {
            radioGroup.setOnCheckedChangeListener { group, checkedId ->
                when (checkedId) {
                    R.id.radioBtn1 -> {
                        mViewModel.setLiveData(0)
                    }
                    R.id.radioBtn2 -> {
                        mViewModel.setLiveData(1)
                    }
                    R.id.radioBtn3 -> {
                        mViewModel.setLiveData(2)
                    }
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }

}
class TextFragment : Fragment() {
    var binding: FragmentTextBinding? = null
    val mViewModel: MyViewModel by activityViewModels()
    val data= arrayListOf<String>("ImageData1", "ImageData2", "ImageData3")

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentTextBinding.inflate(layoutInflater, container, false)
        return binding!!.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val imgNum= requireActivity().intent.getIntExtra("imgNum", -1)

        binding!!.apply {
            if(imgNum!= -1){
                myTextView.text=data[imgNum]
            }else{
                mViewModel.selectedNum.observe(viewLifecycleOwner, Observer {
                    myTextView.text= data[it]
                })
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        binding= null
    }

}

코드들은 위와 같은 구조로 되어 있다.

 

TextFragment는 변경된 sharedData를 수신(observe)하기만 하고,

ImageFragment는 sharedData를 변경(update)하기만 한다.

 

sharedData가 실제로 존재하는 FragmentActivity에는 ViewModel 선언 및 초기화 외에는

frament들을 핸들링하는 코드가 존재하지 않는다.

 

단지 ViewModel만 Activity에서 선언 및 초기화하고,

Fragment에서는 by activityViewModel()을 이용해 Activity에 선언된 ViewModel을 사용할 뿐이다.