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
를 사용할지 테스트 해봤는데, LiveData
는 addOnPropertyChangedCallback
를 사용할 수 없어서 불편할 것 같았다.
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()
안이 달라져서 좀 헷갈리긴하지만, 몇 번 쓰다보면 적응되겠지…