Androidアプリで,レイアウト用XMLの名前をいちいち指定せずに,自動的に画面を描画させよう (Rails風のCoCなレンダリング)
重要なお知らせ:
この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。
より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。
AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...
Androidアプリで,画面レイアウトを描画するためには
setContentView(R.layout.hoge_page);
のように,特定の画面に対応する「XMLファイルの名前」を指定する必要がある。
ここでは,hoge_page.xml というファイルを指定する。
しかし,これは面倒では?
アクティビティの名前がわかれば,それに対応するXMLファイルの名前も,
(変な名前を付けない限り)自動的にわかるはずだ。
例えば,
- 「HogePageActivity」というアクティビティであれば,「hoge_page.xml」だ。
- 「FugaPageActivity」というアクティビティであれば,「fuga_page.xml」だ。
つまり,大抵の場合は
「アクティビティ名から,自動的にレイアウトXMLを判別できる」
はず。
もしアクティビティの名前が決まった時点でレイアウトが一意に特定できる場合,
レイアウトXMLの名前をいちいち毎回手動でコーディングするのは,二度手間なのだ。
下記では,その問題を解決する。
アクティビティ名が決まった時点で,自動的にレイアウトXMLも識別されるようにする。
つまり,Javaの悪しき慣習である「設定地獄」な考え方を抜け出して,
Ruby on Rails風に「CoC」(Convention Over Configuration)にコーディングしよう,
という試みの一つ。
Ruby on Railsの哲学
http://ja.wikipedia.org/wiki/Ruby_on_...
- Railsの基本理念は「同じことを繰り返さない」(DRY:Don't Repeat Yourself)と「設定より規約」(CoC:Convention over Configuration)
- 「設定よりも規約」とは、慎重に設計された規約(Convention)に従うことにより設定(Configuration)を不要に(あるいは軽減)するということである
まず,クラス設計を行う。
次に,動作確認済みのサンプルコードを掲載する。
クラス設計
Androidアプリでクラス設計を行う場合,
「基底となるBaseActivityに,便利メソッドを詰め込もう」
という考え方がある。
この手法のサンプルは,下記のURLを参照。
Androidアプリで,_("リソース名") と書くだけで,簡単に文字列を参照しよう
http://language-and-engineering.hatenablog.jp/entry/20110815/p1
- Androidアプリ開発時には,まず各画面の共通処理を洗い出し,自パッケージ内に abstract な BaseActivity を宣言する。そしてその中に便利メソッドを放り込んでゆく
ところが・・・
Google Maps API を使い始めたあたりから,この方法に無理が生じてくる。
マップを使うアクティビティは,必ず「MapActivity」を継承する必要があるのだ。
Androidアプリで,Google Maps API+GPS+Geocoderを使って,現在地の地図と地名を表示させよう
http://language-and-engineering.hatenablog.jp/entry/20110828/p1
そうすると,マップ系の画面では,独自の「BaseActivity」を継承できず,
別途「BaseMapActivity」みたいな基底クラスを継承しなければならない。
クラス図で言うと,下記のようになる。
Activity ↑ ↑ | | MapActivity | ↑ | | | BaseMapActivity BaseActivity ↑ ↑ | | ○○MapActivity ○○Activity (マップ利用) (マップ使わず)
そうすると・・・
非マップ系の「BaseActivity」と,マップ系の「BaseMapActivity」との間で
クラスのフィールドを共有できない。
つまり,基底クラス内に,共通ロジックを実装できない。
確かに,インタフェースの多重継承を使えば,共通ロジックの「宣言」だけは共有できる。
しかし,宣言されたロジックの中身のコードは共有できない。コピペが生じる。
JavaはRubyと異なり,モジュールのinclude(Mix-in)みたいな事が出来ないので
この問題は避けられない。どうしようもないのだ。
これは,Javaという言語が持つ,よく知られた欠点でもある。
菱形継承問題(ひしがたけいしょうもんだい、英: Diamond problem)
http://ja.wikipedia.org/wiki/%E8%8F%B...
- 多重継承を伴うオブジェクト指向プログラミング言語において、クラス A を2つのクラス B と C が継承し、B と C の両方をクラス D が継承する際に発生するあいまいさ
- 多重継承ができない言語(Objective-C、PHP、C#、Java)ではインタフェースの多重継承が可能である(Objective-C ではプロトコルと呼ぶ)。インタフェースは基本的には抽象基底クラスであり、抽象メソッドからなる(データメンバを持たない)。従って特定のメソッドやメンバ変数には常に1つの実装しかないので、あいまいさは発生しない
Javaの知られざる欠陥(下):二つ以上のクラスの実装を継承できない
http://itpro.nikkeibp.co.jp/members/N...
インタフェースでは実装が引き継げない:
- Javaでは実装の継承が一つしかできない。このため,コードを丸ごとコピーしなければならなくなる
- インタフェースによる疑似多重継承には,致命的な欠陥がある。インタフェースでは実装を継承できない。Rubyの作成者であるまつもとゆきひろ氏は「多重継承の代わりにインタフェースを使ったのはJavaの賢いところ。ただし,実装の継承を落としたことはとても痛い」と指摘
- 二つのクラスの機能を引き継ぐ場合,Javaではどちらかのクラスのソース・コードをコピーすることになる。この結果,同じコードがさまざまな個所に分散し,保守性が極端に低下する。同じ処理をクラスという単位にまとめて保守性を上げようとするオブジェクト指向の利点が生かせない。「マーケティング的には,Javaは大成功を収めた言語だ。しかしプログラミング言語の実装という意味では失敗作だと思う」(まつもと氏)。
- Mix-inで実装の継承を実現するRubyとは異なる
仕方がないので,こういう時は
「継承や汎化を使わず、内包や委譲を使う」
の原則に従う。
[連載]失敗するオブジェクト指向 - 「共通関数はベースクラスにあります。」(3)
http://chikura.fprog.com/index.php?UI...
クラスの関係性を図示すると,下図のような感じ。(ダイヤはAggregation:集約)
Activity ↑ ↑ | | MapActivity | ↑ | | | ┌◇BaseMapActivity BaseNonMapActivity◇┐ | ↑ ↑ | | | | | | ○○MapActivity ○○Activity | | (マップ利用) (マップ使わず) | | | ↓ | CommonActivityUtil ←――――――――――┘ (便利メソッドを 詰め込んだクラス)
このクラス設計でとりあえず実装に取り掛かる。
サンプルコード
まず,アクティビティの便利メソッドを詰め込むクラス。
特に,アクティビティ名からレイアウトXMLを検知するロジックを実装。
CommonActivityUtil.java
package com.example.common; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.app.Activity; import android.util.Log; /** * Map系+非Map系のActivityの共通処理 * */ public class CommonActivityUtil { private String tag = "common util"; /** * 該当アクティビティに対応するレイアウトXMLを検知して描画する */ public void render_xml( Activity activity ) { // クラス名の末尾の「Activity」を除去 String activity_class_name = activity.getClass().getSimpleName(); Pattern reg_pattern = Pattern.compile( "Activity$" ); Matcher reg_matcher = reg_pattern.matcher( activity_class_name ); String activity_basic_name = reg_matcher.replaceFirst(""); // クラス名の基本部分をパスカル形式(PascalCase)から // スネーク形式(snake_case)に変換 StringBuilder sb = new StringBuilder(); int class_name_length = activity_basic_name.length(); boolean previous_char_was_upper = false; // 1つ前の文字が大文字だったかどうか for( int i = 0; i < class_name_length; i ++ ) { Character c = activity_basic_name.charAt(i); // 大文字か if( Character.isUpperCase(c)) { // 直前が大文字でなければ,アンダーバーを追記 if( ( i > 0 ) && ( ! previous_char_was_upper ) ) { sb.append("_"); } // 小文字に変換 c = Character.toLowerCase(c); previous_char_was_upper = true; } else { previous_char_was_upper = false; } // 追記 sb.append(c); } // レイアウトXMLのフィールド名が完成 String xml_base_name = sb.toString(); Log.d(tag, "xml name is " + xml_base_name); // この名称のレイアウトXMLのリソースIDを取得 int xml_resource_id = activity .getResources() .getIdentifier( xml_base_name, "layout", activity.getPackageName() ); // このXMLでレイアウトを描画 activity.setContentView( xml_resource_id ); return; } }
参考:
キャメルケース
http://ja.wikipedia.org/wiki/%E3%82%A...
- 複合語をひとつのプログラム要素としたいときには、camel_caseのようにアンダースコア ( _ )を区切記号として用いる。これをスネーク・ケースという
3.Pascal形式とcamel形式
http://www.pi-sliderule.net/sliderule...
- 1文字目は大文字、2文字目以降は小文字、後は区切りごとに頭を大文字とすることをPascal形式といいます。たとえば、PascalCaseはパスカル形式
- 1文字目は小文字、2文字目以降は小文字、後は区切りごとに頭を大文字とすることをcamel形式といいます。たとえば、camelCaseはカメル形式
Java正規表現の使い方 : 正規表現を使った置換
http://www.javadrive.jp/regex/replace...
次に,マップ系アクティビティの基底クラス。MapActivityを継承。
BaseMapActivity.java
package com.example.common; import com.google.android.maps.MapActivity; /** * Map系Activithyの基底クラス。 * */ public abstract class BaseMapActivity extends MapActivity { // Activityの共通便利クラス protected CommonActivityUtil cau = new CommonActivityUtil(); @Override protected boolean isRouteDisplayed() { // 子クラスでいちいちこのメソッドを記述する必要はない return false; } }
同じく,非マップ系アクティビティの基底クラス。普通のActivityを継承。
BaseNonMapActivity.java
package com.example.common; import android.app.Activity; /** * 非Map系Activithyの基底クラス。 * */ public abstract class BaseNonMapActivity extends Activity { // Activityの共通便利クラス protected CommonActivityUtil cau = new CommonActivityUtil(); }
これで,準備完了。
以下では,これらのライブラリを利用する。
非マップ系のアクティビティの例:
package com.example; import com.example.common.BaseNonMapActivity; public class SampleNonMapActivity extends BaseNonMapActivity implements OnClickListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // レイアウトを描画 cau.render_xml(this); } }
この場合,「sample_non_map.xml」というレイアウト定義ファイルが自動的に探し出され,描画に利用される。
アクティビティの名前と似たようなXML名を,いちいち指定しなくて済む。
次に,マップ系アクティビティの例:
package com.example; import com.example.common.BaseMapActivity; public class SampleMapActivity extends BaseMapActivity implements OnClickListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // レイアウトを描画 cau.render_xml(this); } }
この場合,「sample_map.xml」というレイアウト定義ファイルが利用される。