onActivityResultはFramework APIとAndroidXで挙動が異なる

その前に

現在は startActivityForResult / onActivityResult の代わりに Activity Result API というAndroidXライブラリが用意されており、これら従来のAPIは androidx.activity の最新alpha版で既にdeprecatedになっています。
そのため通常のアプリにおいては、本記事で取り扱うAPIは利用すべきではありません。恐らく、レガシーな開発環境を考慮に入れたライブラリ開発等でのみ必要なナレッジになるかと思います。


Android開発において画面間での結果受け渡しを行う方法は複数思い当たるが、最もオーソドックスかつあらゆるライフサイクル都合に対応可能なのが startActivityForResult / onActivityResult を用いた方法になる。
このAPIはActivityだけでなくFragment側にも存在し、Fragmentから他Activityを呼び出し、その結果を呼び出し元のFragmentで受け取る、なんていうことも可能だ。

ご存知のとおりAndroidは歴史的理由で "Fragment" と呼ばれる実装が2通り存在し、これは継承関係を持っていない。ここでは便宜上、

  • Android SDKに含まれるFragment実装(非推奨になっている)を "Framework API" と呼ぶ
  • AndroidX (Support Library) に含まれるFragment実装(通常こちらを使う)を "AndroidX" と呼ぶ

としていく。

Framework APIでの挙動

検証用アプリを用意した。

github.com

ざっくり、下記のような実装になっている:

  • Activity内にFramework API側のFragment (PlatformLauncherとする) を用意する
  • 上記Fragmentから startActivityForResult を叩き、次のActivityを表示する
  • 次のActivityは何かしらのresultをsetし、finishする

これを実行すると、PlatformLauncher#onActivityResult に結果が返る。呼び出し元はFragmentなので、この親Fragmentには特段通知が行かない。直感に反しない純粋な挙動だ。

Image from Gyazo

AndroidXでの挙動

上記検証用アプリ内のもうひとつのボタンは、このFragmentをAndroidXのFragment実装(SupportLauncherとする)に変更している。
この状態で実行すると、当然期待どおり SupportLauncher#onActivityResult に結果が返るのだが、さらにこの親のActivityの onActivityResult にも結果が返るのである。

Image from Gyazo

直感に反するどころか、そもそもFramework APIと異なる挙動になる。それに、親Activity側には自分の指定した値と全く異なるrequestCodeが返っている。
これは何か。

Framework APIの実装

Framework側のresult実装は(あくまでイメージなので厳格性に欠けるが)ざっくり下記のような仕様になっている:

  • Fragmentの startActivityForResult が実行されると、Fragmentの持つ who という値と共に親Activityまでリクエストを転送する
  • 親Activityはwhoの値と共に、Activity表示リクエストをdispatchする
  • resultの設定されたActivityが終了した際、whoの値を用いて呼び出し元を検索し該当するインスタンス(Fragment)にだけ通知をする
// Activity.java

@Deprecated
public void startActivityFromFragment(@NonNull Fragment fragment,
        @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
    startActivityForResult(fragment.mWho, intent, requestCode, options);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5859-L5886
// Activity.java

@Override
@UnsupportedAppUsage
public void startActivityForResult(
        String who, Intent intent, int requestCode, @Nullable Bundle options) {
    Uri referrer = onProvideReferrer();
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, who,
            intent, requestCode, options);
    if (ar != null) {
        mMainThread.sendActivityResult(
            mToken, who, requestCode,
            ar.getResultCode(), ar.getResultData());
    }
    cancelInputsAndStartExitTransition(options);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5897-L5919
// Activity.java

@UnsupportedAppUsage
void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data,
        String reason) {
    if (false) Log.v(
        TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
        + ", resCode=" + resultCode + ", data=" + data);
    mFragments.noteStateNotSaved();
    if (who == null) {
        onActivityResult(requestCode, resultCode, data);
    } else if (who.startsWith(REQUEST_PERMISSIONS_WHO_PREFIX)) {
        who = who.substring(REQUEST_PERMISSIONS_WHO_PREFIX.length());
        if (TextUtils.isEmpty(who)) {
            dispatchRequestPermissionsResult(requestCode, data);
        } else {
            Fragment frag = mFragments.findFragmentByWho(who);
            if (frag != null) {
                dispatchRequestPermissionsResultToFragment(requestCode, data, frag);
            }
        }
    } else if (who.startsWith("@android:view:")) {
        ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews(
                getActivityToken());
        for (ViewRootImpl viewRoot : views) {
            if (viewRoot.getView() != null
                    && viewRoot.getView().dispatchActivityResult(
                            who, requestCode, resultCode, data)) {
                return;
            }
        }
    } else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
        Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
        getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus());
    } else {
        Fragment frag = mFragments.findFragmentByWho(who);
        if (frag != null) {
            frag.onActivityResult(requestCode, resultCode, data);
        }
    }
    writeEventLog(LOG_AM_ON_ACTIVITY_RESULT_CALLED, reason);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L8123-L8162

AndroidX側の実装

対してAndroidX側実装だが、そもそもSupport LibraryとしてFragmentの仕組みが利用できない(Platformに含まれない)OSでの利用を想定して作られたものであるため、who値と共に直接dispatchしこれを元に手繰るような手段は取れない。そのためこちらでは、

  • Fragmentの startActivityForResult が実行されると、Fragmentのインスタンス共に親Activityまでリクエストを転送する
  • 親まで来たら、ActivityからstartActivityForResultを実行する。この際requestCodeの入替をしており、上位16bitをfragmentのindex、下位16bitを実際のrequestCodeとした値になる。
  • result設定したActivityが終了したら、当然上記の親Activityにまず通知が行く。この中で上位16bitを取り出しFragmentを手繰り、一致するものにresult通知する

というような感じの実装になっている。

// FragmentActivity.java

public void startActivityFromFragment(@NonNull Fragment fragment,
        @SuppressLint("UnknownNullness") Intent intent, int requestCode,
        @Nullable Bundle options) {
    mStartedActivityFromFragment = true;
    try {
        if (requestCode == -1) {
            ActivityCompat.startActivityForResult(this, intent, -1, options);
            return;
        }
        checkForValidRequestCode(requestCode);
        int requestIndex = allocateRequestIndex(fragment);
        ActivityCompat.startActivityForResult(
                this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options);
    } finally {
        mStartedActivityFromFragment = false;
    }
}

// https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L781-L800
// FragmentActivity.java

@Override
@CallSuper
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    mFragments.noteStateNotSaved();
    int requestIndex = requestCode>>16;
    if (requestIndex != 0) {
        requestIndex--;


        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return;
    }
    ActivityCompat.PermissionCompatDelegate delegate =
            ActivityCompat.getPermissionCompatDelegate();
    if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) {
        // Delegate has handled the activity result
        return;
    }


    super.onActivityResult(requestCode, resultCode, data);
}

// https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L149-L182

このとおり、Activity / Fragment両側にresultが通知され、Framework側とは異なる挙動を示すのにはある側面では致し方ない事由が存在するのである。

影響

この仕様によって特段影響があるかというと、多くの場合は影響がない。
AndroidのActivityは、異なるアプリケーションのActivity間(異なるプロセス間)でも相互に連携し動作できる設計思想にある。そのためIntentやrequestCodeとして未知のものが通知される可能性を考慮した実装を行うことを想定している。
事実として(現在は新しいAPIに置き換わったため参照できないが)Android Developersでは自身が処理可能なrequestCodeだけ処理対象にするコードが例示されている。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    // Check which request we're responding to
    if (requestCode == PICK_CONTACT_REQUEST) {
        // Make sure the request was successful
        if (resultCode == Activity.RESULT_OK) {
            // The user picked a contact.
            // The Intent's data Uri identifies which contact was selected.

            // Do something with the contact here (bigger example below)
        }
    }
}

// https://web.archive.org/web/20190703204900if_/https://developer.android.com/training/basics/intents/result?hl=ja#ReceiveResult

すなわち、AndroidX側の親Activityに本来Fragment側で取りたいrequestCode以外が到達していてもこれは無視すればよいし、そのような実装をしている限りFramework API側の実装に切り替わるようなことがあっても問題なく動作するはずだ。

MoPubのLifecycle Callbacksはメディエーションを使うなら実装したほうがよい

MoPubのRewarded Video実装をする時など、ドキュメントを読んでいると下記のような記述に遭遇する:

Step 5. Optionally Implement Lifecycle Callbacks

Network SDKs need to be notified of changes in the activity lifecycle so they can keep track of the current activity and load and show rewarded videos. For example, you can add lifecycle callbacks to all of your activities (including your Launcher Activity) that will load or show rewarded videos.

For Mediation SDKs that track Activity lifecycle events, you must notify MoPub when those events happen, so that MoPub can pass them onto mediated network SDKs, as shown:

developers.mopub.com

MoPubの提供する Lifecycle Callbacks と称するAPI郡に関する記述なのだが、 "Optionally" と書いてあるし、実装しなくても動作する場合があるし、何より複数に渡る画面へこれを埋め込むのは現実的ではない。そんなわけで無視して進めてしまいそうになる。
しかし "Network SDKs need to be notified of changes in the activity lifecycle" とあるように、実はメディエーション機能を使う場合には実態として必須と言って過言ではない。

Custom SDK Network側ではどのように見えてくるか

今回はリワード広告を例に取る。
前提として、MoPubでリワードを表示するには下記のような手順を取る:

// MyActivity.kt

val conf = SdkConfiguration.Builder(adUnitId)
    .withAdditionalNetwork(MyAdapterConfiguration::class.java.name)
    .withLogLevel(MoPubLog.LogLevel.DEBUG)
    .build()

MoPub.initializeSdk(this, conf, listener)

MoPubRewardedVideos.loadRewardedVideo(adUnitId, MyMediationSettings())

MoPubRewardedVideos.showRewardedVideo(adUnitId)

この要求を受け、メディエーション先アドネットワークのSDKは下記のようなコールバックを受ける:

// MyRewardedVideoAdapter.kt

override fun checkAndInitializeSdk(launcherActivity: Activity, adData: AdData): Boolean {
    // do something
    return true
}

override fun load(context: Context, adData: AdData) {
    // do something
    mLoadListener.onAdLoaded()
}

override fun show() {
    // do something
    mInteractionListener.onAdShown()
    mInteractionListener.onAdImpression()

    MoPubReward.success(MoPubReward.NO_REWARD_LABEL, MoPubReward.DEFAULT_REWARD_AMOUNT).let {
        mInteractionListener.onAdComplete(it)
    }

    mInteractionListener.onAdDismissed()
}

これらの詳細な実装は下記を参考にしてほしい。

github.com

loadRewardedVideo の時にはContextを渡していないが、Custom Event側はContextを受け取っている。これは何か?
MoPubの実装を辿ると、これが MoPub::initializeSdk の際に渡したものであることがわかる:

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177
// MoPubRewardedVideoManager.java

final AdAdapter adAdapter = (AdAdapter) adAdapterConstructor.newInstance(
        sInstance.mMainActivity.get(),
        baseAdClassName,
        adDataBuilder.build()
);

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L600-L604

そしてこの値は最初のinit時に与えられたものを保持し続けていることも確認できる。(これを書き換えるのがLifecycle Callbacksである。後述)

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177

すなわち最初に与えられたActivityを保持しつづけ、以後Contextが必要になった際はこれを使い回す。これらのコードを字面だけで読み取るなら、MoPubは下記のようなアプリのみを想定した仕様であることになる:*1

  • シングルアクティビティ・マルチフラグメント等、Activityインスタンスがアプリケーション全体を通して共通であるアプリ
  • Activity利用数はともかく、広告の初期化〜読込〜表示をするポイントが単一であるアプリ

問題が起きうるパターン

しかし上記には問題があり、たとえば下記のようなパターンにおいては当然ながら問題を引き起こしうる:

  • Splash表示をするActivityがあり、ここでinitializeを行う。この画面は数秒後にfinishされる。
  • たとえばショップ画面、たとえばゲームのリザルト画面など、異なる複数種類のActivityから動画リワードが繋ぎこまれる
  • メディエーション先SDKが開放されていない(= 通常の)Contextを用いて何かしらの処理を行うため、解放済みのContextでは広告表示に失敗する仕様になっている。

Android F/Wでは開放されていないContextのインスタンスがないと利用できないAPIが多数存在し、また動画リワードのような商材においては表示元となるActivityとの関係性が正しく定義されていることが期待されるため、MoPubのAPI仕様では対応できないことになる。

Lifecycle Callbacksは何をしているか

上記のようなパターンへの対処手段としてMoPubが用意しているのが Lifecycle Callbacks と呼ばれるAPI郡で、これはinit時に握ったActivityインスタンスを入れ替える処理が実行されるものだ。

// MoPub.java

public static void onCreate(@NonNull final Activity activity) {
    MoPubLifecycleManager.getInstance(activity).onCreate(activity);
    updateActivity(activity);
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L306-L309
// MoPub.java

try {
    Class moPubRewardedVideoManagerClass = Class.forName(
            MOPUB_REWARDED_VIDEO_MANAGER);
    sUpdateActivityMethod = Reflection.getDeclaredMethodWithTraversal(
            moPubRewardedVideoManagerClass, "updateActivity", Activity.class);
} catch (ClassNotFoundException e) {

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L412-L417
// MoPubRewardedVideoManager.java

@ReflectionTarget
public static void updateActivity(@Nullable Activity activity) {
    if (sInstance != null) {
        sInstance.mMainActivity = new WeakReference<>(activity);
    } else {
        logErrorNotInitialized();
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L179-L186

これにより load の際に渡されるContextが入れ替わるため、呼び出し元画面のContextを要求する多くのSDKは初めて期待どおりの挙動を実現可能になる。 load にはContextが渡されるのに対し show ではContextが渡されない問題*2については、Custom Event側で getLifecycleListener を実装することでなんとか回避することも可能だろう。

まとめ

  • 本来MoPubは複数Activityを跨いだ利用を考慮したAPI設計ではない
  • MoPubの提供するLifecycle Callbacksは、メディエーションを利用する限り事実上の必須実装に等しい
  • Custom Eventを実装する場合、期待するContextが常に渡らないことを考慮に入れて設計する必要がある

*1:正直に言って、Android F/W事情に沿っていない至極ナンセンスな仕様である

*2:本当に中途半端である

AndroidでServiceを使っていると、タスク削除してもプロセスは生き残る場合がある

先日、お客様のとこで直感に反する挙動に遭遇したためメモ。気付いてしまえば当たり前だが、少々想像力が必要だった。

Androidには最近使ったアプリ(Recents)という機能があり、ここで他のアプリに戻ったり、現在バックグラウンドで保持されているアプリのタスクを削除することができる。
通常、タスク削除されたアプリはプロセスが終了する。

ごく簡単な実装をしてみた。下記のような、Applicationクラスでフラグを管理し、メインの画面が作成された回数を表示するもの。

class MyApp : Application() {

    var count = 0

}
class MainActivity : AppCompatActivity() {

    private val textView by lazy { findViewById<TextView>(R.id.text) }
    private val myApp by lazy { application as MyApp }

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        myApp.count++

        textView.text = "MyApp#count == ${myApp.count}"
    }
}

何も難しいことはない。ご想像どおりタスクを削除する度に新しいプロセスが作成され、下記のような挙動になる。

この時点の実装は下記のとおり。

github.com


では、下記のようにForeground ServiceをApplicationクラスで立ち上げてみる。

class MyApp : Application() {

    var count = 0

    // 新たに下記を実装
    override fun onCreate() {
        super.onCreate()
        startForegroundService(Intent(this, MyService::class.java))
    }

}

Serviceの中身は特段何も無い。

class MyService : Service() {

    override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

}

するとどうだろう。タスク削除後にアプリを起動し直しても、カウントは上がり続ける。

github.com


ここまでの実装過程を見ていれば、何が起きているかは一目瞭然。タスクとして保持すべきものが存在する場合、プロセスは当然同じものが利用されることになるわけだ。
すなわち、Activityのcloseは必ずしも onBackPressed でブロックしていれば完璧ではない。Activityが意図しない形でcloseされかつ、プロセスが終了していない、というパターンが存在することになる。

このようなパターンを考慮して実装するには、下記のような方法が思いつく:

  1. Serviceに android:stopWithTask="true" を設定する
  2. Service#onTaskRemoved を実装し、自身を終了ないし適切に処理する
  3. Activity自身のonDestroyで適切に後処理をする
  4. Application#registerActivityLifecycleCallbacksonActivityStopped を監視する

通常は1の実装で十分だろう。ユーザが明示的にタスクの終了を求めている場合、当該アプリの処理はすべて停止している状態が理想的だ。
実際に、Spotifyなどの著名なアプリのほとんどは音楽が再生中だった場合もユーザの手でタスクが削除された場合はForeground Serviceが停止する仕様となっている。

結局AndroidX対応は簡単なので身構える必要はないが、外的要因で対応が前倒しになりうる

仕事でちょっと困ったので、調査ログを残しておく。

長い長い前提知識

そもそもAndroidXとはなんなのか、Jetifierとはなんなのかを記しておく。

AndroidXの成り立ち

前提知識として、Android開発における最も一般的な外部ライブラリとして Android Support Library / AndroidX というものが存在する。
もともとAndroidには Android Support Library というものがあって、これの後述する問題を解消したものが AndroidX である。

どの程度一般的かというと、Android Studioで新規にプロジェクトを作成するとそもそも標準で入ってくるし、Googleの公開している (Googleのビジネスに依存した) Android向けライブラリでは当然のごとく依存関係に含まれている。
というのも、Support LibraryしかりAndroidXが目指しているのは「Android OSから独立した、Androidのリリース全体に渡る下位互換性の提供」であり、アプリが実行されるOSバージョン自体がどんなバージョンであってもつつがなく動作することを保証するためのライブラリであるからだ。

すなわち特定バージョンのAndroidリリースと同期的に変更されるアプリ、具体的にはOS標準のシステムアプリなどでない限り、アプリはネイティブのAPIを直接叩くのではなく、Support LibraryしかりAndroidXのAPIを利用すべきである。

冒頭の「なぜAndroidXが登場したのか」という部分だが、これは

  • 最低サポートバージョンごとにモジュールが分離されており、ファットになっていた
  • モジュール間が密に結合していることもあり、想定バージョンごとにmajorバージョンをまとめて全部上げる方針になっていた

などのSupport Libraryの問題を解消すべく、

  • 機能ごとにモジュールを分離した
  • モジュールごとに独立したsemverを採用した

をはじめとする複数の修正を行いイチから再スタートしたのがAndroidX、ということになる。

Jetifierとはなにか

当然ながら、AndroidXはこれから更新が重ねられていく。すなわち新たな機能を利用したい場合、AndroidXへの移行を余儀なくされる。
しかしながらメンテナンスの停止したライブラリを利用しているだとか、アプリ自身が独立してAndroidXの機能をいち早く利用したいだとかの場合、

  • Support Libraryへ依存しているライブラリ実装
  • AndroidXへ依存しているライブラリ実装
  • AndroidXへ依存しているアプリ実装

などが混在してしまう可能性がある。

これを解決するため、GoogleがJetifierというツールを提供している。これを用いると、Support Library v28を利用していることを前提にそれらのクラス参照たちをAndroidXのものへ繋ぎ変えてくれる。スタンドアロン版を直接利用することも可能だが、ビルドスクリプトでフラグを立てるだけで自動で実行させることができる。

AndroidX対応はいつやるのか

本題。これらの前提のもと、じゃあいつ自分たちの作っているアプリでAndroidXに対応するのか、という話がいつかは出てくる。
実際自分の関わっているいくつかの事業者を見ていると、AndroidXへの本格対応は未だ行っていないケースが散見される。

いくらでも例外はあるものの、筆者としては「早めにやるに越したことはないが、最悪AndroidXの機能が必要になるまではJetifier以上の対応はしなくてよい」という意見を推していきたい。
この「AndroidXの機能が必要になるまで」に外的要因も含まれうるという点で、いくらかの危険性をはらんでいることも無視できないのが難しい部分だ。

Android Pluginの挙動

下記のようなアプリがあるとする。このアプリはSupport Library v28の機能に依存していて、具体的には古いAndroid OSにおけるbackgroundTintの実現にAppCompatを利用している。
この機能の実現にはAndroidXでの更新以降の機能は要求しておらず、Support Libraryとして提供されていた期間までの機能で必要十分である。

github.com

この時点での依存関係ツリー

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 821ms
# 1 actionable task: 1 executed

このアプリでandroid.useAndroidXをenableにしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
 android.enableJetifier=false

この時点ではアプリ側のコードを書き換えずビルドが可能である。それもそのはず、依存関係の書き換えは行われないのだ。

このdiffにおける依存関係

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

ではこのフラグはどのような影響を与えるのか?答えはAndroid Developersの記述が全てだ。

android.useAndroidX: true に設定すると、Android プラグインは Support Library ではなく、該当する AndroidX ライブラリを使用します。設定しない場合、このフラグはデフォルトで false です。

Android Pluginが当該ライブラリを利用する箇所としてわかりやすいものには、DataBindingが存在する。たとえば下記のようなDataBindingを用いたアプリの場合。

github.com

この時点でのdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- com.android.databinding:baseLibrary:3.5.0
# +--- com.android.databinding:library:3.5.0
# |    +--- android.arch.lifecycle:runtime:1.0.3 -> 1.1.1
# |    |    +--- android.arch.lifecycle:common:1.1.1
# |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    +--- android.arch.core:common:1.1.1
# |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    +--- com.android.support:support-core-utils:26.1.0 -> 28.0.0
# |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    +--- com.android.support:support-compat:28.0.0
# |    |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- com.android.support:collections:28.0.0
# |    |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- android.arch.lifecycle:runtime:1.1.1 (*)
# |    |    |    \--- com.android.support:versionedparcelable:28.0.0
# |    |    |         +--- com.android.support:support-annotations:28.0.0
# |    |    |         \--- com.android.support:collections:28.0.0 (*)
# |    |    +--- com.android.support:documentfile:28.0.0
# |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    +--- com.android.support:loader:28.0.0
# |    |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
# |    |    |    +--- android.arch.lifecycle:livedata:1.1.1
# |    |    |    |    +--- android.arch.core:runtime:1.1.1
# |    |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
# |    |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
# |    |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
# |    |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
# |    |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
# |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
# |    |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
# |    |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    +--- com.android.support:localbroadcastmanager:28.0.0
# |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    \--- com.android.support:print:28.0.0
# |    |         \--- com.android.support:support-annotations:28.0.0
# |    \--- com.android.databinding:baseLibrary:3.5.0
# +--- com.android.databinding:adapters:3.5.0
# |    +--- com.android.databinding:baseLibrary:3.5.0
# |    \--- com.android.databinding:library:3.5.0 (*)
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0 (*)
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 0s
# 1 actionable task: 1 executed

useAndroidXをenabledにしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
 android.enableJetifier=false

この状態でビルドを試みると失敗することが確認できるだろう。

./gradlew :app:buildDebug --console=plain
# > Task :app:preBuild UP-TO-DATE
# > Task :app:preDebugBuild UP-TO-DATE
# > Task :app:compileDebugRenderscript NO-SOURCE
# > Task :app:generateDebugResValues UP-TO-DATE
# > Task :app:generateDebugResources UP-TO-DATE
# > Task :app:mergeDebugResources UP-TO-DATE
# > Task :app:checkDebugManifest UP-TO-DATE
# > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
# > Task :app:mainApkListPersistenceDebug UP-TO-DATE
# > Task :app:processDebugManifest UP-TO-DATE
# > Task :app:bundleDebugResources UP-TO-DATE
# > Task :app:mergeDebugShaders UP-TO-DATE
# > Task :app:compileDebugShaders UP-TO-DATE
# > Task :app:generateDebugAssets UP-TO-DATE
# > Task :app:mergeDebugAssets UP-TO-DATE
# > Task :app:compileDebugAidl NO-SOURCE
# > Task :app:generateDebugBuildConfig UP-TO-DATE
# > Task :app:prepareLintJar UP-TO-DATE
# > Task :app:prepareLintJarForPublish UP-TO-DATE
# > Task :app:generateDebugSources UP-TO-DATE
# > Task :app:dataBindingExportBuildInfoDebug UP-TO-DATE
# > Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
# > Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
# > Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE
# > Task :app:processDebugResources UP-TO-DATE
#
# > Task :app:compileDebugKotlin FAILED
# e: /Users/s-yoshioka/Documents/android-databinding-support-library-app-example/app/src/main/java/jp/s64/example/android/databindingsupportlibraryapp/MyActivity.kt: (3, 16): Unresolved reference: databinding
# e: /Users/s-yoshioka/Documents/android-databinding-support-library-app-example/app/src/main/java/jp/s64/example/android/databindingsupportlibraryapp/MyActivity.kt: (14, 19): Unresolved reference: DataBindingUtil
#
# FAILURE: Build failed with an exception.
#
# * What went wrong:
# Execution failed for task ':app:compileDebugKotlin'.
# > Compilation error. See log for more details
#
# * Try:
# Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
#
# * Get more help at https://help.gradle.org
#
# BUILD FAILED in 2s
# 19 actionable tasks: 1 executed, 18 up-to-date

この時点のdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- androidx.databinding:databinding-common:3.5.0
# +--- androidx.databinding:databinding-runtime:3.5.0
# |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
# |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
# |    |    |    \--- androidx.annotation:annotation:1.0.0
# |    |    +--- androidx.arch.core:core-common:2.0.0
# |    |    |    \--- androidx.annotation:annotation:1.0.0
# |    |    \--- androidx.annotation:annotation:1.0.0
# |    +--- androidx.collection:collection:1.0.0
# |    |    \--- androidx.annotation:annotation:1.0.0
# |    \--- androidx.databinding:databinding-common:3.5.0
# +--- androidx.databinding:databinding-adapters:3.5.0
# |    +--- androidx.databinding:databinding-common:3.5.0
# |    \--- androidx.databinding:databinding-runtime:3.5.0 (*)
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

内部で利用されるライブラリがAndroidXの置き換わるとは、依存関係に下記のような差分が発生することを指しているのである。

10,51c10,23
< +--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:library:3.5.0
< |    +--- android.arch.lifecycle:runtime:1.0.3 -> 1.1.1
< |    |    +--- android.arch.lifecycle:common:1.1.1
< |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    +--- android.arch.core:common:1.1.1
< |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    +--- com.android.support:support-core-utils:26.1.0 -> 28.0.0
< |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    +--- com.android.support:support-compat:28.0.0
< |    |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- com.android.support:collections:28.0.0
< |    |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- android.arch.lifecycle:runtime:1.1.1 (*)
< |    |    |    \--- com.android.support:versionedparcelable:28.0.0
< |    |    |         +--- com.android.support:support-annotations:28.0.0
< |    |    |         \--- com.android.support:collections:28.0.0 (*)
< |    |    +--- com.android.support:documentfile:28.0.0
< |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    +--- com.android.support:loader:28.0.0
< |    |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
< |    |    |    +--- android.arch.lifecycle:livedata:1.1.1
< |    |    |    |    +--- android.arch.core:runtime:1.1.1
< |    |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
< |    |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
< |    |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
< |    |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
< |    |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
< |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
< |    |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
< |    |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    +--- com.android.support:localbroadcastmanager:28.0.0
< |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    \--- com.android.support:print:28.0.0
< |    |         \--- com.android.support:support-annotations:28.0.0
< |    \--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:adapters:3.5.0
< |    +--- com.android.databinding:baseLibrary:3.5.0
< |    \--- com.android.databinding:library:3.5.0 (*)
---
> +--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-runtime:3.5.0
> |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
> |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
> |    |    |    \--- androidx.annotation:annotation:1.0.0
> |    |    +--- androidx.arch.core:core-common:2.0.0
> |    |    |    \--- androidx.annotation:annotation:1.0.0
> |    |    \--- androidx.annotation:annotation:1.0.0
> |    +--- androidx.collection:collection:1.0.0
> |    |    \--- androidx.annotation:annotation:1.0.0
> |    \--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-adapters:3.5.0
> |    +--- androidx.databinding:databinding-common:3.5.0
> |    \--- androidx.databinding:databinding-runtime:3.5.0 (*)
58c30,42
<      +--- com.android.support:support-compat:28.0.0 (*)
---
>      +--- com.android.support:support-compat:28.0.0
>      |    +--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:collections:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    +--- android.arch.lifecycle:runtime:1.1.1
>      |    |    +--- android.arch.lifecycle:common:1.1.1
>      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    +--- android.arch.core:common:1.1.1
>      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    \--- com.android.support:versionedparcelable:28.0.0
>      |         +--- com.android.support:support-annotations:28.0.0
>      |         \--- com.android.support:collections:28.0.0 (*)
62c46,68
<      +--- com.android.support:support-core-utils:28.0.0 (*)
---
>      +--- com.android.support:support-core-utils:28.0.0
>      |    +--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:support-compat:28.0.0 (*)
>      |    +--- com.android.support:documentfile:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:loader:28.0.0
>      |    |    +--- com.android.support:support-annotations:28.0.0
>      |    |    +--- com.android.support:support-compat:28.0.0 (*)
>      |    |    +--- android.arch.lifecycle:livedata:1.1.1
>      |    |    |    +--- android.arch.core:runtime:1.1.1
>      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
>      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
>      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
>      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
>      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
>      |    |    |    \--- android.arch.core:common:1.1.1 (*)
>      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
>      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    +--- com.android.support:localbroadcastmanager:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    \--- com.android.support:print:28.0.0
>      |         \--- com.android.support:support-annotations:28.0.0
113c119
< BUILD SUCCESSFUL in 0s
---
> BUILD SUCCESSFUL in 1s

Jetifierの挙動

android.enableJetifierによる影響はもう少しわかりやすい。たとえば最初に挙げたSupport Libraryによるアプリで有効にしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..b5dd627 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
-android.enableJetifier=false
+android.useAndroidX=true
+android.enableJetifier=true

これにより、依存関係ツリーが書き換わる。

この時点でのdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0 -> androidx.appcompat:appcompat:1.0.0
#      +--- androidx.annotation:annotation:1.0.0
#      +--- androidx.core:core:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.collection:collection:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
#      |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.arch.core:core-common:2.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.versionedparcelable:versionedparcelable:1.0.0
#      |         +--- androidx.annotation:annotation:1.0.0
#      |         \--- androidx.collection:collection:1.0.0 (*)
#      +--- androidx.collection:collection:1.0.0 (*)
#      +--- androidx.cursoradapter:cursoradapter:1.0.0
#      |    \--- androidx.annotation:annotation:1.0.0
#      +--- androidx.legacy:legacy-support-core-utils:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.core:core:1.0.0 (*)
#      |    +--- androidx.documentfile:documentfile:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.loader:loader:1.0.0
#      |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.lifecycle:lifecycle-livedata:2.0.0
#      |    |    |    +--- androidx.arch.core:core-runtime:2.0.0
#      |    |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    |    \--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    |    +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
#      |    |    |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0 (*)
#      |    |    |    |    +--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    |    |    \--- androidx.arch.core:core-runtime:2.0.0 (*)
#      |    |    |    \--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0
#      |    |         \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.print:print:1.0.0
#      |         \--- androidx.annotation:annotation:1.0.0
#      +--- androidx.fragment:fragment:1.0.0
#      |    +--- androidx.core:core:1.0.0 (*)
#      |    +--- androidx.legacy:legacy-support-core-ui:1.0.0
#      |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
#      |    |    +--- androidx.customview:customview:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    \--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.viewpager:viewpager:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.drawerlayout:drawerlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.interpolator:interpolator:1.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.interpolator:interpolator:1.0.0 (*)
#      |    |    +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    \--- androidx.core:core:1.0.0 (*)
#      |    |    \--- androidx.cursoradapter:cursoradapter:1.0.0 (*)
#      |    +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.loader:loader:1.0.0 (*)
#      |    \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 (*)
#      +--- androidx.vectordrawable:vectordrawable:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.core:core:1.0.0 (*)
#      \--- androidx.vectordrawable:vectordrawable-animated:1.0.0
#           +--- androidx.vectordrawable:vectordrawable:1.0.0 (*)
#           \--- androidx.legacy:legacy-support-core-ui:1.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

\--- com.android.support:appcompat-v7:28.0.0 -> androidx.appcompat:appcompat:1.0.0

ツリー内のこの箇所にあるとおり、アプリ自身で参照していたSupport LibraryがAndroidXのものに置換されるのである。
しかしプロジェクト内のソースコードや出力されるバイトコードそのものには影響が出ないことには注意が必要だ。すなわち、コードの書換えは必要である。

./gradlew :app:buildDebug --console=plain
# > Task :app:preBuild UP-TO-DATE
# > Task :app:preDebugBuild UP-TO-DATE
# > Task :app:generateDebugResValues UP-TO-DATE
# > Task :app:checkDebugManifest UP-TO-DATE
# > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
# > Task :app:mainApkListPersistenceDebug UP-TO-DATE
# > Task :app:compileDebugRenderscript NO-SOURCE
# > Task :app:generateDebugResources UP-TO-DATE
# > Task :app:mergeDebugResources UP-TO-DATE
# > Task :app:processDebugManifest UP-TO-DATE
# > Task :app:bundleDebugResources UP-TO-DATE
# > Task :app:mergeDebugShaders UP-TO-DATE
# > Task :app:compileDebugShaders UP-TO-DATE
# > Task :app:generateDebugAssets UP-TO-DATE
# > Task :app:mergeDebugAssets UP-TO-DATE
# > Task :app:generateDebugBuildConfig UP-TO-DATE
# > Task :app:compileDebugAidl NO-SOURCE
# > Task :app:processDebugJavaRes NO-SOURCE
# > Task :app:processDebugResources UP-TO-DATE

# > Task :app:compileDebugKotlin FAILED
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (4, 24): Unresolved reference: v7
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (6, 20): Unresolved reference: AppCompatActivity
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (8, 5): 'onCreate' overrides nothing
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (9, 15): Unresolved reference: onCreate
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (10, 9): Unresolved reference: setContentView

# FAILURE: Build failed with an exception.

# * What went wrong:
# Execution failed for task ':app:compileDebugKotlin'.
# > Compilation error. See log for more details

# * Try:
# Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

# * Get more help at https://help.gradle.org

# BUILD FAILED in 1s
# 13 actionable tasks: 1 executed, 12 up-to-date

さて、では依存するライブラリではどうだろうか。たとえばSupport Libraryを依存関係に持つライブラリを追加する。Jetifierを有効にしていない場合は下記のような依存関係が出力される。

\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
     +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
     \--- com.android.support:support-compat:28.0.0 (*)

この時、この依存するライブラリをデコンパイルすると下記のようなものが確認できるだろう。

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package jp.s64.android.toolbox.support.notification

@android.support.annotation.RequiresApi public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { /* compiled code */ }

ではJetifierを有効にしてみる。当然ネストした依存関係も適切に処理され、下記のようになる。

\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
     +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
     \--- androidx.core:core:1.0.0 (*)

さきほど検証したとおり、自身のプロジェクトにおけるソースコードは書き換わらない。しかしこの依存ライブラリが持つコードをデコンパイルすると、下記のようなものが確認できる。

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package jp.s64.android.toolbox.support.notification

@androidx.annotation.RequiresApi public fun androidx.core.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { /* compiled code */ }

ライブラリ内での参照は自動で書き換わることが確認できた。

アプリにおける基本的な対応

これらを踏まえ対応におけるポイントをまとめると、

  • Support Library 28まで上げておく
  • useAndroidX, enableJetifier を有効にする
  • 壊れたプロジェクト内の参照を修正する
  • 依存するライブラリは自動で書き換わるので気にしなくてよい

の4点を押さえれば十分ということになる。実に簡単だ。

対応の判断

対応は簡単だが、問題はこの判断を行うタイミングや要因である。

  1. AndroidX以降の更新における機能がアプリにおいて必要になった
  2. AndroidX以降の更新におけるBugfix等がアプリに影響があると判明した
  3. 依存するライブラリがSupport LibraryではなくAndroidXを利用する形へ移行した

1は完全にプロジェクトのコントローラブルな範囲であるため割愛する。
2に関しては一般的なライブラリにおける破壊的変更が実施された以降とほぼ同じと捉えてよい。
3はほぼ2と同じ事由になることが想像されるが、少し問題があるため後述する。

これらを総合して言えることは、やはり「やれる内に更新をしていないと工数の確保が難しくなる」という点は他のライブラリと変わらないということだ。
もしアプリのロジックやビジネスが特定ライブラリに密に依存していて、それらのアップデートが必要となった際に雪だるま式にやることが増えてしまうかもしれない。

非アプリにおける対応

では非アプリ、具体的には複数アプリに導入されうるモジュールの場合にはどうだろうか?対応の判断面は変わらない。要は「機能が必要になった」「依存するライブラリが移行した」場合に対応すればよい、ということだ。

しかし「早めに更新したほうがよいか」というと、そんなことはない。むしろ「できる限り対応を遅めたほうがよい」と筆者は考える。

AndroidXとSupport Libraryの間には名前空間やモジュールの分割といった変更があり、大きな変更のように思わせる。
しかし実際には、アプリ側でJetifierを用いることでSupport Libraryからの移行は容易に行える。
では逆にAndroidXに対応したものをSupport Libraryを前提としたアプリで利用するのが容易かというと、これは難しい。JetifierはSupport Libraryを用いた参照をAndroidXのものに変換する機能こそ利用できるが、その逆は困難だからだ。

AndroidXへの対応は簡単だが、それは十分にそのプロジェクトへアサインされたエンジニアのリソースが存在する場合に限られる。現実問題として、必ずしもプロジェクトにフルタイマーや十分な能力を持ったメンバーがアサインされている保証はない。
複数のアプリで利用されるモジュールがAndroidXの利用を要求するようになった場合、必然的にこのモジュールを利用している全てのアプリは移行の作業を行うこととなる。

「ではAndroidX対応しなくてもいいのか?」と聞かれそうになる。この答えはYesだ。なぜならSupport Libraryさえ使っていれば、実際にどちらを利用するかはアプリ側に判断を委ねることができるからだ。Jetifierを有効にするだけでよい。

まとめ

長々と書いてきたが、この記事における主張は下記のとおりだ。

  • アプリにおけるAndroidX対応は、最悪必要になるまで対応しないという選択が可能である。が、外的要因に影響されないよう早めにやればよいし、通常は依存するライブラリに関わらず今からでも対応可能である。
  • 非アプリにおける対応は、可能な限り遅めたほうがよい。Support Libraryを使っている限りは実際のアプリに判断を委ねることができる。

筆者は業務でエンドユーザ向けアプリからSDKまで様々な開発に携わっているが、AndroidXに関する対応は事業者によってまちまちである。
しかしながらエンジニアを対象としたプロダクトを作る事業者においては、ユーザであるエンジニアの利便性を損なわない対応を求めたいものだ。

おまけ: 実はde-jetifierは存在する

AndroidXからSupport Libraryの利用へ戻すのは困難と記述したが、困難ではあっても可能ではある。
JetifierはAndroid Gradle Pluginと統合されているが、CLIからも利用ができる。そしてこのCLI版には Reverse mode が存在している。

developer.android.com

依存先ライブラリがAndroidXへ移行してしまったが、実態としてAndroidXの機能を利用していない (Support Library時代に提供されていた機能の範囲に留まっている) 場合は、これを利用するのもひとつの手段かもしれない。

Mavenリポジトリの指定は記述順序を考えないと解決時のパフォーマンスがじわじわ下がっていく

Androidアプリ開発などをしていれば、大概のプロジェクトはGradleを用いてライブラリの依存関係を解決しようとすることになる。そして開発・運用期間の長いアプリになればなるほど、後追いで導入されたサードパーティ製ライブラリとその取得元のMavenリポジトリ定義が肥大化していくケースが散見される。

普段なんとなく設定しているMavenリポジトリの定義だが、実はビルド時のパフォーマンス低下の原因となるケースがあるため、注意が必要だ。


たとえば、以下のようなスクリプトがあるとする。追加してあるライブラリ郡にはさほど意味がなく、単に大量のライブラリが必要な状態を再現するための記述である:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    ...
}

repositories {
    google()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.arch.core:core-common:2.0.1'
    implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
    implementation 'androidx.browser:browser:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.collection:collection:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.contentpager:contentpager:1.0.0'
    implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
    implementation 'androidx.cursoradapter:cursoradapter:1.0.0'
    implementation 'androidx.customview:customview:1.0.0'
    implementation 'androidx.documentfile:documentfile:1.0.1'
    implementation 'androidx.drawerlayout:drawerlayout:1.0.0'
    implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
    implementation 'androidx.emoji:emoji:1.0.0'
    implementation 'androidx.exifinterface:exifinterface:1.0.0'
}

これをビルドしてみる。

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 13:48:45
# ... 
# 13:50:50

125秒。


ここに、参照するライブラリは変えずに "リポジトリだけ" 追加する。今回は仮に実在しないリポジトリを追加してみた:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    google()
    jcenter()
}
...

この状態で、さきほど同様に実行する:

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:00:41
# ... 
# 14:03:49

188秒。この程度なら誤差かもしれない。


では、大量に存在するとどうだろう。今回も、仮に実在しないリポジトリを追加する:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
    google()
    jcenter()
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:07:55
# ...
# 14:13:33

338秒。元の125秒比較すると実に2.7倍近い時間が掛かるようになってしまった。


試しに順序を変えてみる。実在しないリポジトリたちは後ろへ移動する:

...
repositories {
    google()
    jcenter()
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:17:31
# ...
# 14:19:34

123秒。リポジトリ自体は定義されているにも関わらず、元の定義に匹敵する速度まで回復した。


なぜこんな現象が起こるのかは、ログを確認すればわかる。 338秒という最も長い時間の必要だったビルドにおけるログを確認してみると、下記のような出力が散見される:

...
05:39:19.831 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:19.955 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...
05:39:20.465 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:20.586 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...

m2-repo-1, m2-repo-2... と順にリポジトリを手繰っている。repositories {}で定義するリポジトリは、純粋に上から下へ優先順位を持っており、それを順々に参照するのである。
これを自身の参照したいライブラリ毎に行うため、当然ながらじわじわと解決の速度を低下させる要因になりうるのである。


広告系のSDKなどの特定の事業に依存したライブラリの多くは、Maven CentralやJCenterのようなパブリックリポジトリではなく 何かしらのプライベートリポジトリを利用している。特に複数のライブラリを併用する場合は、この参照先リポジトリがどんどん肥大化していくことが多い。

依存解決のためのリクエストは 成功するまで順々に投げられていくため、もし上位に(たとえば1つのライブラリしか置かれていないような)ほとんど使われないリポジトリが指定されていれば、すべての依存関係解決のために毎回無駄なリクエストを投げることになってしまう。


よって、このリポジトリ定義の順序は、下記の優先順位で決定するとよい:

  1. 特別な理由で最優先にしなければならないリポジトリ / 信頼性の高いリポジトリ
  2. 自身のアプリが持つ依存関係の中で最も利用数が多いリポジトリ

この記事の趣旨で言うと、本来であれば2だけを気にすればよい。


が、Android開発などの場合には特別な事情があるため併記しておく。

JCenter(JFrog Bintray)ではGoogle Maven Repositoryのミラーリング(Proxyではない)を独自に行っているが、一時Google Maven Repository内に配置してあるChecksumが不正な内容になってしまった時がある。
JCenterは仕組み上Checksumが不正な場合には404ではなく409を返すようになっている。その結果、google()よりもjcenter()の優先順位を高く設定していた場合には依存関係の解決に失敗してしまう、という問題が発生したことが過去にある

ライブラリの参照元は原則として発行元の推奨するものを利用すべきであるため、Android開発においては(たとえほとんどのライブラリが他のリポジトリ由来であっても)Google Maven Repositoryを最優先にすべきである。

FCM用のFirebaseプロジェクトを別プロジェクトへ移行する

AndroidアプリでPush通知を行う場合、Firebase Cloud Messaging (旧Google Cloud Messaging) を経由しGoogle Play Servicesがよしなに処理してくれるよう投げることになる。
FCMを使うということは、すなわちFirebaseのプロジェクトを firebase.google.com 上で作成するということになる。

アプリ開発の現場でよくあるケースに、当初はAndroid用のPush通知目的でのみ作成したFirebaseプロジェクトを、iOS含めAnalyticsやRemote Configを本格的に利用しようとした時にエイヤッと統合されたものへ移行する、というものがある。

この記事は、既にFCMを利用しているアプリのプロジェクトを移行できるのか検証した時のログである。

前提1

アプリ自体のリニューアル(package nameの変更)は行わないという前提のため、APK署名時に使う証明書は同じものとする。

前提2

今回移行するアプリは、以下のように設定して試した:

プロジェクト名 mynotifymigrationapp-orgproj
プロジェクトID mynotifymigrationapp-orgproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp
アプリのニックネーム (省略可) MyNotifyMigrationApp (oldproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
credential登録方法 google-services.jsonをアプリ内に配置し、google-servicesプラグインで認証
SHA証明書フィンガープリント(※リリース用) EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3
通知の利用 数回なげて、受信できることを確認した

1. 旧プロジェクトから証明書のハッシュを削除する

FirebaseがInvites, Dynamic Links, Authenticationなどの機能を利用するアプリが不正なものではないことを確認できるのは、Googleのサーバにpackage nameと証明書フィンガープリントのペアをグローバルに登録し、それと一致することを条件とするため。
よって、FCMではマストではないSHA1フィンガープリントの登録ではあるが、他のプロジェクト(他の人物、というとわかりやすいか?)が同一の組み合わせを登録しようとするとエラーにで弾かれるようになっている。

すなわち一番はじめにすることは、旧プロジェクトに紐付いた証明書に関する設定を削除すること。

旧プロジェクトのProject Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (oldproj)) 内に SHA 証明書フィンガープリント という項目があるため、この内容を削除する。

今回の場合は、

  • 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
  • EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3

の2件。

2. アプリの認証情報を削除する

この時点で既に旧アプリでの認証情報は不要になるため、削除してしまう。
アプリ内に配置したgoogle-services.jsonを削除して、おしまい。

3. 移行先プロジェクトを作成する

既に移行先プロジェクトが存在する場合はスキップ。
今回は以下の内容にした:

プロジェクト名 mynotifymigrationapp-newproj
プロジェクトID mynotifymigrationapp-newproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp ※元と同じ
アプリのニックネーム (省略可) MyNotifyMigrationApp (newproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5 ※元と同じ

4. 新たな認証情報をアプリに入れる

作成の途中でgoogle-services.jsonをダウンロードすることができるので、これをアプリに配置しておく。ステップ2で削除した分。

5. リリース用のハッシュをプロジェクトに追加する

ステップ1で削除した分。移行先プロジェクトの Project Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (newproj)) に、本番の証明書から取ったSHA1ハッシュを追加する。
今回の場合は EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3 となる。

6. 必要なら、FCMのサーバーキーを変更する

Amazon SNSやReproなどを使っている場合、FCMへAPI経由でアクセスするためのサーバーキーを新しいものへ変更する必要がある。
Project Overview -> Settings -> プロジェクトの設定 -> クラウドメッセージング -> プロジェクト認証情報 から取得できるので、これを用いて変更作業をする。

Proxyで傍受可能なオレオレ証明書を通せるAndroidアプリの作り方

人間誰しも1度はProxyを通して通信内容をデバッグしたい時がある。
要はオレオレ証明書を発行して間に挟めば傍受も改ざんもやりたい放題なのだが、Android 7以降からは一筋縄では実現できない。

In Android Nougat, we’ve changed how Android handles trusted certificate authorities (CAs) to provide safer defaults for secure app traffic. Most apps and users should not be affected by these changes or need to take any action. The changes include:

  • Safe and easy APIs to trust custom CAs.
  • Apps that target API Level 24 and above no longer trust user or admin-added CAs for secure connections, by default.
  • All devices running Android Nougat offer the same standardized set of system CAs—no device-specific customizations.

https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html

上記のとおり、AOSPで設定されたCA以外は信頼されず、ユーザが独自にインストールしたCAすらもデフォルトで無効になる。
しかしこれらをオプトインで利用することも当然可能になっており、またアプリ内にderファイルを同梱させ利用させることも可能だ。

これらは設定ファイルを記述して自分のアプリに組み込めば実現できる。

今回目指すこと

今回は、

  • Proxyサーバが、アプリが要求した https://example.com の内容を傍受できるようにする
  • 上記の内容を改竄し、アプリがへ届けられるようにする

の2つの実現を目指す。
ただし、傍受や改竄自体は便利なアプリが複数存在する(たとえば Burp Suite など)ので、実際に行うのは独自CAの許可のみだ。

以降で実装手順を解説する。実際のコードはGitHubに掲載した。

github.com

1. Response bodyを表示する簡単なアプリを作る

まずは挙動が確認できないことには始まらないので、通信した結果を表示するだけの画面を作る。
ざっくり以下のようになるはずだ:

class MainActivity : AppCompatActivity() {

    companion object {

        private const val ENDPOINT = "https://example.com"

    }

    private val client = OkHttpClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button)
            .setOnClickListener {
                val req = Request.Builder()
                    .url(ENDPOINT)
                    .build()

                GlobalScope.launch(Dispatchers.IO) {
                    val result = client.newCall(req).execute().let {
                        it.body()?.string() ?: it.code().toString()
                    }

                    GlobalScope.launch(Dispatchers.Main) {
                        findViewById<TextView>(R.id.text)
                            .text = result
                    }
                }
            }
    }
}

このような表示を期待する。

f:id:S64:20190424173808p:plain:w250

2. 構成ファイルを作成・配置する

今回の設定はアプリ内でのみ有効になる。試しに

  • 全ての通信において、プリインストールのCAとres/raw/myca.derを信頼する

という設定にし、src/main/res/xml/nsc.xml に配置した:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="@raw/myca" />
        </trust-anchors>
    </base-config>
</network-security-config>

3. CAを追加する

前項で設定したとおり、src/main/res/raw/myca.derとして追加する。

4. Manifestに設定する

nsc.xmlというファイル名自体には意味がなく、これを AndroidManifest.xml から参照する必要がある。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    ...

    <application
            ...
            android:networkSecurityConfig="@xml/nsc">
        ...
    </application>

</manifest>

<application/> 要素に android:networkSecurityConfig で設定する。

以上で、独自証明書を用いた通信の傍受や改竄が可能になったはずだ。