안드로이드 프로그래밍[Kotiln Code]/Database(Room, Realm, Live Data )

[MVVM] ROOM Database 정리

훈츠 2020. 3. 31. 01:59
반응형

안녕하세요. 훈츠 입니다. 금일은 ROOM에 대해 정리해 보도록 하겠습니다. 

MODEL - VIEW - VIEW MODEL : MVVM

ROOM 기본 개념
ROOM 아키텍처 다이어그램 

  1. Dependency 추가 
  2. ROOM 생성 (Contact(Entity), ContactDao, ContactDatabase)
  3. Repository 생성 (main Thread 접근 불가 하기 때문에, 별도의 Thread 생성)
  4. ViewModel 생성 (Application context를 사용하기 위해 Application을 인자로 받는다. 이유: 메모리릭 발생가능성)
  5. Activity or Fragment 설정 ( ViewModelProViders 를 이용해 get, observe 로 만들어서 생명주기 관찰을 정함)
  6. RecyclerView 설정 (xml, Adapter)

1. Dependency 추가

AndroidX 에 배포 노트를 확인 하시면 최신 버젼 부터 history를 알수 있습니다. 

https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=en

 

Lifecycle  |  Android 개발자  |  Android Developers

수명 주기 인식 구성요소는 활동 및 프래그먼트와 같은 다른 구성요소의 수명 주기 상태 변경에 따라 작업을 실행합니다. 이러한 구성요소를 사용하면 잘 구성된 경량의 코드를 만들어 더욱 쉽게 유지할 수 있습니다. 자세한 내용은 참조 문서에서 확인하세요. 최근 업데이트 현재 안정화 버전 다음 출시 후보 베타 버전 알파 버전 2020년 1월 22일 2.2.0 - - - 종속성 선언 Lifecycle의 종속성을 추가하려면 프로젝트에 Google Maven 저장소를

developer.android.com

  • Gradle (module app) 코틀린 에서는 kapt 를 annotationProcessor 대신 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apply plugin: 'kotlin-kapt'
....
dependencies {
// Room components
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5" For Kotlin use kapt instead of annotationProcessor
 
    // recyclerview, cardview
    implementation 'com.android.support:recyclerview-v7:29.0.0'
    implementation 'com.android.support:cardview-v7:29.0.0'
 
    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    kapt "androidx.lifecycle:lifecycle-compiler:2.3.0-alpha01"
}
 
cs

 

Room 생성 (Entity, DAO, Database)

Contact() : Entitiy 개체 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity(tableName = "contact")
class Contact (
    @PrimaryKey(autoGenerate = true)
    var id: Long= null,
    
    @ColumnInfo(name =  "receiveNumber")
    var receiveNumber: String = "",
 
    @ColumnInfo(name =  "receiveName")
    var receiveName: String = "",
 
    @ColumnInfo(name =  "transNumber")
    var transNumber: String = "",
 
    @ColumnInfo(name =  "transName")
    var transName: String = "",
)
//construct 를 이용 해서, 초기값을 줘도 됨
cs

 

 

 

 

ContactDao() : DAO 인터페이스

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Dao
interface ContactDao{
    @Query("SELECT * FROM contact ORDER BY id ASC")
    fun getAll() : LiveData<List<Contact>> //어디든 변경이 생기면 UPDATE 할수 있는 라이브 데이타
 
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(contact :Contact)
 
    @Delete
    fun delete(contact :Contact)
 
    @Update
    fun update(contact :Contact)
}
 
 
 
 

 

  • @Query, Insert, Delete, Update 어노테이션을 사용합니다. insert 와 update 에서는 onConflict 속성을 지정해 중복된 데이터의 경우 어떻게 처리할 것인지 처리를 지정할수도 있습니다. 

ContactDatabase() : 데이터베이스 인스턴스 만들기

 

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
@Database(entities = [Contact::class], version = 1)
abstract class ContactDatabase : RoomDatabase() {
 
    abstract fun contactDao() : ContactDao
 
    companion object {
        private var INSTANCE : ContactDatabase? = null
 
        fun getInstance(context : Context): ContactDatabase? {
            if(INSTANCE == null){
              synchronized(ContactDatabase::class){
                  INSTANCE = Room.databaseBuilder(context.applicationContext,
                      ContactDatabase::class.java, "contact")
                      .fallbackToDestructiveMigration() 
                      .build()
              }
            }
            return INSTANCE
        }
 
    }
 
}
 
 
cs
@Database 어노테이션을 이용해 entity를 정의하고 SQLite 버전을 지정합니다. getinstance 함수는 여러 스레드가 접근하지 못하도록 synchronized로 설정합니다. 여기서 Room.databaseBuilder 로 인스턴스를 생성하고, fallbackToDestructiveMigration()을 통해 데이터베이스가 갱신될때 기존의 테이블을 버리고 새로 사용하도록 설정했습니다. 

ContactReposityory 생성

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
32
33
34
35
36
37
38
39
class ContactReposityory (application: Application) {
 
    private val contactDatabase = ContactDatabase.getInstance(application)!!
    private val contactDao : ContactDao = contactDatabase.contactDao()
    private val contacts : LiveData<List<Contact>> = contactDao.getAll()
 
    fun getAll() : LiveData<List<Contact>> {
        return contacts
    }
 
    fun insert(contact : Contact){
        try {
            val thread = Thread(Runnable {
                contactDao.insert(contact)
            })
            thread.start()
        } catch (e : Exception){e.printStackTrace()}
    }
 
    fun delete(contact: Contact) {
        try {
            val thread = Thread(Runnable {
                contactDao.delete(contact)
            })
            thread.start()
        } catch (e : Exception){e.printStackTrace()}
 
    }
    fun update(contact: Contact) {
        try {
            val thread = Thread(Runnable {
                contactDao.update(contact)
            })
            thread.start()
        } catch (e : Exception){e.printStackTrace()}
 
    }
}
 
cs

 

  •  ViewModel 에서 DB에 접근 할때, contactDao를 이용할때 수행할 함수를 만듭니다.
    • 이유는 ROOM DB를 Main Thread 에서 접근하려고 하면 크래쉬가 발생 합니다. 
      • 따라서 Repository를 통한 별도의 thread로 접근 해야 합니다. 

ViewModel 생성

  • viewModel 라이브러리는 내부적으로  프래그먼트를 사용합니다
    • 최초  viewModel을 생성할 때 , ViewModelProvider는 HolderFragment라 명명된프래그먼트를 생성하고
      • 이 프래그먼트에는 setRetainInstance(true)가 설정됩니다.
    • viewModel은 유보된 프래그먼트(RetainFragment)의 연장선이라 할 수 있습니다.
  • viewModel은 추상클래스(abstract)로 상속하는것만으로 viewModel을 만들 수 있습니다 .
    • 추상클래스이므로 객체를 그냥 생성할수 없습니다.
    • ViewModelProvider를 통해 객체를 생성해주어야합니다.
  • 생명주기 함수 onCleared()가 존재합니다.
  • 커스텀 생성자를 가지려면, ViewModel은 ViewModelProvider.Factory 인터페이스를 사용해야합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class ContactViewModel (application: Application) : AndroidViewModel(application) {
     
        private val reposityory = ContactReposityory(application)
        private val contacts = reposityory.getAll()
     
        fun getAll() : LiveData<List<Contact>> {
            return this.contacts
        }
     
        fun insert(contact :Contact){
            reposityory.insert(contact)
        }
     
        fun delete(contact : Contact){
            reposityory.delete(contact)
        }
     
        fun update(contact :  Contact){
            reposityory.update(contact)
        }
     
    }
     
     
    cs
     

AndroidViewModel 에서는 Application 을 파라미터로 사용합니다. (Repository를 통해서) Room 데이터베이스의 인스턴스를 만들때에는 context가 필요합니다. 하지만, 만약 ViewModel이 액티비티의 context를 쓰게되면, 액티비티 생명주기에 따라 destory 된 경우에는 메모리 릭이 발생할 수있기 때문에 Application Context를 사용하기 위해서 Application을 인자로 받습니다. 

viewModel 사용시 주의 사항

  • viewModel내부에 액티비티, 프레그먼트, 뷰에 대한 컨텍스트를 저장해서는 안됩니다.
    • viewModel의 수명주기는 외부에 존재하기때문에 , 메모리 릭의 원인이 될 수 있습니다.
    • ApplicationContext 는 상관없습니다. AndroidViewModel클래스도 제공하고 있습니다.
  • viewModel은 기기의 구성이 변경될때만 유지됩니다.
    • 백버튼이나 , 최근목록에서 앱을 종료했을때는 어떠한 처리도 기대할 수 없습니다.
  • A라는 액티비티에서 사용하는 viewModel을  B 액티비티에서 viewModel에 저장된 값을 사용하고 싶다면 ?
    • ViewModelProvider.Factory를 SingleTon으로 구성하면 됩니다.
    • 하지만 다른 생명주기에서 ViewModel객체를 유지하는것은 안티패턴이므로
    • 생명주기에 따라 데이터를 보관/관리 해주는 LiveData등의 장점을 버리는 것이 되버릴수 있습니다.
    • 따라서  ViewModel 인스턴스를 유지시키는 것이 아닌 Datasoure나 Repository를 싱글톤으로 유지하는것이 더 추천되는 방식입니다.

  • 단일 액티비티에서 2개이상의 프래그먼트 사이 데이터를 공유할때
    • viewModel을 생성할때 프래그먼트의 scope를 사용하는것이아닌 액티비티의 scope를 전달하는것이 좋습니다.

MainActivity 설정

ContactViewModel 인스턴스 만들고, observe 로 Update UI 를 합니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity() {
    private lateinit var contactViewModel : ContactViewModel
 
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        contactViewModel = ViewModelProviders.of(this).get(ContactViewModel::class.java)
        contactViewModel.getAll().observe(this, Observer<List<Contact>>{ contacts ->
            // update UI 
        })
 
 
cs

 

 

 

 

뷰모델 객체는 직접 초기화 해주는 것이 아니라, 안드로이드 시스템을 통해 생성해줍니다. 시스템에서는 만약 이미 생성된 ViewModel 인스턴스가 있다면 이를 반환할 것이므로 메모리 낭비를 줄여줍니다. 따라서 viewModelProviders 를 이용해 get 해줍니다.

또한 Observere 를 만들어서 뷰모델이 어느 액티비티/프래그먼트의 생명주기를 관할할 것인지를 정합니다. 이 액티비티가 파괴되면 시점에 시스템에서 뷰모델도 자동으로 파괴할 것입니다. 코틀린에서는 람다를 이용해 보다 간편하게 사용할수 있습니다.

옵저버는 onchanged 메소드를 가지고 있어서, 관찰하고 있더 LiveData가 변하면 무엇을 할 것인지 액션을 지정할수 있습니다. 이후 액티비티/프래그먼트가 활성화 되어 있다면 View에서 LiveData를 관찰하여 자동을 변경 사항을 파악하고 이를 수행합니다. 이 부분에서 UI를 업데이트 하도록 할수있습니다. 

RecyclerView 설정 (xml , Adapter)

RecyclerView xml 생성

RecyclerView Adapter 생성

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ContactAdapter(val contactItemClick: (Contact) -> Unitval contactItemLongClick: (Contact) -> Unit)
    : RecyclerView.Adapter<ContactAdapter.ViewHolder>() {
 
    private var contacts: List<Contact> = listOf()
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.sub_contact_list_view, parent, false)
        return ViewHolder(view)
    }
 
    override fun getItemCount(): Int {
        return contacts.size
    }
 
    override fun onBindViewHolder(viewHolder:ViewHolder , position: Int) {
        viewHolder.bind(contacts[position])
    }
 
 
    inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        private val imgReceiver = itemView.findViewById<ImageView>(R.id.imgReceiver)
        private val imgRotate = itemView.findViewById<ImageView>(R.id.imgRotate)
        private val imgTransfer = itemView.findViewById<ImageView>(R.id.imgTransfer)
 
        private val txtReceNumber = itemView.findViewById<TextView>(R.id.txtReceNumber)
        private val txtReceName = itemView.findViewById<TextView>(R.id.txtReceName)
        private val txtTransNumber = itemView.findViewById<TextView>(R.id.txtTransNumber)
        private val txtTransName = itemView.findViewById<TextView>(R.id.txtTransName)
 
        fun bind(contact: Contact) {
            imgReceiver.setImageResource(contact.imgReceiver!!)
            imgRotate.setImageResource(contact.imgRotate!!)
            imgTransfer.setImageResource(contact.imgTransfer!!)
 
            txtReceNumber.text = contact.receiveNumber
            txtReceName.text = contact.receiveName
            txtTransNumber.text = contact.transNumber
            txtTransName.text = contact.transName
 
            itemView.setOnClickListener {
                contactItemClick(contact)
            }
 
            itemView.setOnLongClickListener {
                contactItemLongClick(contact)
                true
            }
        }
    }
    // 데이터베이스가 변경될때 마다 함수 호출 
    fun setContacts(contacts: List<Contact>) {
        this.contacts = contacts
        notifyDataSetChanged()
    }
}
 
 
cs

 

ContactAdapter 의 파라미터로 클릭과 롱클릭 했을때, 액션을 액티비티 혹은 프래그먼트에서 넘겨줍니다. View에서 화면을 갱신 할때 사용할 setContacts 함수는 데이터베이스가 변경될 때마다 이 함수가 호출 될것 입니다. 

MainActivity 혹은 프래그먼트에 적용

  • adapter = ContactAdapter(...
  • 뷰모델 Observer 의 onChanged에 해당하는 식에는 Adapter를 통해 UI 업데이트
  • LayoutManager 로 RecyclerView 연결
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class MainActivity : AppCompatActivity() {
        private lateinit var contactViewModel : ContactViewModel
     
    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            // 리싸이클러 어뎁터 설정
            // Set contactItemClick & contactItemLongClick lambda
            val adapter = ContactAdapter({ contact ->
                id = contact.id
     
            }, { contact ->
                deleteDialog(contact) //다이얼로그 창 이용해서 삭제
            })
            contactViewModel = ViewModelProviders.of(this).get(ContactViewModel::class.java)
            contactViewModel.getAll().observe(this, Observer<List<Contact>>{ contact ->
            // update UI 어뎁터에 setContacts에 contact가 변경되었을때 적용하고 notification시킴
                adapter.setContacts(contact!!)
            })
            // LayoutManager 로 RecyclerView 연결 
            val lm = LinearLayoutManager(this
            
            //main_log_view 는 xml UI 에서 RecyclerView 의 id값 입니다. 
            main_log_view.adapter = adapter 
            main_log_view.layoutManager = lm
            main_log_view.setHasFixedSize(true)
                    
    }
     
    //다이얼로그 for deleting contactViewModel
    private fun deleteDialog(contact: Contact) {
        val builder = AlertDialog.Builder(this)
        builder.setMessage("Delete selected contactLog?")
        .setNegativeButton("NO") { _, _ -> }
        .setPositiveButton("YES") { _, _ ->
        contactViewModel.delete(contact)
        }
    builder.show()
    }
               
     
    cs
     

이상입니다. 

반응형