ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MVVM패턴 예제
    안드로이드 2023. 4. 5. 23:51

    MVVM패턴?

    MVVM패턴이란 Model, View, View Model을 분리하여 뷰에 모델간의 의존성을 줄어주는 패턴이다.

    뷰(Activity / Fragment)와 모델(Repository)이 분리되어 있고, 이 분리된 두 로직 사이에서 뷰의 이벤트에 따라 모델이 데이터를 반환/저장하도록 통신하는 뷰모델이 존재한다.

    뷰모델모델
    이벤트를 발생시켜 데이터 요청
    해당 데이터를 불러오는 모델의 메소드를 호출
    뷰모델에서 요청하는 값을 반환
    모델로부터 받은 값을 라이브데이터에 저장
    라이브데이터를 감지해 저장된 값을 뷰에 출력

    위와 같이 뷰와 모델은 서로 어덯게 동작하는지와 상관없이 로직을 작성할 수 있고 뷰모델을 통해 데이터를 통신할 수 있게 된다.

    MVVM 사용 이유

    android의 대중적인 패턴에는 MVC와 MVP도 존재한다. 하지만 구글은 왜 MVVM모델의 사용을 권장하는가?

    MVVM의 장점은 다음과 같다.

    1. View와 Model이 독립성을 유지할 수 있다.
    1. 독립성을 유지하기에 효율적인 유닛테스트가 가능하다.
    1. View를 잘 몰라도 로직을 구현할 수 있다.
    1. viewModel을 재사용할 수 있다.

    MVVM 적용하기

    우선 ViewModel과 LiveData를 build.gradle(:app)의 dependencies안에 implientation해준다.

    아래 2개는 viewModel을 간단하게 생성하기 위해 사용된다. (viewModel을 선언할 한 곳만 삽입해도 된다)

    (참조 : https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko)

    // 2023.04.04 기준
    def lifecycle_version = "2.5.1"
    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    // LiveData
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
    
    def activity_version = "1.6.1"
    //noinspection GradleDependency
    implementation("androidx.activity:activity-ktx:$activity_version")
    
    def fragment_version = "1.5.5"
    implementation("androidx.fragment:fragment-ktx:$fragment_version")

    build.gradle(:app)의 android { } 안에 아래와 같이 넣어준다. 데이터와 뷰 바인딩을 위한 것이다.

    buildFeatures{
    	viewBinding true
      dataBinding = true
    }

    viewModel에서 사용하는 즉, 값이 변경될 수 있는 변수는 MutableLiveData를 사용하고 ‘_’를 접두사로 사용한다. view에서 옵저버를 통해 변화를 감지하는 값의 변수는 LiveData를 사용하고 접두사 없이 사용한다.

    이처럼 두개의 변수로 나누는 이유는 view에서 데이터를 조작할 수 없게 하기 위해서이고 변수명의 스타일은 kotlin의 코드 컨벤션을 따른 것이다.

    Names for backing properties If a class has two properties which are conceptually the same but one is part of a public API and another is an implementation detail, use an underscore as the prefix for the name of the private property:
    백업 속성의 이름 클래스에 개념적으로 동이하지만 하나는 공용 API 의 일부이고 다른 하나는 구현 세부 정보인 두 개의 속성이 있는 경우 개인 속성 이름의 접두사로 밑줄을 사용합니다. (파파고)

    // ViewModel class
    class MainViewModel : ViewModel() {
    private val _num = MutableLiveData<Int>()
        val num: LiveData<Int> = _num
    
        init{
            _num.value = 0
        }
    
        fun incNum() {
            _num.value = _num.value?.plus(1) ?: 1
        }
    
        fun decNum() {
            if(_num.value == 1) return
            _num.value = _num.value?.minus(1)
        }
    }

    xml 구성. <layout> </layout>으로 코드들을 감싸준다. <data> <data>안에 바인딩할 뷰모델과 뷰를 등록해준다. XML상에서 View에 의해 전달된 이벤트를 처리하는 표현식을 사용하기 때문에 layout으로 감싸주는 형식으로 구성된다.

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
    
            <variable
                name="view"
                type="com.example.mvvmsample.MainFragment" />
    
            <variable
                name="viewModel"
                type="com.example.mvvmsample.MainViewModel" />
        </data>
    
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainFragment">
    
        <TextView
            android:id="@+id/tv_num"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="40sp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"/>
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="horizontal">
    
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:textSize="40sp"
                    android:gravity="center"
                    android:onClick="@{()->viewModel.decNum()}"
                    android:text="ㅡ"/>
    
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:textSize="40sp"
                    android:gravity="center"
                    android:onClick="@{()->viewModel.incNum()}"
                    android:text="+"/>
            </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    </layout>

    activity에서 fragment를 등록해준다. fragment의 경우 activity의 라이프 사이틀에 종속되어있기 때문에 viewModel의 값이 초기화되는 문제가 발생하기 때문에 if(savedInstanceState == null){}를 통해 fragment가 destroy되는 것을 막아주어야한다.

    class MainActivity : AppCompatActivity() {
    
        private lateinit var binding: ActivityMainBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            // todo inflate는 무엇인가?
            binding = ActivityMainBinding.inflate(layoutInflater)
            // todo setContentView는 무엇인가?
            setContentView(binding.root)
    
            if(savedInstanceState == null) {
                supportFragmentManager.beginTransaction()
                    .replace(R.id.contentFrame, MainFragment.newInstance())
                    .commit()
            }
        }
    }

    fragment를 생성하고 view와 viewModel을 바인딩 해준다. DataBindingUtil을 통해 xml을 연결시켜준다.

    view에서 <variable>로 선언한 view와 viewModel에 해당하는 값을 넣어준다.

    viewModel의 경우 implementation("androidx.fragment:fragment-ktx:1.5.5")를 선언해 주었기 때문에 by viewModels()를 사용하여 간단하게 선언 해줄 수 있다. (activity의 경우 implementation("androidx.activity:activity-ktx:1.6.1")을 선언해주어야 한다)

    class MainFragment : Fragment() {
    
      private lateinit var binding: FragmentMainBinding
    	private val viewModel: MainViewModel by viewModels()
        
    	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View 
    		binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)
    		binding.view = this
    		binding.viewModel = viewModel
    
    		viewModel.num.observe(viewLifecycleOwner) {
    			binding.tvNum.text = it.toString()
    		}
    		return binding.root
    	}
    
    	companion object {
    		fun newInstance() = MainFragment()
    	}
    }

    fragment의 일부인 아래의 코드는 viewModel의 num값을 observe하며 변화가 있을 때 tvNum의 text값을 변경해주는 코드이다.

    viewModel.num.observe(viewLifecycleOwner) {
    			binding.tvNum.text = it.toString()
    		}

    이처럼 viewModel의 값을 observe하며 view 컨트롤을 해주면 된다.

    참조

    viewModel에 관하여 https://jslee-tech.tistory.com/45

    AAC ViewModel과 MVVM ViewModel https://wooooooak.github.io/android/2019/05/07/aac_viewmodel/

    MVVM 적용방식 https://velog.io/@dddooo9/Android-MVVM-패턴을-사용하는-이유와-방법


    Uploaded by N2T

    댓글

Designed by Tistory.