ついにきましたね、[CA Tech Dojo/Challenge/JOB Advent Calendar 2019]、初日は@hohohorisが書かせていただきます。
自分は、2019年8月にCA Tech Dojo(Kotlin編)に参加させていただいた後、10月にCA Tech JOBでCATS(CyberAgent Advanced Technology Studio)でAndroidエンジニアとしてインターンをさせていただきました。
詳しくはインターン参加記を書いたので是非。
CA Tech Dojo(AndroidアプリKotlin編)という最高のインターンで最優秀賞をもらった話
【CA Tech JOB】CA Tech Dojoからの成長
#概要
この記事ではAndroid開発で避けては通れないLayout周りの話をします。
個人アプリでCloud Firestoreを使おうとしていたのでその話をしたかったのですが、諸々間に合わず初日からニッチ目な記事を書きます...。
タイトルにある通り、Androidで動的にレイアウトを生成する際に自分がつまづいたのでそのときに得られた知見を共有したいと思います。
例えばFragment内のDataBindingのinflateの際も割と脳死で
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(context, R.layout.fragment_hoge, container, false)
return binding.root
}
って書いてるだけの人、いると思います。
以前の自分です。
このinflateの方法、全てに通用すると思っていませんか?意味、わかってますか?
その辺りを書いていきたいと思います。
基本的にLayoutの生成全般に関わることですが、よく使われているDataBindingにフォーカスしていきます。
ので、Android初心者/Androidを書いたことがない読者にもわかるようDataBindingとは、から説明します。
#DataBinding
DataBindingは、MVVMと相性の良い技術で、レイアウトのViewとオブジェクトのデータを紐づける機能です。
MVVMやその他アーキテクチャの共通している思想として、「責務の分離」があります。
UIの表示をするレイヤーと、ロジックを記述するレイヤーを分けることにより、テストがしやすく、それぞれの責務が明確なため運用もしやすくなるメリットがあります。
その際、データソースとUIとの紐づけを完結に記述できるライブラリがDataBindingなのです。
beforeとafterでサンプルコードを見てみるとわかりやすいと思います。
##before
DataBinding導入前
<TextView
android:id="@+id/txtName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.user.observe(this, Observer {
textName.text = it.name
})
}
コードは端折っていますが、ViewModelの何かしらのメソッドによりLiveDataなuser
に更新があったらxmlのtextを変更するというシンプルなコードです。
##after
上記コードにDataBindingを適用すると、
<layout>
<data>
<variable
name="viewModel"
type="com.example.MainViewModel"
/>
</data>
<!-- 略 -->
<TextView
android:id="@+id/txtName"
android:text="@{viewModel.user.name}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<!-- 略 -->
<layout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
}
xmlにデータソースとなるオブジェクトをvariableとして持たせ、インスタンスはActivity/Fragmentで渡してあげます。
xmlに渡したデータソースに変更があれば、@{}
で囲んだ箇所が動的に更新されるという素敵なことができます。
コード量が少ないと恩恵がわかりづらいですが、setText
をする必要がないため、コードが綺麗になります。
他にも、BindingAdapterや双方向バインディングなど便利な機能が備わっているのですが、この記事のスコープ外なので気になるひとは調べてみてください。
#DataBindingによるLayout生成方法
本題に入ります。
上記で、DataBindingによりlayoutを生成したのですが、これにはいくつか種類があります。
①val binding = DataBindingUtil.setContentView(activity, R.layout.hoge)
//②と`②は同義。
②val binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.hoge, container, false)
`②val binding = HogeBinding.inflate(LayoutInflater.from(context), container, false)
//③と`③は同義。
③val binding = HogeBinding.bind(View.inflate(context, R.layout.hoge, container))
`③val binding = DataBindingUtil.bind<HogeBinding>.bind(View.inflate(context, R.layout.hoge, container))
##ハマりどころ
Activityなら①、Fragmentなら②を使うことが多いのかなーという印象で、
なかなか、これらの生成方法の違いを意識して記述することはないかもしれません。
ただRecyclerViewのitemでDataBindingを使いたい時、DialogでDataBindingを使いたい時、CustomViewでDataBindingを使いたい時、これらの違いを意識していないで雰囲気でコードを書くことで落とし穴にはまってしまうかもしれません。
自分、ハマりました。
コンパイルエラーが生じたり、ランタイムでViewが表示されないことがありました。
ポイントとなるのは、レイアウトの生成と表示は別問題であることです。
inflate
やbind
メソッドでよしなに生成と表示をしてくれることもありますが、本質的には別問題です。
Viewやレイアウトの生成時に引数として渡すparent
やattachToRoot
に注目しながら、実際にcommand+Bで内部実装を軽くみてみましょう。
#DataBindingUtil#inflate
我々がDataBindingを利用する際によく使うこのinflateメソッドからみていきます。
説明の都合上、一旦FragmentでのDataBindingにフォーカスします。
基本的にはFragmentではonCreateView
メソッド内で②のようにDataBindingの初期化を行いますが、ここで第3引数にViewGroupであるcontainerを渡し、第4引数にfalseを渡しています。
ここで内部実装にコメントで記載がある引数の説明を読んでみます。
* @param parent Optional view to be the parent of the generated hierarchy
* (if attachToParent is true), or else simply an object that provides
* a set of LayoutParams values for root of the returned hierarchy
* (if attachToParent is false.)
* @param attachToParent Whether the inflated hierarchy should be attached to the
* parent parameter. If false, parent is only used to create
* the correct subclass of LayoutParams for the root view in the XML.
ざっくり翻訳すると、
- attachToParentがtrueであるとき、第3引数で渡したparentは生成されたView(layout)の親になる
- attachToParentがfalseであるとき、第3引数で渡したparentは生成されたViewのLayoutParamsの決定のみに使用される
という感じです。
公式リファレンスでも記載があるように、FragmentはView自体をattachしないほうが良いので、第4引数にfalseを渡しています。
Fragmentの場合はLayoutをinflateした後に、onCreateView
の戻り値としてViewGroupを返すことでLayoutを表示させます。
これが、「レイアウトの生成と表示は別問題」と記述したところに繋がっており、戻り値にnullを指定するとFragmentは表示されません。
#DataBindingUtil#bind
このメソッドを直接呼ぶことは少ないかもしれません。ただ、使えてしまうので、違いを知った上で安全に運用することはDataBindingに寄らずLayout生成周りで役に立つはずです。
この場合、引数にはrootとなるViewを渡してあげる必要があります。
その際にView#inflate
を使用したのですが、こちらは第3引数にparentを渡しますが、DataBindingUtil#inflate
にあったattachToParentがありません。
これは、同じく内部実装の引数説明を読んでみると詳細がわかります。
* @param root A view group that will be the parent. Used to properly inflate the
* layout_* parameters.
こちらは、問答無用に引数で渡したparentがLayoutの親になってしまうようです。
そのため、③に記載したbindingの初期化方法は間違いとわかります。FragmentはLayout自体をparentに追加するのは正しくないからです。
##Fragment
Fragmentの場合は上述した通りです。
##Adapter
前述はFragmentでのDataBindingにおけるLayout生成にフォーカスしましたが、AdapterのitemをDataBindingで生成するにはどうすれば良いでしょうか。
これもLayoutのを生成したあと「どのようにして表示するか」で考えれば良さそうです。
それによってparentを渡すかどうか、渡したparentにattachするかどうかが変わります。
Adapterの場合はinflateしたViewをViewHolderのコンストラクタの引数に渡してあげることで表示ができます。
そのため、AdapterでDataBindingを使う時もparentに直接attachする必要はなく、②の方法でinflateができます。
RecyclerViewの場合は確認していないのですが、数年前にListViewで書かれたAdapterにDataBindingを導入した際に誤って
val binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_hoge, parent, true)
と、parentにattachしてしまい、
android.view.InflateException: addView(View, LayoutParams) is not supported in AdapterView
なるExceptionに遭遇しました。
そもそもgetViewで渡されてくるparentは多くの場合ListViewなのですが、ListViewはそもそもViewGroupを継承していないためaddViewができないためです。
itemをDataBindingで生成したあとにはparentにattachせずに、getViewの戻り値としてbinding.rootを返すことで表示しないといけないのです。
RecyclerViewはViewGroupを継承してはいるものの、表示方法としてはparentにattachするのではなくRecyclerView.ViewHolderにbinding.rootを渡してあげるほうが良いでしょう。
##CustomView
これまで、Layoutを生成した後に表示する方法が別に用意されていたため、attachToParent
はfalse
にしていたのですが、CustomViewの場合には注意が必要です。
CustomViewの場合は、よしなに表示してくれる機構が備わっていないため、これまでのようにattachToParent
をfalse
にしてしまうと、Layoutは生成されているが、宙ぶらりんな状態になってしまいます。
どうするかというと、
class MyCustomView : FrameLayout {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
val binding = ViewMyCustomBinding.inflate(LayoutInflater.from(context), this, true)
}
}
こんな感じで書いてあげます。
parent
にthis
を渡していますが、これはMyCustomView
が継承しているFrameLayout
です。
そして今まではfalse
にしていたattachToParent
をtrue
にしてあげることで生成したLayoutをViewGroupのヒエラルキーにいれてあげるのです。
#まとめ
普段あまり意識しないようなLayoutの生成に関して、内部実装を元に違いをみてみました。
自分はAndroid歴があと2ヶ月で1年になるのですが、
「なんかしらんけど動くw」
の状態は脱して、
「こういう機構で動いていたのか」
というのを意識するようにしています。
Advent Calendar1日目からマニアックめなテーマで記事を書きましたが、
これを機に内部実装ちょこちょこ見るようにするとワンランクレベルアップできるかもしれないので是非!
ほりすでした。