Infinito Nirone 7

白羽の矢を刺すスタイル

RxEither を RxJava2 に対応してみた

もともとのモチベーションとして、Java で Either を扱いたくて色々探していました。 そのなかで RxJava との運用もカバーしてくれている RxEither を見つけたのですが、あいにく RxJava 1.x で止まっていたので RxJava 2.x に対応させてみることにしました。

github.com

github.com

基本的にはパッケージの変更と、Action1 とか Func1 とかを Consumer やら Function やら Predicate やらに書き換えていく作業をポチポチとやっていきます。 テストもあるので、そちらもパッケージ変更とクラスの変更をやります。あとはテストを実行してオールグリーンになるのを見届けました。

一つ気がかりというか困ったのは、RxJava 2.x の Consumer や Function はメソッドのシグネチャthrows Exceptionとあるので、どこかで try-catch が必要になるのですが、このライブラリが依存している SealedUnion2 というライブラリでの Union2 の定義には throws Exception がないので、どうしても Left や Right で try-catch を書かないといけないというところです。握りつぶすのもあまり良くないなと思ったので今のところは RuntimeException でラップしていますが、いまいちしっくりこない…

C93 Android モダンプログラミングに RTL 対応の章を書きました

明日から C93 が始まりますね。TechBooster から Android モダンプログラミングという新刊が出ますが、そのなかにある RTL 対応の章を担当しました。

techbooster.github.io

この章を見れば RTL 対応に必要なことが大体網羅できるはずです。よくあるレイアウトの話以外にも RTL 対応が必要なことはたくさんあるので、その辺をシュッとイイカンジにやるための内容をコンパクトにまとめました。

で、一つこの章には載せきれなかった話があります。レビューをしてもらっているときに「ViewPager ってどうなの」という話が出てきて、そういえばヤツは RTL だからといって向きが逆になったりしないな?と思ってちょっと調べてみました。

結論というか、普通にしてても ViewPager は RTL だろうとなんだろうと左から右にページを追加していくので、何かしらのトリックが必要です。いくつか方法があります。

一つ目は、ViewPager を rotationY で 180度まわしたあと、それぞれのページをさらに rotationY で 180度まわしたら向きを逆にできるのでこれで RTL 対応できる!というものすごくアクロバティックな Workaround。いろいろ犠牲にしているものがありそうな気がしますが、PagerTabStrip を一緒に使っているとそれの様子がおかしくなるようです。

mobikul.com

二つ目は、RTL のときと LTR のときで Adapter でもつコレクションの中身を逆順にする方法。最初に表示する位置を LTR のときは 0 で RTL のときは size - 1 にすれば擬似的に RTL 対応できます。単純でわかりやすい解決方法です。

stackoverflow.com

この他、サードパーティ製の RTL に対応した ViewPager ライブラリがいくつか存在します。ViewPager のロジックに RTL へ対応するためのコードを書き足す一番素直な解決方法です。

github.com

github.com

github.com

何にしても頑張りが必要ですね。一番ラクなのは二つ目の"Adapter でもつコレクションの中身を逆順にする"方法だと思います。

諸君、私はAndroidが好きだ

諸君、私は戦争が好きだ: wids.net

作ってみました。

諸君、私はAndroidが好きだ
諸君、私はAndroidが好きだ
諸君、私はAndroidが大好きだ

クラッシュが好きだ
ANRが好きだ
機種依存問題が好きだ
激安端末が好きだ
売れないタブレットが好きだ

アメリカで
日本で
中国で
インドで
ヨーロッパで

この地上に存在するありとあらゆるAndroidが大好きだ

機種依存問題でクラッシュするときが好きだ
OSバージョンが上がると別の問題が発生するときなど心がおどる

フレームワークをハックすることが好きだ
リフレクションとIPCを駆使して便利機能を実現したときなど胸がすくような気持ちだった

OSのカスタマイズが好きだ
iOS と寸分違わぬ見た目になったときなど感動すらおぼえる

とても小さな筐体の端末で Android が動いているときなどもうたまらない
Google I/O で新しいバージョンが発表されるのは最高だ

iPhone 端末を落として画面を破壊したのを
見た時など絶頂すら覚える

ハードウェアのカスタマイズが好きだ
SDカードの書き込み先が内部ストレージのときはとてもとても悲しいものだ

多様性のある端末の筐体が好きだ
Android なんてダサいよねと言われるのは屈辱の極みだ

諸君 私はAndroidを 重戦車様なAndroidを望んでいる
諸君 私に付き従うAndroid好きの諸君 君たちは一体何を望んでいる?
更なるAndroidを望むか 
糞の様なAndroidを望むか?
核シェルターのようなAndroidを望むか?


Android!! Android!! Android!!


よろしい ならばAndroidだ

だが、Galaxy端末で無限に設定が保存されないバグに耐え続けて来た我々には
ただのAndroidではもはや足りない!!
大Androidを!! 一心不乱の大Androidを!!

我々はわずかに小数
iPhoneユーザーに比べれば物の数ではない
だが諸君は一騎当千のAndroidユーザーだと私は信じている
ならば我らは諸君と私で総兵力100万と1人の幾多のクラッシュを超えて無敗の集団となる
我らを忘却の彼方へと追いやり、iPhoneユーザーを叩きのめそう
髪の毛をつかんで引きずり下ろし 眼(まなこ)をあけて思い出させよう

連中にAutoLayoutの苦行を思い出させてやる
連中にXCodeの苦行を思い出させてやる
Androidには奴らの哲学では思いもよらないGoogleがある事を思い出させてやる
1000人のAndroidユーザーの集団で 世界をAndroidで埋め尽くしてやる

目標 Apple

Pixel作戦 状況を開始せよ

征くぞ 諸君

Canon EOS 6D MarkII を持って散歩した

前まで使っていた EOS Kiss X3 が天寿を全うしまして、新しいのほしいなと思っていたときに神楽坂つむりさんのブログ記事を読んでフルサイズよさそうだな?と思っていたところ、フルサイズはいいぞという声をTwitterでも頂いたので、自転車もあることだしあちこち行った先でパシャパシャ写真でも!ということでいろいろ撮ってみました。写真のウデマエは……構図の基本が知識としてあるくらいなのでウマいわけではないんですが、色んな人の写真を見ているとなにで撮ってても「この構図いい!」と思ったら即パク真似してみたりしています。来年はもっとカメラと一緒に出かけたいなぁ。ちなみに新しいカメラはEOS 6D MarkIIで今年一番高い買い物でした。たぶん。結果フルサイズはいいぞってことで、買ってから既に2ヶ月くらいたってますがあちこちで撮りまくってます。

f:id:KeithYokoma:20171105123249j:plain

f:id:KeithYokoma:20171105124956j:plain

f:id:KeithYokoma:20171105125352j:plain

f:id:KeithYokoma:20171105125403j:plain

f:id:KeithYokoma:20171105153920j:plain

f:id:KeithYokoma:20171105154317j:plain

f:id:KeithYokoma:20171105154434j:plain

f:id:KeithYokoma:20171105154451j:plain

f:id:KeithYokoma:20171106230925j:plain

f:id:KeithYokoma:20171111130529j:plain

f:id:KeithYokoma:20171111180813j:plain

f:id:KeithYokoma:20171112152543j:plain

f:id:KeithYokoma:20171112152643j:plain

f:id:KeithYokoma:20171218134844j:plain

f:id:KeithYokoma:20171218132403j:plain

f:id:KeithYokoma:20171209142001j:plain

f:id:KeithYokoma:20171209140651j:plain

f:id:KeithYokoma:20171210130839j:plain

フルサイズはいいぞ。

レンズは標準のやつを使っています。EOS Kiss のときのものは使いまわせないのでどうにかしよう。フルサイズともなるとレンズ沼にはまったらすごい勢いでお金が溶けていくのが目に見えているので、レンタルとかで楽しむのもアリかなと思っています。一応パンケーキレンズもあるし、困るのはすごい遠い被写体に寄りたいときだけ。でも知ってるんだ、望遠レンズは暗くなりがちでその中でも明るいレンズを選ぼうとするとカメラがもう一台買えるくらいすることを……やはりレンタルだ。

SparseArray から要素を取り出したときに ClassCastException が発生するパターン

SparseArrayAndroidフレームワークにあるコレクションの一種で、Integer を key にした HashMap よりもメモリ効率がよいとされるコレクションです。

SparseArray には 2 通りの値を取り出すメソッドがあります。一つはSparseArray#get(int)もう一つはSparseArray#valueAt(int)です。 どちらのメソッドも同じint型の引数をとりますが、getメソッドの引数はkeyで渡された値をもとにバイナリサーチをかけて内部の配列のindexを決めており、valuesAtメソッドの引数はindexで値がそのまま内部の配列のindexとして扱われます。

SparseArray はまた要素を追加した後に削除することもできます。こちらも 2 通りのメソッドがあり、それぞれSparseArray#delete(int) / SparseArray#remove(int)SparseArray#removeAt(int)で、delete(int) / remove(int)の引数はkeyでこれをもとにバイナリサーチをしてアクセスすべきindexを決め、removeAt(int)の引数はindexでそのまま内部の配列のインデックスとなります。

他にも同じパターンで引数のintkeyなのかindexなのかで挙動の異なるメソッドがあります。

さてここで、一度 SparseArray に保存した値を削除し、再度取り出すことを試してみます。

SparseArray#put(int, V)で指定したkeyに保存したのち、SparseArray#remove(int)で指定したkeyに対応する値を削除、SparseArray#valueAt(int)で先頭の要素を取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")

同じようなことをSparseArray#get(int)で実行する場合は次の通りで、get メソッドに渡すkeykeyAt(int)で取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")

それぞれどのような結果になるかというと、SparseArray#valueAt(int)の場合は最後の行でClassCastExceptionが発生してクラッシュし、SparseArray#get(int)の場合は最後の行でログに"Value at [0] == null"と出力されます。

SparseArray では要素の削除を実行すると、内部で保持している配列の該当箇所に削除したことを示す DELETED という Object 型の定数を代入します。SparseArray は型パラメータでどの型のオブジェクトが保存されるか指定できますが、実際には内部で要素を保持している配列は Object[] です。そして SparseArray#get(int) はその場所にある要素が DELETED なら null ないしは指定した値を返すようになっていますが、SparseArray#valueAt(int)は特にそのようなチェックなしに指定した場所にある要素を返しています。これがSparseArray#valueAt(int)を使ったときにClassCastExceptionが投げられる理由です。

要素が全部なくなったのに要素にアクセスしようとするというのはよくない状況です。get(int)keyAt(int)を組み合わせて null チェックをすることでClassCastExceptionは回避できますが、根本的に並行処理に問題がある(SparseArray はスレッドセーフではない)ということなので、クラッシュレポート等で身に覚えのない ClassCastException がある時にはこのパターンを疑ってみると良いと思います。

例外をネストしたクラスとして定義するときは必ず static なネストしたクラスにする

いい具合にトラブってしまったのでメモです。

クラスの定義にはいくつかの種類がありますが、あるクラスの内部にネストした(入れ子になった)クラスを定義することができます。このとき、修飾子として static をつけたネストしたクラスと、それがないネストしたクラスのどちらも正しい記述です。

public class OuterClass {

  // static でないネストしたクラス
  public class InnerClass {

  }

  // static なネストしたクラス
  public static class StaticInnerClass {

  }
}

static の有無で何が違うかといえば、static でない方は暗黙的に外側のクラス(上記の例ではOuterClass)への参照を持つため、外側のクラスのメンバにアクセスできるという点があります。 ここで、例外を何かのクラスにネストしたクラスとして定義してみたとすると、直列化(シリアライズ)で問題が起こります。

public class OuterClass {

  // static でないネストしたクラスとして例外を定義
  public class BadException extends RuntimeException {}
}

暗黙のうちにもつ外側のクラスへの参照は transient ではないので、シリアライズの対象になります。例外はSerializableを実装しているので直列化ができますが、このBadExceptionを直列化すると外側のクラスへの参照もシリアライズの対象となるものの、肝心のOuterClassSerializableを実装していないので、直列化できずに例外が投げられます。

暗黙の内に持つ外側のクラスへの参照が引き起こす問題は、メモリリークの文脈で語られることが多いように思いますが、このように直列化でも問題となります。

public class OuterClass {

  // static なネストしたクラスとして例外を定義
  public static class MyException extends RuntimeException {}
}

例外にかぎらず、ネストしたクラスがSerializableなクラスを継承する場合も同様に、static なネストしたクラスとして定義しましょう。

RxJava1 から RxJava2 へ移行する時に nullable な値とうまくつきあう

次のような RxJava1 のコードを RxJava2 に移行することを考えます。

import rx.Observable; // RxJava1

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.fromEmitter(emitter -> emitter.onNext(nullableValue))
      .filter(nullableValue -> nullableValue != null);
}

Observable のソースとなる値は Nullable で、それを filter で null チェックをかませることで値を受け取る側は NonNull を前提にできるような感じですね。 RxJava2 では onNext() に値を渡す時点で NonNull であることを求められるため、これをそのまま RxJava2 に書き換えると実行時に例外がスローされます。

import io.reactivex.Observable; // RxJava2

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.create(emitter -> emitter.onNext(nullableValue)) // nullable な値を onNext には渡せない
      .filter(nullableValue -> nullableValue != null); // 無意味な filter になる
}

ここで、nullableValue を onNext() に渡す時点で Optional*1 を使ってラップしてみると、うまく onNext には NonNull な値を渡せるようになります。 また、その後に filter と map を駆使すると、メソッドの返り値の宣言を変更することなく RxJava2 に移行できます。

import io.reactivex.Observable; // RxJava2

private Value nullableValue;

public Observable<Value> observeValue() {
  return Observable.create(emitter -> emitter.onNext(Optional.of(nullableValue))) // Optional<Value> は NonNull
      .filter(Optional::isPresent) // RxJava1 での null チェックと同じ効果が得られる
      .map(Optional::get); // filter で値が存在することをチェックしているので安全に get できる
}

Android の場合 Optional は GitHub - memoizr/retro-optional: A backport of Java8 optionals for Java7 を使うと古い OS バージョンでも利用できます。

*1:java.util.Optional では ofNullable でラップします。バックポート版ではofでラップします。