기록장
[코틀린] Databinding, LiveData, ViewModel, Room 사용 예제 with MVVM 본문
이번 포스팅에서는 Databinding + LiveData + ViewModel + Room 을 결합하여 이것을 MVVM 패턴으로 작성하는 예제를 진행합니다.
저도 공부를 하며 이해한 바탕으로 코드를 작성하는 것이라 MVVM 패턴에 대해 잘못된 점이 있을 수 있습니다. 혹시나 보시고 의견이 있으시면 코멘트 부탁드립니다.
먼저 MVVM 패턴은 View, ViewModel, Model로
View는 ViewModel을 알지만, ViewModel은 View를 모르게 하고, ViewModel은 Model을 알지만, Model은 ViewModel을 알 수 없도록 하여 서로에 대한 분리를 확실하게 할 뿐만 아니라 이로 인한 유지보수성을 높여줍니다.
그림으로 보면 이렇습니다.
View는 UI 부분을, ViewModel은 View에 대한 데이터나 데이터 처리 로직을 담당하고, Model은 네트워크(Retrofit 등)나 Database(Room, SQLite 등)를 사용하여 데이터 변경 및 처리를 담당합니다.
예들 들어, 사용자가 View에서 버튼을 눌러 어떠한 로직이 수행된다면, 그에 대한 변경 로직을 ViewModel에서 수행하고 중간에 데이터 변경이 필요한 경우, Model을 호출하여 데이터 변경 시켜줍니다.
이 과정을 거쳤다면, 변경된 데이터를 받아서 View에 적용시켜 주어야 합니다.
ViewModel은 항상 Model을 관찰하고 있고, View는 ViewModel을 관찰하므로써 데이터 변경을 알아채고 사용할 수 있는 것 입니다.
데이터 바인딩과 라이브데이터, 뷰모델, 룸에 대해 잘 모르시겠다면 아래를 참고해주세요.edit0.tistory.com/46
[코틀린] ViewModel, Databinding 사용해보기 (with LiveData)
Databinding은 말그대로 데이터를 묶는 의미로, XML(UI)과 데이터(ViewModel 내)를 결합하여 사용할 수 있도록 도와주는 라이브러리입니다. 물론 UI를 담당하는 Java나 Kotlin 소스 코드에서 데이터나 UI 업
edit0.tistory.com
[코틀린] LiveData, BindingAdapter 사용해보기 (with ViewModel and DataBinding)
바로 이전 포스팅에서는 데이터바인딩과 뷰모델에 대해서 알아보았습니다. 라이브데이터가 나오긴 했었지만 거의 설명이 없었습니다. 그래서 이번에는 데이터바인딩, 뷰모델과 함께 라이브데
edit0.tistory.com
[코틀린] Room (SELECT, INSERT, UPDATE, DELETE) 1
이번 포스팅에서는 안드로이드 내부 데이터베이스 Room에 대해 알아보도록 하겠습니다. 다들 아시겠지만, 데이터베이스는 기본적으로 데이터를 담아두고 있는 공간입니다. 그리고 이 데이터를 S
edit0.tistory.com
예제를 보기 전에 결과물 먼저 보도록 하겠습니다.
정보를 입력하면 데이터가 리사이클러뷰에 추가됩니다.
데이터베이스를 시각화로 보면 이렇게 잘 들어가 있습니다.
추가된 멤버들 중 한 명을 누르면 책 이름을 입력하는 곳이 나오고 각 멤버가 가지고 있는 책 이름을 저장할 수 있습니다.
책을 누르면 책에 대한 책 이름 수정이 가능합니다.
책 데이터베이스
이제 예제를 보도록 하겠습니다.
빌드 그래들 설정 해줍니다.
android {
...
dataBinding {
enabled true
}
}
dependencies {
...
//Room
implementation 'androidx.room:room-runtime:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
//ViewModel, LiveData
implementation 'androidx.activity:activity-ktx:1.1.0'
//RecyclerView
implementation "androidx.recyclerview:recyclerview:1.1.0-beta03"
//Room Viewer 룸 데이터베이스를 보기 위해 추가
debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'
//Coroutines 스레딩 작업을 위해 사용 (Database 호출 때 말고는 거의 안쓰임)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}
모든 클래스를 올리면 가독성이 떨어지기에 중요하다고 생각되는 부분들만 주석을 달고 깃허브 링크 남기도록 하겠습니다.
전체 구조입니다.
View(View), ViewModel(ViewModel), Model(Repository, Room)에 해당합니다.
첫 번째로, Room 데이터베이스를 만들어 보도록 하겠습니다.
Dao와 Entity, Database를 만들어줍니다.
테이블 구성: MemberEntity 테이블 BookEntity 테이블
키: Member 테이블의 memberId가 기본키이고, Book 테이블의 memberIdInBook(외래키)과 1:n 관계, Book 테이블의 기본키는 bookOrder
MemberDao.kt (인터페이스) (데이터베이스와 상호작용, SQL문 사용)
@Dao
interface MemberDao {
@Insert
fun insert(memberEntity: MemberEntity)
@Delete
fun delete(memberEntity: MemberEntity)
@Query("SELECT * FROM MemberEntity")
fun getAllMembers(): LiveData<List<MemberEntity>>
}
MemberEntity.kt (테이블 역할)
@Entity(tableName = "MemberEntity")
data class MemberEntity(
@PrimaryKey
val memberId: String,
val name: String,
val info: String,
val city: String
) {
}
BookDao.kt (인터페이스)
@Dao
interface BookDao {
@Insert
fun insert(bookEntity: BookEntity)
@Query("UPDATE BookEntity SET bookName = :newBookName WHERE bookOrder = :bookOrder")
fun updateBook(newBookName: String, bookOrder: Int)
@Delete
fun delete(bookEntity: BookEntity)
@Query("SELECT * FROM BookEntity WHERE memberIdInBook = :memberIdInBook")
fun getAllBooks(memberIdInBook: String): LiveData<List<BookEntity>>
}
BookEntity.kt
@Entity(tableName = "BookEntity",
foreignKeys = [ForeignKey(entity = MemberEntity::class, parentColumns = ["memberId"], childColumns = ["memberIdInBook"], onDelete = ForeignKey.CASCADE)]
)
data class BookEntity(
val bookName: String,
val memberIdInBook: String
) {
@PrimaryKey(autoGenerate = true)
var bookOrder: Int = 0
}
LibraryDatabase.kt (데이터베이스 생성)
@Database(entities = [MemberEntity::class, BookEntity::class], version = 1)
abstract class LibraryDatabase : RoomDatabase() {
abstract fun memberDao(): MemberDao
abstract fun bookDao(): BookDao
companion object{
var instance: LibraryDatabase? = null
@Synchronized
open fun getInstance(context: Context): LibraryDatabase? {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(), LibraryDatabase::class.java, "Library")
.fallbackToDestructiveMigration()
.build()
}
return instance
}
}
}
여기까지 하셨다면 내부 데이터베이스가 구축되었습니다.
구축된 데이터베이스를 확인하고 싶으시다면 아까 추가하였던 아래 라이브러리를 통해 볼 수 있습니다.
애뮬레이터 기준)
애뮬레이터를 실행 후 앱을 실행합니다.
애뮬레이터가 설치된 폴더로 들어갑니다. (cmd창 or Android studio 터미널창)
그 다음, 아래와 같이 포트(포트번호 아무 번호로 해도 무관)를 설정해줍니다.
이런식으로 설정..
다음, 인터넷을 켜시고, http://localhost:포트번호/ 주소로 들어가면 데이터베이스를 보실 수 있습니다.
참고) 앱이 실행되고 있을 시에 보실 수 있습니다.
이제 두 번째로 데이터베이스를 사용해야 합니다.
먼저, 데이터베이스에 접근하기 위한 Repository 클래스입니다.
class MainRepository(application: Application) {
//데이터베이스 인스턴스를 얻습니다.
var libraryDatabase = LibraryDatabase.getInstance(application)
//데이터베이스를 통해 접근하려하는 데이터베이스의 DAO 객체를 얻습니다.
var memberDao: MemberDao = libraryDatabase?.memberDao()!!
//LiveData로 데이터베이스 값의 변화를 관찰합니다.
var LiveMembers: LiveData<List<MemberEntity>> = memberDao?.getAllMembers()
//멤버 추가 시 insert를 하는데 DB 실행 시 메인 스레드를 통하여 사용할 수 없습니다.
//그러므로 코루틴으로 감싸 비동기로 실행해줍니다.
fun insertMember(memberEntity: MemberEntity){
CoroutineScope(IO).launch {
memberDao.insert(memberEntity)
}
}
//멤버 삭제 시도 insert와 마찬가지 입니다.
fun deleteMember(memberEntity: MemberEntity){
CoroutineScope(IO).launch {
memberDao.delete(memberEntity)
}
}
}
이제 ViewModel에서 Repository에 있는 프로퍼티나 함수를 호출하여 사용하면 될 것 같습니다.
다음 ViewModel 입니다.
MainViewModel.kt
class MainViewModel(application: Application) : AndroidViewModel(application) {
//앞에서 만들었던 Repository 클래스를 사용하기 위한 객체를 만듭니다.
var mainRepository = MainRepository(application)
//Repository에 있는 전체 멤버들을 가져오는 LiveMember 변수를 관찰
var LiveMembers = mainRepository.LiveMembers
var id = String()
var name = String()
var info = String()
var city = String()
var alram = MutableLiveData<Boolean>()
//데이터바인딩을 통해 ADD버튼 클릭 시 로직이 수행됩니다.
//Repository 클래스에 있는 insertMember 호출
fun addButton(){
if(id != "" && name != "" && info != "" && city != ""){
var member = MemberEntity(id, name, info, city)
mainRepository.insertMember(member)
}
else{
if(alram.value == true){
alram.value = false
}
else if(alram.value == false){
alram.value = true
}
else{
alram.value = false
}
}
}
//데이터바인딩을 통해 로직 수행
//멤버 삭제 시 Repository 클래스에 있는 deleteMember 호출
fun deleteMember(memberEntity: MemberEntity){
mainRepository.deleteMember(memberEntity)
}
}
현재 여기까지 본다면 ViewModel에서 Model로 데이터 변경 요청을 하고 ViewModel의 LiveMembers 변수를 통해 Model의 LiveMembers 변수를 관찰하고 있습니다. 이렇게 최신 데이터를 바로바로 얻을 수 있습니다.
다음 View인 메인 액티비티를 보도록 하겠습니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var MainBinding: ActivityMainBinding
lateinit var mainViewModel: MainViewModel
var mainAdapter = MainAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
MainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
MainBinding.setLifecycleOwner(this)
MainBinding.mainViewModel = mainViewModel
settingRCV() //리사이클러뷰 셋팅
deleteMember() //리사이클러뷰 아이템 삭제 시
RCVitemOnClick() //리사이클러뷰 아이템 클릭 시
//현재 멤버들 리스트를 가지고 있는 ViewModel 내에 있는 LiveMembers에 변화가 감지된다면
//호출되어 최신 데이터를 리사이클러뷰에 반영합니다.
var LiveMembers = Observer<List<MemberEntity>>{
mainAdapter.setMembers(it)
mainAdapter.notifyDataSetChanged()
}
mainViewModel.LiveMembers.observe(this, LiveMembers)
var alarm = Observer<Boolean> {
Toast.makeText(MainBinding.root.context,"모두 입력해주세요.",Toast.LENGTH_SHORT).show()
}
mainViewModel.alram.observe(this, alarm)
}
//리사이클러뷰 셋팅
fun settingRCV(){
var layoutmanager = LinearLayoutManager(MainBinding.memberListRCV.context)
MainBinding.memberListRCV.layoutManager = layoutmanager
MainBinding.memberListRCV.adapter = mainAdapter
}
//리사이클러뷰 아이템 삭제 시
fun deleteMember(){
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT){
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
mainViewModel.deleteMember(mainAdapter.getMemberAt(viewHolder.getAdapterPosition()))
}
}).attachToRecyclerView(MainBinding.memberListRCV)
}
//리사이클러뷰 아이템 클릭 시
fun RCVitemOnClick(){
mainAdapter.setOnItemClickListener(object : MainAdapter.OnItemClickListener {
override fun onItemClick(holder: MainAdapter.ViewHolder?, view: View?, position: Int) {
var intent = Intent(applicationContext, MemberInfoActivity::class.java)
intent.putExtra("memberId", mainViewModel.LiveMembers.value?.get(position)?.memberId)
intent.putExtra("name", mainViewModel.LiveMembers.value?.get(position)?.name)
intent.putExtra("info", mainViewModel.LiveMembers.value?.get(position)?.info)
intent.putExtra("city", mainViewModel.LiveMembers.value?.get(position)?.city)
startActivity(intent)
}
})
}
}
View는 데이터 관리, 변경이 아닌 사용자가 보고 있는 View에 관련된 작업을 처리합니다.
ViewModel의 LiveMembers 변수를 관찰하므로써 데이터가 변경되었을 경우, Observer가 호출되면서 바로 리사이클러뷰에 대한 데이터를 변경하여, 데이터를 최신화 시켜줍니다.
여기까지 데이터바인딩, 라이브데이터, 뷰모델, 룸을 사용하는 한 사이클을 보았습니다.
이 설명만 이해하신다면 전체 코드는 문제 없이 참고하실 수 있을 것 입니다.
MVVM 패턴을 적용하면서 클래스도 많아지고, 얼핏 보기엔 복잡해 보일 수 있습니다.
그러나 각각의 독립성을 보장하므로 코드를 수정하거나 추가, 삭제할 경우 용이할 것 이라고 생각됩니다.
나머지 코드들도 이러한 형식으로 구성되어 있습니다.
전체 코드는 아래 깃허브에 업로드 하였습니다.
필요 시 참고하시면 될 것 같습니다.
github.com/EDIT0/DatabindingLiveDataViewModelRoomWithMVVM
EDIT0/DatabindingLiveDataViewModelRoomWithMVVM
Contribute to EDIT0/DatabindingLiveDataViewModelRoomWithMVVM development by creating an account on GitHub.
github.com
감사합니다.
'코틀린' 카테고리의 다른 글
[코틀린] Navigation Component를 이용한 BottomNavigation bar 구현 및 AAC ViewModel을 활용한 프래그먼트 간 데이터 공유 (0) | 2021.06.05 |
---|---|
[코틀린] 코루틴 개념 익히기 URI (0) | 2021.04.21 |
[코틀린] Room (ViewModel, DataBinding, LiveData) 2 (0) | 2021.03.28 |
[코틀린] Room (SELECT, INSERT, UPDATE, DELETE) 1 (0) | 2021.03.27 |
[코틀린] LiveData, BindingAdapter 사용해보기 (with ViewModel and DataBinding) (0) | 2021.03.27 |