読者です 読者をやめる 読者になる 読者になる

負け犬プログラマーの歩み

負け組だった人間が今一度PGとして人生の飛躍を模索するも加齢と共に閉ざされる未来に直面しているブログ。

Androidの<merge>の大きな欠点?

StackOverflowに投稿しようと思ったら同じ質問が既に投稿されていて放置されているので、悔しいからここに投稿する。つかね、あそこで解決できない事象はもう解決不可能なんだろうということだから、あっさり諦められるんだけどね。


Activity.findViewById(int id)は、そのIDを持つViewが複数存在する場合も常に1つのViewしか探してこない。多分DecorViewというAndroidのヒエラルキーの頂点に立つViewから見て一番最初(何を以ってして「一番最初」と言うかはさておき)のViewを拾ってくるのだろう。だから、もし1つのXML(=1つのID)から複数のインスタンスを生成した時に、個々のインスタンスへの参照変数が欲しい場合は、findViewById(int id)を実行させるメソッドをDecorViewより下の階層に設定し「その階層から見たら当該IDを持つViewは1つ」という所まで絞り込む必要がある。

「1つのIDから複数のインスタンスを作る」場合、一番手っ取り早いのはincludeだけど、これはincludeさせるView達の親となるViewGroupが必要になるので、無駄に階層が深くなってしまうし、そのViewGroupのインスタンス生成処理が入るから重くなる。そこで紹介されているのが、そのViewGroupを不要にさせるmergeだけど・・・、これには大きな欠点があり、仮に同一のXMLからおこしたViewを一つの親Viewにくっつける場合、個々のViewが一意にならなくなる。

共通部品のXML(foo.xml)

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/txt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="center_vertical"
        android:text="Foo Bar"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</merge>

メインのXML(main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/body"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >
        
    <include
        android:id="@+id/include1"
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	layout="@layout/foo" />

    <include
        android:id="@+id/include2"
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	layout="@layout/foo" />
    
    <include
        android:id="@+id/include3"
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	layout="@layout/foo" />        
        
</LinearLayout>

Javaコード

public class MainActivity extends Activity
{
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  View view = null;
        
  view = this.findViewById(R.id.txt);                  //(1)
  view = this.getWindow().getDecorView().findViewById(R.id.txt);  //(2) 
  view = this.findViewById(R.id.body).findViewById(R.id.txt);    //(3)
  view = this.findViewById(R.id.include1).findViewById(R.id.txt);  //(4)
  view = this.findViewById(R.id.include2).findViewById(R.id.txt);  //(5)
  view = this.findViewById(R.id.include3).findViewById(R.id.txt);  //(6)      
 }     
}

(1)と(2)では、findViewById()を行うViewから見ると、R.id.txtを持つViewは複数あるので一意に定まらない。結果として、常にinclude1のfooにあるid:txtを拾ってくることになる(んだが、内部の実装は精査してない)。(3)の時点でもまだ絞りきれていない。(4) (5) (6)は、そもそもmergeだと、includeしてもViewGroupが生成されないので、nullが返る。

単純な解決方法は、もうmergeを諦めることだが、だからと言ってincludeを使う気にもなれない。結局、肥大なXMLを作るとsetContentView()で100ミリ秒(0.1秒)以上かかってしまったりするので前からあまり好きではなかった。というわけで、XMLを部品化する場合は、XMLタグではなく、Javaで実装してくっつけることにした。つーか、ただの推定だけど、includeが内部的にはLayoutInflatorを呼び出してる場合は、パフォーマンス面でもそんな差はないと思われる。

実装面では少しだけ作業が増える。まず、1つXMLをinflate()したあと次のを起こす前に、View.setId()でidを書き換えておかないと、その後inflate()したViewにfindViewById()でアクセスできない。また、R.idのフィールドメンバーは更新されないんで、getResourceEntryName()は例外が出る・・・など。