Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
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
Tags
more
Archives
Today
Total
관리 메뉴

기록장

[코틀린] Databinding, LiveData, ViewModel, Room 사용 예제 with MVVM 본문

코틀린

[코틀린] Databinding, LiveData, ViewModel, Room 사용 예제 with MVVM

edit0 2021. 5. 1. 01:40

이번 포스팅에서는 Databinding + LiveData + ViewModel + Room 을 결합하여 이것을 MVVM 패턴으로 작성하는 예제를 진행합니다.

 

저도 공부를 하며 이해한 바탕으로 코드를 작성하는 것이라 MVVM 패턴에 대해 잘못된 점이 있을 수 있습니다. 혹시나 보시고 의견이 있으시면 코멘트 부탁드립니다.

 

먼저 MVVM 패턴은 View, ViewModel, Model로

View는 ViewModel을 알지만, ViewModel은 View를 모르게 하고, ViewModel은 Model을 알지만, Model은 ViewModel을 알 수 없도록 하여 서로에 대한 분리를 확실하게 할 뿐만 아니라 이로 인한 유지보수성을 높여줍니다.

 

https://medium.com/@joongwon/android-aac%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-mvvm-%ED%8C%A8%ED%84%B4-e24a685fc25d

그림으로 보면 이렇습니다.

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

edit0.tistory.com/47

 

[코틀린] LiveData, BindingAdapter 사용해보기 (with ViewModel and DataBinding)

바로 이전 포스팅에서는 데이터바인딩과 뷰모델에 대해서 알아보았습니다. 라이브데이터가 나오긴 했었지만 거의 설명이 없었습니다. 그래서 이번에는 데이터바인딩, 뷰모델과 함께 라이브데

edit0.tistory.com

edit0.tistory.com/48

 

[코틀린] 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

 

 

감사합니다.