スポンサーリンク

Androidアプリで,_("リソース名") と書くだけで,簡単に文字列を参照しよう

重要なお知らせ:

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

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


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


Androidアプリ中で,日本語の文字列は「リソース」としてXML内に保管される。

Javaのコードからリソース文字列(メッセージ)を参照するためには

アクティビティのインスタンス.getResources().getString( R.string.リソース名 )

という長いコードを書く必要があり,非常に面倒。


もし画面を動的に構築する場合,

表示文言を参照するための処理が何度も繰り返され,

コードが非常に冗長になる。



もしPHPとかで,「getText」なんかを使って

メッセージを国際化した場合,たった

_("リソース名")

と書くだけで済むのに。

Androidでも,この「_( )」のような便利メソッドが欲しい。



以下は,その実装案。 代案も2つ紹介する。

※型安全なコードは,末尾の「補足2」に掲載されている。


便利メソッドの案

クラスを2つ作る。

  • CommonUtil.java:共通処理を集約したクラス
  • BaseActivity.java:アクティビティの基底クラス


CommonUtil.java

package my_package.common;

import android.app.Activity;

/**
 * 共通の便利メソッドを定義するクラス
 *
 */
public class CommonUtil {

	/**
	 *  getText風に文字列を取得。
	 *  getResources().getString(R.string.someString) のシンタックスシュガー
	 */
	public static String getText(String target_string_name, 
		Activity activity_instance)
	{
		// リソースIDを取得
		int target_string_id = activity_instance
			.getResources()
			.getIdentifier(
				target_string_name,
				"string",
				activity_instance.getPackageName()
			);

		// リソースから取得
		String target_string = activity_instance
			.getResources()
			.getString( target_string_id )
		;

		return target_string;
	}

}


BaseActivity.java

package my_package.common;

import android.app.Activity;

/**
*
* 本アプリの基底アクティビティ。
* 画面上の便利メソッドを放り込むためのクラス。
*/
public abstract class BaseActivity extends Activity {

	/**
	 *  getText風に文字列を取得
	 */
	public String _(String target_string_name)
	{
		return CommonUtil.getText(target_string_name, this);
	}
}

新規画面作成時には,「extends Activity」ではなく「extends BaseActivity」とする。


そうすれば,Activity中では

// getResources().getString(R.string.string_hoge) の代わり
_("string_hoge")

のようにして文字列を参照できる。なんともスマート。



また,CommonUtilをインポートした任意のクラスからも

// acはアクティビティのインスタンス
CommonUtil.getText("string_hoge", ac)

のようにして文字列を参照できる。

これならコードも簡素化されるし,

「R」などの低レイヤのクラスやAPIを意識しないで済む。



参考:

getIdentifierでリソース名を動的に指定する
http://muchag.undo.jp/archives/878

  • 動的にリソース名を指定してリソースを取得するためには,getResources().getIdentifier() でリソースIDを取得すればよい


Resourcesクラスを使ったリソースの参照
http://www.javadrive.jp/android/xml_l...

  • 普通にリソース参照する方法


このように,Androidアプリ開発時には,まず各画面の共通処理を洗い出し,

自パッケージ内に abstract な BaseActivity を宣言する。


そしてその中に便利メソッドを放り込んでゆく,

というのが,開発の流れとして妥当だろう。


代案1

getText() 関数の本体は,リフレクションを使って格好よく書くこともできる。


CommonUtil.java

package my_package.common;

import java.lang.reflect.Field;

import my_package.R;
import android.app.Activity;
import android.content.res.Resources;

/**
 * 共通の便利メソッドを定義するクラス
 *
 */
public class CommonUtil {

	/**
	 *  getText風に文字列を取得。
	 *  getResources().getString(R.string.someString) のシンタックスシュガー
	 */
	public static String getText(String target_string_name, 
		Activity activity_instance)
	{
		// リフレクションでRクラス経由で文字列を取得する。
		// もし,XMLではなくstaticクラス内に文言を集約したくなったら
		// ここを書き換える。

		Class<R.string> c = R.string.class;

		// 文字列のnameに対応するidを取得する
		Field target_string_field = null;
		int target_string_id = 0;
		try
		{
			// idのフィールドを取得
			target_string_field = c
				.getDeclaredField( target_string_name );

			// idの値を取得
			target_string_id = (Integer)target_string_field
				.get(null);
		}
		catch( NoSuchFieldException e )
		{
			return null;
		}
		catch( IllegalAccessException e )
		{
			return null;
		}

		// 該当idに基づいて文字列を取得
		Resources res = activity_instance.getResources();
		String target_string = res.getString( target_string_id );

		return target_string;
	}

}

このコードは例外処理を含んでいて長いし,R というクラスの存在も明示的に意識する必要がある。



とはいえ,もし Android SDK のAPI仕様を十分知らず,

getIdentifier() なんて便利なメソッドが存在するというのを知らない場合,

こういうコードが自然に生まれると思う。


参考:

Javaで,private変数・privateメソッド・privateコンストラクタを,外部から呼び出そう (リフレクションの方法)
http://language-and-engineering.hatenablog.jp/entry/20101111/p1

代案2

もっと簡単な(安直な)案として,1つのstaticなクラス内に

全てのメッセージを詰め込んで,メッセージ管理クラスとして使う,

という手がある。


これは,小規模アプリであれば常套手段だ。

Railsで,簡単にメッセージ管理する方法 (メッセージ定義書からメッセージ処理クラスを自動生成するVBAマクロ)
http://language-and-engineering.hatenablog.jp/entry/20090704/p1


もちろん,Androidアプリの場合も,1つのクラス内に定数を詰め込んで

メッセージを集中管理することは可能だ。そのほうが動作も速い。



しかし,国際化(i18n)を念頭に置くと,その考えは浅はかだ。

特に,「Androidマーケット」という市場が持つ性質およびポテンシャルを熟考し,考え直すべきだ。

これについては下記URLなどを参照。

getResources() or create own static class?
http://stackoverflow.com/questions/69...

  • getting resources through getResources() is costly compared to a static class.
  • However, using getResources() has its advantages: It helps you externalize your resources.
  • When you feel this cost, be sure that, at that point, any thought of localization would be extremely costly.


個人開発のAndroidアプリで月収116万円に
http://www.atmarkit.co.jp/news/201003...

  • iPhoneでは運か実力かは別としてトップに儲けが集中する構造であるのに対して、アプリを回転させているAndroidマーケットには、より多くのアプリ開発者にチャンスがある
  • 「もし今までにもAndroid開発を検討していたのなら、思い切って始めてみることをおすすめする。個人開発者には理想的なプラットフォームだと確信している」


アプリを国際化してAndroid Marketから世界へ発信
http://www.atmarkit.co.jp/fsmart/arti...

  • アプリがカバーする言語や端末を増やすことが多くのユーザーを取り込む第一歩
  • そこで重要なのは、「英語をサポートすること」

補足

本稿の getText() を使ってリソース文字列を参照すると,

文字列の指定は,クラスのフィールドではなく「文字列型」で行われることになる。


そうすると,Eclipse上でのコーディング時に,文字列名のサジェスチョンが出ない。

コードアシスト(自動補完)が使えない,という事になる。


静的検証ができなくなるので,文字列のリソース名をタイプミスしたりすると

コンパイル前にもコンパイル時にもミスに気づかない。

エミュレータや端末上での動作確認(テスト)時に,初めて気がつくことになる。


Javaのような静的言語を使う大きな動機づけ,大きな魅力の一つは,

このような静的バリデーションが自動で走ってくれることなのだが。


その点が不便だが,そこは仕方がないのでトレードオフか。



ちなみに,同じくリソースの取得において「ビュー要素の取得」もあるわけだが,

そちらはラッパーメソッドの必要はないと思われる。

TextView my_textlabel = (TextView)findViewById(R.id.my_textlabel);

この程度のコードの長さであれば,まだ耐えられるだろう。


JavaScriptのgetElementByIdと比べた場合,差分は

「R.id.」と,あとキャスト先の型をタイピングするだけなので。


それに,こうやって全部ガチガチに型を決め込んで,

ある程度冗長(=堅牢)なコードを積み上げてゆく作業こそ,

Javaのコーディングの「最大の楽しみ」の一つと言えるのだし。



まあしかし,そうは言っても,

もしRuby on RailsやjQueryのような「DSL」として使えるフレームワークが

早く Android の世界にも登場してくれたら,それは最高だろう。


補足2

上の「補足」で取り上げた点を考慮し,

IDEでの自動補完を可能にしたバージョンも作ってみる。


実装方針として,呼び出し時に「R.string」とタイプする手間は惜しまない事にする。


CommonUtil.java

CommonUtil.java

package my_package.common;

import android.app.Activity;

/**
 * 共通の便利メソッドを定義するクラス
 *
 */
public class CommonUtil {

	/**
	 *  getText風に文字列を取得。
	 *  getResources().getString(R.string.someString) の
	 *  シンタックスシュガー
	 */
	public static String getText(String target_string_name,
		Activity activity_instance)
	{
		// リソースIDを取得
		int target_string_id = activity_instance
			.getResources()
			.getIdentifier(
				target_string_name,
				"string",
				activity_instance.getPackageName()
			);

		// リソースから取得
		String target_string = getText(target_string_id, 
			activity_instance);

		return target_string;
	}

	/**
	 * 上記メソッドを型セーフにしたもの
	 */
	public static String getText(int target_string_id, 
		Activity activity_instance)
	{
		// リソースから取得
		String target_string = activity_instance
			.getResources()
			.getString( target_string_id )
		;

		return target_string;
	}

}

}


BaseActivity.java

package my_package.common;

import android.app.Activity;

/**
*
* 本アプリの基底アクティビティ。
* 画面上の便利メソッドを放り込むためのクラス。
*/
public abstract class BaseActivity extends Activity {

	/**
	 *  getText風に文字列を取得
	 */
	public String _(String target_string_name)
	{
		return CommonUtil.getText(target_string_name, this);
	}

	/**
	 *  getTextの型セーフ版
	 */
	public String _(int target_string_id)
	{
		return CommonUtil.getText(target_string_id, this);
	}
}


これなら,R.stringクラスのメンバを引数として渡すことが可能になり,

その際にはIDEによる自動補完が可能になる。

だから,タイプミスの心配もない。



リソース名をどうしても動的に指定したい場合に限って,

オーバーロードされた同一メソッドに対して文字列を渡せばよい。


そうする事によって,「文字列リソースを適切に呼び出せているかどうかのテスト」

は最小限で済む。



Javaメソッド設計の原則:

  • 便利メソッドを作りたければ,オーバーロードせよ。
  • 静的言語を使うのであれば,タイプセーフの強みを最大限に生かせ。
  • オーバーロードの多用によって手動キャストが多くなり,コード実行時の型安全性が脅かされる場合,代わりにGenericsを使え。

強く型付けされているJavaの理解に必修の“型変換”
http://www.atmarkit.co.jp/fjava/rensa...

  • メソッド名、パラメータの数とデータ型の並びの組み合わせ=「シグニチャ(signature)」
  • System.out.printlnが便利なのはオーバーロードされているから


オブジェクト指向初心者にありがちな後に後悔しない為の7の事 in JAVA
http://d.hatena.ne.jp/nodamushi/20110...

  • 同じ関数名を定義できるのが嬉しくてしょうがない状態はOOP初心者
  • オーバーロードの基本は一つのメインとなる関数を作成し、他の関数は、その関数を呼べる形に変数に変換を加えるだけにする


Generics とはナンだろう
http://www.javainthebox.net/laborator...

  • Listのような「便利な入れ物オブジェクト」にgetterとsetterを定義すると,setterがオーバーロードで多数になってしまい,getterが危険になったりする。genericsで型安全になる。