Kotlin에서 mvvm 패턴을 위해 Koin과 Databinding 적용해보기

오늘은 안드로이드에 MVVM 디자인 패턴을 적용하기 위해, DataBinding을 프로젝트에 적용하는 과정을 기록해두려고 한다.

내가 구현하고자 하는 프로젝트의 대략적인 구조는 아래와 같다.

  • MVVM 디자인 패턴
    • Koin 라이브러리를 통해 ViewModel에 의존성 주입
    • DataBinding을 통한 가독성 높고 사용하기 편한 코드 작성
  • 3개의 Tab를 ViewPager2로 구현
    • 각 Tab마다 RecyclerView를 지니며, 각각 Header, Footer, 모두를 Sticky하게 구현

Koin 적용

Koin은 앱에 의존성 주입을 간단히 할 수 있도록 돕는 라이브러리다.

사용자의 수준이나 요구에 따라, DataBase, Glide, Retrofit, OkHttp등, 이외에도 다양한 모듈을 쉽게 만들고 관리할 수 있는 뛰어난 라이브러리이다.

현재 작성 중인 예제의 수준에서는 ViewModelModule를 통해 앱 내의 ViewModel을 일괄로 관리하고, 의존성을 부여하는 간단한 수준에서 사용하려고 한다.

우선, Koin과 DataBinding으로 앱의 기초 구조를 다지고 시작하려고 한다.

그 중 DataBinding부터 적용해보자.

App 수준의 Gradle 파일 수정

build.gradle (:app)

dependencies {
	// Koin - DI
    implementation "org.koin:koin-core:2.0.1"
    implementation "org.koin:koin-android:2.0.1"
    implementation "org.koin:koin-androidx-viewmodel:2.0.1"
}

MVVM 패턴을 사용하는 앱에서는, koin에서 제공하는 viewmodel 라이브러리까지 함께 implement하면 더욱 효율적으로 패턴을 관리할 수 있다.

ViewModelModule .kt 구현

ViewModelModule.kt

package com.example.recyclerview_sticky_headerfooter_example.di

import com.example.recyclerview_sticky_headerfooter_example.ui.main.MainTabViewModel
import com.example.recyclerview_sticky_headerfooter_example.ui.main.MainViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val ViewModelModule = module {
    viewModel { MainViewModel() } /* 1 */
} 

Module이란, 객체를 생성하여 의존성 주입을 진행 할 대상들에 대한 목록이라고 생각할 수 있다.

현재는 ViewModel에 별도의 생성자가 존재하지 않지만, Application, API, Prefrence 등을 생성자로 추가하고, Module에서 get()을 통해 호출하여 ViewModel에서도 쉽게 사용하도록 확장할 수 있다.

  1. ViewModel에 생성자가 있을 경우, Koin 모듈이 객체를 생성하여 주입하도록 사용할 수 있다.
    • viewModel { MainViewModel(get()) }

App.kt 구현

App.kt

class App: Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger()
            androidContext(this@App)
            modules(ViewModelModule) /* 1 */
        }
    }
}

Application 클래스에서 Koin을 위와 같이 start 할 수 있다.

혹은, 자신이 이미 사용하는 Application Class가 있다면, startKoin { ... }만 추가하면 된다.

  1. listOf() 함수를 사용하여 둘 이상의 Module를 사용 할 수 있다.
    • modules(listOf(networkModule, ViewModelModule))

만든 App.kt를 Manifest에 연동하기

AndroidManifest.xml

 <application
        android:name=".App"
        ...

기본적으로 AndroidManifest.xml 파일에는 application이라는 태그로 이루어진 부분에, name 패러미터만 추가로 입력해주면 된다.

이를 통해, 앱 시작과 동시에 startKoin() 함수를 실행하게 된다.

DataBinding 적용

Koin과 함께 DataBinding을 사용하는 것으로, MVVM 패턴에 날개를 달아줄 수 있다.

App 수준의 Gradle 파일 수정

build.gradle (:app)

plugins {
    id 'kotlin-kapt'
}
android {
	buildFeatures {
        dataBinding = true
    }
}

dataBinding을 사용하기 위해서는, kapt 플러그인을 사용하는 상태여야한다.

DataBinding을 적용 할 Layout 파일을 <layout> 태그로 감싸기

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">
    
    <!-- (1) ↓기존 코드↓ -->
    <FrameLayout
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainActivity" />
    <!-- (1) ↑기존 코드↑ -->

</layout>
  1. 이 과정에서, 기존 최상단 태그(예시에서는 FrameLayout)에 있던 <xmlns:…..> 태그들은 모두 제거해주어야 한다.

    한 레이아웃 당, 동일한 xmlns 태그는 유일해야하기 때문에, 제거하지 않으면 빌드 시 에러가 발생한다.

xml 파일 내에 ViewModel 데이터 정의하기

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">
    
    <!-- (1) ↓데이터 정의↓ -->
    <data>
        <variable
            name="vm"
            type="com.example.recyclerview_sticky_headerfooter_example.ui.main.MainViewModel"/>
    </data>
    <!-- (1) ↑데이터 정의↑ -->

    <!-- ↓기존 코드↓ -->
    <FrameLayout
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainActivity" />
    <!-- ↑기존 코드↑ -->

</layout>

위와 같이 데이터를 정의 하면, xml 파일 내에서 @{vm}이라는 코드를 작성하는 것으로, ViewModel 내의 데이터를 사용할 수 있다.

  1. xml 파일 내에서 데이터를 정의할 수 있다. 정의해둔 데이터는 Activity, Fragment에서 할당해주는 것으로 xml 파일 내에서도 자유롭게 사용할 수 있다.

BindingActivity, BindingFragment 구현하기

DataBinding된 xml 내의 태그들을 id로 간편하게 찾기 위해서는 구현되는 모든 Activity, Fragment마다 바인딩 된 Layout 파일의 ResourceID를 통해 Layout을찾는 과정을 거쳐야한다.

하지만 이후에 구현할 Activity, Fragment에서 아래 클래스들을 구현해두고, 상속받는 것으로 binding만 입력하는 것으로 바로 Layout파일에 접근할 수 있다.

BindingActivity.kt

abstract class BindingActivity<T: ViewDataBinding> : AppCompatActivity() {
    @LayoutRes
    abstract fun getLayoutResId(): Int

    protected lateinit var binding: T
        private set

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, getLayoutResId())
        
    }
} 

BindingFragment.kt

abstract class BindingFragment<T: ViewDataBinding> : Fragment() {
    @LayoutRes
    abstract fun getLayoutResId(): Int

    protected lateinit var binding: T
        private set

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return DataBindingUtil.inflate<T>(inflater, getLayoutResId(), container, false).apply{ binding = this }.root
    }
}

이후 작성하는 Class들은, 아래와 같이 위 클래스를 상속받는 것으로 쉽게 사용할 수 있다.

MainFragment.kt

class MainFragment : BindingFragment<FragmentMainBinding/* 1 */>() {

    private lateinit var mVM: MainViewModel

    @LayoutRes
    override fun getLayoutResId() = R.layout.fragment_main	/* 2 */
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.vm/* 3 */ = getViewModel()
        binding.lifecycleOwner = viewLifecycleOwner
    }
}
  1. FragmentMainBinding은, fragment_main.xml파일이 DataBinding를 통해 Bind되어있는 View를 의미한다.
  2. 해당 Fragment가 의미하는 Layout 파일의 이름을 그대로 입력해주면 된다.
  3. xml 파일에서 데이터 정의해둔 vm에 해당 Fragment의 ViewModel을 연동해주는 과정.

여기까지 성공적으로 진행했다면, MVVM 패턴으로 앱을 개발하기 위한 사전준비는 끝났다고 할 수 있다.

다음 게시글에는 ViewPager2로 MainFragment에 있는 ViewPager로 이루어진 3개의 Tab Fragment를 리팩토링 해보려고 한다.

Raw Code

RawCode는 여기에서 확인할 수 있다.

그 중, 이 커밋에 해당하는 내용이 이 글의 대부분의 내용이다.

댓글남기기