Edited at

Android JetPack Fragment-ktx 1.1.0 でlateinitを駆逐できる

Android JetPackにActivity-ktx1.0.0とFragment-ktx1.1.0がStableリリースされました。

面白い追加機能の1つとして、コンストラクタにLayoutIDを渡すことができるようになりました。

class MainActivity : AppCompatActivity(R.layout.activity_main) {

}

こうすることでsetContentViewが不要になります。


lateinitがやめられない

ところでDataBindingやViewModelを初期化する時はActivityやFragmentの初期化と同時にはできないのでlateinitで後で初期化しているケースが多いと思います。

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get()
}
}

全ActivityやFragmentでこの儀式を行われければならなかったのですが、今回のアップデートをうまく使うとlateinitを使わずに宣言できるようになります!


by dataBinding()

AppCompatActivityにLayoutIDを渡した時はAppCompatActivity側でViewがセットされます。

これとDataBindingを組み合わせる時は DataBindingUtil.bind() でインスタンスを取得できます。

val binding: ActivityMainBinding = DataBindingUtil.bind(findViewById<ViewGroup>(android.R.id.content)[0])!!)

これをさらにlazyと組み合わせることでDataBindingインスタンスを遅延初期化できます。

private val binding: ActivityMainBinding by lazy { findViewById<ViewGroup>(android.R.id.content)[0])!! }

このlazyをさらにラップした拡張関数を作ります。


DataBindingExtension.kt


/**
* DataBindingを遅延初期化します。
* このDelegateを使うにはAppCompatActivityのコンストラクタにレイアウトIDを指定ください。
*
* ex:
* ```
* class MainActivity : AppCompatActivity(R.layout.activity_main) {
* private val binding: ActivityMainBinding by dataBinding()
* }
* ```
*/
@MainThread
fun <V : ViewDataBinding> AppCompatActivity.dataBinding(): Lazy<V> {
return object : Lazy<V> {
private var holder: V? = null

override val value: V
get() = holder ?: run {
val binding = DataBindingUtil.bind<V>(findViewById<ViewGroup>(android.R.id.content)[0])!!
holder = binding
binding
}

override fun isInitialized(): Boolean = holder != null
}
}

/**
* DataBindingを遅延初期化します。
* このDelegateを使うにはFragmentのコンストラクタにレイアウトIDを指定ください。
*
* ex:
* ```
* class MainFragment : Fragment(R.layout.fragment_main) {
* private val binding: FragmentMainBinding by dataBinding()
* }
*/
fun <V : ViewDataBinding> Fragment.dataBinding(): ReadOnlyProperty<Fragment, V> {
return object : ReadOnlyProperty<Fragment, V> {
private var holder: V? = null

override fun getValue(thisRef: Fragment, property: KProperty<*>): V {
return holder ?: DataBindingUtil.bind<V>(view!!)!!.also {
holder = it
}
}
}
}


結果👇

private val binding: ActivityMainBinding by dataBinding()

👏👏👏

上のコードでFragment用のdataBinding()も作りましたので、Fragmentの場合はonCreateView()が不要になります。


by viewModels()

この勢いでViewModelの初期化も遅延初期化していきましょう。と思ったらこのバージョンから公式で by viewModels()が提供されました。

private val viewModel: MainViewModel by viewModels()

これまでFragmentでViewModelProviders.of()で指定していたスコープはそれぞれ

viewModels()

activityViewModels()

の2つで指定するようになりました。

ViewModelFactoryを使っている場合は、

// MainFactoryがViewModelProvider.Factoryを継承している

private val viewModel: MainViewModel by viewModels { MainFactory() }

で初期化ができます。


結果

class MainActivity : AppCompatActivity(R.layout.activity_main) {

private val binding: ActivityMainBinding by dataBinding()
private val viewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}

bindingとviewModelからlateinitが消えてvalで宣言できるようになりました!