개발하는 두더지

[Android/Kotlin] aac + Viewmodel + LiveData + Room을 사용하여 Todo 리스트 만들기 본문

Kotlin

[Android/Kotlin] aac + Viewmodel + LiveData + Room을 사용하여 Todo 리스트 만들기

덜지 2018. 12. 13. 15:51

안드로이드 아키텍쳐 컴포넌트는 보일러 플레이트 코드를 적게해주며 견고하고, 테스트 가능하고, 유지보수가 쉬운 앱을 만들 수 있도록 도와주는 프레임 워크 입니다.


아래 아키텍쳐 컴포넌트가 어떻게 함께 동작하는지 나타내는 그림입니다. 요즘 안드로이드 커뮤니티에서 추천되는 방식이죠. ViewModelLiveData, Room과 같은 컴포넌트들을 집중해서 다뤄보도록 하겠습니다. 




  • Entity는 db 테이블에 해당되는 클래스입니다. 

  • DAOdata access object로 실 데이터에 접근하도록 도와주는 helper 클래스입니다

  • RoomDatabaseSQLiteOpenHelper을 처리했던 작업들을 다룹니다. SQL 쿼리를 컴파일 타임에 검사하는 기능을 제공하며 반드시 RoomDatabase를 상속받은 추상클래스를 만들어야 합니다. 그리고 그 추상 클래스로 만든 인스턴스는 주로 싱글톤으로 만듭니다. 같은 시간에 여러개의 인스턴스에서 데이터베이스에 접근하는 것을 막기 위함입니다.

  • Repository는 싱글톤 패턴으로 뷰모델마다 동일한 Repository 인스턴스로 접근하여 데이터를 로드하도록 도와줍니다. 여러개의 데이터 소스를 관리하는 클래스입니다.

  • ViewModel은 UI에 데이터를 제공하는 역할을 합니다. Repository와 UI 사이에서 커뮤니케이션 센터 역할을 합니다. 액티비티의 라이프 사이클 상태에 맞게 데이터를 전달해주기 때문입니다. 그리고 화면 회전과 같은 상태 변화에도 인스턴스를 그대로 유지하는 특징이 있어서 데이터를 다시 로드해야하는 불편함이 없습니다.

  • LiveData는 관측가능한(Observed한) 데이터 홀더입니다. Rx의 Observable과 같은 개념입니다. 항상 마지막 상태의 데이터를 보관하고 있으며, 상태가 변하면 구독자들에게 변화된 데이터를 전달해주는 역할을 합니다. 그리고 관측하는 동안 라이프 사이클 상태와 관련된 변화들을 알고있기 때문에 상태에 따라 스스로 구독을 시작하고 중지할 수 있습니다.


RoomLifecycle 컴포넌트를 사용하기 위해 아래의 내용을 추가합니다

프로젝트 단위의 build.gradle

ext {
roomVersion = '1.1.1'
archLifecycleVersion = '1.1.1'
}

앱 단위의 build.gradle

// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVersion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"


Room에 저장하고 로드할 데이터 모델을 만들어줍니다. Room 데이터베이스에서 사용하기 위해서는 @Entity 라는 어노테이션을 사용해야 합니다. Realm과는 다르게 id 값을 자동으로 증가시켜주는 기능이 있습니다.

@Entity(tableName = "todo")
data class Todo(

@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var title: String = "",
var date: Long = 0
)


데이터 모델을 만들었으니 SQL 쿼리를 지정하고 메서드 호출과 연결시켜주는 DAO 인터페이스를 만들어줍니다. DAO는 반드시 인터페이스 또는 추상클래스여야 하고 모든 쿼리는 별도의 스레드에서 실행되어야 합니다. @DAO 어노테이션을 이용해 DAO라는 것을 명시적으로 알려줘야 하고 기본적으로 CRUD에 필요한 쿼리는 알고있어야 합니다. 

insert 메서드는 Todo객체를 인자로받아서 데이터베이스에 저장하는 기능입니다. 중복된 id가 들어올 경우 충돌이 발생하는데 이때 어떻게 동작할지에 대한 전략도 설정할 수 있습니다. 수정의 경우 데이터를 교체해야 하므로 Replace 전략으로 설정했습니다.

@Dao
interface TodoDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(todo: Todo)

@Query("DELETE FROM todo WHERE id = :id")
fun deleteById(id: Long)

@Query("SELECT * FROM todo ORDER BY date DESC")
fun getAllTodos(): LiveData<List<Todo>>
}


데이터를 조회하고 수정, 삭제하여 상태가 변화되면 UI에서 변화된 상태를 받기 위해 LiveData를 사용하면 됩니다. LiveData는 데이터를 저장할 수 없기때문에 MutableLiveData를 주로 사용합니다. ViewModel에서 MutableLiveData를 이용해 데이터를 set 해주고 외부에 노출시킬 변수는 수정 불가능한 LiveData 객체를 이용해서 옵저버들에게 데이터를 전파합니다.


다음 단계로는 Room 데이터 베이스 클래스를 만들어야 합니다. UI 성능을 위해 Room은 기본적으로 UI 스레드에서 데이터베이스 쿼리를 실행하는 것을 허용하지 않습니다. 그래서 LiveData와 함께 사용하면 백그라운드 스레드에서 쿼리를 비동기적으로 자동 실행시켜줍니다. 

위에서도 설명했듯이 RoomDatabase를 상속받은 추상클래스를 만들어야하고 같은 시간에 여러 인스턴스가 하나의 데이터베이스에 접근하는 것을 막기위해 싱글톤 패턴을 구현해야 합니다.

@Database(entities = [Todo::class], version = 1)
abstract class TodoRoomDatabse: RoomDatabase() {
abstract fun todoDao(): TodoDao

companion object {
private var INSTANCE: TodoRoomDatabse? = null

fun getInstance(context: Context): TodoRoomDatabse? {
return INSTANCE ?: synchronized(TodoRoomDatabse::class) {
INSTANCE ?: Room.databaseBuilder(context.applicationContext,
TodoRoomDatabse::class.java, "todo.db").build().also { INSTANCE = it }
}
}

fun destoryInstance() {
INSTANCE = null
}
}
}


여러 데이터 소스에 대한 액세스를 추상화하는 Repository 클래스를 만들어야 합니다. Repository는 아키텍쳐 컴포넌트 라이브러리는 아니지만 코드 분리 및 아키텍쳐에 적합합니다. 일반적으로 Repository에서 네트워크에서 데이터를 가져올지 또는 로컬 데이터베이스에서 캐시된 결과를 사용할지 결정하는 논리를 구현해야 합니다.



모든 Todo를 가져오는 메서드, id에 해당하는 Todo를 가져오는 메서드는 LiveData로 구현되어있습니다. 옵저버 패턴으로 구현되어있어

액티비티 또는 프레그먼트에서 구독하여 아이템이 추가되거나 삭제될 때, 해당 아이템 상세보기를 할 경우 이용됩니다.

아이템을 추가, 삭제하는 메서드는 fromCallable을 사용합니다. AsyncTask 비동기 스레드 대신 RxJava2로 구현했고, 구독이 발생할 때 Callablecall 함수가 호출되는 API입니다. 데이터베이스에 데이터를 추가/삭제하는 작업은 UI 스레드가 아닌 스레드에서 작업을 해야합니다. 

class TodoRepository(application: Application) {

private val todoDao: TodoDao by lazy {
val db = TodoRoomDatabase.getInstance(application)!!
db.todoDao()
}
private val todos: LiveData<List<Todo>> by lazy {
todoDao.getAllTodos()
}

fun getAllTodos(): LiveData<List<Todo>> {
return todos
}

fun getTodoById(id: Long): LiveData<Todo> {
return todoDao.getTodoById(id)
}

fun insert(todo: Todo): Observable<Unit> {
return Observable.fromCallable { todoDao.insert(todo) }
}

fun delete(id: Long): Observable<Unit> {
return Observable.fromCallable { todoDao.deleteById(id) }
}
}


ViewModel은 화면 회전 같은 상태 변화에도 UI 데이터를 보존하는 역할을 합니다. UI와 Repository는 ViewModel에 의해서 완전히 분리되어 있습니다. ViewModel에서 직접적으로 데이터베이스 콜하는 부분이 없습니다. Todo를 추가/삭제의 경우 RxJava2의 Observable을 이용했습니다. Schedulers.io()를 통해 데이터 가공은 비동기 스레드에서 하고, AndroidSchedulers.mainThread()를 통해 데이터 처리는 UI 스레드에서 합니다.  Observable의 사용이 끝나면 메모리 릭을 피하기 위해 ViewModel이 종료될 때 dispose를 해서 메모리를 종료해야 합니다.

class TodoViewModel(application: Application): AndroidViewModel(application) {

private val disposable: CompositeDisposable = CompositeDisposable()

private val repository: TodoRepository by lazy {
TodoRepository(application)
}

private val todos: LiveData<List<Todo>> by lazy {
repository.getAllTodos()
}

fun getAllTodos() = todos

fun getTodoById(id: Long): LiveData<Todo> {
return repository.getTodoById(id)
}

fun insert(todo: Todo, next: () -> Unit) {
disposable.add( repository.insert(todo).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { next() }
)
}

fun delete(id: Long, next: () -> Unit) {
disposable.add( repository.delete(id).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { next() }
)
}

override fun onCleared() {
super.onCleared()
disposable.dispose()
}
}


메인 액티비티에서 저장된 데이터를 가져오는 방법을 살펴보겠습니다. 데이터가 변화될때마다 onChanged() 콜백이 호출되고 전달받은 데이터를 RecyclerView Adapter의 리스트를 갱신합니다.

todoViewModel = ViewModelProviders.of(this).get(TodoViewModel::class.java)
todoViewModel.getAllTodos().observe(this, Observer {
list.clear()
list.addAll(it!!)
adapter.notifyDataSetChanged()
})

아이템을 추가 삭제하는 부분은 아래와 같습니다.

private fun insertTodo() {
val todo = Todo(0,
todoEditText.text.toString(),
calendar.timeInMillis
)
todoViewModel.insert(todo) { finish() }
}

private fun updateTodo(id: Long) {
val todo = Todo(id,
todoEditText.text.toString(),
calendar.timeInMillis
)
todoViewModel.insert(todo) { finish() }
}

private fun deleteTodo(id: Long) {
todoViewModel.delete(id) { finish() }
}


나머지 내용은 전체 소스코드에서 확인 가능합니다.



Comments