Develop/Kotlin

[kotlin] 코틀린 Android MVVM 패턴 구현

JunJangE 2021. 8. 30. 13:54

이전에 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 패턴을 구현해보자.

MVVM 패턴

Room DB에 대한 내용은 다음 링크를 통해 확인해보면 좋을 것 같다.

 

[kotlin] 코틀린 Android Room DB 활용

이번에는 안드로이드 앱을 개발하면서 DB를 사용할 때 활용할 수 있는 Room DB에 대해서 알아보자. Room DB(DataBase) - AAC(Android Architecture Component) 중 하나이다. - ACC는 앱을 견고하고, 실험 가능하고..

fre2-dom.tistory.com

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>

위 코드를 모두 작성하고 실행하면 다음 결과 화면과 같이 나오는 것을 확인할 수 있다.

<결과 영상>

참고

 

ViewModel을 능가하는 LiveData — Transformations와 MediatorLiveData를 사용하는 반응형 패턴

여러 해 동안 안드로이드 개발자들 사이에서 반응형 구조(Reactive Architecture)는 꾸준히 인기있던 주제였는데 보통 RxJava를 예시로 설명합니다(아래의 Rx 섹션 참조). 반응형 프로그래밍은 데이터가

developers-kr.googleblog.com

 

LiveData 개요  |  Android 개발자  |  Android Developers

LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.

developer.android.com

 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

github

 

GitHub - junjange/Kotlin-Learning: 코틀린 학습

코틀린 학습. Contribute to junjange/Kotlin-Learning development by creating an account on GitHub.

github.com