AAC 실습
이전에 포스팅했던 자료를 토대로 MVVM과 Room DB를 사용해 AAC기반의 RecyclerView를 구성해보는 실습을 하였다.
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를 사용해도 됨)
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 적용 (0) | 2022.01.20 |
WorkManager / 워크매니저 (0) | 2022.01.04 |