スポンサーリンク

Androidアプリの画面レイアウトを,まるでjQueryのようなコードで動的構築できるライブラリ (の試作品。UIコーディングのためのDSL)

重要なお知らせ:

この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。

より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。


AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...


まるでjQueryのような簡潔なコードによって,

AndroidアプリのUI・レイアウトをコーディングできるようなJavaライブラリ。


その試作に成功した。


アクティビティ中で,下記のようなコードが書ける。


  // 動的に定義したViewは,これらのフィールドに代入される。
  MButton button1;
  MLinearLayout layout1;
  MTextView tv1;
  MEditText et1;
    

  @Override
  public void onCreate(Bundle savedInstanceState) 
  {
    super.onCreate(savedInstanceState);


    // レイアウトを動的に描画する。
    
    new UIBuilder(context)
      .add(

          // Buttonを定義
          button1 = new MButton(context)
            .text("ボタン1")
            .click(new OnClickListener(){ // クリックイベント

              @Override
              public void onClick(View v) {
                // イベント発生時,他のViewの値を参照可能
                Toast.makeText(
                  context, 
                  "ボタン1が押されました。入力値:" + et1.getText(),
                  Toast.LENGTH_LONG
                ).show();
              }

            })
      )
      .add(

          // LinearLayoutを定義
          layout1 = new MLinearLayout(context)
            .orientationHorizontal() // 水平に並べる
            .widthFillParent()
            .add(

                // TextViewを定義
                tv1 = new MTextView(context)
                  .text("ラベル" )
                  .widthWrapContent()
                ,

                // EditTextを定義
                et1 = new MEditText(context)
                  .widthPx(200)

            )
      )
    .display(); // 表示する

  }


上記のコードを実行すると,下記の画面キャプチャのようなレイアウトとなる。


ガチガチの静的型付け言語である Java 上であるにも関らず,

バリバリの動的言語である JavaScript や jQuery っぽいコーディングができる。

少し夢みたいな話だ。


しかし,これは現に実現した。


このライブラリの基本的なアイデア,およびその内部のコードの一部抜粋を,下記に記載する。

着想・企画

Androidアプリを開発する際,レイアウトXMLをチマチマ編集するのは,極めて面倒だ。


もちろん,手の込んだ画面をしっかり作りこみたい場合には,

レイアウトXML上で時間をかけてUIデザインする事になる。

それも大事。(静的なUI定義



しかし,たとえばプロトタイプ作成の段階だったり,

あるいは画面構成がシンプルで,XMLを書くまでもないUIだったりする場合は,

レイアウトXMLには触れないで済ませたいもの。


プログラム上から動的に,パパッと画面を構築したい,と思うはずだ。(動的なUI定義


これは,UI構築プロセスがJavaコードで制御可能になることを意味する。

そうすれば,例えば画面描画の過程をログ出力して,描画処理の途中の異常を発見する事もできる。



UIの実体は,設定ファイルではなくなる。


ジャンルの違う例を引き合いに出すが,

Webアプリ開発だと,画面・UIはどうやって書くか?

JSP,PHP,ERBといった「コード」で制御するのではないか?

少なくとも,「設定ファイル」というポジションでメイン実装を施すことはないはずだ。

(※でもまあ正確に言うと,これらもXMLの中にコードを埋め込んでるんだけど…。)


もちろん,素のAndroidでもレイアウトXMLにイベントリスナぐらいは埋め込めるし,XMLも静的型付けのある「コード」とみなす事は可能だけど。

イベントリスナーは無名クラスよりxmlが楽でいいかも?
http://blog.livedoor.jp/shizuku_kun/a...

  • android:onClick属性に関数名を記述可能

設計にView-Helperパターンを適用したい場合もあるだろう。

ビューと,ビューのイベントリスナやアクションを分離するという事だ。

それをちゃんとやるためには,まずは「ビュー自体を上手い仕方でJavaの制御化に置く」ことが必要なのではないか。



なお動的なUI構築だと,Eclipseのレイアウトエディタにはレイアウトが表示されない事になるが,

実機で動作確認しているならそっちの方がはるかに早い動作なので,問題はない。



今まで,こういった側面でサポートしてくれるライブラリが存在しなかった。

レイアウトの動的な構築のためには,Googleが用意したAPIに従って,

一つ一つ手間をかけてViewを追加してゆくようなサンプルコードしかなかった。



この状況はおかしい。あまりにも生産性が悪い。

Androidアプリ開発の世界においても,

jQueryのようにメソッドチェインを駆使した,UI操作のためのDSLが必要だ。

できればfluent interfaceとして設計されたものが欲しい。



無いなら自作するまでだ。

そういう経緯で,冒頭のコードを実現させるためのライブラリ作成に取り掛かった。


基本的な設計アイデア

各クラスとも,設計はそれほど難しくはない。



まず冒頭で掲載した,Activity本体から。

基底クラスで事前にcontextを自動的に保持するようになっているので,

派生クラス上では,何もしなくとも「context」という変数を使ってコーディングできる。


ここで取り上げる「動的なUI構築のためのライブラリ」を使う際,

インデントなどの作法は,jQueryのプラクティス・発想法をできるだけ模倣する。


この点,カヤックの解説記事が詳しいので読むとよい。

jQuery言語入門
http://tech.kayac.com/archive/jquery-...

  • Traversing methodでインデントを下げて .end()でインデントを上げる
  • Traversing以外のmethodはインデントを維持
  • 末尾には開始行と同じ位置に「;」を置く


さらに,Viewをnewした際に,メソッドの引数部分であっても「代入文」を記述できる,というのがミソ。

たとえメソッドチェインの真っただ中であっても,生成したViewを

Activityのフィールド内にどんどん代入していけるのである。


そのおかげで,冒頭のコードでは,ButtonからEditTextの入力値を参照できている。


他のプログラミング言語と比べた場合,Javaの言語仕様には

「クロージャ(無名関数)が存在しない」という明らかな欠陥が存在するため

生産的なプログラミングをする上で致命的な足かせとなるのは事実なのだが,

そこを「カンマ区切りで代入文を記述する。」という代替手段でやりくりできるのだ。

これなら,メソッドのラッパーで匿名クラスを定義するまでもない。


このからくりについては,詳しくは下記ページなどを参照のこと。

Java言語規定 15. 式
http://www.y-adagio.com/public/standa...

  • 左右のオペランドが評価される順序や,括弧を付けた場合の優先順位の変化
  • a += ( a = 3 ); のようなコードが言語仕様として許容される件

次に,個別Viewのラッパークラスについて。

これは,クラス数もメソッド数も発散しがちなところなので,必要なものを適宜作っていけばいいだろう。

ボタンの例だけ掲載する。

public class MButton extends Button implements MyView
{

    public MButton(Context context) {
        super(context);
    }


    // パラメータ保持
    HashMap<String, Object> view_params = new HashMap<String, Object>();

    @Override
    public Object getViewParam(String key) {
        return view_params.get(key);
    }

    @Override
    public void setViewParam(String key, Object val) {
        view_params.put(key, val);
    }


    // 以下は属性操作


    public MButton text( String s )
    {
        setText(s);
        return this;
    }

    public MButton click(OnClickListener l) {
        setOnClickListener(l);
        return this;
    }

    // TODO:その他の属性操作も今後追加してゆく

}

とても単純だ。

  • 基本的には,親クラスと同じ性質を持ったオブジェクトとしてふるまう。
  • 親クラスのネーミングが,あまりにも「JavaJava」し過ぎていて冗長な場合は,ためらうことなくプロキシメソッドを設ける。
  • MyViewインタフェースを実装する。これは,getViewParamとsetViewParamだけを宣言した,小さなインタフェース。
  • set○○で直接Viewに値をセットできない場合に備え,そういった属性をparamとして内部に保管しておく。
  • メソッドチェイン実現のため,常にthisを返す。

そして,主役となるUIBuilderクラス。

考えを伝えられるよう,要所要所のみを記載。

public class UIBuilder
{

    // 〜〜〜 前略 〜〜〜


    /**
     * 1つ以上の描画したい要素を追加。
     *
     * @param v 可変個のView
     */
    public UIBuilder add(View...v)
    {
        for( int i = 0; i < v.length; i ++ )
        {
            this.includingViews.add( v[i] );
        }

        return this;
    }


    /**
     * 定義し終えたレイアウトを,実際に描画する。
     */
    public UIBuilder display()
    {
        // ひな型XMLを使い,その中に各Viewを次々に放り込んでゆく。
        activity.setContentView(R.layout.template_layout);

        // 定義済みの全Viewをレイアウトに動的登録
        LinearLayout rootLayout = (LinearLayout)activity.findViewById(R.id._RootLayout);
        inflateInsideOneLayout(rootLayout, includingViews);

        return this;
    }


    /**
     * 特定のレイアウトの中身を描画する。
     * 中身にレイアウトを含む場合,再帰的に呼び出される。
     */
    private void inflateInsideOneLayout(LinearLayout parentLayout, ArrayList<View> viewsInsideLayout)
    {
        for(View v : viewsInsideLayout )
        {
            registerOneView(parentLayout, v);
        }
    }


    /**
     * 1つのビューを親レイアウト内に描画する。
     */
    private void registerOneView(LinearLayout parentLayout, View v)
    {

        // 〜〜〜 中略 〜〜〜

        if( v instanceof MLinearLayout )
        {
            MLinearLayout innerLayout = (MLinearLayout)v;
            inflateInsideOneLayout(innerLayout, innerLayout.includingViews);
        }

        // 親レイアウト内に描画
        parentLayout.addView(v, new LinearLayout.LayoutParams(intWidth, intHeight));
    }

}

これも,特別なことはやってない。

  • 最上位にぶらさがっているView達を,ArrayListとして保持する。addされたら可変個だけ追加する。
  • inflateInsideOneLayoutというメソッドを,再帰的に呼び出す。親レイアウトの中身を全て描画するメソッドである。最上位のレイアウト,その中に存在する別のレイアウト・・・という具合に,入れ子に順々に描画してくれる。
  • LinearLayoutのラッパークラスも,ほとんど同じようなことをやっている。
  • ひな型となるXMLを1つだけ用意しておくが,中身は空っぽである。最低限のScrollViewなど,トップレベルのLayoutが枠組みとして書かれているだけ。そのLayoutの中に,ビルダーがゴリゴリとViewを突っ込んでゆくのだ。

ライブラリのコードの説明は,以上。

これで,基本的なアイデアは伝わるはず。


結び


現状のAndroidの標準的なUI実装方式が「静的」の極みとすれば,

ここで取り上げる方式は,対極に位置する事になる。


だから,ゆくゆくはその間に位置する「中間的なもの」が必要かもしれない。

View-Helperな設計を実現し,ダイナミックでevent-fulなUIを効率よく実装するためには,そうなってゆくだろう。


なので,本記事は一つの「方針」や考え方,もしくはアイデアを示しているにすぎない。



ときに,Titaniumユーザにとっては,

スマホのネイティブアプリをHTML+CSS+jQueryのコンビで常に開発しているので,この情報に新規性を感じないかもしれない。

しかし:

  • Javaという静的言語のプラットフォーム上であってもこういったライブラリを実現可能である,という点や
  • 素のJavaによるAndroidアプリの開発の効率化にも,まだまだ発掘の余地がある

という点は,きっとご理解いただけるだろう。

本稿で試作したようなモノ達が,今まで存在しなかったからこそ,Titaniumみたいなフレームワークの需要が生じたのだから・・・。



補足


近々,本ブログ上で,良い発表がある。