저번에는
MVVM 패턴으로 데이터를 삽입하고 읽는 것을 간단하게 구현해보았다.
이번에는 새로 프로젝트를 만들어 데이터를 리싸이클러뷰에 출력하여 관리할 수 있는 형태로 만들어보고자 한다.
plugins {
id 'kotlin-kapt'
}
android {
buildFeatures {
dataBinding true
}
}
dependencies {
def room_version = "2.3.0"
// room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// room 코루틴
implementation "androidx.room:room-ktx:$room_version"
def lifecycle_version = "2.3.0"
// ViewModel - 라이프 사이클
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData - 데이터의 변경 사항을 알 수 있음
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}
플러그인에 kapt, android에 데이터바인딩, 종속성에 뷰모델, 라이브데이터, 룸을 추가해준다.
2021.05.12 - [안드로이드/AAC, MVVM] - [Android] Room 활용하여 데이터 저장하기
Room의 요소와 설명은 이 글에서 다뤘으므로 생성하는 부분은 간단히 작성하겠다.
@Entity(tableName = "user_table")
data class User(
// autoGenerate = true , 자동으로 PrimaryKey 생성해줌
@PrimaryKey(autoGenerate = true)
val id : Int,
val name : String,
val age : Int
)
@Dao
interface UserDao {
// OnConflictStrategy.IGNORE = 동일한 아이디가 있을 시 무시
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addUser(user : User)
@Query("SELECT * FROM user_table ORDER BY id ASC")
fun readAllData() : LiveData<List<User>>
}
/* entities = 사용할 엔티티 선언, version = 엔티티 구조 변경 시 구분해주는 역할
exportSchema = 스키마 내보내기 설정 */
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
companion object{
/* @Volatile = 접근가능한 변수의 값을 cache를 통해 사용하지 않고
thread가 직접 main memory에 접근 하게하여 동기화. */
@Volatile
private var instance : UserDatabase? = null
// 싱글톤으로 생성 (자주 생성 시 성능 손해). 이미 존재할 경우 생성하지 않고 바로 반환
fun getDatabase(context : Context) : UserDatabase? {
if(instance == null){
synchronized(UserDatabase::class){
instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user_database"
).build()
}
}
return instance
}
}
}
// 앱에서 사용하는 데이터와 그 데이터 통신을 하는 역할
class UserRepository(private val userDao: UserDao) {
val readAllData : LiveData<List<User>> = userDao.readAllData()
suspend fun addUser(user: User){
userDao.addUser(user)
}
}
// 뷰모델은 DB에 직접 접근하지 않아야함. Repository 에서 데이터 통신.
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val readAllData : LiveData<List<User>>
private val repository : UserRepository
init {
val userDao = UserDatabase.getDatabase(application)!!.userDao()
repository = UserRepository(userDao)
readAllData = repository.readAllData
}
fun addUser(user : User){
viewModelScope.launch(Dispatchers.IO) {
repository.addUser(user)
}
}
// ViewModel에 파라미터를 넘기기 위해서, 파라미터를 포함한 Factory 객체를 생성하기 위한 클래스
class Factory(val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return UserViewModel(application) as T
}
}
}
데이터베이스의 요소들을 생성하고 리스트 읽기, 추가 메소드만 미리 작성하였다.
<?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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:clickable="true"
android:focusable="true"
android:onClick="onFabClicked"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
리싸이클러뷰로 데이터를 표시해주고, Fab 으로 요소를 추가할 수 있도록 하려고 한다.
fab에 focusable 를 추가하면 버튼처럼 눌리는 효과를 준다.
추가 창은 다이얼로그로 띄울 것이며 커스텀 다이얼로그 방법은
2021.05.19 - [안드로이드/기본] - [Android] 액티비티 팝업으로 띄우기 (Custom Dialog)
에서 다루었다.
// 추가 버튼 클릭 시 onAddButtonClicked 호출 후 종료
addButton.setOnClickListener {
val name = nameText.text.toString()
var age = ageText.text.toString()
// 입력하지 않았을 때
if ( TextUtils.isEmpty(name) || TextUtils.isEmpty(age) ){
Toast.makeText(context, "데이터를 입력해주세요.", Toast.LENGTH_SHORT).show()
}
// 입력 창이 비어 있지 않을 때
else{
customDialogInterface.onAddButtonClicked(name, Integer.parseInt(age))
dismiss()
}
}
다이얼로그에서 TextUtils 으로 텍스트 입력여부를 체크하고 onAddButtonClicked 에 넘겨준다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
userViewModel = ViewModelProvider(this,UserViewModel.Factory(application)).get(UserViewModel::class.java)
}
뷰모델을 연결해주고
// 다이얼로그에서 추가버튼 클릭 됐을 때
override fun onAddButtonClicked(name : String, age : Int) {
val user = User(0,name,age)
userViewModel.addUser(user)
Toast.makeText(this, "이름 : $name , 나이 : $age 추가", Toast.LENGTH_SHORT).show()
}
다이얼로그에서 이름과 나이를 받아와 뷰모델의 addUser 메소드를 호출해준다.
실행하여 임의의 데이터를 추가해본 후,
2020.09.10 - [안드로이드/기본] - [Android] 안드로이드 스튜디오 SQLite Database 경로(위치)
위와같이 데이터베이스를 조회해보면
잘 추가된 것을 확인할 수 있다.
이제 추가한 데이터를 리싸이클러뷰로 보여주려고 한다.
<?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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp">
<TextView
android:id="@+id/id_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="1"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:text="name"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/id_text"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/age_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="age"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/name_text"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
아이템으로 쓸 레이아웃을 생성해준다.
class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
private var userList = emptyList<User>()
class MyViewHolder(val binding: LayoutItemBinding) : RecyclerView.ViewHolder(binding.root)
// 어떤 xml 으로 뷰 홀더를 생성할지 지정
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = LayoutItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return MyViewHolder(binding)
}
// 뷰 홀더에 데이터를 바인딩
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItem = userList[position]
holder.binding.idText.text = currentItem.id.toString()
holder.binding.nameText.text = currentItem.name
holder.binding.ageText.text = currentItem.age.toString()
}
// 뷰 홀더의 개수 리턴
override fun getItemCount(): Int {
return userList.size
}
// 유저 리스트 갱신
fun setData(user : List<User>){
userList = user
notifyDataSetChanged()
}
}
리싸이클러뷰에 사용할 어댑터를 생성해준다.
onBindViewHolder에서 각자의 정보를 바인딩해주고
setData 에서 유저 리스트를 갱신 해줄 것이다.
2021.05.17 - [안드로이드/기본] - [Android] 자주쓰는 RecyclerView 사용하기 (+ ViewBinding)
리싸이클러뷰에 대한 자세한 내용은 여기서 확인할 수 있다.
// 아이템을 가로로 하나씩 보여줌
binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL,false)
// 어댑터 연결
val adapter = MyAdapter()
binding.recyclerView.adapter = adapter
userViewModel.readAllData.observe(this, Observer {
adapter.setData(it)
})
리싸이클러뷰에 어댑터를 연결해주고
뷰모델의 readAllData 라이브데이터를 관찰하여 변경시 어댑터 안의 유저리스트를 갱신해주어 리싸이클러뷰를 갱신해준다.
앱 실행 시 아까 넣은 데이터가 보여지는 것을 알 수 있으며
데이터 추가시 리싸이클러뷰가 다시 갱신된다.
https://github.com/HanYeop/AndroidStudio-Practice/tree/master/RoomEx
참조
https://www.youtube.com/watch?v=lwAvI3WDXBY&list=PLSrm9z4zp4mEPOfZNV9O-crOhoMa0G2-o