본문 바로가기
Jetpack Library

[Android JetPack] DataBinding + LiveData + ViewModel + Repository 적용해보기

by JongSeok 2023. 1. 5.

포스팅 제목처럼 이번에는 DataBinding, LiveData, ViewModel, Repository를 모두 적용하며 간단한 프로그램을 구현해 보겠습니다. 

이전 포스팅에서 공부했던 라이브러리는 간단히 살펴보고 넘어가겠습니다.

 

이전 포스팅

 

[안드로이드] ViewModel 이란? ( + ViewModelFactory)

이번에 알아볼 ViewModel은 이전 포스팅에서 공부했던 MVVM 디자인 패턴의 구성요소 중 하나인 ViewModel입니다. 이해를 돕기 위해 예제와 함께 작성하겠습니다. 이전 포스팅 [안드로이드] MVVM 디자인

develop-oj.tistory.com

 

[안드로이드] LiveData 기본

Android JetPack 라이브러리 중 하나인 LiveData에 대해 알아보겠습니다. 특히, LiveData는 ViewModel과 DataBinding과 함께 유용하게 사용됩니다. [안드로이드] ViewModel 이란? ( + ViewModelFactory) 이번에 알아볼 ViewM

develop-oj.tistory.com

 

[안드로이드] DataBinding 알아보기

앞서 공부한 ViewModel, LiveData와 함께 유용하게 쓰이는 DataBinding에 대해 공부해 보겠습니다. DataBinding이란? 과거 xml의 뷰에 접근하기 위해서는 findViewById()를 사용했습니다. 하지만 findViewById() 방식

develop-oj.tistory.com


먼저 예제를 실행하기 위해 build.gradle(Module : app)을 세팅합니다.

plugins {
    ...
    id 'kotlin-kapt'
}
android {
    ...
    buildFeatures {
        dataBinding true
    }
}
// by viewModels()를 이용하기 위한 종속성
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
// Livedata
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'

Repository

interface Repository {
    fun getCount() : LiveData<Int>
    fun increaseCount()
    fun decreaseCount()
}

Repository는 VIewModel이 DB에 접근할 때 요청하는 논리를 캡슐화하여 클래스로 구성하는 것입니다.

ViewModel과 DB 사이의 일종의 API 역할을 한다고도 볼 수 있습니다.

Repository 패턴을 사용함으로써 ViewModel와 DB의 결합도를 낮추고 로직을 분리할 수 있습니다.

인터페이스에서 선언된 함수의 역할은 RepositoryImpl에서 구체화합니다.

RepositoryImpl

class RepositoryImpl(count : Int) : Repository {
    private val liveData = MutableLiveData(count)
    override fun getCount(): LiveData<Int> {
        return liveData
    }
    override fun increaseCount() {
        liveData.value = liveData.value?.plus(1)
    }
    override fun decreaseCount() {
        liveData.value = liveData.value?.minus(1)
    }
}

현재 간단한 카운터 프로그램을 구현하고 있기 때문에 LiveData 객체를 선언해서 현재 liveData를 반환하거나 LiveData의 value를 1씩 증가/감소시키도록 작성했습니다.

MainViewModel

class MainViewModel(val repositoryImpl: RepositoryImpl) : ViewModel() {
    val countFromRepository = repositoryImpl.getCount()
    fun increaseCount() {
        repositoryImpl.increaseCount()
    }
    fun decreaseCount() {
        repositoryImpl.decreaseCount()
    }
}

위의 RepositoryImpl에서 작성한 내용을 MainViewModel에서 동일하게 구현할 수 있지만 VIewModel과 DB를 분리하고Repository 패턴을 적용하기 위해 MainViewModel에서 RepositoryImpl 객체를 파라미터로 받으며 DB에 접근하고 있습니다.

MainActivity

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val repositoryImpl = RepositoryImpl(0)
    private val factory = MainViewModelFactory(repositoryImpl)
    private val viewModel: MainViewModel by viewModels() { factory }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.lifecycleOwner = this         // 생명주기가 액티비티를 따르도록
        binding.viewmodel = viewModel         // xml의 viewModel 연결
        binding.activity = this@MainActivity  // xml의 activity 연결
    }
    // 증가 버튼 클릭
    fun plus() {
        viewModel.increaseCount()
    }
    // 감소 버튼 클릭
    fun minus() {
        viewModel.decreaseCount()
    }
}

MainViewModel 클래스의 경우 생성자가 있기 때문에 ViewModelFactory를 사용해 초기화 해줍니다.

버튼의 이벤트도 setOnClickListener로 처리하지 않고 xml에서 처리해 주겠습니다.

MainActivity의 코드가 간결해지고 가독성도 좋아진 것 같습니다.

activity_main.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="viewmodel"
            type="com.example.practiceaac.MainViewModel" />
        <variable
            name="activity"
            type="com.example.practiceaac.MainActivity" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <TextView
            android:id="@+id/number_output"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewmodel.countFromRepository.toString()}"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"
            android:textSize="30sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/minus_button"
            android:onClick="@{() -> activity.minus()}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:layout_marginTop="32dp"
            android:text="감소"
            android:textSize="18sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/number_output" />

        <Button
            android:id="@+id/plus_button"
            android:onClick="@{() -> activity.plus()}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="100dp"
            android:text="증가"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/number_output" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="30dp"
            android:max="10"
            app:layout_constraintBottom_toTopOf="@+id/number_output"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintWidth_percent="0.3"
            app:progressScaled="@{viewmodel.repositoryImpl.count}" />

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

xml에서 클릭 리스너를 설정하는 방법은 onClick 속성에서 "@{() -> 변수명.함수명}"로 설정합니다.

실행 결과

MainActivity에 모든 코드를 작성하면 매우 단순한 프로그램이지만 위 패턴을 적용을 적용하다 보면 클래스와 .kt 파일이 많아져 다소 복잡해 보이기도 합니다.

하지만 구글에서 권장하는 AAC 라이브러리와 MVVM 패턴 적용에 익숙해지기 위해 노력해야겠습니다.

728x90
반응형