AAC 실습
이전에 포스팅했던 자료를 토대로 MVVM과 Room DB를 사용해 AAC기반의 RecyclerView를 구성해보는 실습을 하였다.
Android AAC (Android Architecture Component)
Android Architecture Component 테스트와 유지보수가 쉬운 앱을 디자인할 수 있도록 돕는 라이브러리의 모음 ViewModel 앱의 Lifecycle을 고려하여 UI 관련 데이터를 저장하고 관리하는 컴포넌트 UI Controller..
bumjae.tistory.com
[Android] MVVM (Model, View, ViewModel)
MVVM이란? MVVM( Model View ViewModel )은 Microsoft 설계자 인 Cooper & Peters에 의해 탄생된 디자인 패턴 John Gossman에 의해 2005년 발표 되어 클라이언트 기반의 플랫폼에서 조금씩 사용되기 시작 Model ..
bumjae.tistory.com
Android Room Database / 룸 데이터 베이스
Google에서 제공하는 ORM(Object-relational mapping) SQLite에 대한 추상화 레이어를 제공하여 원활한 데이터베이스 액세스를 지원하며 SQLite를 완벽히 활용함 실행 기기에 앱 데이터 캐시를 만들고 네트워
bumjae.tistory.com
dependency
사실상 Room을 제외하고 다 지워도 사용이 가능해 다 지우고 구성하였다.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.activity:activity-ktx:1.3.1' //by viewModels() // SdkVersion 30 사용으로 인한 1.3.1
//Room
implementation "androidx.room:room-runtime:2.4.0-alpha03"
kapt"androidx.room:room-compiler:2.4.0-alpha03"
Room을 이용해 DB 생성 및 삽입 삭제
Entity(개체)
관련이 있는 속성들이 모여 하나의 정보 단위를 이룬 것으로 Database내에 테이블을 클래스로 나타내주는 것
@Entity를 통해 생성해주고 기본적으로 클래스 이름이 테이블 이름이 되지만 별도로 설정을 해주고 싶다면
@Entity옆에 (tableName = "테이블 이름")을 통해 정해주면 된다.
@PrimaryKey는 키 값이기 때문에 유일한(Unique) 값이어야 한다.
직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.
그리고 따로 칼럼명을 지정하고 싶다면 @ColumnInfo(name="칼럼명")을 사용하면 된다.
@Entity
data class Test(
//autoGenerate null을 받으면 ID 값을 자동으로 할당
@PrimaryKey(autoGenerate = true)
var id : Int?,
@ColumnInfo(name ="title")
var title: String,
@ColumnInfo(name="description")
var description: String,
@ColumnInfo(name="imageUrl")
var imageUrl: String
){
constructor() : this(null,"","","")
}
DAO
Data Access Object의 줄임말로, 데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.
@Dao 어노테이션을 붙이고 그 안에 메서드를 만들면 된다.
- DAO 속성설명
@Insert | 데이터 삽입 |
@Delete | 데이터 삭제 |
@Update | 데이터 수정 |
이 외에 원하는 명령을 하고싶다면 @Query("원하는 Sql문")을 사용하면 된다.
@Insert로 데이터를 추가 할 때 같은 primary key 값을 가진 객체를 추가하면 에러가 발생하기 때문에 충돌처리방식을 통해 에러가 나는 것을 방지할 수 있다.
- 충돌처리 방식 설명
OnConflictStrategy.ABORT | 충돌이 발생할 경우 처리 중단 |
OnConflictStrategy.FAIL | 충돌이 발생할 경우 실패 처리 |
OnConflictStrategy.IGNORE | 충돌이 발생할 경우 무시 |
OnConflictStrategy.REPLACE | 충돌이 발생할 경우 덮어쓰기 |
OnConflictStrategy.ROLLBACK | 충돌이 발생할 경우 이전으로 되돌리기 |
@Dao
interface TestDao {
@Query("SELECT * FROM Test")
fun getAll(): LiveData<List<Test>>
@Insert(onConflict = OnConflictStrategy.REPLACE) // 중복 ID일 경우 교체
fun insert(todo: Test)
@Update
fun update(todo: Test)
@Delete
fun delete(todo: Test)
}
Database
@Database 어노테이션으로 데이터베이스임을 표시한다.
RoomDatabase 클래스를 상속받고, 데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 추상 클래스를 만들어 줘야 한다.
version은 Entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할을 한다.
처음 데이터베이스를 생성하는 상황이라면 그냥 1을 넣어주면 된다.
만약 구조가 바뀌었는데 버전이 같다면 에러가 뜨며 디버깅이 되지 않는다.
@Database(entities=[Entity 명::class], version=버전정보)
여러개의 Entity를 사용시 배열 형식으로 작성
@Database(entities=arrayOf(Entity 명::class, Entity 명::class...), version=버전정보)
@Database(entities = [Test::class], version = 1)
abstract class TestDatabase : RoomDatabase() {
abstract fun testDao() : TestDao
companion object{
private var Instance : TestDatabase? = null
fun getInstance(context: Context): TestDatabase?{
if (Instance == null) {
synchronized(TestDatabase::class) { //synchronized: 여러 스레드가 동시에 접근 불가. 동기적으로 접근
Instance = Room.databaseBuilder(
context.applicationContext,
TestDatabase::class.java,
"test")
.build()
}
}
return Instance
}
}
}
MVVM 패턴을 사용, Test Data Class를 만들고 Room을 사용하여 DB 저장 및 삭제
Model
데이터에 대한 작업을 마치면 ViewModel에게 결과를 알려준다.
여기서는 LiveData를 통해 값이 변화하는지 알려주게 된다.
class TestRepository(application: Application) {
private val testDatabase: TestDatabase = TestDatabase.getInstance(application)!!
private val testDao: TestDao = testDatabase.testDao()
fun getAll(): LiveData<List<Test>> {
return testDao.getAll()
}
fun insert(test: Test){
testDao.insert(test)
}
fun delete(test: Test){
testDao.delete(test)
}
fun update(test: Test){
testDao.update(test)
}
}
ViewModel
Model에서 가져온 데이터를 UI에 필요한 정보로 가공, View가 가져갈 수 있도록 해당 데이터 변경에 대한 "notify" 를 보낸다.
onCleared는 ViewModel을 더 이상 사용하지 않거나, ViewModel이 관찰하고 있는 데이터를 초기화해야 할 때 사용한다.
class RoomDbViewModel(application: Application) : ViewModel() {
private val repository = TestRepository(application)
fun getAll():LiveData<List<Test>>{
return repository.getAll()
}
fun insert(test: Test){
repository.insert(test)
}
fun delete(test: Test){
repository.delete(test)
}
override fun onCleared() {
super.onCleared()
}
}
View
AddActivity
이름과 설명 값을 입력하고 저장버튼을 눌렀을 때 값이 비어있지 않다면 Viewmodel의 insert를 통해 값들을 넣어준다. 이를 통해 View에서 ViewModel에게 Action을 전달하게 된 것
class AddActivity : AppCompatActivity() {
private val viewModel: RoomDbViewModel by viewModels{
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T =
RoomDbViewModel(application) as T
}
}
private var id: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add)
val binding = DataBindingUtil.setContentView<ActivityAddBinding>(this, R.layout.activity_add)
if(intent!=null
&& intent.hasExtra(EXTRA_TODO_TITLE)
&& intent.hasExtra(EXTRA_TODO_DESC)
&& intent.hasExtra(EXTRA_TODO_ID)
){
binding.addEdittextTitle.setText((intent.getStringExtra(EXTRA_TODO_TITLE)))
binding.addEdittextDescript.setText(intent.getStringExtra(EXTRA_TODO_DESC))
id=intent.getIntExtra(EXTRA_TODO_ID, -1)
}
binding.addButton.setOnClickListener {
if(binding.addEdittextTitle.text.isNotEmpty() && binding.addEdittextDescript.text.isNotEmpty()){
val test = Test(id, binding.addEdittextTitle.text.toString(), binding.addEdittextDescript.text.toString(),"")
lifecycleScope.launch(Dispatchers.IO){viewModel.insert(test)}
finish()
}else{
Toast.makeText(this,"Please enter title and desc", Toast.LENGTH_LONG).show()
}
}
}
companion object{
const val EXTRA_TODO_TITLE = "EXTRA_TODO_TITLE"
const val EXTRA_TODO_DESC = "EXTRA_TODO_DESC"
const val EXTRA_TODO_ID = "EXTRA_TODO_ID"
}
}
AddActivity의 레이아웃 파일 : activity_add.xml
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".retrofit_recycler.AddActivity">
<TextView
android:id="@+id/add_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="일정"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.41000003" />
<EditText
android:id="@+id/add_edittext_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintBottom_toBottomOf="@+id/add_tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/add_tv_title"
app:layout_constraintTop_toTopOf="@+id/add_tv_title" />
<TextView
android:id="@+id/add_tv_descript"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="설명"
app:layout_constraintStart_toStartOf="@+id/add_tv_title"
app:layout_constraintTop_toBottomOf="@+id/add_tv_title" />
<EditText
android:id="@+id/add_edittext_descript"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:ems="10"
app:layout_constraintBottom_toBottomOf="@+id/add_tv_descript"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/add_edittext_title"
app:layout_constraintTop_toTopOf="@+id/add_tv_descript" />
<Button
android:id="@+id/add_button"
android:layout_width="0dp"
android:layout_height="49dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
RoomDbActivity
observe를 통해서 ViewModel의 getAll(조회)에 변화가 생기면 감지해준다.
class RoomDbActivity : AppCompatActivity() {
lateinit var binding: ActivityRoomDbBinding
private val viewModel: RoomDbViewModel by viewModels{
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T =
RoomDbViewModel(application) as T
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_room_db)
binding.viewModel = viewModel
val adapter = TestAdapter({ test -> deleteDialog(test)}, { test -> deleteDialog(test)})
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(applicationContext)
viewModel.getAll().observe(this, Observer {
adapter.setTestItemList(it)
})
binding.mainButton.setOnClickListener {
val intent = Intent(this, AddActivity::class.java)
startActivity(intent)
}
}
private fun deleteDialog(test: Test) {
val builder = AlertDialog.Builder(this)
builder.setMessage("삭제/편집 하시겠습니까?")
.setNegativeButton("취소") { _, _ -> }
.setPositiveButton("편집") { _, _ ->
val intent = Intent(this, AddActivity::class.java)
intent.putExtra(AddActivity.EXTRA_TODO_TITLE, test.title)
intent.putExtra(AddActivity.EXTRA_TODO_DESC, test.description)
intent.putExtra(AddActivity.EXTRA_TODO_ID, test.id)
startActivity(intent)
}.setNeutralButton("삭제"){_, _ ->
lifecycleScope.launch(Dispatchers.IO){viewModel.delete(test)}
}
builder.show()
}
}
RoomDbActivity의 레이아웃 파일 activity_room_db.xml
<?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="viewModel"
type="com.example.jetpacksemina.room.viewmodel.RoomDbViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".room.view.RoomDbActivity">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Hello Room!"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/main_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textview"
tools:listitem="@layout/item_test" />
<Button
android:id="@+id/main_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
RecyclerViewAdapter
Data List들로 RecyclerView를 구성
생성자에는 클릭이벤트를 바로 줄 수 있도록 클릭 Listener 람다 함수를 매개 변수로 설정
(기본으로 하던것처럼 interface를 사용해도 됨)
Android RecyclerView Click Event(안드로이드 리사이클러뷰 클릭 이벤트)
RecyclerView 클릭 이벤트 Recycler View는 List VIew와 다르게 뷰에서 클릭 이벤트를 다루지 않고 아이템 뷰에서의 이벤트를 통해 처리한다. 따라서 뷰 홀더가 생성되는 시점에 이벤트 리스너를 추가한
bumjae.tistory.com
RecyclerView 의 Item에도 DataBinding도 사용해 onCreateViewHolder에서 DataBindingUtil로 Layout Binding
ViewHolder의 생성자에 Binding을 받아 DataBinding 적용
class TestAdapter(val todoItemClick: (Test) -> Unit, val todoItemLongClick: (Test) -> Unit): RecyclerView.Adapter<TestAdapter.ViewHolder>() {
private var testList:List<Test> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DataBindingUtil.inflate<ItemTestBinding>(LayoutInflater.from(parent.context), R.layout.item_test, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(testList[position])
}
override fun getItemCount(): Int {
return testList.size
}
inner class ViewHolder(val binding: ItemTestBinding): RecyclerView.ViewHolder(binding.root){
fun bind(test: Test) {
binding.test = test
binding.root.setOnClickListener {
todoItemClick(test)
}
binding.root.setOnLongClickListener {
todoItemLongClick(test)
true
}
}
}
fun setTestItemList(test:List<Test>){
this.testList = test
notifyDataSetChanged()
}
}
Item으로 사용될 iten_test.xml
<?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="test"
type="com.example.jetpacksemina.room.model.Test" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/item_tv_initial"
android:layout_width="66dp"
android:layout_height="0dp"
android:gravity="center"
android:padding="4dp"
app:imageUrl="@{test.imageUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/item_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:textSize="20dp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/item_tv_descript"
app:layout_constraintStart_toEndOf="@+id/item_tv_initial"
app:layout_constraintTop_toTopOf="parent"
tools:text="@{test.title.toString()}" />
<TextView
android:id="@+id/item_tv_descript"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/item_tv_title"
app:layout_constraintTop_toBottomOf="@+id/item_tv_title"
tools:text="@{test.description.toString()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
BindingAdpater
Databinding 으로 xml에서 사용할 함수 생성
object BindingAdapter {
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(imageView: ImageView, url: String){
Glide.with(imageView.context).load(url).error(R.drawable.ic_launcher_background).into(imageView)
}
@BindingAdapter("listData")
@JvmStatic
fun bindData(recyclerView: RecyclerView, test: List<Test>?){
val adapter = recyclerView.adapter as TestAdapter
if (test != null) {
adapter.setTestItemList(test)
}
}
}
결과화면

'Android > Reference' 카테고리의 다른 글
Navigation, BottomNavigation 클릭시 Fragment 재생성 막기 (0) | 2022.02.17 |
---|---|
LiveData setValue(), postValue() 차이 (0) | 2022.02.16 |
AAC (Android Architecture Component) (1) | 2022.02.07 |
string.xml 에 %d, %s 사용 / Databinding 에 StringFormat 적용 (1) | 2022.01.20 |
WorkManager / 워크매니저 (0) | 2022.01.04 |
AAC 실습
이전에 포스팅했던 자료를 토대로 MVVM과 Room DB를 사용해 AAC기반의 RecyclerView를 구성해보는 실습을 하였다.
Android AAC (Android Architecture Component)
Android Architecture Component 테스트와 유지보수가 쉬운 앱을 디자인할 수 있도록 돕는 라이브러리의 모음 ViewModel 앱의 Lifecycle을 고려하여 UI 관련 데이터를 저장하고 관리하는 컴포넌트 UI Controller..
bumjae.tistory.com
[Android] MVVM (Model, View, ViewModel)
MVVM이란? MVVM( Model View ViewModel )은 Microsoft 설계자 인 Cooper & Peters에 의해 탄생된 디자인 패턴 John Gossman에 의해 2005년 발표 되어 클라이언트 기반의 플랫폼에서 조금씩 사용되기 시작 Model ..
bumjae.tistory.com
Android Room Database / 룸 데이터 베이스
Google에서 제공하는 ORM(Object-relational mapping) SQLite에 대한 추상화 레이어를 제공하여 원활한 데이터베이스 액세스를 지원하며 SQLite를 완벽히 활용함 실행 기기에 앱 데이터 캐시를 만들고 네트워
bumjae.tistory.com
dependency
사실상 Room을 제외하고 다 지워도 사용이 가능해 다 지우고 구성하였다.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.activity:activity-ktx:1.3.1' //by viewModels() // SdkVersion 30 사용으로 인한 1.3.1
//Room
implementation "androidx.room:room-runtime:2.4.0-alpha03"
kapt"androidx.room:room-compiler:2.4.0-alpha03"
Room을 이용해 DB 생성 및 삽입 삭제
Entity(개체)
관련이 있는 속성들이 모여 하나의 정보 단위를 이룬 것으로 Database내에 테이블을 클래스로 나타내주는 것
@Entity를 통해 생성해주고 기본적으로 클래스 이름이 테이블 이름이 되지만 별도로 설정을 해주고 싶다면
@Entity옆에 (tableName = "테이블 이름")을 통해 정해주면 된다.
@PrimaryKey는 키 값이기 때문에 유일한(Unique) 값이어야 한다.
직접 지정해도 되지만 autoGenerate를 true로 주면 자동으로 값을 생성한다.
그리고 따로 칼럼명을 지정하고 싶다면 @ColumnInfo(name="칼럼명")을 사용하면 된다.
@Entity
data class Test(
//autoGenerate null을 받으면 ID 값을 자동으로 할당
@PrimaryKey(autoGenerate = true)
var id : Int?,
@ColumnInfo(name ="title")
var title: String,
@ColumnInfo(name="description")
var description: String,
@ColumnInfo(name="imageUrl")
var imageUrl: String
){
constructor() : this(null,"","","")
}
DAO
Data Access Object의 줄임말로, 데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.
@Dao 어노테이션을 붙이고 그 안에 메서드를 만들면 된다.
- DAO 속성설명
@Insert | 데이터 삽입 |
@Delete | 데이터 삭제 |
@Update | 데이터 수정 |
이 외에 원하는 명령을 하고싶다면 @Query("원하는 Sql문")을 사용하면 된다.
@Insert로 데이터를 추가 할 때 같은 primary key 값을 가진 객체를 추가하면 에러가 발생하기 때문에 충돌처리방식을 통해 에러가 나는 것을 방지할 수 있다.
- 충돌처리 방식 설명
OnConflictStrategy.ABORT | 충돌이 발생할 경우 처리 중단 |
OnConflictStrategy.FAIL | 충돌이 발생할 경우 실패 처리 |
OnConflictStrategy.IGNORE | 충돌이 발생할 경우 무시 |
OnConflictStrategy.REPLACE | 충돌이 발생할 경우 덮어쓰기 |
OnConflictStrategy.ROLLBACK | 충돌이 발생할 경우 이전으로 되돌리기 |
@Dao
interface TestDao {
@Query("SELECT * FROM Test")
fun getAll(): LiveData<List<Test>>
@Insert(onConflict = OnConflictStrategy.REPLACE) // 중복 ID일 경우 교체
fun insert(todo: Test)
@Update
fun update(todo: Test)
@Delete
fun delete(todo: Test)
}
Database
@Database 어노테이션으로 데이터베이스임을 표시한다.
RoomDatabase 클래스를 상속받고, 데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 추상 클래스를 만들어 줘야 한다.
version은 Entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할을 한다.
처음 데이터베이스를 생성하는 상황이라면 그냥 1을 넣어주면 된다.
만약 구조가 바뀌었는데 버전이 같다면 에러가 뜨며 디버깅이 되지 않는다.
@Database(entities=[Entity 명::class], version=버전정보)
여러개의 Entity를 사용시 배열 형식으로 작성
@Database(entities=arrayOf(Entity 명::class, Entity 명::class...), version=버전정보)
@Database(entities = [Test::class], version = 1)
abstract class TestDatabase : RoomDatabase() {
abstract fun testDao() : TestDao
companion object{
private var Instance : TestDatabase? = null
fun getInstance(context: Context): TestDatabase?{
if (Instance == null) {
synchronized(TestDatabase::class) { //synchronized: 여러 스레드가 동시에 접근 불가. 동기적으로 접근
Instance = Room.databaseBuilder(
context.applicationContext,
TestDatabase::class.java,
"test")
.build()
}
}
return Instance
}
}
}
MVVM 패턴을 사용, Test Data Class를 만들고 Room을 사용하여 DB 저장 및 삭제
Model
데이터에 대한 작업을 마치면 ViewModel에게 결과를 알려준다.
여기서는 LiveData를 통해 값이 변화하는지 알려주게 된다.
class TestRepository(application: Application) {
private val testDatabase: TestDatabase = TestDatabase.getInstance(application)!!
private val testDao: TestDao = testDatabase.testDao()
fun getAll(): LiveData<List<Test>> {
return testDao.getAll()
}
fun insert(test: Test){
testDao.insert(test)
}
fun delete(test: Test){
testDao.delete(test)
}
fun update(test: Test){
testDao.update(test)
}
}
ViewModel
Model에서 가져온 데이터를 UI에 필요한 정보로 가공, View가 가져갈 수 있도록 해당 데이터 변경에 대한 "notify" 를 보낸다.
onCleared는 ViewModel을 더 이상 사용하지 않거나, ViewModel이 관찰하고 있는 데이터를 초기화해야 할 때 사용한다.
class RoomDbViewModel(application: Application) : ViewModel() {
private val repository = TestRepository(application)
fun getAll():LiveData<List<Test>>{
return repository.getAll()
}
fun insert(test: Test){
repository.insert(test)
}
fun delete(test: Test){
repository.delete(test)
}
override fun onCleared() {
super.onCleared()
}
}
View
AddActivity
이름과 설명 값을 입력하고 저장버튼을 눌렀을 때 값이 비어있지 않다면 Viewmodel의 insert를 통해 값들을 넣어준다. 이를 통해 View에서 ViewModel에게 Action을 전달하게 된 것
class AddActivity : AppCompatActivity() {
private val viewModel: RoomDbViewModel by viewModels{
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T =
RoomDbViewModel(application) as T
}
}
private var id: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add)
val binding = DataBindingUtil.setContentView<ActivityAddBinding>(this, R.layout.activity_add)
if(intent!=null
&& intent.hasExtra(EXTRA_TODO_TITLE)
&& intent.hasExtra(EXTRA_TODO_DESC)
&& intent.hasExtra(EXTRA_TODO_ID)
){
binding.addEdittextTitle.setText((intent.getStringExtra(EXTRA_TODO_TITLE)))
binding.addEdittextDescript.setText(intent.getStringExtra(EXTRA_TODO_DESC))
id=intent.getIntExtra(EXTRA_TODO_ID, -1)
}
binding.addButton.setOnClickListener {
if(binding.addEdittextTitle.text.isNotEmpty() && binding.addEdittextDescript.text.isNotEmpty()){
val test = Test(id, binding.addEdittextTitle.text.toString(), binding.addEdittextDescript.text.toString(),"")
lifecycleScope.launch(Dispatchers.IO){viewModel.insert(test)}
finish()
}else{
Toast.makeText(this,"Please enter title and desc", Toast.LENGTH_LONG).show()
}
}
}
companion object{
const val EXTRA_TODO_TITLE = "EXTRA_TODO_TITLE"
const val EXTRA_TODO_DESC = "EXTRA_TODO_DESC"
const val EXTRA_TODO_ID = "EXTRA_TODO_ID"
}
}
AddActivity의 레이아웃 파일 : activity_add.xml
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".retrofit_recycler.AddActivity">
<TextView
android:id="@+id/add_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="일정"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.41000003" />
<EditText
android:id="@+id/add_edittext_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintBottom_toBottomOf="@+id/add_tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/add_tv_title"
app:layout_constraintTop_toTopOf="@+id/add_tv_title" />
<TextView
android:id="@+id/add_tv_descript"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="설명"
app:layout_constraintStart_toStartOf="@+id/add_tv_title"
app:layout_constraintTop_toBottomOf="@+id/add_tv_title" />
<EditText
android:id="@+id/add_edittext_descript"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:ems="10"
app:layout_constraintBottom_toBottomOf="@+id/add_tv_descript"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/add_edittext_title"
app:layout_constraintTop_toTopOf="@+id/add_tv_descript" />
<Button
android:id="@+id/add_button"
android:layout_width="0dp"
android:layout_height="49dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
RoomDbActivity
observe를 통해서 ViewModel의 getAll(조회)에 변화가 생기면 감지해준다.
class RoomDbActivity : AppCompatActivity() {
lateinit var binding: ActivityRoomDbBinding
private val viewModel: RoomDbViewModel by viewModels{
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T =
RoomDbViewModel(application) as T
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_room_db)
binding.viewModel = viewModel
val adapter = TestAdapter({ test -> deleteDialog(test)}, { test -> deleteDialog(test)})
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(applicationContext)
viewModel.getAll().observe(this, Observer {
adapter.setTestItemList(it)
})
binding.mainButton.setOnClickListener {
val intent = Intent(this, AddActivity::class.java)
startActivity(intent)
}
}
private fun deleteDialog(test: Test) {
val builder = AlertDialog.Builder(this)
builder.setMessage("삭제/편집 하시겠습니까?")
.setNegativeButton("취소") { _, _ -> }
.setPositiveButton("편집") { _, _ ->
val intent = Intent(this, AddActivity::class.java)
intent.putExtra(AddActivity.EXTRA_TODO_TITLE, test.title)
intent.putExtra(AddActivity.EXTRA_TODO_DESC, test.description)
intent.putExtra(AddActivity.EXTRA_TODO_ID, test.id)
startActivity(intent)
}.setNeutralButton("삭제"){_, _ ->
lifecycleScope.launch(Dispatchers.IO){viewModel.delete(test)}
}
builder.show()
}
}
RoomDbActivity의 레이아웃 파일 activity_room_db.xml
<?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="viewModel"
type="com.example.jetpacksemina.room.viewmodel.RoomDbViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".room.view.RoomDbActivity">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Hello Room!"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/main_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textview"
tools:listitem="@layout/item_test" />
<Button
android:id="@+id/main_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
RecyclerViewAdapter
Data List들로 RecyclerView를 구성
생성자에는 클릭이벤트를 바로 줄 수 있도록 클릭 Listener 람다 함수를 매개 변수로 설정
(기본으로 하던것처럼 interface를 사용해도 됨)
Android RecyclerView Click Event(안드로이드 리사이클러뷰 클릭 이벤트)
RecyclerView 클릭 이벤트 Recycler View는 List VIew와 다르게 뷰에서 클릭 이벤트를 다루지 않고 아이템 뷰에서의 이벤트를 통해 처리한다. 따라서 뷰 홀더가 생성되는 시점에 이벤트 리스너를 추가한
bumjae.tistory.com
RecyclerView 의 Item에도 DataBinding도 사용해 onCreateViewHolder에서 DataBindingUtil로 Layout Binding
ViewHolder의 생성자에 Binding을 받아 DataBinding 적용
class TestAdapter(val todoItemClick: (Test) -> Unit, val todoItemLongClick: (Test) -> Unit): RecyclerView.Adapter<TestAdapter.ViewHolder>() {
private var testList:List<Test> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DataBindingUtil.inflate<ItemTestBinding>(LayoutInflater.from(parent.context), R.layout.item_test, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(testList[position])
}
override fun getItemCount(): Int {
return testList.size
}
inner class ViewHolder(val binding: ItemTestBinding): RecyclerView.ViewHolder(binding.root){
fun bind(test: Test) {
binding.test = test
binding.root.setOnClickListener {
todoItemClick(test)
}
binding.root.setOnLongClickListener {
todoItemLongClick(test)
true
}
}
}
fun setTestItemList(test:List<Test>){
this.testList = test
notifyDataSetChanged()
}
}
Item으로 사용될 iten_test.xml
<?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="test"
type="com.example.jetpacksemina.room.model.Test" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/item_tv_initial"
android:layout_width="66dp"
android:layout_height="0dp"
android:gravity="center"
android:padding="4dp"
app:imageUrl="@{test.imageUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/item_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:textSize="20dp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/item_tv_descript"
app:layout_constraintStart_toEndOf="@+id/item_tv_initial"
app:layout_constraintTop_toTopOf="parent"
tools:text="@{test.title.toString()}" />
<TextView
android:id="@+id/item_tv_descript"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/item_tv_title"
app:layout_constraintTop_toBottomOf="@+id/item_tv_title"
tools:text="@{test.description.toString()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
BindingAdpater
Databinding 으로 xml에서 사용할 함수 생성
object BindingAdapter {
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(imageView: ImageView, url: String){
Glide.with(imageView.context).load(url).error(R.drawable.ic_launcher_background).into(imageView)
}
@BindingAdapter("listData")
@JvmStatic
fun bindData(recyclerView: RecyclerView, test: List<Test>?){
val adapter = recyclerView.adapter as TestAdapter
if (test != null) {
adapter.setTestItemList(test)
}
}
}
결과화면

'Android > Reference' 카테고리의 다른 글
Navigation, BottomNavigation 클릭시 Fragment 재생성 막기 (0) | 2022.02.17 |
---|---|
LiveData setValue(), postValue() 차이 (0) | 2022.02.16 |
AAC (Android Architecture Component) (1) | 2022.02.07 |
string.xml 에 %d, %s 사용 / Databinding 에 StringFormat 적용 (1) | 2022.01.20 |
WorkManager / 워크매니저 (0) | 2022.01.04 |