이전에 Room DB를 통해 텍스트를 입력하면 텍스트가 저장되어 UI에 뿌려지는 방식을 구현해봤다.
이번에는 MVVM 패턴에 대해서 알아보자.
MVVM 패턴이란 Model, View, ViewModel을 가리키며 ViewModel을 사용하여 Model과 View를 분리하는 것이 특징이다. View는 xml, Activity, Fragment 등이 되고, Model은 내부/ 외부 DB가 된다.
Repository로 Room이나 다른 웹 서비스에 접근하고 ViewModel에 collback 해주면, ViewModel이 observer에 response해준다. (LiveData를 통해 View를 관찰)
그럼 LiveData와 이전에 만들었던 Room을 통해 MVVM 패턴을 구현해보자.
Room DB에 대한 내용은 다음 링크를 통해 확인해보면 좋을 것 같다.
Gradle 설정
우선 프로젝트를 하나 생성하고 build.gradle(Module) 파일에 다음 코드를 적고 우측 상단에 있는 'Sync Now'를 누른다.
이전에 위 링크를 통해 RoomDB를 구현했다면 똑같은 프로젝트를 가지고 코드를 수정하면서 실행해도 된다.
plugins {
...
}
apply plugin: 'kotlin-kapt'
// 뷰 바인딩
buildFeatures {
viewBinding true
}
// 데이터 바인딩
buildFeatures {
dataBinding true
}
}
dependencies {
...
// Room components
def roomVersion = '2.2.5'
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
// Kotlin components
def coroutines = '1.3.4'
def kotlin_version = "1.3.72"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
// Lifecycle components
def archLifecycleVersion = '2.2.0'
implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.room:room-runtime:2.3.0'
}
Entity 생성
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "user_table")
data class Entity (
@PrimaryKey(autoGenerate = true)// PrimaryKey 를 자동적으로 생성
val id: Int,
var number1: String,
)
DAO 생성
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface DAO {
// 데이터 베이스 불러오기
@Query("SELECT * from user_table ORDER BY id ASC")
fun getAll(): LiveData<List<Entity>>
// 데이터 추가
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entity: Entity)
// 데이터 전체 삭제
@Query("DELETE FROM user_table")
fun deleteAll()
// 데이터 업데이트
@Update
fun update(entity: Entity);
// 데이터 삭제
@Delete
fun delete(entity: Entity);
}
- return 값 List<Entitiy>를 LiveData로 감싸주어 변화를 감지한다.
- onConflict = OnConflictStrategy.IGNORE 을 통해 같은 값이 들어왔을 때 무시한다.
Constants | |
ABORT | 트랜잭션을 중단하기 위한 OnConflict 전략 상수입니다. |
FAIL | 트랜잭션을 실패하는 OnConflict 전략 상수입니다. |
IGNORE | 충돌을 무시하는 OnConflict 전략 상수입니다. |
REPLACE | OnConflict 전략 상수는 이전 데이터를 교체하고 트랜잭션을 계속합니다. |
ROLLBACK | 트랜잭션을 롤백하기 위한 OnConflict 전략 상수 |
AppDatabase 생성
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import kotlinx.coroutines.CoroutineScope
@Database(entities = [Entity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun dao(): DAO
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database"
) .fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
여기까지 완료했다면 Model부분이 끝이난다.
이제 View 와 Model을 연결하는 ViewModel을 만들고 ViewModel에서 Model에 접근하기 위해 Repositoryd와 RecyclerViewAdapter를 만들어보자.
Repository 생성
import androidx.lifecycle.LiveData
class Repository(mDatabase: AppDatabase) {
private val dao = mDatabase.dao()
val allUsers: LiveData<List<Entity>> = dao.getAll()
companion object {
private var sInstance: Repository? = null
fun getInstance(database: AppDatabase): Repository {
return sInstance
?: synchronized(this) {
val instance = Repository(database)
sInstance = instance
instance
}
}
}
suspend fun insert(entity: Entity) {
dao.insert(entity)
}
suspend fun delete(entity: Entity) {
dao.delete(entity)
}
}
MainViewModel 생성
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainViewModel(application: Application) : AndroidViewModel(application) {
val Repository: Repository =
Repository(AppDatabase.getDatabase(application, viewModelScope))
var allUsers: LiveData<List<Entity>> = Repository.allUsers
fun insert(entity: Entity) = viewModelScope.launch(Dispatchers.IO) {
Repository.insert(entity)
}
fun deleteAll(entity: Entity) = viewModelScope.launch(Dispatchers.IO) {
Repository.delete(entity)
}
fun getAll(): LiveData<List<Entity>>{
return allUsers
}
}
RecyclerViewAdapter 생성
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.recycler_item.view.*
class RecyclerViewAdapter internal constructor(context: Context, var onDeleteListener : MainViewModel)
: RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>()
{
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var users = emptyList<Entity>() // Cached copy of words
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val num1 = itemView.text
val deletebutton = itemView.delete_button
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = inflater.inflate(R.layout.recycler_item, parent, false)
return ViewHolder(itemView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val num = users[position]
holder.num1.text = num.number1
holder.deletebutton.setOnClickListener(View.OnClickListener {
onDeleteListener.deleteAll(num)
return@OnClickListener
})
}
internal fun setUsers(users: List<Entity>) {
this.users = users
notifyDataSetChanged()
}
override fun getItemCount() = users.size
}
MainActivity(activity)
MainActivity 경우 activity와 fragment 두가지 경우를 생각해서 코드를 작성한다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.junjange.roomdb.databinding.ActivityMainBinding
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.viewModel = viewModel
val mAdapter = RecyclerViewAdapter(this, viewModel)
recyclerview.apply {
adapter = mAdapter
layoutManager = LinearLayoutManager(applicationContext)
}
viewModel.allUsers.observe(this, Observer { users ->
// Update the cached copy of the users in the adapter.
users?.let { mAdapter.setUsers(it) }
})
// 버튼 클릭시 edit에 적혀있는 텍스트를 db에 저장
button.setOnClickListener{
lifecycleScope.launch(Dispatchers.IO) {
viewModel.insert(
Entity(
0, edit.text.toString())
)
}
}
}
}
MainActivity(fragment)
class StarFragment : Fragment() {
private val viewModel: MainViewModel by viewModels()
fun newInstance() : StarFragment {
return StarFragment()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentStarBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_star, container, false)
activity?.let {
binding.viewModel= viewModel
binding.lifecycleOwner = this
}
return binding.root
}
override fun onViewCreated(itemView: View, savedInstanceState: Bundle?) {
super.onViewCreated(itemView, savedInstanceState)
val mAdapter = RecyclerViewAdapter(requireContext(),viewModel)
recyclerview.apply {
adapter = mAdapter
layoutManager = LinearLayoutManager(requireContext())
}
viewModel.allUsers.observe(viewLifecycleOwner, Observer { users ->
// Update the cached copy of the users in the adapter.
users?.let { mAdapter.setUsers(it) }
})
}
}
activity_main.xml(Data Binding)
<?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.junjange.roomdb.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="10dp">
<EditText
android:layout_weight="1"
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName"
android:hint="할 일"/>
<Button
android:text="button"
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="match_parent">
</Button>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="18dp"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/btnAdd"
app:layout_constraintStart_toEndOf="@+id/btnAdd"
app:layout_constraintTop_toBottomOf="@+id/add_button"
tools:listitem="@layout/recycler_item" />
</LinearLayout>
</layout>
recycler_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/text"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="20sp" />
<Button
android:id="@+id/delete_button"
android:layout_gravity="right"
android:text="삭제"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
위 코드를 모두 작성하고 실행하면 다음 결과 화면과 같이 나오는 것을 확인할 수 있다.
참고
github
'Develop > Kotlin' 카테고리의 다른 글
[kotlin] 코틀린 Android local.properties에 API Key 숨기고 활용하는 방법 (0) | 2022.04.19 |
---|---|
[kotlin] 코틀린 Android 코루틴(Coroutine) 활용 (0) | 2021.11.19 |
[Android] 안드로이드 앱 배포 시 필요한 이미지 (0) | 2021.08.27 |
[kotlin] 코틀린 Android Room DB 활용 (2) | 2021.08.26 |
[kotlin] 코틀린 Android 현재 위치를 GPS 좌표로 구하기 (0) | 2021.08.13 |