# Table of Contents

# Jetpack

Jetpack은 더 좋은 품질의 안드로이드 개발을 돕는 라이브러리의 집합입니다.

Jetpack은 크게 네 가지 카테고리로 분류됩니다.

  • Architecture Component
  • UI Component
  • Foundation Component
  • Behavior Component

이번 포스트에서는 Lifecycle OwnerLifecycle-aware Component에 대해 알아보겠습니다.

# 탄생 배경

우선 간단한 카운터 앱을 살펴봅시다. 코드는 다음과 같습니다.

// activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/activity_main_tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="50sp"
        android:text="0"/>

    <Button
        android:id="@+id/activity_main_btn_plus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onButtonClicked"
        android:text="Plus"/>

    <Button
        android:id="@+id/activity_main_btn_minus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onButtonClicked"
        android:text="Minus"/>
</LinearLayout>
// MainActivity.kt

class MainActivity : AppCompatActivity() {

    private var value = 0

    private val textViewValue: TextView by lazy { findViewById(R.id.activity_main_tv_value) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun onButtonClicked(view: View) {
        when(view.id) {
            R.id.activity_main_btn_plus -> {
                value += 1
                textViewValue.text = value.toString()
            }
            R.id.activity_main_btn_minus -> {
                value -= 1
                textViewValue.text = value.toString()
            }
        }
    }
}

이 앱은 화면 방향을 회전하면 데이터가 초기값으로 돌아갑니다. 화면이 회전되면서 액티비티가 소멸한 후 재생성되기 때문입니다.

이러한 문제를 해결하기 위해 기존에는 ActivityonSaveInstanceState()에서 데이터를 저장하고 onCreate()에서 데이터를 복구했습니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    companion object {
        const val KEY = "key"
    }

    private var value = 0

    private val textViewValue: TextView by lazy { findViewById(R.id.activity_main_tv_value) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 데이터 복구
        if (savedInstanceState != null && savedInstanceState.containsKey(KEY)) {
            value = savedInstanceState.getInt(KEY)
            textViewValue.text = value.toString()
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        // 데이터 저장
        outState.putInt(KEY, value)
    }

    fun onButtonClicked(view: View) {
        when(view.id) {
            R.id.activity_main_btn_plus -> {
                value += 1
                textViewValue.text = value.toString()
            }
            R.id.activity_main_btn_minus -> {
                value -= 1
                textViewValue.text = value.toString()
            }
        }
    }
}

# ViewModel

ViewModel은 UI와 관련된 데이터를 별도의 객체에 저장하고 관리하도록 설계되었습니다. UI의 상태와 관련된 데이터를 ViewModel에 저장하기 때문에 화면 회전과 같이 구성이 변경될 때에도 데이터를 유지할 수 있습니다.

ViewModel은 다음과 같이 ViewModel클래스를 상속하여 정의합니다.

// MainViewModel.kt
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {

    var value = 0

}

Activity에서는 ViewModelProvider를 사용하여 ViewModel 객체를 생성합니다.

// MainActivity.kt
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

    private lateinit var mainViewModel: MainViewModel

    private val textViewValue: TextView by lazy { findViewById(R.id.activity_main_tv_value) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModel 초기화
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        textViewValue.text = mainViewModel.value.toString()
    }

    fun onButtonClicked(view: View) {
        when(view.id) {
            R.id.activity_main_btn_plus -> {
                mainViewModel.value ++
                textViewValue.text = mainViewModel.value.toString()
            }
            R.id.activity_main_btn_minus -> {
                mainViewModel.value --
                textViewValue.text = mainViewModel.value.toString()
            }
        }
    }
}

이제 화면 전환이 되어도 UI 상태가 유지됩니다.

# 다양한 ViewModel 생성 방법

ViewModel은 다양한 방법으로 생성할 수 있습니다.

# ViewModelProvider

import androidx.lifecycle.ViewModel

class LoginViewModel: ViewModel() {
    fun login() {
        // do something.
    }
}
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.lifecycle.ViewModelProvider
import com.yologger.viewmodel.R

class LoginActivity : AppCompatActivity() {

    private val loginViewModel: LoginViewModel by lazy { ViewModelProvider(this).get(LoginViewModel::class.java) }

    private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        loginButton.setOnClickListener {
            loginViewModel.login()
        }
    }
}

# ViewModelFactory

아래 예제를 살펴봅시다. LoginViewModel의 생성자에 LogInUseCase라는 파라미터가 있습니다.

import androidx.lifecycle.ViewModel
import com.yologger.viewmodel.domain.LoginUseCase

class LoginViewModel(
    private val loginUseCase: LoginUseCase
) : ViewModel() {

    fun login() {
        loginUseCase.execute()
    }
}
class LoginUseCase {

    fun execute() {
        // do something.
    }
}

이처럼 ViewModel에 파라미터가 있는 경우 ViewModelFactory를 정의해야합니다.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.yologger.viewmodel.domain.LoginUseCase
import java.lang.IllegalArgumentException

class LoginViewModelFactory(private val loginUseCase: LoginUseCase) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
            LoginViewModel(loginUseCase) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

이제 다음과 같이 ViewModel을 생성할 수 있습니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.lifecycle.ViewModelProvider
import com.yologger.viewmodel.R
import com.yologger.viewmodel.domain.LoginUseCase

class LoginActivity : AppCompatActivity() {

    private val loginUseCase: LoginUseCase by lazy { LoginUseCase() }
    private val loginViewModel: LoginViewModel by lazy { ViewModelProvider(this, LoginViewModelFactory(loginUseCase)).get(LoginViewModel::class.java) }

    private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        loginButton.setOnClickListener {
            loginViewModel.login()
        }
    }
}

# Android KTX

Android KTX 라이브러리를 사용하면 ViewModel을 보다 쉽게 생성할 수 있습니다. 이를 위해 다음 의존성을 추가해야합니다.

dependencies {

    // Android KTX
    implementation("androidx.activity:activity-ktx:1.4.0")
    implementation("androidx.fragment:fragment-ktx:1.3.6")
}

이제 by viewModels()ViewModel를 생성할 수 있습니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.viewModels
import com.yologger.viewmodel.R
import com.yologger.viewmodel.domain.LoginUseCase

class LoginActivity : AppCompatActivity() {

    private val loginViewModel by viewModels<LoginViewModel>()

    private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        loginButton.setOnClickListener {
            loginViewModel.login()
        }
    }
}

ViewModel에 파라미터가 있다면 ViewModelFactory를 정의해야합니다.

class LoginUseCase {

    fun execute() {
        // do something.
    }
}
class LoginViewModelFactory(private val loginUseCase: LoginUseCase) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
            LoginViewModel(loginUseCase) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}
import androidx.lifecycle.ViewModel
import com.yologger.viewmodel.domain.LoginUseCase

class LoginViewModel(
    private val loginUseCase: LoginUseCase
) : ViewModel() {

    fun login() {
         loginUseCase.execute()
    }
}

파라미터가 있는 ViewModel은 다음과 같이 생성합니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.viewModels
import com.yologger.viewmodel.R
import com.yologger.viewmodel.domain.LoginUseCase

class LoginActivity : AppCompatActivity() {

    private val loginUseCase: LoginUseCase by lazy { LoginUseCase() }

    private val loginViewModel by viewModels<LoginViewModel> { LoginViewModelFactory(loginUseCase) }

    private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        loginButton.setOnClickListener {
            loginViewModel.login()
        }
    }
}