본문 바로가기

개발일기

8월 한달 토이 프로젝트를 해보자! -08 : LogIn Data Binding & ViewModel

이제 ViewModel을 만들어, LiveData를 활용하여 Data Binding을 시켜줄 차례다. 시작이 반이라더니, 시작은 역시 그냥 시작인 것 같다. 여전히 한참 남았다. 처음부터 이 아키텍처로 시작했으면 당연히 좀더 빨랐겠지만, 그래도 이왕 만드는 거, 처음부터 차근차근 만들어보는 경험이 스스로에게도 리마인드 하기에 도움이 크게 된다 느껴졌다.

app단 build.gradle에 아래와 같이 의존성을 추가해준다.

build.gradle(:app)

...
def lifecycle_version = "2.6.0-alpha01"
def optional_lifecycle_version = "2.5.1"
def arch_version = "2.1.0"

dependencies {
    ...

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
    // alternately - if using Java8, use the following instead of lifecycle-compiler
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
    // optional - helpers for implementing LifecycleOwner in a Service
    implementation "androidx.lifecycle:lifecycle-service:$optional_lifecycle_version"
    // optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
    implementation "androidx.lifecycle:lifecycle-process:$optional_lifecycle_version"

    // optional - Test helpers for LiveData
    testImplementation "androidx.arch.core:core-testing:$arch_version"
}

디렉토리에 viewmodel을 추가해준다.

일단은 ViewModel()을 상속하되, 필요시 AndroidViewModel로 바꿔 컨텍스트를 가져올 것이다.

그 다음, login_fragment.xmllayout 태그와 ConstraintLayout 태그 사이에 LogInViewModel을 변수로 추가해준다.

login_fragment.xml

<data>
    <variable
        name="logInViewModel"
        type="com.movingroot.toyproject.viewmodel.LogInViewModel" />
</data>

이제, TextInputEditText들에 2-way binding을 걸어주어 Observing 할 것이다. 이를 위해 우선 LogInViewModel에 아래와 같이 변수를 추가해주었다.

LogInViewModel.kt

val _idInput = MutableLiveData<String>()
val idInput: LiveData<String> get() = _idInput
val _pwInput = MutableLiveData<String>()
val pwInput: LiveData<String> get() = _pwInput

login_fragment에서는 다음과 같이 text를 set 해준다.

android:text="@={logInViewModel._idInput}"

단순히 observing 하는 one-way binding과 달리 실시간으로 수정이 되는 데이터를 반영해야 하는 EditText의 특성상 2-way binding 처리해주었으며, 이때 @={}와 같이 작성해줘야 함에 주의한다.

더하여, 로그인 함수 및 Toast 띄우는 함수도 모두 ViewModel로 이관할 것이다. 이를 위해 아래와 같이 변수들을 ViewModel에 선언해주고, 함수 세팅을 해준다. 클릭 함수를 ViewModel로 가져가 xml에 다이렉트로 링크를 할 것이다. (hideKeyboard는 그냥 남겨둘 것이다. 이것까지 가져올 이유는 없다고 본다. 물론 AndroidViewModel로 하여 applicationContext 사용을 통해 구현할 수도 있을 것이다.)

LogInViewModel.kt

private val _logInFlag = MutableLiveData<Boolean>()
val logInFlag: LiveData<Boolean> get() = _logInFlag

private val _toast = MutableLiveData<String>()
val toast: LiveData<String> get() = _toast

...

fun logIn() {
    if (!validateInputs()) {
        _toast.value = "계정 또는 비밀번호를 입력해주세요."
        return
    }
    if (!validatePassword()) {
        _toast.value = "비밀번호는 최소 6자리 이상입니다."
        return
    }

    _logInFlag.value = true
}

private fun validateInputs() = idInput.value!!.isNotEmpty() && pwInput.value!!.isNotEmpty()

private fun validatePassword() = pwInput.value!!.length > 5

이제 Fragment를 세팅해주면, UI 요소 변수들은 baseLyt 제외하고 전부 지워준다. 그리고 binding 변수를 아래와 같이 추가하여, Binding.root를 onCreateView에서 return 받도록 수정한다.

이때, viewModel을 아래와 같이 inline으로 선언해줄 수 있는데, 지연선언을 통한 방식으로는 아래와 같이도 작성해줄 수 있다.

private lateinit var viewModel: LogInViewModel

...

viewModel = ViewModelProvider(this)[LogInViewModel::class.java]

아무튼, Fragment의 코드는 다음과 같다.

LogInFragment.kt

private var _binding: LoginFragmentBinding? = null
private val binding: LoginFragmentBinding get() = _binding!!

private val viewModel: LogInViewModel by viewModels()

...

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = LoginFragmentBinding.inflate(inflater, container, false)
    initFragment()
    setObservers()

    return binding.root
}

private fun initFragment() {
    _binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        logInViewModel = viewModel
    }
}

private fun setObservers() {
    with(viewModel) {
        logInFlag.observe(viewLifecycleOwner) {
            if (it) {
                toList()
            }
        }

        toast.observe(viewLifecycleOwner) {
            if (it.isNotEmpty()) {
                makeToast(it)
                viewModel.initToast()
            }
        }
    }
}

xml의 버튼에는 android:onClick="@{()->logInViewModel.logIn()}" 리스너를 달아준 후, 실행해보자. 의도한 대로 작동한다. 체크박스는 실제로 해당 기능을 활용하게 될 때 달아줄 생각이다.