2020年2月21日金曜日

Application と ViewModel の使い分けはどうすればいいのか?

Activity か Fragment を基点として「ある程度安定して動けばいいや」的スタイルで Android プログラミングを続けてきたので、Application はたまに使用例に従って採用していただけで、主体的に判断して使用したことはない。それで最近、ボンヤリと、Android プログラミングに関する情報を調べていたら、Activity のライフサイクルに巻き込まれない、モデル(ビジネスロジック)的なデータは Application で扱う(格納する)べしみたいな話があって、ちょっと気になっていた。

その辺りは特に他のアイデアがなく、自分は Activity や Fragment でそのままデータを持たせていたので、onSaveInstanceState / onRestoreInstanceState で Bundle を使って退避してライフサイクルの輪廻転生をくぐり抜けられるようにしたり、SharedPreferences を使って逐次 Persistent なデータとして退避するなどして、力技で対抗していた。

Application を使えば、もうちょっと楽にできたのかなと思って気にしていたところ、ViewModel なんてものも存在することを(今さら)知り、Application と ViewModel の使い分けでどうなんだろう? と思った。もちろん、継承関係からしてそもそも存在意義が全然違うんだろうけど、「Activity のライフサイクルの輪廻転生をくぐり抜けて過去・現在・未来の三世を通じた一貫した記憶を保つ」という意味で、Application を使うか ViewModel を使うかといことに、どういう違いがあるのだろうかといことだ。

まあそもそも、Application の方は、ライフサイクルというよりは、スコープ的なグローバル性やシングルトン性の方がテーマとなるのだろうし、ViewModel の方はライフサイクルまで Activity に巻き添えを食らわないというだけで、あくまでも Activity / Fragment に従属したローカルなインスタンスなんだろうけど。

書いているうちに、全然違うよね〜、誰も疑問に思わないよね〜、という気持ちになってきた……。

Model とは言っても、ViewModel で扱うデータなんて、所詮 UI 用のものであって、MVC の Model という訳ではないだろう。本来的な Model については、Application とか Singleton に所属させて扱うのが正解なんだろうね。


Singleton vs. Application

Application を Singleton 的な目的で使うことはよくある話だが、実際に Singleton を使うのか、Application を使うのかには、議論の余地があるようだ。公式リファレンスには以下のようにある:

★ Note: There is normally no need to subclass Application. In most situations, static singletons can provide the same functionality in a more modular way. If your singleton needs a global context (for example to register broadcast receivers), include Context.getApplicationContext() as a Context argument when invoking your singleton's getInstance() method.

この記述を見る限り、どちらかというと、Singleton 的なことをしたいのであれば、実際に Singleton を使う方がお勧めのようだ。global context を使いたいのであれば、Context.getApplicationContext() で得た Context オブジェクトを Singleton の getInstance() で引数として渡せば用が足るとしている。「Singleton 的なもので、さらに処理の中にグローバルコンテキストを扱うものが含まれるから Application を使う必要がある」と考えるのは誤解だということだ。Singleton の方がモジュール化された使い方ができるので、どちらかというとお勧めのようなニュアンス。

さらに、StackOverFlow の議論:Singletons vs. Application Context in Android? によると、ここでも Singleton 派と Application 派に分かれていて、両派とも対等な支持を得ているが、よりモジュール化、よりきめ細かいカスタム化を重視するのであれば Singleton、そうでない場合は Application という大雑把な認識でいいかな。

2020年2月12日水曜日

HTML ドキュメントのオフライン化

HTML ドキュメントをアプリ内の asset(場所は app/src/main/assets/ )として APK に内包して WebView で表示するサンプル。画像や CSS、HTML 同士の相対リンクも機能する。バックキーでリンクを戻れるようにもしている。

MainActivity

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends Activity {
    WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webView);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("file:///android_asset/index.html");
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
            webView.goBack();
            return true;
        }

        return super.onKeyDown(keyCode, event);
    }
}

layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="(パッケージ名)">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

複式簿記システムの構築 2

複式簿記システムの構築 2 の続き。今回は、減価償却資産とそこから減価償却費を算出するためのシステムの構築について考える。スキーマ的にはこんな感じで十分か。

create table Depreciable_Assets (
 id integer primary key,
 name text,
 date date,
 value integer,
 depreciation_years integer,
 apportion real,
 description text
);

上を基にして Google Spreadsheets の表を作ってみた。仕訳帳と違って適宜更新するようなものではないので、Google Spreadsheets にする必要もなく、直接 TSV を作っても違いはない。

この表を基にして、その年度での減価償却費用を算出し、決算整理仕訳の処理の一環として組み込めばいいわけだ。

確定申告的には、上の処理は、国税庁側のシステムが行うので、自前で用意したとしても二度手間になり、どうしても必要というほどのものでもない。

2020年2月11日火曜日

Firebase Crashlytics

Firebase 推しの Google に従って Firebase Crash Reporting を導入したと思ったら、少し経ったら Google 都合で Crashlytics に変更を余儀なくされることになった。

Crash Reporting に比べて、結構、設定が複雑で、適当に一つ目に導入したアプリのプロジェクトを見て二つ目にも導入しようとしたら、意外と抜けていたので、公式の説明を一つ一つ確認してやるのが無難だと思った。Google の日本語のチームが糞なのか、単に日本語のサイトが糞なのかどちらかわからないが、Crashlytics の公式のページが Android を選択しても iOS の説明しか表示せず、内容以前にサイトの表示機能自体がバグバグだし、そういう問題がない場合でも Developers サイトの日本語版の情報は内容的にも大体が、古くて腐敗した有害ですらある情報だったりもするのであくまでも英語の公式ページを参照すべし。

また上記の通りに設定しても、依然として LogCat に Crashlytics 絡みの謎のエラーログが出ていたので、StackOverFlow で調べたところ、Firebase のコンソールでアプリの署名の SHA-1/SHA-256 のハッシュ値を登録しておいてその設定 JSON ファイルを使う必要があるとのこと。鳴り物入りで Crashlytics へ変更された割には、まだ全然洗練されてないような気がするのだが……。(この件も、英語版ではちゃんと修正されていた。日本語版情報が腐っているだけの話のようだ)

リンク集

firebase 全般
1: Web コンソールでプロジェクトを作成し(場合によっては App nickname と Debug signing certificate SHA-1 を登録し)、google-services.json をダウンロードして、モジュールのルートに置く; 2: プロジェクトレベルの Gradle とモジュールレベルの Gradle に一定の設定を追加する; 3: モジュールレベルの Gradle に必要な implementation 設定を追加する。
firebase-analytics
モジュールレベル Gradle のimplementation 設定
firebase-ads
1: モジュールレベル Gradle のimplementation 設定(※ Analytics の implementation も必須); 2: AndroidManifest.xml への App ID の 記載; 3: MobileAds.initialize() メソッドの実行
Crashlytics
firebase-remoteconfig
モジュールレベル Gradle のimplementation 設定

2020年2月10日月曜日

ANDROID_ID の Android 8.0 における仕様変更

ANDROID_ID が Android 8.0(Oreo: API-26)において仕様変更されたようだ。このため、従来のように ANDROID_ID がデバイス固有の ID と思って扱っていると、意図しない結果につながる。

Android ID

In O, Android ID (Settings.Secure.ANDROID_ID or SSAID) has a different value for each app and each user on the device. Developers requiring a device-scoped identifier, should instead use a resettable identifier, such as Advertising ID, giving users more control. Advertising ID also provides a user-facing setting to limit ad tracking.

Additionally in Android O:

  • The ANDROID_ID value won't change on package uninstall/reinstall, as long as the package name and signing key are the same. Apps can rely on this value to maintain state across reinstalls.
  • If an app was installed on a device running an earlier version of Android, the Android ID remains the same when the device is updated to Android O, unless the app is uninstalled and reinstalled.
  • The Android ID value only changes if the device is factory reset or if the signing key rotates between uninstall and reinstall events.
  • This change is only required for device manufacturers shipping with Google Play services and Advertising ID. Other device manufacturers may provide an alternative resettable ID or continue to provide ANDROID ID.
Changes to Device Identifiers in Android O (10 April 2017)

基本的に ANDROID_ID の値はデバイス固有ではなくなり、アプリ毎に提示される ANDROID_ID が異なる。さらに、同じアプリであっても、APK にされたサインが異なると別アプリの扱いとなるので、debug ビルドと release ビルドとでは別アプリの扱いとなり、提示される ANDROID_ID が異なることとなる。

自分の場合、開発者モードを識別するために予め登録した ANDROID_ID(を基に算出した Admob ID)に基いてテストデバイスを識別していたが、なぜか意図通りに認識されないので悩んでいたが、こういうことだった。

GooglePlay には、Admob ID を表示できるアプリなどがあり、最近のレビューではちゃんと表示されないという文句が多く見られたが、それもこれが原因だろう。Android 8 以降かそれ以前かで挙動が異なるわけである。