봄날은 갔다. 이제 그 정신으로 공부하자

MVVM + Koin 최소 샘플 앱 개발 - part 1 본문

Android jetpack

MVVM + Koin 최소 샘플 앱 개발 - part 1

길재의 그 정신으로 공부하자 2020. 12. 4. 09:43

해당 글은 koin, DataBinding, Rxjava, retrofit, Coroutine, PagingLibrary을 사용하여. MVVM 구조로 된 

github 사용자 검색 앱을 개발하는 과정을 기술합니다.

 

해당 글을 작성하는 이유는 위 기술이 적용된 최소 샘플 앱을 만들어 놓고 필요 시 참고하기 위함입니다.

 

실제 서비스 로직이 적용된 앱의 경우 앱에 적용된 서비스 로직으로 인해 해당 기술의 최소 적용 기준을 파악하기 어렵고

각각의 기술을 설명한 부분은 독립적이라 앱에 적용하기 위해서는 별도의 노력이 필요한 부분이 있습니다.

자주 사용하는 기술을 최소 샘플앱으로 만들어 신규앱 개발 시에 필요한 기술들을 편하게 참고 하기 위함입니다.

 

무엇을 만들 것인가?

github에서 사용자를 검색해 리스트로 보여주는 앱을 만들 예정 입니다.

화면은 Activity + Fragment 구성됩니다.

 

Activity에서 검색할 사용자 이름을 입력하면 Activity에 검색된 사용자 수가 보여지고 Fragment에 검색된 사용자 목록이 보여집니다.

 

시작 및 종속성 추가

Android Studio에서 Activity 1개인 앱을 생성합니다.

App 레벨의 build.gradle 파일에 종속성을 추가합니다.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
}

...

dataBinding {
    enabled = true
}

dependencies {

    ...

    // swiperefreshlayout
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'

    // rxjava
    implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
    implementation "io.reactivex.rxjava3:rxandroid:$rxjava_version2"

    // koin
    implementation "org.koin:koin-core:$koin_version"
    implementation "org.koin:koin-android:$koin_version"
    implementation "org.koin:koin-androidx-viewmodel:$koin_version"

    // retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"

	// fastjson
    implementation "org.ligboy.retrofit2:converter-fastjson-android:$fastjson_version"
    
    // coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

    // paging lib
    implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
    testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
    implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx

    // multidex
    implementation "androidx.multidex:multidex:$multidex_version"

    // rx EventBus
    implementation "org.greenrobot:eventbus:$eventbus_version"

    // permission utility
    implementation "gun0912.ted:tedpermission:$tedpermission_version"
}

projet의 build.gradle 파일에 각 라이브러리별 버전을 추가해줍니다.

이와 같이 버전을 분리해서 명시하는 이유는 프로젝트에 여러개의 앱이 존재할 경우 라이브러리의 버전 관리를 일관성있게 지원하기 위합입니다.

buildscript {
    ext {
        rxjava_version = '3.0.6'
        koin_version= '2.0.1'
        retrofit_version = '2.9.0'
        fastjson_version = '2.2.0'
        kotlin_version = '1.3.71'
        coroutines_version = '1.3.0'
        paging_version = '2.1.2'
        multidex_version = "2.0.1"
        eventbus_version = '3.0.0'
        tedpermission_version = '2.2.0'
    }
    ….
}

 

기본 구조 만들기 (MVVM)

 

View 폴더에 MainActivity.kt 파일을 이동 시킵니다.

같은 레벨에 공통으로 사용할 class들을 모아놓을 common 폴더를 추가해줍니다.

View 만들기

만들려는 앱이 Activity + Fragment 구조 이므로 view 폴더의 기본 제공 파일인 MainActivity.kt파일을 Activity + Fragment 구조로 변경합니다.

 

여러개의 Activity와 Fragment를 만들어야 하므로 아래와 같이 BaseActivity와 BaseFragment를 만들어 MainActivity 상속받도록 합니다.

abstract class BaseActivity<T : ViewDataBinding> : AppCompatActivity() {

    lateinit var viewDataBinding: T
    abstract val layoutResourceId: Int

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

}
abstract class BaseFragment<T : ViewDataBinding> : Fragment(){

    lateinit var viewDataBinding: T
    abstract val layoutResourceId: Int

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewDataBinding = DataBindingUtil.inflate(inflater, layoutResourceId, container, false)
        return viewDataBinding.root
    }
}

MainActivity BaseActivity 상속받도록 아래와 같이 수정해줍니다.

// MainActivity.kt
class MainActivity : BaseActivity<ActivityMainBinding>() {
    
    override val layoutResourceId: Int get() = R.layout.activity_main
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewDataBinding.vmMain = getViewModel()
        viewDataBinding.lifecycleOwner = this
    }
}

아직 작업이 안되서 에러가 발생합니다. 

일단은 무시하고 사용자 목록을 보여줄 UserListFragment 파일을 아래와 같이 만들어줍니다.

// UserListFragment.kt
class UserListFragment: BaseFragment<FragmentUserListBinding>(){

    override val layoutResourceId: Int get() = R.layout.fragment_user_list

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewDataBinding.vmUserList = getViewModel()
    }
}

 

ViewModel 만들기

공통으로 사용할 BaseViewModel 부터 아래와같이 만들어줍니다.

// BaseViewModel.kt
abstract class BaseViewModel : ViewModel() {
    private val compositeDisposable = CompositeDisposable()

    fun addDisposable(disposable : Disposable){
        compositeDisposable.add(disposable)
    }

    override fun onCleared() {
        compositeDisposable.clear()
        super.onCleared()
    }
}

이제 MainActivity 연결될 MainViewModel class 아래와 같이 만들어줍니다.

class MainViewModel: BaseViewModel() {

}

Main View에 필요한 부분은 사용자 검색 EditBox와 검색 Button 그리고 검색된 사용자 수를 보여줄 TextView 이렇게 3개가 필요하므로 layout xml과 Binding 시켜줄 LiveData 선언 정의하고 검색 버튼 클릭 연동할 함수를 아래와 같이 추가합니다.

val keyword = NotNullMutableLiveData<String>("")
val count = NotNullMutableLiveData<Int>(0)

fun onClickSearch(view: View){

}

NotNullMutableLiveData 데이터는 null check의 편의를 위해 common 폴더에 생성한 base LiveData class 코드는 아래와 같습니다.

class NotNullMutableLiveData<T : Any>(_defaultValue: T) : MutableLiveData<T>() {

    init {
        value = _defaultValue
    }

    override fun getValue()  = super.getValue()!!
}

이제 사용자 리스트를 보여줄 UserListViewModel class 만들어 줍니다.

class UserListViewModel: BaseViewModel() {
    
}

 

Layout xml 편집하기 (ViewModel layout xml 연동하기)

위 항목에서 만들어진 viewmodel을 layout xml에 연동하기 위해서는 layout xml을 layout tag root 선언한뒤 아래와 같이 <data> tag 안에 mainViewModel 선언합니다.

<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="vmMain"
            type="com.kiljae.mygitsample.viewmodel.MainViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

검색어를 입력하고 결과를 받은 control을 배치하고 MainViewModel의 LiveData와 연결합니다.

아래 코드에서 눈여겨 보아야 할 부분은 EditText에서 입력된 값을 가져오기 위해 Twoway binding을 사용한 부분과 검색 카운드를 보여주는 TextView에서 직접 Int Value를 넣으면 에러가 발생하므로 String으로 값을 변경해서 넣어주는 부분입니다.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <EditText
        android:id="@+id/edtKeyword"
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_marginTop="20dp"
        android:layout_marginStart="20dp"
        android:text="@={vmMain.keyword}"
        android:textSize="12dp"
        android:hint="@string/search_keyword_hint"
        android:inputType="phone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnSearch"
        app:layout_constraintTop_toTopOf="parent"/>
    <Button
        android:id="@+id/btnSearch"
        android:layout_width="120dp"
        android:layout_height="60dp"
        android:layout_marginTop="20dp"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="20dp"
        android:text="@string/search"
        android:textSize="16dp"
        android:background="@color/colorTomato"
        android:textColor="@color/colorWhite"
        android:onClick="@{vmMain::onClickSearch}"
        app:layout_constraintStart_toEndOf="@+id/edtKeyword"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
    <TextView
        android:id="@+id/txvCount"
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_margin="10dp"
        android:textSize="16dp"
        android:text="@{String.valueOf(vmMain.count)}"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnSearch"/>
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vpUserList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txvCount"
        app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

이제 UserList layout xml을 만들어줍니다.

이것도 위와 동일하게 layout tag root 해서 아래와 같이 작성해줍니다.

<?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="vmUserList"
            type="com.kiljae.mygitsample.viewmodel.UserListViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorOrange">
        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/swpRefresh"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rcvUserList"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="20dp"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

종속성 주입하기(koin으로 View ViewModel 연동하기)

common 폴더에 종속성 주입을 위한 MyDi.kt 파일을 만들어줍니다.

다음에 해당 파일에 viewModel module 아래와 같이 만들어줍니다.

var viewModelpart = module{
    viewModel{
        MainViewModel()
    }

    viewModel{
        UserListViewModel()
    }
}

var myDiModule = listOf(
    viewModelpart
)

여기까지 하면 모든 에러가 사라지고 앱이 빌드 됩니다.

 

Activity Fragment 연결하기

MainActiviy UserListFragment 연결하기 전에 우선 ViewPager 아래와 같이 만들어줍니다.

class PagerAdapterUserList(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager){
    override fun getItem(position: Int): Fragment {
        return UserListFragment()
    }

    override fun getCount(): Int {
        return 1
    }

}

이렇게 만들어진 PagerAdapter 사용해 Activity와 Fragment를 아래와 같이 연결해줍니다.

vpUserList.adapter = PagerAdapterUserList(supportFragmentManager)

빌드해서 실행해보면 아래와 같은 화면이 보입니다.

Activity 영역은 배경이 흰색이고 Fragment 영역은 오렌지 색인 것을 확인할 수 있습니다.

다음 글에서는 Github 사용자 검색 API를 호출하는 부분을 만들어보도록 하겠습니다.

 

여기까지 작업된 소스는 아래 경로에 업로드 되어 있습니다.

https://github.com/lee-kil-jae/MyGitSample/tree/dev_first

 

lee-kil-jae/MyGitSample

최소 샘플 앱 개발 (Kotlin, MVVM, koin, DataBinding, LiveData, PagingLibrary, RxJava, retrofit, Coroutine) - lee-kil-jae/MyGitSample

github.com

 

Comments