막무가내 삽질 블로그

Android ViewModel + LiveData + Data Binding 본문

Android

Android ViewModel + LiveData + Data Binding

joong~ 2020. 3. 1. 14:12
728x90

개발자 문서, 유튜브, 코드랩을 참고했다.

 

 

ViewModel ?

AAC ViewModel은 UI관련 데이터를 저장하고 관리하기 위하여 설계된 뷰모델클래스이다.

ViewModel의 LifeCycle은 액티비티가 onCreate되고 onDestroy될 때까지 존재한다.

앱이 회전 할 때와 같이 액티비티가 여러번 호출 될 수 있지만(onCreate) ViewModel은 계속 유지된다.

 

장점 : 싱글톤 객체처럼 사용가능하다, 프래그먼트 중개자로 액티비티를 사용하지 않아도 된다, 화면 회전 문제

 

주의사항 : ViewModel 내부에 액티비티,프래그먼트,뷰에 대한 Context를 저장해서는 안된다. (Application Context제외)

 

ViewModel LifeCycle

 

 

LiveData ? 

LiveData는 LifeCycle의 Observer다. LiveData는 관찰 가능한 데이터 홀더 클래스이다. 즉, 생명주기를 알고 있는 데이터 타입? 이라고 생각하면 될 것 같다.

LiveData는 옵저버 패턴을 따른다. 데이터의 변경이 일어났을 때 콜백으로 받아 처리 한다. 데이터의 변경이 될때 마다 콜백을 실행하는데 LifeCycle을 알기 때문에 필요하지 않을 땐 콜백이 실행되지 않는다. 

 

장점 : Data와 UI간 동기화, 메모리 누수 방지, 수동적인 생명주기가 필요없음, UI컨트롤러의 상태변경이 쉬움, 자원공유

 

UI가 활동 중일 때
UI가 활동중이지 않을 떄

 

 

 

 

ViewModel

class UserProfileViewModel : ViewModel() {
    
    // MutableLiveData는 변경할 수 있는 LiveData
    private val _user = MutableLiveData<User>()
    
    // LiveData 변경할 수 없음
    val user : LiveData<User>
      get() = _user
}

 

Activity

override fun onCreate(saveInstanceState: Bundle?) {
    //..
    
    userViewModel.user.observe(this, Observer { user ->
        userNameTextView.text = user?.name
    })
}


user.setValue(newUser) // UI Thread
user.postValue(newUser) // Background Thread

 

LiveData는 다른 관측 가능한 객체와 다른 점은 LifeCycle를 인식한다는 것이다. 즉, UI가 화면에 있는지 없는지 여부를 알고 있을 때는 데이터가 변경되고 없으면 데이터가 변경되지 않는다. 왜냐면 LiveData는 UI에 상태에 대해 알고 있기 때문에이다. LiveData를 더 잘쓰려면 데이터 바인딩을 사용해야 한다.

 

 

 

Data Binding ?

데이터를 레이아웃에 바인딩을 해주는 것을 의미한다. 

즉, 데이터를 바인딩 하여 View에서 발생하는 이벤트를 ViewModel에 알려 ViewModel은 업데이트한 데이터를 View에게 보여준다.

 

 

xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="vm"
            type="com.example.android.guesstheword.screens.game.GameViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/game_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".screens.game.GameFragment">

        <TextView
            android:id="@+id/word_is_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:fontFamily="sans-serif"
            android:text="@string/word_is"
            android:textColor="@color/black_text_color"
            android:textSize="14sp"
            android:textStyle="normal"
            app:layout_constraintBottom_toTopOf="@+id/word_text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_chainStyle="packed" />

        <TextView
            android:id="@+id/word_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:animateLayoutChanges="true"
            android:fontFamily="sans-serif"
            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
            android:textColor="@color/black_text_color"
            android:textSize="34sp"
            android:textStyle="normal"
            android:text="@{@string/quote_format(vm.word)}"
            app:layout_constraintBottom_toTopOf="@+id/score_text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/word_is_text"
            app:layout_constraintVertical_chainStyle="packed"
            tools:text="&quot;Tuna&quot;" />

        <TextView
            android:id="@+id/timer_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:fontFamily="sans-serif"
            android:textColor="@color/grey_text_color"
            android:textSize="14sp"
            android:textStyle="normal"
            android:text="@{vm.curretTimeString}"
            app:layout_constraintBottom_toTopOf="@+id/score_text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="0:00" />

        <TextView
            android:id="@+id/score_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:fontFamily="sans-serif"
            android:textColor="@color/grey_text_color"
            android:textSize="14sp"
            android:textStyle="normal"
            android:text="@{@string/score_format(vm.score)}"
            app:layout_constraintBottom_toTopOf="@+id/guideline"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Score: 2" />

        <Button
            android:id="@+id/skip_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:text="@string/skip"
            android:theme="@style/SkipButton"
            android:onClick="@{() -> vm.onSkip()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/correct_button"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/guideline" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_end="96dp" />

        <Button
            android:id="@+id/correct_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:text="@string/got_it"
            android:theme="@style/GoButton"
            android:onClick="@{() -> vm.onCorrect()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/skip_button"
            app:layout_constraintTop_toTopOf="@+id/guideline" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

viewmodel

class GameViewModel : ViewModel() {

   	private val _word = MutableLiveData<String>()
    val word: LiveData<String>
        get() = _word

    private val _score = MutableLiveData<Int>()
    val score: LiveData<Int>
        get() = _score

    fun onSkip() {
       /*...*/
    }

    fun onCorrect() {
       /*...*/
    }
    
    override fun onCleared() {
        super.onCleared()
		/*...*/
    }
}

 

fragment

class GameFragment : Fragment() {

    private lateinit var viewModel: GameViewModel
    private lateinit var binding: GameFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        binding = DataBindingUtil.inflate(
                inflater,
                R.layout.game_fragment,
                container,
                false
        )

        viewModel = ViewModelProvider(this).get(GameViewModel::class.java)

        binding.vm = viewModel
        binding.lifecycleOwner = this

        return binding.root

    }

}

 

 

LiveData, LiveData observers

www.notion.so/imwj/LiveData-LiveData-observers-f8e8f70f75264dc48f3812bd6875ee56

 

LiveData 와 LiveData observers

LiveData는 LifeCycle내에서 관찰 할 수 있는 데이터 홀더 클래스이다.

www.notion.so

 

 

 

 

 

참:https://www.youtube.com/watch?v=OMcDk2_4LSk

참:https://developer.android.com/topic/libraries/architecture/livedata

참:https://developer.android.com/topic/libraries/data-binding

Comments