にんにんにん

エンジニアな日々を書いていきます

Activityのライフサイクルを管理する

アクティビティのライフサイクルについて、具体的にアプリの画面だとどんな状態になっているのかスクショを用いて説明している記事とか見かけないので、自分なりに解釈してみました。

アクティビティの状態は大きく分けると3つに分かれるようです。

  1. 再開状態

  2. 一時停止状態

  3. 停止状態

再開状態

別名「実行中」 つまり、アプリがフォアグランド(一番前)にいて、ユーザからのフォーカスが当たっている状態 つまり、

f:id:moroku0519:20180127121901p:plain

この状態かなと

一時停止状態

他のアクティビティがフォアグラウンドに出ているが、アクティビティが生きている状態。 他のアクティビティは画面全体を覆っているわけではなく、一部が表示されている感じ。 イメージでいうと、

f:id:moroku0519:20180127122907p:plain

こんな感じ? 厳密にはアクティビティではなく、DialogFragmentなので違いますが、あくまでイメージとして。 バックのアクティビティは生きていますが、極端にメモリが足りなくなった時は破棄されます。

停止状態

他のアクティビティがフォアグランドに来たので、現在のアクティビティがバックグラウンドに隠れて見えない状態です。(若干表現はおかしいかも?)

例えば、

f:id:moroku0519:20180127123520p:plain

さがすアクティビティから上部の検索バーを押すと、

f:id:moroku0519:20180127123610p:plain

検索アクティビティに遷移します。

これは、さがすアクティビティがバックグラウンドにいき、フォアグラウンドに検索アクティビティがきた状態です。

この時、さがすアクティビティは停止状態となっています。

別のアクティビティでメモリが必要となった場合は、さがすアクティビティはシステムに強制終了される可能性があります。

以上が、大きく分けて、アクティビティの3つの状態です。

その上で、よく見かけるアクティビティのライフサイクル図を見てみます。

f:id:moroku0519:20180127120327p:plain

コールバックがいくつもありますが、結局は先ほどの3つの状態のいずれかで呼び出されることを念頭に置けば、さほどややこしくはなさそうです。

次回は各コールバックがどういう時に呼び出されるのか整理していきます。

AndroidのActivityについて整理してみる

Androidコンポーネントとして、基本中の基本であるActivity

開発している中で必ずと言っていいほど触るし、目にしない日はありません。

しかし、身近にあるからこそ灯台もと暗し、という訳ではないが見落としがちになります。

今回、そんなわかってそうでわかっていないActivityについて整理してみます。 今回はActivityの概要とバックスタックについて考えます。

Activityとは

developer guideの言葉を借りるならば、電話をかける、メールをするといった、アプリケーションを使ってユーザがしたい操作をユーザができるようにするためのUIを提供するためのアプリケーションコンポーネント

大抵のアプリはこのActivityを複数持っていて、それらは緩やかな繋がりを持っています。(密結合になってはいけない)

ユーザがアプリを起動した時にまず表示されるActivityはMainActivityです。

そこから派生して、各Activityを起動していきます。

バックスタック

画面上から見えなくなったActivityはバックスタックと呼ばれるスタックに積まれていきます。

f:id:moroku0519:20180112093047p:plain

f:id:moroku0519:20180112093047p:plain
バックスタックイメージ

図では、MainActivityにSubActivityが積まれていますが、これはMainActivityからSubActivityを起動した状態です。 起動時にSubActivityがプッシュされて、画面上からはSubActivityが見えています。

バックボタンを押した時などは、SubActivityがバックスタックからポップされて、MainActivityが再び画面上に配置されます。 この辺はブラウザの仕組みに近いかなと。

今回はここまで

次回はライフサイクルについて整理してみます。

脳を休める

最近、頭がずっとボーっとしている

体力が回復してないのかなー?と思って、温泉に行ってみたり、マッサージを受けてみたりしているが、疲れている。

考えるのが難しい。考えようとするのだけど、頭がボーっとしている。

「疲れ」というものを分析してみる。

たぶん、「肉体的」疲れと「精神的」疲れがあって、「肉体的」には健康なんだと思う。

実際、湯船には毎日つかるようにしているし、マッサージにも行っている。何なら、睡眠の質をあげるためにベッドまで買い替えた。

となると、「精神的」疲れだが、これには大いに心当たりがある。

悩みは腐るほどあるからだ。

アドラー心理学的には、人間の悩みは人間関係に帰着するらしいけれど、確かに自分の悩みはだいたい人間関係である。

生活中、つねにいろいろ考えてしまって、脳を常に使っている気はしている。余計に。

脳を休ませるにはどうしたらいいのか?軽くググってみると、マインドフルネスという単語を見つけた。

細かいことまで調べていないが、要は瞑想のことらしい。(正しいかは不明)

確かに、意図的に考えることをやめる時間を設けるのはいいかもしれない。 無心になる訓練をしよう。

ということで、毎日5分ほど瞑想してみることにします。

脳を休ませたいねえ。

AndroidのDataBindingをtwo way にしたいとき

公式のドキュメントだとパッと見つけられないのでメモ

developer.android.com

通常のDataBinding ViewModelの値を更新 → Viewも更新

TwoWayの場合 ViewModelの値を更新 → Viewも更新 Viewを更新 → ViewModelの値を更新

というように、ViewModel、View双方にObserveし合って、更新することができます。 ぐっとコードの記述量が減り、便利です。

BaseObservableを継承したViewModelを実装

public  class  ProfileViewModel extends BaseObservable {

    private int id;
    private String name;
    private boolean hasDetails;

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public boolean isHasDetails() {
        return hasDetails;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setHasDetails(boolean hasDetails) {
        this.hasDetails = hasDetails;
    }
}

ViewModelクラスはJavaBeansのプロパティ設定に則って、setter/getterが getプロパティ名 setプロパティ名

なので、 booleanの場合は isプロパティ名 setプロパティ名

です。

@={viewModel.name}

通常であれば、

<EditText
        android:height="wrap_content"
        android:width="wrap_content"
        android:text="@{viewModel.name}"/>

とするところを、

<EditText
        android:height="wrap_content"
        android:width="wrap_content"
        android:text="@={viewModel.name}"/>

のように、@の後ろに=を足してあげることで、EditTextに入力したStringがviewModelのsetName(String name)の引数nameに渡されます。 しかし、このままでは、viewModelのnameが変更されたことがViewに反映されません。

<TextView
        android:height="wrap_content"
        android:width="wrap_content"
        android:text="@{viewModel.name}"/>

<EditText
        android:height="wrap_content"
        android:width="wrap_content"
        android:text="@={viewModel.name}"/>

のように、nameが別のViewに使われていた場合、そちらに変更が伝わりません。 そこで、@Bindableアノテーションを使います。

@Bindable

変更を通知してもらいたいところに、@Bindableアノテーションを付けます。

@BIndable
 public String getName() {
        return name;
 }

@Bindableアノテーションを付けたフィールドは、BRクラスというクラスのフィールドとして定義されます。 そして、

public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
}

のように、変更されたときに、変更をnotifyPropertyChanged()メソッドによって伝えることができ、Viewに変更が伝えられます。 なお、notifyPropertyChanged(BR._all) を指定することで、@Bindableアノテーションを付けたフィールド全部に変更を伝えることができます。

オブジェクトのやり取りにもの思う

Androidの話になりますが、例えばAPIからのレスポンスをJavaのentityとして保持していて、 Intentを飛ばして受け取る先でもそのentityを使いたい、という場面があるとします。

その際、考えられるのは

  1. Intentを受け取る先で再びAPIを叩く
  2. entityをIntentのpuExtra系メソッドでなんとか受け渡しする
  3. メモリキャッシュなど、キャッシュしておいてIntent受け取り先でそのキャッシュからentity復元

あたりかなと思います。(他にあったらごめんなさい)

ナンセンスなのは1です。値が必要なたびにAPIを叩いていたらロードでユーザを待たせることになりますし、APIサーバへのアクセスを頻繁にさせることで電池の消耗も激しくします。(Ream Academyにそんな話があった気が... Becoming a Better Battery Citizen

2のIntentで受け渡しするのはいくつか方法があります。

  • Parcelableとしてentityを実装
  • Gsonを使ってjson変換したものをStringとして受け渡し

ちなみに、うちのプロダクトコードではParcelableとして実装されたentityが受け渡されています。

うちのプロダクトのコードでよく見る例

public class Profile implements Parcelable {

    private int id;
    private String name;

    protected Profile(Parcel in) {

        id = in.readInt();
        name = in.readString(); 

    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
    }

    @SuppressWarnings("unused")
    public static final Parcelable.Creator<Profile> CREATOR = new Parcelable.Creator<Profile>() {
        @Override
        public CustomerProfile createFromParcel(Parcel in) {
            return new Profile(in);
        }

        @Override
        public Profile[] newArray(int size) {
            return new Profile[size];
        }
    }
}

entityとなるオブジェクトをParcelableとして実装しています。 Parcelableインタフェースを実装することで、そのオブジェクトをParcelとして扱うことができます。 (Parcelは世の中色々ありますが、Androidにおいてはこちらを参照 https://developer.android.com/reference/android/os/Parcel.html

Intentには、putExtra系のメソッドがあり、Intentにデータを渡すことができます。 その中に、putExtra(String name, Parcelable value) があり、オブジェクトを直接渡すことはできませんが、Parcelableとして実装したオブジェクトを渡すことができます。

しかし、これには難点があり、なんといっても実装が面倒臭い。(コードジェネレータ・プラグインアノテーションライブラリでなんとかする方法もあるが)

事実、この形のコードは今置き換えられているところです。

次に、Gson変換してStringとして受け渡しする方法もあります。

Gsonは、Googleの開発、保守しているライブラリで、シンプルなインタフェースでPOJOからJsonへのシリアライズ、またでシリアライズを行うことができます。 シリアライズされたJsonは、Stringとして扱うことができます。

public class Profile {

    private static final Gson GSON = new GsonBuilder().create();

    @SerializedName("user_id")
    private int id;

    @SerializedName("user_name")
    private String name;

    public static Profile fromJson(String json) {
        if (json == null) return null;
        return GSON.fromJson(json, Profile.class);
    }

    public String toJson() {
        return GSON.toJson(this);
    }
}

先ほどと比べてだいぶすっきりしました。 Intentを投げる側ではputExtra(String key, String value)で投げて、受け取り側は受け取った後でGson.fromJsonででシリアライズしてあげればいいだけです。

Intentのデータとして受け渡しする難点

しかし、Parcelableとして実装するにしろ、Gsonでシリアライズするにしろ、難点がいくつかあります。

  • アクティビティの結合が密になる。
  • オブジェクトのデータがでかくなると、クラッシュの危険が高くなる。

まず、必要なIntentのパラメータが増えるとアクティビティ感の結合が密になります。 例えば、MainActivityからProfileActivityに遷移する際、ProfileActivityが前のアクティビティからのProfileを期待している場合、 Profileを持っていないアクティビティからの遷移はできなくなります。 ProfileActivityの振る舞いは、遷移元のActivityに依存します。

また、オブジェクトがListで、アルバムなどの大量のデータを保持する場合、Intentはそんなに大きなデータを持つよう想定されていないため、クラッシュします。(実体験)

Intentのパラメータはuser_idなどのインデックスに留めておきたいものです。

キャッシュをしよう

そこで、 3. メモリキャッシュなど、キャッシュしておいてIntent受け取り先でそのキャッシュからentity復元 が今の所望ましい形なのかなと考えています。

アーキテクチャとしてrepositoryを用意し、Activityは何も考えずにrepositoryからデータを取ってくるメソッドを叩く。 repositoryはキャッシュの有無に応じて、RemotoDataSourceから取ってくるか、LocalDataSourceからとってくるか選択する。 repositoryのクライアントはデータの出どころは知る必要なく、データを取得することができる。

この方法がアクティビティ間の結合を疎にできて、ユーザがロード時間にストレスを感じなくて済み、開発者は実装に時間を取られずに済む方法かな?と最近は思っています。

実際、プロダクトにまだrepositoryを完全に導入できていなくて、ParcelableになっているところをGsonで簡素化してごまかしている節があるので、早いところ整理したいものです。

REALFORCE買いました

REALFORCEの87UB-5587買いました。

f:id:moroku0519:20171203205457j:plain キーボードに対してこだわりはなかったのですが、 入社してから先輩エンジニアたちが割とこだわっていたこと また、ビルド中にマシンが熱くなって手を置いているのが辛いこと から購入しました。

静電容量無接点方式、良い。 この手に馴染む感じ、最高です。

HHKBと悩んだのですが、UNIX配列に馴染みがなかったため、こちらにしました。 日本語配列にすればいいじゃんかと思うかもしれませんが、US配列に慣れていきたいため、US配列を基準に選びました。

そして、マシンがMacなため、Macでつかえるよう設定しました。

Karabiner-Elements

Karabiner-Elementsを使うことで、キーバインドをカスタマイズすることができます。

https://pqrs.org/osx/karabiner/

f:id:moroku0519:20171203210723p:plain

Devicesにて、REALFORCEを選択します。

f:id:moroku0519:20171203210939p:plain

Virtual Keyboardにて、自分の配列を選択。

f:id:moroku0519:20171203211150p:plain

そしてこのSimple Modification にて、キーバインドの割り当てをすることができます。 直感的なUIなのでなんとなくでわかるかなと。

真っ先に自分はCaps LockをControlに変更しました。

このブログ書きながら感触に酔いしれています。 うーん、良き。