경험의 기록

2021.04.19 - [안드로이드/AAC, MVVM] - [Android] 안드로이드 AAC & MVVM

 

[Android] 안드로이드 AAC & MVVM

액티비티, 프래그먼트에 너무 많은 코드를 넣게 되면 점점 무거워져 다루기 힘들어지게 된다. 앱이 카메라 인텐트를 트리거합니다. 그러면 Android OS에서 요청을 처리하기 위해 카메라 앱을 실행

hanyeop.tistory.com

MVVM 패턴으로 코드를 작성하게 되면 유지보수에 용이해지고 깔끔해진다.

이번에는 Room을 LivaData와 ViewModel, DataBinding을 사용하여 MVVM 패턴으로 작성해보려고 한다.

 

2021.05.12 - [안드로이드/AAC, MVVM] - [Android] Room 활용하여 데이터 저장하기

 

[Android] Room 활용하여 데이터 저장하기

안드로이드에서는 로컬 DB에 데이터를 저장하기 위해 SQLite를 지원한다. 허나 SQLite는 사용하기 복잡하여 어렵기 때문에 SQLite에 대한 추상화 레이어를 제공하는 Room 라이브러리를 지원한다. 2021.0

hanyeop.tistory.com

기본 코드는 이 코드를 바탕으로 할 것이다.

 

 

 

 


사용해보기

종속성 추가

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"

뷰모델과, 라이브데이터 사용하기 위해 추가해준다.

또한 뷰바인딩 대신 데이터바인딩을 사용하기 위해 추가해준다.

 

1️⃣ 라이브데이터 사용하기

UserDao

@Query("SELECT * FROM User")
    fun getAll() : LiveData<List<User>>

기존 Dao 객체의 getAll()에서 List<User> 가 아닌 LiveData<List<User>> 로 바꿔준다.

 

 

MainActivity

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 함수를 호출하기 때문에 별도로 갱신이 필요하지 않다.

 

 

2️⃣ 뷰모델, 데이터바인딩 사용하기

Repository 생성

/* 앱에서 사용하는 데이터와 그 데이터 통신을 하는 역할
뷰모델은 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 만드는 것이 권장되므로 생성한다.

 

ViewModel 생성, 데이터바인딩

/* 뷰모델은 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

 

HanYeop/AndroidStudio-Practice

안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice development by creating an account on GitHub.

github.com

 

 

 

 

참조

www.youtube.com/watch?v=fUbiWZ2g6-g

todaycode.tistory.com/34

https://0391kjy.tistory.com/37

https://ddangeun.tistory.com/86

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading