안녕하세요. 훈츠 입니다. 금일은 ROOM에 대해 정리해 보도록 하겠습니다.
MODEL - VIEW - VIEW MODEL : MVVM
- Dependency 추가
- ROOM 생성 (Contact(Entity), ContactDao, ContactDatabase)
- Repository 생성 (main Thread 접근 불가 하기 때문에, 별도의 Thread 생성)
- ViewModel 생성 (Application context를 사용하기 위해 Application을 인자로 받는다. 이유: 메모리릭 발생가능성)
- Activity or Fragment 설정 ( ViewModelProViders 를 이용해 get, observe 로 만들어서 생명주기 관찰을 정함)
- RecyclerView 설정 (xml, Adapter)
1. Dependency 추가
AndroidX 에 배포 노트를 확인 하시면 최신 버젼 부터 history를 알수 있습니다.
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=en
- Gradle (module app) 코틀린 에서는 kapt 를 annotationProcessor 대신 사용
- apply plugin: 'kotlin-kapt'
- kapt "androidx.room:room-compiler:2.2.5" For Kotlin use kapt instead of 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로 접근 해야 합니다.
- 이유는 ROOM DB를 Main Thread 에서 접근하려고 하면 크래쉬가 발생 합니다.
ViewModel 생성
- viewModel 라이브러리는 내부적으로 프래그먼트를 사용합니다
- 최초 viewModel을 생성할 때 , ViewModelProvider는 HolderFragment라 명명된프래그먼트를 생성하고
- 이 프래그먼트에는 setRetainInstance(true)가 설정됩니다.
- viewModel은 유보된 프래그먼트(RetainFragment)의 연장선이라 할 수 있습니다.
- 최초 viewModel을 생성할 때 , ViewModelProvider는 HolderFragment라 명명된프래그먼트를 생성하고
- viewModel은 추상클래스(abstract)로 상속하는것만으로 viewModel을 만들 수 있습니다 .
- 추상클래스이므로 객체를 그냥 생성할수 없습니다.
- ViewModelProvider를 통해 객체를 생성해주어야합니다.
- 생명주기 함수 onCleared()가 존재합니다.
- 커스텀 생성자를 가지려면, ViewModel은 ViewModelProvider.Factory 인터페이스를 사용해야합니다.
123456789101112131415161718192021222324class 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) -> Unit, val 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 연결
1234567891011121314151617181920212223242526272829303132333435363738394041class MainActivity : AppCompatActivity() {private lateinit var contactViewModel : ContactViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// 리싸이클러 어뎁터 설정// Set contactItemClick & contactItemLongClick lambdaval 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 = adaptermain_log_view.layoutManager = lmmain_log_view.setHasFixedSize(true)}//다이얼로그 for deleting contactViewModelprivate fun deleteDialog(contact: Contact) {val builder = AlertDialog.Builder(this)builder.setMessage("Delete selected contactLog?").setNegativeButton("NO") { _, _ -> }.setPositiveButton("YES") { _, _ ->contactViewModel.delete(contact)}builder.show()}
cs
이상입니다.
'안드로이드 프로그래밍[Kotiln Code] > Database(Room, Realm, Live Data )' 카테고리의 다른 글
Live Data + View Model + Data Binding 설명 (0) | 2020.04.02 |
---|---|
[Live Data] 안드로이드 라이브 데이터 1 (0) | 2020.04.02 |