버그 잡이

(AAC 응용)ViewModel+LiveData+Room으로 ToDoList 앱 만들기 본문

모던 안드로이드/Udacity Android with kotlin

(AAC 응용)ViewModel+LiveData+Room으로 ToDoList 앱 만들기

버그잡이 2020. 4. 14. 20:38

Udacity강의를 들으며 열심히 따라 치고 글로 정리해봤지만 내가 처음부터 직접 만든 것이 아니니 체화가 안 된다.

그래서 간단하게 아래와 같은 todoList를 직접 만들어 보고자 한다.

 

 

 

0. Gradle 추가

 

//ViewModel
implementation "androidx.lifecycle:lifecycle-extensions:$version_lifecycle_extensions"

// Room
implementation "androidx.room:room-runtime:$version_room"
kapt "androidx.room:room-compiler:$version_room"

// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_coroutine"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_coroutine"

 

+

apply plugin: 'kotlin-kapt'

 

//버전은 알맞게

buildScript{

ext.kotlin_version = '1.3.50'
ext.version_coroutine = "1.1.0"
ext.version_lifecycle_extensions = "2.0.0"
ext.version_room = "2.0.0"

}

 

 

 

1. Room 추가 (+LiveData)

 

- 룸을 위해선 Data, Dao, DB 등 만들어 줄 것이 많다. 코드가 크게 달라지지 않으니 패턴에 익숙해져야겠다.

 

- LiveData 는 fun getAllTodo(): LiveData<List<TodoData>> 이 코드를 통해서 Room과 연결된다. viewModel 에서 해당 메서드를 호출하면 LiveData를 반환함으로 이를 observe에서 ui를 최신화 할 수 있다.

 

- data class를 사용했는데 이는 toString()메서드를 지원하는 등의 장점이 있다.

 

*TodoData

@Entity(tableName = "todo_table")
data class TodoData(    //data Class은 toString 메서드를 지원한다.

    @PrimaryKey(autoGenerate = true)
    var todoId: Long = 0L,

    @ColumnInfo(name = "todo_content")
    var todoContent : String =  "",

    @ColumnInfo(name = "todo_completed")
    var todo_completed: Boolean = false

)

 

*TodoDao

@Dao
interface TodoDao {

    @Insert
    fun insert(todoData: TodoData)

    @Update
    fun update(todoData: TodoData)

    @Query("SELECT * FROM todo_table")
    fun getAllTodo(): LiveData<List<TodoData>>

    @Query("SELECT * FROM todo_table where todoId = :key")
    fun getTodo(key : Long): TodoData

}

 

*TodoDB

@Database(entities = [TodoData::class], version = 1, exportSchema = false)
abstract class TodoDB : RoomDatabase(){

    abstract val todoDBDao: TodoDao

    companion object{

        @Volatile   //하나의 스레드에서 데이터가 최신화 되면 다른 스레드에서도 데이터 최신화
        private var INSTANCE: TodoDB? = null

        fun getInstance(context: Context): TodoDB{

            synchronized(this){     //여러개의 스레드에서 데이터를 최신화 해주는 역할
                var instance = INSTANCE

                if(instance == null){
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        TodoDB::class.java,
                        "todo_database"
                    )
                        .fallbackToDestructiveMigration()
                        .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

 

Room은 확실히 SQLite에 비해 너무나도 친절해졌고 LiveData와의 콜라보는 코드를 더욱 깔끔하게 만들어준다.

 

 

 

 

 

2. ViewModel 생성

 

- db 인스턴스를 가져오기 위해서는 context가 필요한데 이를 위해서 AndoridViewModel을 상속받았다.

 

- db에 insert하는 작업은 main thread에서 작업할 수 없어 코루틴을 이용해서 다른 스레드에서 작업했다.

 

 

*MainViewModel.kt

//db 인스턴스를 받기 위해서는 context가 필요하다. -> AndroidViewModel상속
class MainViewModel(application: Application) : AndroidViewModel(application) {

    //db인스턴스 가져오기
    private val todoDao = TodoDB.getInstance(application).todoDBDao

    //todoDao.getAllTodo()의 반환값은 LiveData. 즉, todoList는 LiveData로써 역할을 수행할 수 있다.
    var todoList = todoDao.getAllTodo()

    private val dbScope = CoroutineScope(Dispatchers.IO)
    
    //db에 todoData 추가
    fun addList(content : String){
        val todoData = TodoData()
        todoData.todoContent = content

        dbScope.launch {    //DB작업은 mainThread에서 작업할 수 없다. 코루틴을 활용해서 다른 스레드에서 작업.
            todoDao.insert(todoData)
        }
    }
    
}

 

여기서 ViewModel은 configuration change를 방지하기 위함보다는 data와 ui를 분리하는 장점이 있다.

그 결과 유지보수 하기 쉽고 test하기 쉽다는데 유지보수는 아직 큰 프로젝트에 적용을 안 해봐서 잘 모르겠고 test는 아직 안 해봐서 모르겠다...(test가 참 중요한 것 같다. advancend android 과정에 있으니 꼭 수강해보자.)

 

 

 

3. MainActivity

 

*MainActivity.kt

class MainActivity : AppCompatActivity() {

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

        val viewModel = ViewModelProviders.of(this@MainActivity).get(MainViewModel::class.java)

        //todoList를 관찰하여 자료 추가시 리스트 최신화
        viewModel.todoList.observe(this, Observer {
            todoListText.setText(it.toString())
        })

        //버튼 클릭시 todoData 추가
        pluBtn.setOnClickListener {
            viewModel.addList(todoEditText.text.toString())
        }
    }
}

 

 

 

 

한 발자국만 더 나아가서 Binding까지 해보자.

 

Binding을 활용하면 MainActivity에서 아래 두 코드를 지울 수 있다.

 

        viewModel.todoList.observe(this, Observer {
            todoListText.setText(it.toString())
        })
        pluBtn.setOnClickListener {
            viewModel.addList(todoEditText.text.toString())
        }

 

 

 

+ DataBinding

 

0. Gradle 추가 + layout 기본 설정

 

*gradle

 

dataBinding {
    enabled = true
}

 

*layout

 - <layout>을 최상위 레이아웃으로 만든다.

 - 원하는 data를 <data>안에 추가해준다.

 

<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="viewModel"
            type="com.sangjin.todolist.viewmodel.MainViewModel" />
    </data>

 

1. Activity

 

- setcontentview가 아닌 BindingUtil을 이용해서 layout 그리기

- binding.setLifecycleOwner(true) 설정 -> 이게 있어야 liveData가 최신화 될때마다 xml도 최신화 된다.

- binding.vieModel = viewModel -> layout에 data로 viewmodel을 추가해줬으니 viewmodel을 넣어준다.

 

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.setLifecycleOwner(this) //이게 있어야 liveData가 최신화될때마다 xml도 최신화 됨.

        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

        binding.viewModel = viewModel   //xml에서 data로 설정해줬으니 넣어주는거야.

    }
}

 

 

2. activity_main.xml

 

- viewModel을 자유롭게 사용 가능하기 때문에 

- android:text="@{viewModel.todoList.toString()}" 으로 liveData를 받아 넣어준다.

 

- 버튼 클릭 이벤트와 같은 메서드도 넣을 수 있다.

- android:onClick="@{()->viewModel.addList(viewModel.newTodo)}"

 

- newTodo는 editText의 값을 받기 위해서 viewmodel에 추가한 변수이다.

- android:text="@={viewModel.newTodo}" 

- 위 코드를 통해서 edittext.text를 viewmodel의 newTodo 변수에 넣을 수 있다.

 

<?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="viewModel"
            type="com.sangjin.todolist.viewmodel.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/todoListText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.todoList.toString()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/pluBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_plus"
            android:onClick="@{()->viewModel.addList(viewModel.newTodo)}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/todoEditText"
            tools:layout_editor_absoluteY="27dp" />

        <EditText
            android:id="@+id/todoEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:text="@={viewModel.newTodo}"
            android:hint="@string/et_todo"
            android:inputType="textPersonName"
            app:layout_constraintEnd_toStartOf="@+id/pluBtn"
            app:layout_constraintStart_toStartOf="parent"
            tools:layout_editor_absoluteY="35dp" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

*MainViewModel.kt

 

//db 인스턴스를 받기 위해서는 context가 필요하다. -> AndroidViewModel상속
class MainViewModel(application: Application) : AndroidViewModel(application) {

    //db인스턴스 가져오기
    private val todoDao = TodoDB.getInstance(application).todoDBDao

    //todoDao.getAllTodo()의 반환값은 LiveData. 즉, todoList는 LiveData로써 역할을 수행할 수 있다.
    var todoList : LiveData<List<TodoData>>
    
    var newTodo: String?= null	//edittext의 값을 받아오기 위한 변수

    private val dbScope = CoroutineScope(Dispatchers.IO)

    init {
        todoList  = todoDao.getAllTodo()
    }

    //db에 todoData 추가
    fun addList(content : String){
        val todoData = TodoData()
        todoData.todoContent = content

        dbScope.launch {    //DB작업은 mainThread에서 작업할 수 없다. 코루틴을 활용해서 다른 스레드에서 작업.
            todoDao.insert(todoData)
        }
    }

}

 

 데이터 runtime이 아닌 compile시 뷰를 참조하여 앱의 성능 향상에 기여하고 UI에서의 코드를 줄여주는 장점이 있다.

하지만 xml로 코드가 이동함으로써 debugging 이 힘들어지는 치명적인 단점이 있다.

 

 

 

 

 

 

 

*참고

 

- https://www.youtube.com/watch?v=5BUGO9YnDz8&list=PLxTmPHxRH3VXHOBnaGQcbSGslbAjr8obc&index=10

반응형
Comments