なるようになるかも

力は多くの場合、その人の思いを超えない。

最近のアプリ界隈での「設計」の違和感

アプリ界隈で「設計」の話をするときに MVC / MVP / MVVM のような「設計パターン」だけが語られるようになった気がする。

往々にして、「アプリの規模によってどれを採択すべきかは変わる」みたいなお茶を濁すような結論で終わることが多い。

私的な結論

  • 「設計」と、「設計パターン」は別物だと思う。
  • 「設計」のレベルを上げたい。
  • アーキテクチャシンドロームから抜け出して、価値のあるものを作りたい。

以下、思うところのメモ。

MVC は古い / 劣ったやり方か?

MVC は Model をどう構築するかについてとくに規定していない。

MVC への批判をするときに、FatVC が持ち出されることが多いのですが、FatVC を実装してしまうのは単に実装者の能力不足だと考えていて、MVVM を採用しても FatVM を作るだけだと思っている。

また、比較的新しめの Flux アーキテクチャは、良く言えば MVC を詳細化したもの、悪く言えば MVC の再発見とみなすことができる。

VC / Activity が GOD クラスであることから目を背けるのはよくない

VC や Activity をクリーンにすることは重要なものの、そこに熱中しすぎるのはよくないと考えている。

VC や Activity は単一の View における状態の管理と、それらの View 同士の遷移の状態の管理を担っているため非常にやれることが多く、また、管理している状態も極めて複雑である。

これを上手く抽象化してパターンに当てはめるという試みが多数行われているものの、基本的にはトレードオフであり、正解はない。

もともと単体の VC や Activity で簡単にできたことがとてもとても複雑になってしまうというのはまだマシなほうで、インタラクティブ画面遷移のような「View の状態」と「遷移の状態」の両方を扱う操作は欠落してしまうようなケースもある。

GOD クラスの影響を低減させる試み自体は概ね好ましいものの、そこで消耗してもアプリの価値が劇的に向上することはないので、ほどほどに割り切って上手く付き合っていくのが良いと思っている。

DDD や Clean Architecture を設計パターンと同列に語る

MV* は実装フェーズ、それも GUI 実装のみに限定されたパターンの話でしかないはず。

対して、DDD はソフトウェア製造全体に対する巨視的な考え方をしているものだと思うし、Clean Architecture はアーキテクチャを構築する上での指針というか、設計パターンに対するメタな話だと解釈してるのですけれど、なんか同列で語られることが多いのが個人的にはもにょい。

あと DDD や Clean Architecture を独自解釈(というか、ネットの情報ベースで語っている?)ことについて思うところがないわけではないのですが、まだ原著完全に読めてないのでとりあえずノーコメント。

MVVM without DataBinding

まともなデータバインディング機構がないにも関わらず、MVVM が採用されるパターンが多い。自分もやる。

必ずしも悪いわけではないものの、以下のような副作用は散見される。

  • 双方向バインディングの記述がひどくぎこちない書き方になる
  • 一行のデータ更新によってリストを全て書き換えるような、大富豪な処理が平然と実行される
  • ひとつのボタン UI に対して、「状態に応じたボタンのラベル」「ボタンを実行するかどうか」「ボタンで実行する処理」のような複数のバインディングが必要になる
  • コンパイル・実行時エラーが不親切でデバッグ作業が非効率化する
  • データバインディングの作法が統一されていないので、オレオレ VM が横行する

個人的に「設計」をやる人に望むこと

要件に対する興味と理解

「設計」をやるときに GUI をどういうパターンで組むのか?から入るのは、ぶっちゃけよほど簡単で先の見通しがはっきりしている状況でもない限りは、悪手だと思う。

どういう要件があり、どういった機能を実現する必要があるのか?にまず興味を向けるべきで、ビジネスロジックドメイン層と言われる領域をうまく抽象化し、UI や DB などと隔離されたかたちで実装できるか?というところがもっとも肝要で、それこそが本来やるべき「設計」じゃないのかなぁと思っている。

プログラミングの原則の理解

設計パターンやアンチパターンの根底には大体プログラミングの原則の考え方が根付いている。

「BaseVC はアンチパターンだからダメ」みたいなのは単なる思考停止なのであまり好きではない。

たとえば「開放閉鎖原則の観点から、この実装では変更に対する柔軟性を失っており好ましくない BaseVC である」という指摘が入ると、具体的な問題箇所が明らかになり、どうやって修正するか?という建設的な議論に入ることができる。

設計パターンにはとりあえず手を出す

最終的に GUI を作る以上は MVC や MVVM や MVP などの GUI の設計パターンを採用することになる。

このとき、人や本を権威にして「MVC はダメ」「MVVM は実はこんなデメリットが」みたいに流されるだけではなく、自分で手を動かして実装するなり、コードリーディングはしなければならないと考えている。

というのも、作ったことも触ったこともないものに対して、どれが最適かなどという判断を下すことなど不可能なので。

ひとつの設計パターンに成功体験を持ってしまうと、その設計パターンがあらゆる状況で通用するものだと勘違いしてしまいがちだったり、その逆にも注意をする必要があると思う。

UISearchController を Rx で書けるように

iOS11 だと UISearchControllerUINavigationBar に入れられて楽しいので、入力した文字列をうまい感じで Observable<String> にしてくれる DelegateProxy を作ってみた。

UISearchResultUpdating+Rx の私的実装

なんで RxCocoa にないの?

PR 自体は何度か出てたけど蹴られてたっぽい。

./scripts/all-tests.sh 通してないなこの PR …。

ちゃんと RxSwift の作法っぽい感じで書き直したらこんな感じです。

これはちゃんと ./scripts/all-tests.sh も通ります。

よほどメジャーな delegate じゃない限り、この手のラッパーは各自で作ってねってスタンスだと思うので本家に PR は出さないですが。

UISearchController の使い方が分からない場合

ググってコードを探すよりも、Table Search with UISearchController というサンプルコードを見ると早いです。

なんと、Swift 版があるだけでなく、iOS11 対応済。

hidesSearchBarWhenScrolling が動作しない

UIScrollView と連動して動作してくれる条件は、以下の通り。

  • UIViewController の view が UIScrollView
  • UIViewController の view の subviews[0] が UIScrollView

Shrink large title when scrolling (not UITableViewController) iOS 11 に図解入りの超分かりやすい説明があります。

作った経緯とか

最初、UISearchResultsUpdating に準拠しなくても、searchController.searchBar.rx.value を subscribe すればいいんじゃ?と思ってたんですけれど、これだとうまくいかなかった。

  • キャンセル時に next が流れて来ない
  • 検索中に別の VC に push すると、ナビゲーションバーが消え去る

後者について、UISearchController が内部的に modal を出してるっぽいので、これを消すワークグラウンドとかを思いついたのですけれど、正規の方法で実装した方がよさそうだなぁということで素直に実装しましたとさ。

ついでだったので、SearchBar に入力したテキストを取れる Observable を追加したのですけれど、ちゃんと動いてるっぽい。

var queryText: Observable<String> {
    return Observable.merge(Observable.just(""),
                            RxSearchResultUpdatingProxy.proxy(for: base).didUpdateSearchResultSubject.map { $0.searchBar.text ?? "" })
        .distinctUntilChanged()
}

空文字と merge しているのは、combineLatest でフィルタリングしている実装の都合上です。

Activity Tracing どう使うのか謎だったので調べたメモ。

static void hoge() {
    static os_log_t log;
    static os_activity_t activity;
    static os_activity_t activity2;
    static os_activity_t activity3;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        log = os_log_create("activity", "a butterfly's dream");
        activity = os_activity_create("dream", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT);
        activity2 = os_activity_create("real", activity, OS_ACTIVITY_FLAG_DEFAULT);
        activity3 = os_activity_create("dream", activity2, OS_ACTIVITY_FLAG_DEFAULT);
    });
    
    struct os_activity_scope_state_s state = {};
    os_log_info(log, "Graveyard Memory");
    os_activity_scope_enter(activity, &state);
    os_log_info(log, "Nightmare Counselor");
    struct os_activity_scope_state_s state2 = {};
    os_activity_scope_enter(activity2, &state2);
    os_log_info(log, "Historical Vacation");
    struct os_activity_scope_state_s state3 = {};
    os_activity_scope_enter(activity3, &state3);
    os_log_error(log, "Cosmic Horoscope");
    os_activity_scope_leave(&state3);
    os_activity_scope_leave(&state2);
    os_activity_scope_leave(&state);
}

os_activity_scope_state_s は空の構造体を作って os_activity_scope_enter にポインタを渡せばいいだけなのね。

f:id:quesera2:20180429190527p:plain

こんな感じで階層表示される。

block でネストしても良いのであれば、os_activity_initiate マクロだと、Activity の作成+スコープ開始を同時にやってくれるので便利なのだけれど、Activity Tracing のリファレンス には載っていない…。

os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
    os_log_info(log, "Northeast Nostalgia");
    os_activity_initiate("real", OS_ACTIVITY_FLAG_DEFAULT, ^{
        os_log_info(log, "Forgotten Paradise");
        os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
            os_log_error(log, "Shining Future");
        });
    });
});

OSLog が記録しているファイル名や行数の情報を知りたい

Don’t include symbolication information or source file line numbers in messages. The system automatically captures this information.

「OSLog は自動的にソースファイル名と行数を収集するから、そういった情報をメッセージに含めるべきではない」というリファレンスの記述に対して、じゃあどうやって見ればいいの?っていう疑問が当然あると思うんですけれど、何も書いてないのですよね…。

Developer Forum を調べたところ、os_log source file and line numbers に記載を発見。

log コマンドを --source オプションを付けて実行したときに見れるっぽい。DevForum だと Swift だと動作しないという話があったけれど、今はちゃんと Swift コードでも行数が出るっぽい。

関数でラップすると、出る行数はラッパーになるので、やはり os_log を直接呼ぶしかないっぽい。@inline(__always) を使ってもダメだった。

$ log stream --level debug --source --predicate 'subsystem == "activity"'
2018-04-29 21:34:40.264429+0900 0x28b38e   Info        0x0                  31286  <Activity`hoge (hogehoge.c:28)> [activity:a butterfly's dream] Graveyard Memory
2018-04-29 21:34:40.264499+0900 0x28b38e   Info        0x786a3              31286  <Activity`hoge (hogehoge.c:30)> [activity:a butterfly's dream] Nightmare Counselor
2018-04-29 21:34:40.264540+0900 0x28b38e   Info        0x786a4              31286  <Activity`hoge (hogehoge.c:33)> [activity:a butterfly's dream] Historical Vacation
2018-04-29 21:34:40.264599+0900 0x28b38e   Error       0x786a5              31286  <Activity`hoge (hogehoge.c:36)> [activity:a butterfly's dream] Cosmic Horoscope
2018-04-29 21:34:40.264848+0900 0x28b38e   Info        0x786a6              31286  <Activity`__hoge_block_invoke.3 (hogehoge.c:40)> [activity:a butterfly's dream] Northeast Nostalgia
2018-04-29 21:34:40.264918+0900 0x28b38e   Info        0x786a7              31286  <Activity`__hoge_block_invoke.6 (hogehoge.c:40)> [activity:a butterfly's dream] Forgotten Paradise
2018-04-29 21:34:40.264982+0900 0x28b38e   Error       0x786a8              31286  <Activity`__hoge_block_invoke.9 (hogehoge.c:40)> [activity:a butterfly's dream] Shining Future
2018-04-29 21:34:40.269216+0900 0x28b38e   Default     0x0                  31286  <Activity`main (main.swift:26)> [activity:a butterfly's dream] Dominated Realism

os_activity_initiate の方が圧倒的に簡潔に書けるのですけれど、block を開始した行数しか記録に残らないので、os_activity_scope_enter を使った方がいいのかもしれない。

OSLog にいい加減に移行するべきなの?

と思って調べた内容のメモ。

結論から言って、watchOS みたいなパフォーマンスにシビアなデバイスで、マルチスレッドが絡む部分をロギングしたいとかじゃない限り、普通のロガーでよさそう…。

OSLog の優位点

メモリ上の循環バッファにバイナリでログを保持しておき、何らかの障害が発生した時点で初めて外部に出力するので、観測者効果が最小限で、余計な I/O が発生せずログが容量を逼迫することがない。

Activity Tracking により、そのログがどういうコンテキストで出力されたのかを視覚的に表示することができる。

その他、機微情報の取り扱いなどが変わっているらしい。小容量な SSD がメインストレージで、紛失によるセキュリティリスクの高い、モバイル端末時代を考えたロガーとして設計されている感じがある。

OSLog の欠点

ログが単純なテキストファイルではないので、これを横断的に検索する手段が存在しない。閲覧に際しても、基本的には Sierra 以降のコンソール.app を使う必要がある。

肝心の Activity Tracking が長らく Swift に対応していない。

ログ収集時に文字列組み立てによる負荷を発生させないために、Obj-C ライクな書式文字列が存在して、それを覚える必要がある(その書式文字列の一覧がどこにあるのかは分からない…)。

OSLog の使い方でいまいち分かりにくかったところ

Xcode の Console に出る情報が少なすぎて使いにくい

コンソール.app を使うのがほぼ前提なところがある。

OSLog.default って使っていいの?

subsystem と category の情報をログに含める必要がないなら別に使っても問題ないらしい。

subsystem と category って何?

ログを識別するために設定できる任意の文字列。

  • subsystem は慣例的に Reverse domain name notation で実行しているアプリの識別子などを渡す。
  • category はその中でログメッセージを分類するのに使用する。

ビデオの中では、メッセージ本体とタイムスタンプに別の category を設定するという使い方が紹介されていた。

ログレベル良くわからない

ログレベル 意味
Default 循環バッファにログを出力し、メモリが溢れたとき圧縮して外部に保存する。追跡に使うデータは基本これ。
Info 循環バッファにログを出力する。エラーが発生しない限り、外部に書き出されず捨てられる。
Debug 設定を変えない限り、メモリには取り込まれない。開発用。
Error 常に外部に書き出される。プロセスレベルのエラーを報告するためのもの。
Fault 常に外部に書き出される。システムレベルおよびマルチプロセスのエラーを報告するためのもの。

とりあえず残しておいて損はない情報は Info にざくざく詰んでおくと、障害発生時にだけ書き出してくれる。

Error / Fault の違いは、Activity Tracking の振る舞いに起因しているっぽい?あんまり分かってない。

ラッパークラスやラッパー関数を作ってもいいの?

ラップするとどこでエラーが発生したのかという情報が失われるのでダメらしい。(参考にしたビデオの Transcript より)

Avoid wrapping os log APIs in other functions. If you wrap it in another function you then lose our ability to collect the file and line number for you.

これについて信憑性を調査しようと思ったものの、OSLog の実体のマクロを展開してもよくわからず。

参考にしたもの

Unified Logging and Activity Tracking

OSLog について知るならまずこのビデオを見ないと始まらない。(という風潮が最近多いけれど、ちゃんとリファレンスを充実させて欲しい…)

Logging: Using the os_log APIs

サンプルコード。コンソールでログを見る方法とかも書いてある。

Fix Bugs Faster using Activity Tracing

2014 年の WWDC の頃にあった Activity Tracing の話。

トレーシングは UIKit に統合されてて Target-Action で勝手に Activity を作るみたいな挙動だったはずなんだけど…。

Unified Logging and Activity Tracing

Unified Logging の話とか、Activity の Swift3 Wrapper の話とかがある。

iOS デザイン実装はつらい話のあとがきみたいなの。

Qiita に久々に記事を書いたのですけれど、なんか途中で力尽きていろいろツッコミどころが多い感じになってしまった。

qiita.com

わたしとしてはコメントがとても勉強になったので良かったのですけれど…。

以下、書ききれなかった内容の供養みたいなの。

画像リソース周りの話

  • ついにラスタライズされた偽ベクタ画像じゃなくて、本物のベクタ画像が使えるようになった
  • Slicing の機能はわりと便利だと思うのですけれど、あまり使われてるのを見ない…

みたいなことを書こうと思って忘れたらしい。

Named Color の話

iOS11 以降しか使えないので(R.swift とか SwiftGen とかでバックポートできると思いますが)、まだ本格普及しないと思うのですけれど、これの動作はわりと謎です。

単に Assets Catalog から UIColor を生成して KVC で設定してるだけかと思いきや、シミュレータで見ると View がロードされてから、実際に表示されるまでの間に色空間が別物になっていたりとか。iOS10 辺りで入ったカラーマネジメントとか Display P3 が関係しているのだと思いますが、あんま詳しくないので適当なことしか書けない…。

// viewDidLoad()
UIExtendedSRGBColorSpace 0.562 0.529 0.342 1
// viewDidAppear()
kCGColorSpaceModelRGB 0.562 0.529 0.342 1 

なので、Named Color で指定した場所は、viewDidLoad() で色を変えても反映されないですし、KVC で値を渡すだけの @IBInspectable で Named Color を指定しても (Missing) という表示になります。

Named Color の登場で割を食ったのが Color Literal で、実体が RGBA 値をコードにベタ書きしてるだけなので、Named Color としては使えません。(なのに、Media Library から Named Color をドラッグすると、Color Literal に化けてしまうのがよくない)

Attributed String の話

JLREQ の要求が高すぎて目眩がしつつ、せめてベタ組みと連続約物の処理くらいはちゃんとやりたいなと思いながら、どう実装すればいいのか悩んでいる今日この頃。

iOS でリッチテキストの描画をするときは、Attributed String を使うわけですが、System Font で日本語の1行だけの Attributed String を作ると、Paragraph Style に lineSpacing を指定したときに、高さ計算に失敗するというバグ?があります。

qiita.com

これはそもそも Attributed String に System Font を渡すのが、想定された使い方ではない…という感じなんでしょうか。実際、Storyboard 上では System Font の Attributed String は作れませんし。

普通のテキストには System Font を推奨しているわりに、Attributed な場合は System Font を使うとダメっぽいのが罠として優秀。

追記:もうちょっと調査した

UIFontDescriptorヒラギノを持つカスケードリストを作っても上記の問題が回避できないですね。

// これは擬似コードなんですけれど、こういう書き方をすると bese にない文字は .cascadeList で指定したものにフォールバックされる
let baseFontDescriptor = UIFontDescriptor(name: "HelveticaNeue-Medium", size: size)
let hiragino = UIFontDescriptor(name: "HiraginoSans-W3", size: size)
let font =  UIFont(descriptor: baseFontDescriptor.addingAttributes([.cascadeList: [hiragino]]), size: size)

System Font 固有の問題というわけではなく、iOS 内臓のヒラギノの抱える闇の一端っぽい感じ?UIFontDescriptor はめっちゃバギーな感じがするのでもっと使われてヤバさが広がってほしい…。

@IBDesignable の話

@IBDesignable って実際使われてるんでしょうか。分かってくると結構便利だと思うんですけれど、トラップがいろいろあってとっかかりが悪い感じ。

  • init(frame:) を実装する必要がある
    • UITextView の場合は、textContainer を引数に取るイニシャライザが必須
  • layer プロパティの操作や addSubLayer(_:)maskLayer など CALayer に対する各種操作はできる
    • class function の layerClass を変更しても反映されない
  • TARGET_INTERFACE_BUILDER で処理を分岐させると、その範囲はリファクタリングの対象外になったり、IB でのみビルドエラーになっていても気付かなかったりする
  • Storyboard 上で致命的なクラッシュが発生した(IUO の Outlet とかを触ったとか)は、~/library/Logs/DiagnosticReports/ にログが吐かれるので読めば分かる
  • Editor > Debug Selected Views を実行すると、当該のカスタム View のイニシャライザや draw(_:) が走って、breakpoint で止まる

アプリケーションを実行せずに View だけでデバッグできるのは良い機能だと思います。

JSONDecoder で型がごちゃ混ぜの JSON 配列をデコードする

ルートが配列で、最初の要素はその後の要素の個数を表す、という JSON があったときに、どういう Decodable を書けばいいのか?で少し悩んだので。

[
{"count" : 5 },
{"name" : "itemA", "value" : "valueA" },
{"name" : "itemB", "value" : "valueB" },
{"name" : "itemC", "value" : "valueC" },
{"name" : "itemD", "value" : "valueD" },
{"name" : "itemE", "value" : "valueE" }
]

それぞれの要素が、以下のいずれかの型に decode されるというところまでは共通。

struct Count: Codable {
    let count: Int
}

struct Item: Codable {
    let name: String
    let value: String
}

方法1. 取り得る値の両方でパースする

Stack Overflow の回答で見つけたのが、singleValueContainer() でそれぞれの型に対して decode() できるかチェックする方法。

enum Response: Decodable {
    case count(Count)
    case item(Item)
    
    init(from decoder: Decoder) throws {
        if let count = try? decoder.singleValueContainer().decode(Count.self) {
            self = .count(count)
            return
        }
        if let item = try? decoder.singleValueContainer().decode(Item.self) {
            self = .item(item)
            return
        }
        fatalError()
    }
}

singleValueContainer() のリファレンスを見ると、

Returns the data stored in this decoder as represented in a container appropriate for holding a single primitive value.

「単一のプリミティブな値を持つ」というような説明があって、SO の回答でも IntString を decode していたので、ES の仕様でいうところの Primitive Data Types しか decode できないのだと勝手に勘違いしていたけど、Decodable でも普通にいけた。

方法2. 配列を直接全部走査する

このケースだと、「先頭の要素は全件のカウントが入っている」ということが自明なので、unkeyedContainer() で配列のコンテナを取得して、要素一つ一つに対して decode() していくという方法の方が使い勝手が良かった。

struct Response: Decodable {
    let count: Int
    let items: [Item]
    
    init(from decoder: Decoder) throws {
        var unkeyedContainer = try decoder.unkeyedContainer()
        
        count = try unkeyedContainer.decode(Count.self).count
        
        var result: [Item] = []
        result.reserveCapacity(count)
        
        while !unkeyedContainer.isAtEnd {
            result.append(try unkeyedContainer.decode(Item.self))
        }
        items = result
    }
}

JSON の構造が固定ならこちらの方がよさそう。

逆の操作をすれば encode() もできるので、Codable にすることもできる。

extension Response: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(Count(count: count))
        try items.forEach { try container.encode($0) }
    }
}

Android Architecture Components 雑感2。

主に LifecycleLiveData について。

Lifecycle

某ポエムサイトにあったクラス図がしっくりこなかったので、自分で書き直してみました。

f:id:quesera2:20170529210511p:plain

Lifecycle のキモは、ライフサイクルメソッドのオーバーライドはもうやめましょう、ということ。

なぜかというと、ActivityFragment を容易に Fat にする上に、処理を追うのが直感的ではなくなってしまうから。

その代わり、ActivityFragmentState を持つように変わります。ライフサイクルイベントが発生すると、どこからともなく(後述) Event が投げられて、これを受けて State の更新が行われます。

StateEvent は公式サイトの以下の図の通りです。

f:id:quesera2:20170529211323p:plain

Handling Lifecycles より)

このイベントの変更を受け取りたいクラスは、LifecycleObserver インターフェースを実装します。これはマーカーインターフェースです。任意のオブジェクトが LifecycleObserver になれます。

その上で、@OnLifecycleEvent アノテーションを付与して、実際のそれぞれの State 変更時の処理を実装します。

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
    // RESUMED State に遷移したときに行って欲しい処理
}

あとはこの LifecycleObserver を、ライフサイクルイベントを受け取りたい LifecycleOwnerActivity または FragmentService)の LifecycleRegistry に登録して、購読させます。

これにより、画面を構成する部品単位で、ActivityFragment から切り離された状態で、ライフサイクルに応じた処理が書けるようになります。

同じライフルサイクルメソッドに、複数の処理が書かれることがないので、コードは読みやすくなりますし、LifecycleObserver マーカーインターフェースを実装するだけでよいので、再利用性も高くなります。

  1. 任意のコンポーネントがライフサイクルを持てる
  2. Activity にベタに書かなくて良い
  3. 再利用可能

という特徴から、「不可視の Fragment じゃん」という感じですが、実装上でもそのテクニックが使われています。

LiveData

さて、LiveData はリアクティブな感じで値を通知する簡易なモジュールで、Rx のオペレータが使えないので正直メリットを感じないものなのですが、LifecycleObserver を実装している、という点は見逃せません。

例えばバックグラウンドで通信を行う場合、結果を表示する UI が既にない、という状況はありがちです。

LiveData は 「STARTED または RESUMED でない状態ならば、値を通知しない」という仕組みになっています。購読、購読解除については、instanceofLifecycleOwner であれば勝手にやってくれると思いますので(たぶん)、あんまり細かいことを気にしなくていいのがウリです。

類似品に、RxJava の親戚の RxLifecycle がありますが、こちらは RxFragment のようなライフサイクルを監視する特定のクラスを継承しなければいけないのに対して、Lifecycle は Andorid の仕様の穴を突いたような素敵実装になっており、ActivityFragment の実装を選ばない、という点がメリットとして挙げられると思います。

とはいえ、個人的には使わなさそうな感じです。

重要なのは、今までのライフサイクルメソッドを太らせていく記述方法から、データドリブンな記述(Rx に近い考え方)へ転換しましょうね、というのが LivecycleLiveData の狙いなので、もし Rx があんまり好きじゃないならスルーしていいと思います。

Lifecycle の実装

www.jianshu.com

内部を追いかけていたらこのサイトに行き当たって、知りたいことはほぼほぼ説明されていました。

Android Architecture Components のソースコードはまだ公開されていないので、現時点で内部構造を語っているのは世界で唯一このサイトだけだと思います、たぶん。

registerActivityLifecycleCallbacks()Activity のライフサイクルコールバックを受け取って、onActivityCreated() で不可視の ReportFragment を埋め込んでライフサイクルを監視してるわけですね。

この ReportFragment のライフサイクルメソッドの呼び出し時に、

private void dispatch(Event event) {
    if(this.getActivity() instanceof LifecycleRegistryOwner) {
        ((LifecycleRegistryOwner)this.getActivity()).getLifecycle().handleLifecycleEvent(event);
    }
}

という感じで、もし ActivityLifecycleRegistryOwner を実装していたら、getLifecycle().handleLifecycleEvent() を呼び出すという単純ながら力強い実装になっています。

registerActivityLifecycleCallbacks() を行うタイミングがないように思えますが、LifecycleRuntimeTrojanProvider というダミーの ContentProvider で実現されている、というのが面白ポイントです。ContentProvider がアプリケーションプロセス起動時に立ち上がるのを利用しているわけですね。

ViewModel と画面遷移

いまだにしっくりこないのが、ViewModel で画面遷移をどう実現するか?という部分です。

Handling LifecyclesViewModel のベストプラクティスの項目には、

Never reference a View or Activity context in your ViewModel. If the ViewModel outlives the activity (in case of configuration changes), your activity will be leaked and not properly garbage-collected.

【私訳】 ViewModel に View または Activity context への参照を持たせないでください。もし ViewModel が Activity より長生きした場合(configuration changes の発生時など)、Activity はリークして、適切に GC に回収されません。

とある通り、ViewModel に画面遷移に必要な Context を持たせたくないのですが、とはいえどうすればよいのか。

公式のサンプルだと、NavigationController を DI しているのですが、これは単一の Activity だからできるという感じだし、画面の数だけ navigateToXXX() メソッドを増やしていくのは微妙だなーという感想しかないわけです。

実際的には、Intent フラグを与えたり、Bundle でパラメータを受け渡ししたいというようなケースも多々あるので、もうちょっと汎用的に使えるものが欲しい。

また、このサンプル実装だと、commitAllowingStateLoss() で誤魔化していますが、FragmentManager もライフサイクルと切っても切れない Android のつらい問題のひとつで、ViewModel のスコープから画面遷移をキックすると、IllegalStateException に悩まされるのがオチだと思っていて、その辺のライフサイクルの問題までは、まだまだ解決しそうにないなーという感想なのでした。