今更遅れてDataBinding事始め(3)[include]

前回のお話

今回は単方向の拡張として includeレイアウトの話

inlcude bindingを使った 体感的な結論

  • include bindingするだけでビルド時間が増える

    • InstantRun有効で、
      • 変更なし5秒
      • レイアウト変更1分超え
      • CPU 200-300とかになる
      • メモリどか食い
  • レイアウトプレビューとかエラー必然でまともに見れない

  • よく変換処理?で暴走する
    • gradlew –stop で勿論止まらない
    • jpsとかでみてforkされているjavaタスクを殺さないと駄目

自分は殺すシェルを用意して常に使ってたりします いまさら再導入 peco for mac (2) - exception think

  • レイアウトを生成後、エディタ補完用のindexを作っているはずだが
    • 結構な確率で コード補完/importショートカット が効かなくなる*1
      • clean rebildをし直せば改善はする
      • gralde sync だけだとファイル変更済みフラグが相変わらずイマイチなので効かないことも

まあ、data-binding layout自体

  • layout.xml を変更しただけだと
    • XXXBindingにid追加やid変更などの変更が適応されない

時点で、未だ実用レベルではないとは思うんだけどな。。

まあXCodeと比較されて揶揄される下記の状況に戻っただけでは有りますが。。

  • InstantRunで爆速開発?
    • => ビルド中にお茶が飲める開発

JPPとかのAPT利用だと、それほどストレスはたまらないので

毎回フル変換が必須な作りが駄目な気がする

対策的な案

  • Fragmentとかつかってbindingレイアウトをシンプルにする
  • include-binding は極力使わない
    • テスト的に2−3段階 include-binding試してみたら、解釈が劇的に時間がかかる感じ

あたりが無難かなーと

よく記載されているbindingサンプル例

  • app/layout_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:id="@+id/root_layout"
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include 
           android:id="@+id/name_layout"
           layout="@layout/name"
           bind:user="@{user}"/>
       <include 
           android:id="@+id/contact_layout"
           layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>

  <TextView 
       android:id="@+id/name"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@{user.name}"/>
</layout>
  • app/contact.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>

  <TextView 
       android:id="@+id/contact"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@{user.contact}"/>
</layout>

動かないらしいケース(以前はそうだったらしい)

  • app/layout_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>

   <!-- merge直下のincludeなのでこれはダメ -->
   <merge>
       <include 
          android:id="@+id/name_layout"
          layout="@layout/name"
           bind:user="@{user}"/>
       <include 
          android:id="@+id/contact_layout"
          layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

これ、多分次のidが認識しない件と同件なだけだと思う

で、現在は変換前チェックタスク辺りで

  • rootタグにmergeタグは使えないよ

エラーになるので、記述自体ができないイメージ。


data-bindingの変換処理的な挙動について

data-bindingをつかったレイアウトをビルドした場合

  • app/build/generated/source/apt/debug
    • bind記述をしたレイアウトのid一覧、bind:user等の処理を抽出した 生成コード
  • app/build/intermediates/data-binding-info
    • bind:user等の処理を抽出したxml部分
  • app/build/intermediates/data-binding-layout-out
    • 上記を除いた通常のレイアウト
    • bindingを使っていないレイアウトや画像とかも一切合切ここに放り込まれる

が作成されます*2

  • app/layout_main.xml
 <LinearLayout
       android:id="@+id/root_layout"
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:bind="http://schemas.android.com/apk/res-auto">
       
       <include 
            android:id="@+id/name_layout" 
            layout="@layout/name"/>
       <include 
            android:id="@+id/contact_layout" 
            layout="@layout/contact"/>
   </LinearLayout>

なレイアウトが生成されてapkに梱包されるわけです

でこれに対して

  • data-binding処理を使わなければ、
    • binding記述を除かれた上記レイアウトが利用される
  • binding宣言された属性はレイアウト的に除去されるので、
    • 必須属性に関して記述すると勿論レイアウトエラーになる

で通常のレイアウトとして成立すれば、上記のmergeタグとかも問題ないわけです。*3

data-bindingでふられるidに関して(1)

  • root_layout => LayoutMainBinding.rootLayout
  • name_layout => LayoutMainBinding.nameLayout.name
  • contact_layout => LayoutMainBinding.contactLayout.contact

includeされるレイアウトで注意しなければいけないのは

  • nameLayout は NameBinding
  • contactLayout は ContactBinding

であり、includeされるレイアウトにidを振っておかないと実体参照が出来ないということです。

RootLayoutBinding binding = DataBindingUtil.setContentView(this, R.id.root_layout);

TextView name = binding.nameLayout.name;
TextView contact = binding.contactLayout.contact;

  • 通常の findViewByIdだと
setContentView(R.id.root_layout);

TextView name = (TextView)findViewById(R.id.nameLayout);
TextView contact = (TextView)findViewById(R.id.contactLayout);

となるので混乱するかも

data-bindingでふられるidに関して(2)

  • あと、これは現状の制限かもしれませんが、include先でふられるidに関して
    • name ・・OK
    • name_code ・・NG

の形で、2段階目の変数は大文字変換されてnameCodeと変換されないようです*4


DataBinding時に、includeタグのidが認識しない件

DataBindingの対象のレイアウトは、

idを振っていると自動的に認識される仕様らしい。*5

findViewByIdが要らなくなるのでButterKifeの代わりになると紹介されているサイトも有るのですが。。。

XXXBinding.ID と うまく認識しないケース

  • app/layout.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="activity" type="com.example.Activity"/>
   </data>
   <LinearLayout
       android:id="@+id/root_layout"
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include 
           android:id="@+id/name_layout"
           layout="@layout/name"/>
       <include 
           android:id="@+id/contact_layout"
           layout="@layout/contact"/>
   </LinearLayout>
</layout>

動いたケース

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="activity" type="com.example.Activity"/>
   </data>
   <LinearLayout
       android:id="@+id/root_layout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"   
       android:orientation="vertical">
       <include 
           android:layout_width="match_parent"
           android:layout_height="wrap_content"    
           android:id="@+id/name_layout"
           layout="@layout/name"/>
       <include 
           android:layout_width="match_parent"
           android:layout_height="wrap_content"    
           android:id="@+id/contact_layout"
           layout="@layout/contact"/>
   </LinearLayout>
</layout>

ようは、

  • android:layout_width/android:layout_height を書かないと 単純なincludeレイアウトの場合、id抽出対象のView として認識されない
  • ただincludeレイアウトに 引数的な bind記述があると認識する。
    • その場合、includeタグにidをふらないと、プログラム上からはアクセスが出来ない

Bindする変数をlayout上で指定するケース

http://wpdev.hatenablog.com/entry/2016/05/30/205434

にかかれているお話。

  • app/layout_main.xml
<include
   layout="@layout/commmon_view"
   bind:iconRes="@drawable/icon"
   bind:textRes="@string/text"
/>

は、引数は実体のみなので出来ないよ。というお話

実際の一般的な?例

G様推奨のAppCompat系レイアウトでの利用例を書き出してみます

android:transitionName あたりの話は下記を参照してください

  • app/layout_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:bind="http://schemas.android.com/apk/res-auto"
        xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
        <import type="hoge.fuga.example.R" />
   </data>


<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">


    <android.support.design.widget.CoordinatorLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <!--//bind includeするtoolbar-->
        <include
            bind:trans_name="@{R.string.trans_main}"
            bind:title="@{R.string.title_main}"
            layout="@layout/toolbar"
            android:id="@+id/app_bar_timeline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        
        <!--//何らかのUIコンテンツ-->


        <!--//浮かんでるFabボタン-->
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/first_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|right"
            android:layout_marginBottom="16dp"
            android:layout_marginRight="16dp"
            android:src="@drawable/ic_star_white_24dp"/>

    </android.support.design.widget.CoordinatorLayout>

    <!-- Navi-->
    <android.support.design.widget.NavigationView
        android:id="@+id/navi_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/naiv_header"
        app:menu="@menu/navi_body"/>

</android.support.v4.widget.DrawerLayout>

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

   <data>
        <import type="hoge.fuga.BindingUtils" />
        <variable name="trans_name" type="int" />
        <variable name="title" type="int" />
   </data>

<android.support.v7.widget.Toolbar
        android:transitionName="@{BindingUtils.string(trans_name)}"
        app:title="@{BindingUtils.string(title)}"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light">

</layout>
public class BindingUtils {

    public static String string(int resId) {
        if(resId == 0 || resId == -1){
            return "";
        }
        HogeApplication instance = HogeApplication.getInstance();
        return instance.getApplication()
               .getResources().getString(resourceId);
    }
}

ポイント的な所

  • include引数に対してレイアウト上で宣言する方法が現状ない
  • main_layout で対象属性(bind:trans_name)指定なし
    • 0が渡ってくる
    • -1等のあえて動かないような値を宣言等も出来る

引数指定なし、引数に変数を入れる例

  • app/layout_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:bind="http://schemas.android.com/apk/res-auto"
        xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
        <import type="hoge.fuga.example.R" />
   </data>

<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

<!--//中略-->

        <!--//bind includeするtoolbar-->
        <include
            bind:trans_name="@{-1}"
            layout="@layout/toolbar"
            android:id="@+id/app_bar_timeline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

dataタグを使わない例

  • app/layout_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:bind="http://schemas.android.com/apk/res-auto"
        xmlns:app="http://schemas.android.com/apk/res-auto">

<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

<!--//中略-->

        <!--//bind includeするtoolbar-->
        <include
            bind:trans_name="@{hoge.fuga.example.R.string.trans_main}"
            bind:title="@{hoge.fuga.example.R.string.title_main}"
            layout="@layout/toolbar"
            android:id="@+id/app_bar_timeline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

data-bindingを使わず、data-bindingで利用した子レイアウトを使う例

一応普通に使えてしまう

  • app/layout_main.xml
<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

<!--//中略-->

        <!--//bindしない includeするtoolbar-->
        <include
            layout="@layout/toolbar"
            android:id="@+id/app_bar_timeline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

応用例っぽい話

  • values/string.xml
<resources>
    <string name="list_date_format">Date discovered: %1$s</string>
</resources>
  • app/layout_main.xml
<TextView
    android:id="@+id/date_text_view"
    android:text="@{@string/list_date_format(date.toString() ?? `(no date)`)}"
    android:allCaps="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_toLeftOf="@id/solved_check_box"
    android:layout_below="@id/title_text_view"
    tools:text="Crime Date"
    android:padding="@{@dimen/list_item_padding * 2}"/>

ようは

  • 文字整形をリソースだけで処理できる(?? はnullだったらの省略二項演算子らしい)
  • dimen定義をレイアウト上で調整できる

デメリットとしては

  • 完全に文字列扱いになるため
    • Ctrl+B で定義にジャンプできていたのができなくなる
    • 補完が効かないので Gradle Syncのタイミングでしかエラー判別ができない

ココらへんの話は、下記のアノテーション書くときに補完が効かないのと似てるんだよな。(汗

@SuppressWarnings("deprecation")
@SuppressWarnings("unused")

stringリソースだけでなく、Plurals リソースというのも有るらしい。

例は下記のQittaとかを参照すると良

*1:まさに気分はストレッサー上級大将状態?

*2:library-projectに記載していても、main側で処理される作りのよう

*3:現在は変換前チェックタスクでエラーにしているようですが

*4: AS2.3/AS2.4の時点でも変わらず・・

*5:これについて findViewByIdから開放ヤッホ~とか書いてる人がいる。。。