본문 바로가기
Development/Android

[Android] Kotlin + MVVM + AAC 로 Todo 앱 만들기 - 4

by du.it.ddu 2020. 3. 1.
반응형

이전 포스팅에서 예고했던 대로 데이터베이스에 Todo 아이템을 삽입할 때 별도의 쓰레드를 생성하여 하던 것을 RxKotlin 으로 변경하고 RecyclerView에 DiffUtil 을 적용 할 것이다.

- Denpendency 추가

RxKotlin을 사용하기 위해선 앱 모듈 Gradle에 종속성을 추가 해 주어야 한다.

https://github.com/ReactiveX/RxKotlin

 

ReactiveX/RxKotlin

RxJava bindings for Kotlin. Contribute to ReactiveX/RxKotlin development by creating an account on GitHub.

github.com

위 사이트를 참고하여 종속성을 추가하자.

Rx에 대해 이해가 필요하므로 http://reactivex.io/를 참고하거나 유튜브 등의 강의를 보는 것을 추천한다. 처음엔 생소하고 어려울 수 있으나 막상 하다보면 생각보다 쉽다.

종속성을 추가하면 아래와 같다.

...

android {
	...
}

dependencies {
	...
    implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
}

 위에서 RxAndroid도 함께 추가하였는데, 이는 안드로이드는 UI를 메인 쓰레드에서만 변경 가능한데, 이를 위해 사용한다. 즉 안드로이드 플랫폼에서의 Rx 사용을 위한 라이브러리다.

이 글을 작성하는 시간 기준으로 버전은 업데이트 되어 변경될 수 있다.

- TodoRepository 코드 수정

TodoModel을 전달받아 Observable의 just 연산자를 사용하여 비동기 작업을 수행 할 것이다.

just에 대해서는 http://reactivex.io/documentation/ko/operators/just.html를 참고하자.

아래와 같이 코드를 작성한다.

    fun insertTodo(todoModel: TodoModel) {
        Observable.just(todoModel)
            .subscribeOn(Schedulers.io())
            .subscribe({
                mTodoDAO.insertTodo(todoModel)
            }, {
                // Handle error.
            })
    }

TodoModel을 just의 인자로 넘겨주고 이를 io 쓰레드에서 처리하겠다는 의미이다. 에러가 발생하지 않는다면 subscribe의 onNext가 호출되고 이를 DAO를 통해 삽입한다. 만약 에러가 발생하면 onError가 발생하고 이를 처리하는 코드의 추가가 필요 할 것이다.

이는 단순한 예를 보여주는 것이므로 정답은 아니고 더 좋은 방법이 있다면 좋은 방법을 사용하자.

또한 실제로 위처럼 Observable을 사용하면 Disposable이란 것이 반환되는데, 이를 처리하는 CompositeDisposable 이란 것도 사용해야 한다. 이 프로젝트는 Rx에 대해 설명하는 것이 아니므로 넘어가겠다.

위 처럼 수정하고 앱을 실행하면 이전 포스팅과 결과가 동일 할 것이다.

- DiffUtil이란?

우리가 작성한 어댑터는 Todo 아이템들이 갱신되면 리스트 전체를 전달받고 notifyDataSetChanged 메서드를 호출한다. 리스트가 작으면 별 문제가 없겠지만, 만약 아이템들이 굉장히 많아진다면 리스트 전체에 대해 데이터 변경 메서드를 호출하므로 성능이 떨어질 것이다.

물론 RecyclerView는 특정 인덱스의 아이템이 업데이트 되었음을 알리는 메서드도 있지만, 인덱스를 계산하는 것도 쉽지 않을 수 있고 오류를 만들어내기 쉽다.

그래서 전체 리스트 중에 업데이트 된 항목만을 골라 업데이트할 수 있는 DiffUtil을 사용하여 성능을 향상시키려 한다.

DiffUtil에 대해선 https://developer.android.com/reference/android/support/v7/util/DiffUtil를 참고한다.

자세한 것은 포스팅하지 않지만, 업데이트 전 리스트와 업데이트 후 리스트를 비교하는 알고리즘을 사용하며 공간복잡도는 높지 않지만 시간복잡도가 높다. 이를 코드에 적용해보자.

- DiffUtil.Callback 클래스 작성

우선 DiffUtil을 사용하기 위해 클래스를 하나 작성해야 한다. adapter 폴더 안에 TodoListDiffCallback 이란 클래스를 만들고 아래와 같이 작성한다.

class TodoListDiffCallback(val oldTodoList: List<TodoModel>, val newTodoList: List<TodoModel>): DiffUtil.Callback() {
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun getOldListSize(): Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun getNewListSize(): Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        return super.getChangePayload(oldItemPosition, newItemPosition)
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

DiffUtil.Callback 이란 클래스를 상속받고 위 다섯가지 함수에 대해 오버라이드 할 수 있다.

생성자로부터 변화 이전의 리스트(oldTodoList), 변화 후의 리스트(newTodoList)를 전달받고 이 둘을 비교 할 것이다.

여기서 getChangePayload는 꼭 구현해야 하는 것은 아니므로 구현하지 않을 것이다.

1. getOldListSize와 getNewListSize는 굉장히 명확하다. 리스트의 사이즈를 반환하므로, 생성자에서 전달받은 리스트를 사용해 각각에 맞는 사이즈를 반환한다. 아래처럼 작성하자.

    override fun getOldListSize(): Int = oldTodoList.size

    override fun getNewListSize(): Int = newTodoList.size

Kotlin은 함수를 위 처럼 작성할 수 있어 라인수를 줄일 수 있으니 참고하자. 다른 함수들처럼 괄호를 열고 return 키워드를 사용하여도 동일하다.

2. areItemsTheSame 클래스는 어떤 포지션의 두 아이템이 동일한지 묻는 것이다. 아이템의 기본키같은 고유한 값 같은 것으로 비교하여 반환한다. 우리의 TodoModel은 기본키가 있으므로 이 둘이 같으면 아이템이 동일한 것이다. 아래처럼 작성한다. 

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldTodoList[oldItemPosition].id == newTodoList[newItemPosition].id
    }

간단하다. 두 아이템의 id가 같으면 true, 다르면 false가 반환 될 것이다.

3. areContentsTheSame는 areItemsTheSame에서 두 아이템이 같으면 두 아이템의 내용물까지 같은지 묻는다. TodoModel에 equals의 메서드를 오버라이드 하거나 본인이 비교하고 싶은 값을 비교하면 된다. 우리는 data class로 TodoModel을 작성했으므로 별도로 오버라이드 하지 않아도 equals 메서드가 생성된다. data class가 자동으로 생성 해 주는 equals는 생성자의 필드들에 대하여 결과를 반환한다. 즉, 모든 필드가 같으면 true를 반환할 것이다. 아래와 같이 코드를 작성한다.

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return newTodoList[newItemPosition].equals(oldTodoList[oldItemPosition])
    }

이렇게 구현이 끝났다. 이제 어댑터에 적용 해  보자.

- DiffUtil.Callback 클래스를 어댑터에 적용

어댑터의 데이터를 셋 해주는 setTodoItems 코드를 아래처럼 수정하자.

    fun setTodoItems(todoItems: List<TodoModel>) {
        val diffCallback = TodoListDiffCallback(this.todoItems, todoItems)
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        this.todoItems = todoItems
        diffResult.dispatchUpdatesTo(this)
    }

굉장히 간단하다. Callback클래스에 이전 리스트와 새로 받은 리스트를 전달하여 생성하고 DiffUtil 클래스의 calculateDiff메서드를 통해 결과를 얻는다. 

리스트를 변경한 후 RecyclerView를 업데이트 한다.

- DiffUtil에 RxKotlin 적용

위에서 설명했듯, DiffUtil은 시간복잡도가 높다. 그렇기 때문에 별도의 쓰레드에서 작업하는 것이 더 좋은 방법이 될 수 있다. Rx를 활용하여 코드를 수정 해 보자.

    fun setTodoItems(todoItems: List<TodoModel>) {
        Observable.just(todoItems)
            .subscribeOn(AndroidSchedulers.mainThread())
            .observeOn(Schedulers.io())
            .map { DiffUtil.calculateDiff(TodoListDiffCallback(this.todoItems, todoItems)) }
            .subscribe({
                this.todoItems = todoItems
                it.dispatchUpdatesTo(this)
            }, {

            })
    }

위 코드를 보면 observeOn이 두 번 사용 되었다.

위 코드를 보면 subscribeOn에 메인 쓰레드(UI 쓰레드)를 지정하고 observeOn에 io 쓰레드를 지정하였다. observeOn 아래의 행위들은 io쓰레드에서 수행되고 subscribe는 메인쓰레드에서 수행하겠다는 의미가 된다. onNext가 호출되면 새로운 리스트로 변경하고 DiffResult인 it으로 부터 리스트를 변화시킨다.

앱을 실행하여 테스트 해 보면 이전처럼 잘 동작하는 것을 볼 수 있다.

- 다음으로

현재까지 만든 RecyclerView는 아이템을 추가하는 것은 가능하지만 아이템들을 클릭하는 등의 이벤트는 처리되어 있지 않다. ListView를 사용할 때는 어댑터에서 이벤트 리스너를 생성하는 것이 가능했지만 RecyclerView는 그렇지 않다. 다음 포스팅때는 이벤트 처리를 통해 아이템을 수정하고 삭제 해 보도록 하겠다.

반응형