본문 바로가기
Development/Android

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

by du.it.ddu 2020. 2. 29.
반응형

이전 포스팅에서 만든 코드를 리팩토링하여 MVVM 패턴을 구현하고 AAC를 적용 해 보자.

- Dependency

이 앱에서는 ViewModel, LiveData, Room 를 사용할 것이다. 여기서 LiveDat와 Room은 앱 모듈의 dependencies에 추가 해 주어야 한다.

https://developer.android.com/jetpack/androidx/releases/room

https://developer.android.com/jetpack/androidx/releases/lifecycle 를 참고하여 아래와 같이 수정한다.

 

Room  |  Android 개발자  |  Android Developers

Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 강력한 데이터베이스 액세스를 지원하는 추상화 계층을 SQLite에 제공합니다. 최근 업데이트 현재 안정화 버전 다음 출시 후보 베타 버전 알파 버전 2019년 12월 18일 2.2.3 - - - 종속성 선언 Room에 종속성을 추가하려면 프로젝트에 Google Maven 저장소를 추가해야 합니다. 자세한 내용은 Google Maven 저장소를 읽어보세요. Room의 종속성에는 Room 이전 테스

developer.android.com

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'

android {
    ...
}

dependencies {
    ...

    implementation "androidx.room:room-runtime:2.2.4"
    kapt "android.arch.persistence.room:compiler:2.2.4"

    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
    kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}

apply plugin 'kotlin-kapt' 와 라이브러리들이 추가되었다.

- Room 사용하기

Room을 사용하기 위하여 TodoModel을 아래와 같이 수정한다.

@Entity(tableName = "Todo")
data class TodoModel (
    @PrimaryKey(autoGenerate = true)
    var id: Long?,
    
    @ColumnInfo(name = "title")
    var title: String,
    
    @ColumnInfo(name = "description")
    var description: String,
    
    @ColumnInfo(name = "createdDate")
    var createdDate: Long
) {
    constructor(): this(null, "", "", -1)
}

SQLite, MySQL 같은 RDBMS를 사용해봤다면 이해하기 어렵지 않다. @Entity는 이 객체를 하나의 테이블로 여긴다고 생각하면 된다. 그리고 기본키, 컬럼 등 RDBMS에서 흔히 하던 것들을 어노테이션을 통해 하고 있다.

이제 데이터베이스 처리를 위한 DAO 클래스를 만들 것이다.

database 폴더를 생성하고 그 안에 TodoDAO를 만드는데, TodoDAO는 인터페이스로 만든다.

@Dao
interface TodoDAO  {

    @Query("SELECT * from Todo ORDER BY createdDate ASC")
    fun getTodoList(): LiveData<List<TodoModel>>

    @Insert
    fun insertTodo(todoModel: TodoModel)
}

위와 같이 작성하면 모든 Todo를 읽어오는 쿼리와 삽입하는 함수를 생성 한 것이다.

여기서 LiveData란 것이 사용되었다. LiveData는 액티비티의 생명주기를 인식하고 활동한다. 이 LiveData는 제네릭 클래스이며, 위에서는 List<TodoModel>을 값으로 갖고 있다. 자세한 내용은 구글링이 더 나을 것이다. 중요한 점은 액티비티의 생명주기를 인식하며 데이터의 변화를 Observe할 수 있다는 것이다.

이제 데이터베이스 클래스를 작성한다.

아래 코드는 https://github.com/android/architecture-components-samples/tree/master/BasicRxJavaSampleKotlin 저장소의 내부 코드를 참고하여 작성하였다. 

@Database(entities = [TodoModel::class], version = 1)
abstract class TodoDatabase: RoomDatabase() {
    abstract fun todoDao(): TodoDAO
    
    companion object {
        @Volatile private var INSTANCE: TodoDatabase? = null
        
        fun getInstance(context: Context): TodoDatabase = INSTANCE ?:
                synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }
        
        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext,
                TodoDatabase::class.java, "Todo.db").build()
    }
}

마찬가지로 어노테이션을 통해 데이터베이스를 싱글톤 패턴으로 생성하고 있다. 이제 이 클래스를 활용하여 보자.

- Repository 패턴 적용하기

Repository 패턴은 데이터베이스 혹은 네트워크를 통하여 데이터를 얻는 기능을 분리한다. ViewModel에서는 이 Repository를 통해 데이터를 얻는다. repository 폴더를 생성하고 TodoRepository 클래스를 작성하자.

class TodoRepository(application: Application) {
    private var mTodoDatabase: TodoDatabase
    private var mTodoDAO: TodoDAO
    private var mTodoItems: LiveData<List<TodoModel>>

    init {
        mTodoDatabase = TodoDatabase.getInstance(application)
        mTodoDAO = mTodoDatabase.todoDao()
        mTodoItems = mTodoDAO.getTodoList()
    }

    fun getTodoList(): LiveData<List<TodoModel>> {
        return mTodoItems
    }

    fun insertTodo(todoModel: TodoModel) {
        Thread(Runnable {
            mTodoDAO.insertTodo(todoModel)
        }).start()
    }
}

데이터베이스 객체와 DAO 객체, Todo 아이템 리스트를 객체가 init될 때 생성해주고, 이를 활용하여 Todo 아이템들을 읽어오고 삽입하는 함수를 갖고 있다. 한 가지, 생성자에서 applicaiton을 받는 것은 ViewModel에서 설명하겠다. 이제 이를 활용하는 ViewModel 클래스를 작성한다.

insert 함수에서 Thread를 사용하는 이유는, 연산 시간이 오래 걸리는 작업은 메인 쓰레드가 아닌 별도의 쓰레드에서 하도록 되어있다. 그렇지 않으면 런타임에 크래시가 발생한다. 추후 이 작업을 RxKotlin을 활용하여 수정하도록 할 것이다.

- ViewModel 클래스 작성

viewmodel 폴더에 TodoViewModel 클래스를 만든다. 이 클래스는 View에 해당하는 MainActivity의 요청을 받아 TodoModel을 생성, 데이터베이스에 저장하고 Todo 아이템 리스트들을 가져온다.

View는 ViewModel의 Todo 아이템 리스트를 Observe 하고 리스트를 갱신 할 것이다. ViewModel 클래스를 아래와 같이 작성한다.

class TodoViewModel(application: Application): AndroidViewModel(application) {
    private val mTodoRepository: TodoRepository
    private var mTodoItems: LiveData<List<TodoModel>>
    
    init {
        mTodoRepository = TodoRepository(application)
        mTodoItems = mTodoRepository.getTodoList()
    }
    
    fun insertTodo(todoModel: TodoModel) {
        mTodoRepository.insertTodo(todoModel)
    }
    
    fun getTodoList(): LiveData<List<TodoModel>> {
        return mTodoItems
    }
}

repository를 활용하여 코드가 굉장히 간단하다. 이렇게 실제로 데이터를 가져오는 구현부를 repository에 넘겨 ViewModel의 구현을 간단하게 만든다.

생성자에서 application을 받는 이유는 AndroidViewModel 클래스를 상속 받아서 그렇다. AndroidViewModel외에 ViewModel이란 것도 있는데, ViewModel에서 Context가 필요한 경우 AndroidViewModel 클래스를 상속받아 Application 객체를 넘길 것을 권장하고 있다. 그 이유는 ViewModel에서 Context를 갖고 있으면 메모리 누수의 원인이 된다고 한다. 자세한 것은 구글링하자.

이제 MainActivity에서 ViewModel을 생성하고 이를 활용하도록 코드를 수정한다.

- MainActivity 수정

    private lateinit var mTodoViewModel: TodoViewModel
    private lateinit var mTodoListAdater: TodoListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        initViewModel()
    }
    ...

    private fun initViewModel() {
        mTodoViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(TodoViewModel::class.java)
    }
}

ViewModel을 초기화 하는 방법은 일반 객체처럼 생성자로부터 호출하는 것이 아닌 Provider에 의해 초기화 되어야 한다. 자세한 내용은 역시나 구글링..! 또한 ViewModelFactory 등 알아야 할 것들이 많이 있는데, 여기선 생략하겠다.

아무튼 ViewModel을 생성했으니, 이제 Todo를 추가하는 코드와 Observe하는 코드, 이를 위하여 어댑터 클래스의 코드를 수정할 것이다. 우선 Todo를 추가하는 다이얼로그의 코드를 아래와 같이 수정한다.

    private fun openAddTodoDialog() {
        val dialogView = layoutInflater.inflate(R.layout.dialog_add_todo, null)
        val dialog = AlertDialog.Builder(this)
            .setTitle("추가하기")
            .setView(dialogView)
            .setPositiveButton("확인", { dialogInterface, i ->
                val title = dialogView.et_todo_title.text.toString()
                val description = dialogView.et_todo_description.text.toString()
                val createdDate = Date().time

                val todoModel = TodoModel(null, title, description, createdDate)
                mTodoViewModel.insertTodo(todoModel)
            })
            .setNegativeButton("취소", null)
            .create()
        dialog.show()
    }

변화된 부분은 거의 없다. todoModel에 id가 추가되었기 때문에 id값을  전달 해 주어야 하며, null로 전달하면 Room에 의해 삽입되면서 자동으로 할당된다.

이제 ViewModel의 Todo 리스트를 Observe하도록 initViewModel 클래스에 작성한다.

    private fun initViewModel() {
        mTodoViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(TodoViewModel::class.java)
        mTodoViewModel.getTodoList().observe(this, Observer {
            mTodoListAdater.setTodoItems(it)
        })
    }

간단하다. observe를 통해 Todo 리스트에 변화가 생기면 변화된 데이터가 넘어온다. 이 때 데이터는 별도의 이름을 지정하지 않아 it이다. 변화된 데이터가 리스트로 넘어오기 때문에 어댑터에서 리스트를 받을 수 있도록 메서드를 구현해야 한다. 위 setTodoItems 메서드를 구현하자.

class TodoListAdapter(): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var todoItems: List<TodoModel> = listOf()
    ...
    
    fun setTodoItems(todoItems: List<TodoModel>) {
        this.todoItems = todoItems
        notifyDataSetChanged()
    }
    ...
}

더 이상 생성자에서 리스트를 받지 않고, Observe할 데이터가 List<TodoModel> 이므로 데이터 형을 바꾼다. 또한 리스트가 세팅되면 notifyDataSetChanged()를 호출하여 갱신하도록 한다. MainActivity의 initRecyclerView에서 어댑터를 생성하는 부분에서 리스트를 넘겨주던 것을 없애야 한다.

- 앱 실행

 

이전에 했던 작업과 동일하게 동작하며, 데이터베이스를 활용하기 때문에 앱을 종료했다 켜도 아이템들이 그대로 살아있는 것을 확인할 수 있다.

간단하지만 AAC를 활용하여 MVVM 패턴(+ Repository 패턴)을 적용하고 데이터베이스까지 활용 해 보았다.

- 다음으로

Repository에서 별도의 쓰레드를 통해 비동기로 작업하던 것을 RxKotlin을 사용하는 것으로 변경하고, RecyclerView의 성능향상을 위한 DiffUtil 이란 것을 적용 해 보도록 할 것이다.

반응형