일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- convert base64
- 기존 앱
- GeometryReader
- detect url
- Swift Package Manager
- Android
- List
- swift
- notifychanged
- DevelopmentRegion
- Side Menu
- base64 변환
- development language
- swift #swift keychain #keychain 사용법
- SwiftUI
- ios
- UIViewControllerTransitioningDelegate
- pod install
- transformation.map
- DataBinding
- oberve url
- ViewBuilder
- 상단 탭바
- url 관찰
- scrolling tab
- 스크롤 탭
- Tuist
- 개발자 면접
- UIPresentationController
- url 추적
- Today
- Total
버그 잡이
(AAC 응용)ViewModel+LiveData+Room으로 ToDoList 앱 만들기 본문
(AAC 응용)ViewModel+LiveData+Room으로 ToDoList 앱 만들기
버그잡이 2020. 4. 14. 20:38Udacity강의를 들으며 열심히 따라 치고 글로 정리해봤지만 내가 처음부터 직접 만든 것이 아니니 체화가 안 된다.
그래서 간단하게 아래와 같은 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