2015/02/28

[Android]LabeledItem:RadioButtonを簡単に使うためのクラス


Stringについてのうんちくを使用と思ったのですが、書き始めるといろいろ裏を取らなければならないことがあり、そのためにはJavaの言語仕様やバーチャルマシン、Java Byte Codeの仕様まで読み込まなければならないと気が付いて気持ちが萎えています。
ネットに情報を上げている方々ってすごいですね、やっぱり。
一日考えたらいろいろ思いついたので追記しています。
ネットで発言はやっぱり難しい。

前に継承できるStringクラス、AbstractCharSequenceを紹介しました。

実は私は最終的にこのクラスを使用していません。
そのあとすぐに、もう少し便利で潰しが利くクラスを思いついたからです。

public class LabeledItem<T> implements CharSequence{
 private String label;
 private T value;

 public LabeledItem(String label, T value){
  this.label = label;
  this.value = value;
 }

 public T getValue(){
  return value;
 }

 @Override
 public String toString(){
  return label;
 }

 @Override
 public char charAt(int index) {
  return label.charAt(index);
 }

 @Override
 public int length() {
  return label.length();
 }

 @Override
 public CharSequence subSequence(int start, int end) {
  return label.subSequence(start, end);
 }

 public static int indexOf(LabeledItem<?>[]items, Object value){
  for(int i=0; i<items.length; ++i){
   if(value.equals(items[i].getValue())){
    return i;
   }
  }
  return -1;
 }
}

ジェネリッククラスになっていますので、データ型は自由に定義できます。こうするとAbstractCharSequenceを継承せずとも十分拡張性があるので、直接実装しています。
indexOfの第一引数はLabeledItemにしたかったのですが、文法エラーになります。
Tはインスタンスにかかる形容詞なのでstaticメソッドには使えません、とのこと。
型は指定しないがなんらかジェネリッククラス、という意味のにしてエラーは消えました。

ところで、LabeledItemをsetSingleChoiceItemsに適用するにはまだひと手間かかります。
第一引数に与えるためにLabeldItemの配列を作らなければならないのですが、素直に作った下記のクラスはコンパイルエラーとなります。

LabeledItem<Integer>[]items = new LabeledItem<Integer>items(){
 new LabeledItem<Integer>("item1",123),


ジェネリッククラスの配列は作ってはいけない模様。
釈然としないのですが、今は受け入れて回避策を練ってみました。

回避策1:CharSequenceの配列とする

CharSequence[]items = new LabeledItem<Integer>items(){
 new LabeledItem<Integer>("item1",123),

setSingleChoiceItemsの第一引数はCharSequence[]ですから、これで十分です。問題は選択された後、CharSequenceのインスタンスをLabeledItemにキャストしてあげる必要があり、ちょっとカッコ悪く、しかもワーニングになります。

回避策2:LabeledItemの配列とする
LabeledItem<?>[]items = new LabeledItem<Integer>items(){
 new LabeledItem<Integer>("item1",123),

型を不定にすると配列が作れるようになる、というのはやはり釈然としません。ここらへんの仕組みが血肉になっていないからなんだと思います。これもやはり選択された後にキャストが必要です。
なんとなく仕組みがわかってきました。そのうち解説しますが、このやり方はジェネリック文法の穴を突いた感じなのでよろしくないですね。

回避策3:LabeledItem<>を継承したクラスを作る
public class LabeledIntItem extends LabeledItem<Integer> {
 public LabeledIntItem(String label, Integer value) {
  super(label, value);
 }
}

LabeledIInttem[]items = new LabeledIntItem[](){
 new LabeledItem<Integer>("item1",123),


ただ1枚クラスを被せるだけでコンパイルを通ってしまうのもまた不思議です。これならキャストなしで使用できますが、せっかく流用できるように作ったジェネリッククラスをデコレートするクラスを作るというのが本末転倒です。
なんか違う方向に努力しているような気はしています。

回避策4:Object型の配列に格納する。

Object[]items = new LabeledItem<Integer>items(){
 new LabeledItem<Integer>("item1",123),

そもそもJavaの原始デザインにジェネリック型はありませんでした。コンテナクラスにはObject型で格納する、取り出す時にキャストするというのがルールだったのを思い出しました。
回避策1、2のようにワーニングすら出ませんからいっそすがすがしいと思います。

回避策5:コンテナクラスを使用する

  final List<LabeledItem<Integer>> items= new ArrayList<LabeledItem<Integer>>(){{
    new LabeledItem<Integer>("Zero", 0);
    new LabeledItem<Integer>("One", 1);
  }};

  .setSingleChoiceItems(items.toArray(new CharSequence[0]), 0, new OnClickListener(){

いろいろ考えた挙句、現在のJavaではこれが正解なのかな、と行きついた形です。
ジェネリッククラスで配列を作ることはできませんが、コンテナクラスを作ることはできます。これはCから受け継いだ配列という概念の限界を示しているのかもしれません。
上のように宣言すればコード上の扱いやすさは配列と大差ありません。
ポイントは、CharSequenceで扱われるときには正しくCharSequenceの配列として使用するというところでしょう。ここでジェネリッククラスの配列にしようとすると無限ループにはまります。

突っ込み歓迎します。

2015/02/22

[Java]extendsできるStringを作る

Stringがfinalで宣言されていて困ったので、自前で作ってみました。

最初に断っておきますが、Stringがfinalになっているのには相応の意味がありますので、理解してから、適切な使い方をしてください。

AndroidのAlertDialogで選択するアイテムを上品に作ろうとしていました。
setSingleChoiceItemsの第一引数は一覧に表示する文字列の配列です。普通はここにStringの配列を渡しますね。
私はここに選択するべきデータを与えたいと思いました。ラベルとデータで2つの配列を持つのは恰好わるいじゃないですか。

データのtoStringを目的に合わせて実装しておけばいいよね、と思っていたのですが、そう簡単にはいきませんでした。
データがCharSeqece(なんでStringじゃないの?の謎はまたあとで)を継承していないので適用できませんとのこと。toStringを持ってるんだからObjectでいいじゃん、とも思ったのですがこれもまた掘り下げると長くなりそうなのであとで。

それならStringを継承すればいいよね、と思ったら今度はStringがfinal宣言されているのでextendsできませんということ。これもまた理由があるのですが、長くなるのであとで。

素直にインターフェイスCharSequenceを実装すればいいんでしょうが、CharSequenceは複数のメソッドを持っていて実装はそこそこ面倒です。私の場合、同じ目的でいくつかのメニュにこの仕掛けを適用しようとしていましたので、データ毎にCharSequence実装するのは上品ではないなぁと思わざるを得ませんでした。

そこで、発想を転換して作ったのがこのAbstractCharSequenceです。

public class AbstractCharSequence implements CharSequence{
 @Override
 public char charAt(int index) {
  return toString().charAt(index);
 }

 @Override
 public int length() {
  return toString().length();
 }

 @Override
 public CharSequence subSequence(int start, int end) {
  return toString().subSequence(start, end);
 }
}

AbstractCharSequenceはGoFデザインパターンでいうところの典型的なAdapterです。
AbstractCharSequenceはtoString()で返される文字列を元に、CharSequenceを実装するという実に本末転倒なクラスです
表示に使う文字列はtoString()をオーバーライドして与えます。
toStringはデバッグ用、というのがJavaの基本概念のような気もしてます。このやり方は邪道かもしれません。参考まで。

public class AValue : extends AbstractCharSequence{

 private String label;
 private String value;

 public AValue(String label, String value){
  this.label = label;
  this.value = value;
 }

 @Override
 public String toString(){
  return label;
 }

 public String getValue(){
  return value;
 }
}

AbstractCharSequenceを実装したクラスをsetSingleChoiceItemsに与えることによって、データそのものを表示するインスタンスにできます。

最初にも書きましたが、この手法はわりとJavaという言語の設計思想の根幹に踏み込んだことをしています。今回のような明確な目的を持たずに乱用するとよくないことがおこるかもしれません。
少なくとも、AbstractCharSequenceは実体を持ったクラスなので、真にObjectmもしくはStringから継承されるべきクラスにのみ適用すべきです。ほかにも継承する候補があるクラスはそちらを優先すべきです。対応は簡単で、CharSequenceを継承、実装してください。

2015/02/21

[Android]有料版と無料版の切り替え

[2015/06/01追記]Android Studioでは下記手順のほとんどを無視して簡単にパッケージの切り替えができることがわかりました。また、やり方についてもベターな方法を編み出しました。Eclipseユーザー向けにこのページを残しておきますが、Android Studioユーザーは新しい方法を推奨します。

Google Playにアプリを公開するとき、有料版と無料版は別のアプリとして登録しなければなりません。
広告が入るくらいしか違いがないのだから簡単に切り替えたいですが、パッケージ名を変えるといろいろ祟りがあってイヤですよね。
Android開発し始めたばかりでまだツールの使い方や、AndroidManufestのいじり方もわからなかったころは本当に恐怖でした。

いくつかアプリを作ってみて、なんとなくコツが掴めてきたのでまとめておきます。

まず最初に、Eclipseはパッケージの切り替えについて効率の良いツールを提供してはいない、というのが私の結論です。上品プログラマーとしては、用意されたツールを使わない手はないとおもってあれこれ調べたのですが、あきらめざるを得ませんでした。
考えてみれば、パッケージはアプリの名前みたいなものですから、さくさく切り替えられてはたまらない、というのは理解できます。

AndroidManufest.xmlにかかれてるpackageを書き換えるとソースコード上に一気にコンパイルエラーが発生しますが、これはリソースIDを管理しているgen以下のパッケージ名がpackageに依存しているからです。
リソースIDを管理している<パッケージ名>+".R"というファイルのファイル名が変わるので、コード内のimport文をすべて書き換えるのは必須作業です。
逆に言うと、手間がかかる作業はこれだけで、あとは合理的に扱うことができます。

余談ですが、AndroidManufest.xmlに記述されるパッケージ名とアプリで使われているパッケージ名が同じである必要はないようです。わざわざ変える必要もないのですが、二つの間に関連性がないと気が付かないで嵌ったことがあるので、念のために。

以下は私がやっている手順です。

  1. 無料版用のパッケージを作ります。起動Activity、EclipseのウイザードがMainActivity.javaという名前で作るあれの格納されたパッケージと同列に”XXX_free”という名前で作っています。
  2. 新しく作ったパッケージに無料版に必要なクラスを移動します。上品プログラマーなら無料版と有料版のクラスを切り分けるためのデザインはできているはずです。最低限、起動Activty、例ではMainActivity.javaだけは複製する必要があります。起動時に有料版と無料版を切り替えるからです。単純に広告が入っているだけの違いなら起動Activityの複製だけで済むはずです。
  3. AndroidManufest.xmlの内容を書き換えます。
    - ?xmlタグのpackageをfree版のパッケージ名に切り替えます。例だとXXX_hureeです
    - 起動Activityをフリー版に切り替えます。intent-filterでが指定されているActivityです。これで無料版の起動Activityが呼ばれるようになります。
    - 無料版で必要なPermisionを追加します。広告を入れるなら、ネットワークなどの権限を足さなければなりません。逆に不要なPermissionは削除します。不要なPermissionが残っているアプリはセキュアではありませんし、マルウェアと思われかねません。PermissionはProguardも落としてくれません。
    - バージョン番号の更新が必要なら更新しておきましょう。上品プログラマーはバージョン番号を大事にします。
  4. String.xmlのapp_nameを無料版に書き換えます。多言語対応している場合はすべての言語で更新するのを忘れずに。
  5. Cleanしてからフルビルドします。このときアプリによっては大量のエラーが出ます。エラーの原因は自動生成されたクラスのパッケージ名が変わったからだけですから、あわてずにimport文の.Rで終わる一文を新しいパッケージ名に書き換えます。
  6. proguardをかけます。不要なクラスやライブラリはproguardが落としてくれるので、気にする必要はありません。proguardの設定は有料版と無料版の共用で構いません。
以上が私のやっている方法です。
一気に書き換えるツールを作ったら需要ありますかね。5分もかからないので手作業でやっていますが。