2021.04.19 - [안드로이드/AAC, MVVM] - [Android] 안드로이드 AAC & MVVM
MVVM 패턴으로 코드를 작성하게 되면 유지보수에 용이해지고 깔끔해진다.
이번에는 Room을 LivaData와 ViewModel, DataBinding을 사용하여 MVVM 패턴으로 작성해보려고 한다.
2021.05.12 - [안드로이드/AAC, MVVM] - [Android] Room 활용하여 데이터 저장하기
기본 코드는 이 코드를 바탕으로 할 것이다.
android {
// 데이터바인딩
buildFeatures {
dataBinding true
}
}
def lifecycle_version = "2.3.0"
// ViewModel - 라이프 사이클
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData - 데이터의 변경 사항을 알 수 있음
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
뷰모델과, 라이브데이터 사용하기 위해 추가해준다.
또한 뷰바인딩 대신 데이터바인딩을 사용하기 위해 추가해준다.
@Query("SELECT * FROM User")
fun getAll() : LiveData<List<User>>
기존 Dao 객체의 getAll()에서 List<User> 가 아닌 LiveData<List<User>> 로 바꿔준다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var db : UserDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 데이터베이스 연결
db = UserDatabase.getInstance(applicationContext)!!
// 옵저버가 리스트의 변화를 감지하여 갱신 함수 (updateUserList) 호출
db.userDao().getAll().observe(this, Observer {
updateUserList(it)
})
}
// 리스트를 받아서 뷰에 표시해줌
fun updateUserList(userList : List<User>){
var userListText = "사용자 목록"
CoroutineScope(Dispatchers.Main).launch {
val load = async(Dispatchers.IO) {
for(i in userList){
userListText += "\n${i.id} ${i.name}, ${i.age}"
}
}
load.await()
binding.textView.text = userListText
}
}
// 새로운 유저정보 추가시 옵저버가 감지하여 updateUserList 함수를 호출하기 때문에 자동으로 뷰 갱신
fun addUser(view : View){
val user = User(binding.nameEditView.text.toString(),binding.ageEditView.text.toString())
CoroutineScope(Dispatchers.IO).launch {
db.userDao().insert(user)
}
}
// 유저정보 삭제시 옵저버가 감지하여 updateUserList 함수를 호출하기 때문에 자동으로 뷰 갱신
fun deleteAllUser(view : View){
CoroutineScope(Dispatchers.Main).launch {
val delete = async(Dispatchers.IO) {
db.userDao().deleteAll()
}
delete.await()
}
}
}
기존의 fetchUserList 함수를 리스트를 받아 뷰를 업데이트해주는 함수로 변경한다.
리스트를 감시하는 옵저버를 생성하여 리스트가 바뀔때마다 updateUserList 함수를 호출해준다.
추가,삭제 시에는 옵저버가 감지하여 알아서 updateUserList 함수를 호출하기 때문에 별도로 갱신이 필요하지 않다.
/* 앱에서 사용하는 데이터와 그 데이터 통신을 하는 역할
뷰모델은 DB에 직접 접근하지 않아야함
*/
class UserRepository(application: Application) {
private val userDao : UserDao
private val userList : LiveData<List<User>>
init{
var db : UserDatabase = UserDatabase.getInstance(application)!!
userDao = db.userDao()
userList = db.userDao().getAll()
}
fun insert(user : User){
userDao.insert(user)
}
fun update(user : User){
userDao.update(user)
}
fun delete(user : User){
userDao.delete(user)
}
fun getAll() : LiveData<List<User>>{
return userDao.getAll()
}
fun deleteAll(){
userDao.deleteAll()
}
}
Repository는 MVVM 에서 Model에 해당하며 앱에서 사용하는 데이터와 그 데이터 통신을 하는 역할을 한다.
ViewModel에서 이 작업을 수행해도 되지만 Repository 만드는 것이 권장되므로 생성한다.
/* 뷰모델은 DB에 직접 접근하지 않아야함. Repository 에서 데이터 통신.
뷰와 Repository(Model) 사이의 인터페이스, 데이터바인딩 전달하여 뷰를 그리기 위한 데이터 처리
*/
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val repository = UserRepository(application)
// ViewModel에 파라미터를 넘기기 위해서, 파라미터를 포함한 Factory 객체를 생성하기 위한 클래스
class Factory(val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return UserViewModel(application) as T
}
}
// 새로운 유저정보 추가시 옵저버가 감지하여 updateUserList 함수를 호출하기 때문에 자동으로 뷰 갱신
fun addUser(name : String, age : String){
val user = User(name,age)
CoroutineScope(Dispatchers.IO).launch {
repository.insert(user)
}
}
fun getAll() : LiveData<List<User>> {
return repository.getAll()
}
// 유저정보 삭제시 옵저버가 감지하여 updateUserList 함수를 호출하기 때문에 자동으로 뷰 갱신
fun deleteAll(){
CoroutineScope(Dispatchers.IO).launch {
repository.deleteAll()
}
}
}
repository에서 application context가 필요하기 때문에 AndroidViewModel을 상속받고, 메인액티비티에서 뷰모델에 application 파라미터를 넘기도록 하기 위하여 팩토리 메소드를 작성해준다.
메인에 있던 유저추가 메소드를 뷰모델로 옮겨서 Repository 에서 삽입 작업을 하도록 한다.
여기서 2개의 String 변수를 받게 되는데
<?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="userViewModel"
type="org.techtown.roomtest.UserViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/nameEditView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="113dp"
android:ems="10"
android:inputType="textPersonName"
android:hint="이름"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/ageEditView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="textPersonName"
android:hint="나이"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/nameEditView" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="등록"
android:textSize="24sp"
android:onClick="@{() -> userViewModel.addUser(nameEditView.getText().toString(),ageEditView.getText().toString())}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ageEditView" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="326dp"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.045"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="52dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:text="전부삭제"
android:textSize="20sp"
android:onClick="@{() -> userViewModel.deleteAll()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button"
app:layout_constraintTop_toBottomOf="@+id/ageEditView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
데이터바인딩을 위해 레이아웃으로 변경하고
뷰모델을 추가해준다.
android:onClick="@{() -> userViewModel.addUser(nameEditView.getText().toString(),ageEditView.getText().toString())}"
등록버튼에서 아까 있던 addUser 메소드를 바인딩 해준다. 파라미터는 xml에 있는 에디트뷰 아이디를 참조하여 사용할 수 있다.
또한 메인에 있던 삭제 메소드도 뷰모델로 옮기고 Repository 에서 작업하도록 하며
전부삭제 버튼에 바인딩해준다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var userViewModel : UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 가져오기. 뷰모델이 Application 파라미터를 받아야하기 때문에
// 파라미터를 포함한 Factory 객체를 생성하여 넘겨줌.
userViewModel = ViewModelProvider(this,UserViewModel.Factory(application)).get(UserViewModel::class.java)
binding.userViewModel = userViewModel
// 옵저버가 리스트의 변화를 감지
userViewModel.getAll().observe(this, Observer {
updateUserList(it)
})
}
// 리스트를 받아서 뷰에 표시해줌
fun updateUserList(userList : List<User>){
var userListText = "사용자 목록"
for(i in userList){
userListText += "\n${i.id} ${i.name}, ${i.age}"
}
binding.textView.text = userListText
}
}
뷰바인딩을 하던 것을 데이터바인딩으로 변경해주고
ViewModelProvider에서 아까 만든 팩토리 메소드에 application을 담아 뷰모델을 연결해준다.
뷰모델에 옵저버를 생성하여 리스트 변경이 감지되었을 때 뷰를 업데이트 해주는 함수를 호출하도록 한다.
사용자를 등록하거나 삭제할 때 텍스트가 자동으로 변경된다.
https://github.com/HanYeop/AndroidStudio-Practice/tree/master/RoomTest
참조
www.youtube.com/watch?v=fUbiWZ2g6-g
https://0391kjy.tistory.com/37
https://ddangeun.tistory.com/86