[kotlin] Kotlin + Databinding + Navigation + MVVM + RxKotlin 기초 설정

kotlin으로 새 프로젝트를 할 기회가 생겼다. 짬짬이 시간내서 테스트를 해봤는데, 아무래도 익숙치 않아서 그런지 자꾸 까먹는다. 그래서 이번 기회에 정리부터 한다. 

새 프로젝트를 만들 때 include Kotlin support 체크같은 사소한 건 설명없이 넘길 예정이다.

android studio 3.2부터 AndroidX를 지원하기 때문에 일단 Migrate to AndroidX부터 해준다. 알아서 다 해주기 때문에 손으로 뭔가를 해줄 필요는 없고, 마지막에 확인차 물어볼 때 버튼 한번만 눌러주면 된다.
https://github.com/susemi99/kotlin-mvvm-setting-sample/commit/58a2166200e92671798e1c60109d91da5debdeb0

DataBinding

// app/build.gradle
apply plugin: 'kotlin-kapt'
android{
  dataBinding {
    enabled true
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

이것만 해주고 나머지는 차근차근 추가하면 된다. 자세한 설명은 https://developer.android.com/topic/libraries/data-binding/?hl=ko 에 있다. 

Navigation

아주 맘에드는 기능이다. xcode의 스토리보드 같은 느낌인데, 좀 더 강력한 것 같다. 

// app/build.gradle
dependencies {
    implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha07'
    implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha07'
}

res/naviagation 폴더를 만들고, 폴더에서 오른쪽 클릭을 하면 new -> Navigation resource file 이란 메뉴를 실행하면 만들 수 있다.

<navigation
  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/main_navigation"
  app:startDestination="@id/mainFragment">

  <fragment
    android:id="@+id/mainFragment"
    android:name="kr.susemi99.kotlinmvvmsettingsample.main.MainFragment"
    android:label="main_fragment"
    tools:layout="@layout/main_fragment">
    <action
      android:id="@+id/action_mainFragment_to_userFragment"
      app:destination="@id/userFragment"/>
    <action
      android:id="@+id/action_mainFragment_to_toDoFragment"
      app:destination="@id/toDoFragment"/>
  </fragment>
  <fragment
    android:id="@+id/userFragment"
    android:name="kr.susemi99.kotlinmvvmsettingsample.user.UserFragment"
    android:label="user_fragment"
    tools:layout="@layout/user_fragment"/>
  <fragment
    android:id="@+id/toDoFragment"
    android:name="kr.susemi99.kotlinmvvmsettingsample.todo.ToDoFragment"
    android:label="to_do_fragment"
    tools:layout="@layout/to_do_fragment"/>
</navigation>

이런 식으로 가장 처음 실행할 fragment 하나와, 거기서 이동할 fragment를 추가하고, 선으로 이어주면 된다. 그런다음 MainActivity에서 설정만 해주면 끝난다. 

<layout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <androidx.appcompat.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@color/colorPrimary"
      android:theme="@style/ThemeOverlay.AppCompat.Dark"
      app:contentInsetStartWithNavigation="0dp"/>

    <fragment
      android:id="@+id/nav_host_fragment"
      android:name="androidx.navigation.fragment.NavHostFragment"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:defaultNavHost="true"
      app:navGraph="@navigation/main_navigation"/>

  </LinearLayout>
</layout>
class MainActivity : AppCompatActivity() {

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

    setSupportActionBar(toolbar)

    val navigation = Navigation.findNavController(this, nav_host_fragment.id)
    NavigationUI.setupActionBarWithNavController(this, navigation)
    toolbar.setNavigationOnClickListener { navigation.navigateUp() }
  }
}

navigation이 참 좋은 게 표시할 fragment 생성하고, 넘길 값 설정하고 그런게 없이 아주 깔끔하다. 

class MainFragment : Fragment() {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.main_fragment, container, false)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val bundle = Bundle().apply {
        this.putString("key1", "value1")
        this.putFloat("key2", 123.45f)
    }
    
    val navController = Navigation.findNavController(view)
    goToUser.setOnClickListener { navController.navigate(R.id.action_mainFragment_to_userFragment, bundle) }
    goToToDo.setOnClickListener { navController.navigate(R.id.action_mainFragment_to_toDoFragment) }
  }
}

값을 넘길 때는 bundle을 만들어서 넘기면되고, 아닐 때는 그냥 호출만 하면 알아서 한다. 

MVVM

// app/build.gradle
dependencies {
  implementation 'android.arch.lifecycle:extensions:1.1.1'
}
<layout>

  <data>

    <variable
      name="model"
      type="kr.susemi99.kotlinmvvmsettingsample.user.UserFragmentModel"/>
  </data>

  <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"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".user.UserFragment">

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="user_fragment"/>

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{model.value1}"/>

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{String.valueOf(model.value2)}"/>
  </LinearLayout>
</layout>

layout에 사용할 뷰모델을 추가해주고, 필요한 필드를 사용해서 표시하면 된다. 

class UserFragmentModel : ViewModel() {
  var value1 = ObservableField<String>()
  var value2 = ObservableFloat(0.0f)

  fun setValues(value1: String?, value2: Float?) {
    value1?.let { this.value1.set(it) }
    value2?.let { this.value2.set(it) }
  }
}

user fragment에서 사용할 뷰모델도 이런 식으로 만들어주면 된다. LiveData를 사용할지 ObservableField를 사용할지 테스트 해봤는데, LiveDataaddOnPropertyChangedCallback를 사용할 수 없어서 불편할 것 같았다. 

class UserFragment : Fragment() {
  private lateinit var binding: UserFragmentBinding
  private lateinit var model: UserFragmentModel

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.user_fragment, container, false)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding = UserFragmentBinding.bind(view)

    model = ViewModelProviders.of(this).get(UserFragmentModel::class.java)
    binding.setVariable(BR.model, model)
  }

  override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    model.setValues(arguments?.getString("key1"), arguments?.getFloat("key2"))
  }
}

bind() 후에 setVariable()을 해주면 레이아웃에서 뷰모델을 읽기 시작하고, 넘길 값이 있다면 뷰모델에 직접 넘겨주면 된다. 
ViewModelProviders 를 쓰면 onCleared() 를 사용할 수 있어서 편하다.

RxKotlin

// app/build.gradle
dependencies {
  implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
}
class ToDoFragmentModel : ViewModel() {
  var title = ObservableField<String>()
  private var intervalDispose: Disposable? = null

  init {
    intervalDispose = Observable.interval(1, TimeUnit.SECONDS)
      .subscribe(
        {
          title.set(System.currentTimeMillis().toString())
        },
        {
          it.printStackTrace()
        })

    title.addOnPropertyChangedCallback(object : androidx.databinding.Observable.OnPropertyChangedCallback() {
      override fun onPropertyChanged(p0: androidx.databinding.Observable?, p1: Int) {
        Log.i("APP#", "title: " + title.get())
      }
    })
  }

  override fun onCleared() {
    super.onCleared()

    Log.i("APP#", "=== cleared ===")
    intervalDispose?.dispose()
  }
}

subscribe() 안이 달라져서 좀 헷갈리긴하지만, 몇 번 쓰다보면 적응되겠지…