본문 바로가기
안드로이드 공부 노트

Android Jetpack - 3편 (LiveData) MVVM 패턴을 이용한 사용법 Room + ViewBinding + LiveData 통합

by 지게요 2022. 1. 27.
728x90
반응형

이번에는 Android Jetpack - 1편에 있는 예제를 이용해 Room + ViewBinding + LiveData를 다 결합해서 MVVM 패턴으로 만들어보겠다.

MVVM패턴이 뭔지 궁금하다면 여기를 눌러서 보고 오는것이 이해가 빠를 거 같다

# LiveData란?

Android JetPack 라이브러리의 하나의 기능이다.

LiveData는 Data의 변경을 관찰 할 수 있는 Data Holder 클래스이다.

LiveData는 안드로이드 생명주기(LifeCycle)를 알고 있다.

즉, 액티비티나, 프레그먼트, 서비스 등과 같은 안드로이드 컴포넌트의 생명주기(Lifecycle)를 인식하며 그에 따라 LiveData는 활성 상태(active)일 때만 데이터를 업데이트(Update) 한다.
활성 상태란 STARTED 또는 RESUMED를 의미한다.

 

# LiveData의 장점

- 메모리 누수 없음

Observer는 Activity나 Fragment의 수명 주기를 따르며 수명 주기가 끝나면 자동으로 삭제된다. 따로 메모리를 해제하거나 하는 작업을 하지 않아도 된다는 뜻이다.

 

- 항상 최신 데이터를 유지

화면 구성이 변경되어도 데이터를 유지한다.
예를 들어, 디바이스를 회전하여 세로에서 가로로 화면이 변경될 경우에도 LiveData는 회전하기 전의 최신 상태를 즉시 받아온다.

 

- Data와 UI 간 동기화

LiveData는 Observer 패턴을 따른다. 그에 따라 LiveData는 안드로이드 생명주기에 데이터 변경이 일어날 때마다 Observer 객체에 알려준다.

그리고 이 Observer 객체를 사용하면 데이터의 변화가 일어나는 곳마다 매번 UI를 업데이트하는 코드를 작성할 필요 없이 통합적이고 확실하게 데이터의 상태와 UI를 일치시킬 수 있다.

 

- 생명주기에 대한 추가적인 handling을 하지 않아도 된다

LiveData가 안드로이드 생명주기에 따른 Observing을 자동으로 관리를 해주기 때문에 UI 컴포넌트는 그저 관련 있는 데이터를 "관찰"하기만 하면 된다.

 

# LiveData사용법

## 의존성(dependencies) 추가

dependencies{
	// ViewModel - 라이프 사이클
	implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
	// LiveData - 데이터의 변경 사항을 알 수 있음
	implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
}

MVVM패턴을 위해 ViewModel도 추가해준다. 보통 두 개가 한세 트라고 생각한다.

버전 정보는 공식문서에 나와있다.

 

## DAO클래스에서 쿼리 함수 반환 자료형 변환

@Dao
interface UserDao {
 @Query("SELECT * FROM User ORDER BY name ASC") // 테이블의 모든 값을 가져와라
    // LiveData 쓰기 전
    // fun getAll() : List<User>
    // LiveData 쓴 후
    fun getAll() : LiveData<List<User>>
}

반환 자료형이 원래 List <User>였는데 LiveData를 사용함으로써 getAll()를 호출하면 LiveData가 반환되게 LiveData <List <User>>로 바꿔 준다.

## ViewModel 클래스 생성

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.example.android_room.db.User
import com.example.android_room.db.UserDB
import com.example.android_room.db.UserDao

class MainViewModel(application: Application) : AndroidViewModel(Application()) {

    var users : LiveData<List<User>>
    private var userDao : UserDao
    init {
        val db = UserDB.getInstance(application)
        userDao = db!!.userDao()
        users = db.userDao().getAll()
    }
    fun insert(user : User){
        userDao.insert(user)
    }

    fun deleteAll(){
        userDao.deleteAll()
    }

    fun deleteUserByName(name : String){
        userDao.deleteUserByName(name)
    }

}

AndroidViewModel를 상속받는 이유는 ViewModel은 생성자에 애플리케이션에 대한 context을 받을 수 없다.

그래서 여기에서는 1편에 abstract class UserDB()에서 db를 생성하는 메서드(getInstance()) 파라미터를 context를 받도록 했기 때문에 AndroidViewModel를 상속받았다.

 

## MainActivity.class 변경

class MainActivity : AppCompatActivity() {
	// by lazy로 값을 선언 해준 후 ViewModelProvider로 ViewModel를 만든다
	private val model: MainViewModel by lazy {
		ViewModelProvider(this).get(MainViewModel::class.java)
	}
	private lateinit var binding : ActivityMainBinding
	private lateinit var rvAdapter: MainRvAdapter
	private val TAG = "MainActivity"
    
	override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)

            // DB 생성을 할 필요가 없어짐 ViewModel를 참조 할꺼라
           // db = UserDB.getInstance(this)!!

            rvAdapter = MainRvAdapter()


            binding.mainRV.apply {
                setHasFixedSize(true)
                adapter = rvAdapter
                layoutManager = LinearLayoutManager(context,LinearLayoutManager.VERTICAL,false)
            }

            // model에서 넘어온 livedata를 observe를 달아서 변화가 일어날 때만 Adapter 데이터에 추가
            model.users.observe(this,{
                rvAdapter.mData = it
                rvAdapter.notifyDataSetChanged()
            })

            // 추가 버튼
            binding.addBtn.setOnClickListener {
                with(binding){
                val name = nameInput.text.toString()
                val age = ageInput.text.toString()
                val phone = phoneInput.text.toString()
                    when {
                        nameInput.text.isNullOrBlank() -> {
                            Toast.makeText(this@MainActivity, "이름을 입력해주세요", Toast.LENGTH_SHORT).show()
                        }
                        ageInput.text.isNullOrBlank() -> {
                            Toast.makeText(this@MainActivity, "나이를 입력해주세요", Toast.LENGTH_SHORT).show()
                        }
                        phoneInput.text.isNullOrBlank() -> {
                            Toast.makeText(this@MainActivity, "전화번호를 입력해주세요", Toast.LENGTH_SHORT).show()
                        }
                        else -> {
                            // DB 접근은 Main thread에서 불가하므로 CoroutineScope 사용해 처리 한다
                            CoroutineScope(Dispatchers.IO).launch {
                                // 입력한 값 읽어와서 삽입 해주기
                                // 원래 코드
                                // db.userDao().insert(User(name, age, phone))
                                // ViewModel를 이용한 코드
                                   model.insert(User(name, age, phone))
                            }
                            // 마지막 위치에서 삽입이 되었다고 Adapter에 알려줘서 갱신 시키는 과정이 필요 없어졌다
                            // rvAdapter.notifyItemInserted(dataList.size)

                            nameInput.text.clear()
                            ageInput.text.clear()
                            phoneInput.text.clear()
                        }
                    }
                }
            }

            // 삭제 버튼
            binding.delBtn.setOnClickListener {
                val name = binding.nameInput.text.toString()
                if(  binding.nameInput.text.isEmpty())
                {
                    Toast.makeText(this, "삭제하실 이름을 입력하세요", Toast.LENGTH_SHORT).show()
                }
                else {
                    CoroutineScope(Dispatchers.IO).launch {
                        model.deleteUserByName(name)
                    }
                    binding.nameInput.text.clear()
                }
            }

            // 전체삭제 버튼
            binding.delAllBtn.setOnClickListener {
                CoroutineScope(Dispatchers.IO).launch {
                    model.deleteAll()
                }
            }
        }
}

일단 by lazy로 값을 선언 해준 후 ViewModelProvider로 ViewModel를 만든다

by lazy로 하는 이유는 조금 더 안전하고 최초 초기화 후 다시 초기화할 일이 없기 때문이다(val)

by lazy에 대해서는 나중에 포스팅하기로 하겠다.

private val model: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}

만들어준 MainViewModel.class에 있는 users에 observe를 달아서 변화가 일어날 때만 Adapter 데이터에 추가 후 갱신을 해준다.

model.users.observe(this,{
        rvAdapter.mData = it
        rvAdapter.notifyDataSetChanged()
   })

원래는 db를 통해 값을 핸들링했는데 이제는 model을 통해 값을 핸들링한다. 

    else -> {
             // DB 접근은 Main thread에서 불가하므로 CoroutineScope 사용해 처리 한다
             CoroutineScope(Dispatchers.IO).launch {
             // 입력한 값 읽어와서 삽입 해주기
             // 원래 코드
             // db.userDao().insert(User(name, age, phone))
             // ViewModel를 이용한 코드
             model.insert(User(name, age, phone))
       }

깃허브 : https://github.com/JiSeokYeom/Android_MVVM_EX.git

 

 

참고 사이트
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko#declaring_dependencies
https://todaycode.tistory.com/49
https://velog.io/@jojo_devstory/Android-LiveData...%EB%84%8C-%EB%88%84%EA%B5%AC%EB%83%90
반응형