AndroidアプリをSingle Activityに移行するためにやったこと

N予備校 Android チームでは、tatsuyafujisaki さん、hiesiea さん、yoshiya12x さんにご協力いただき、画面構成のほとんどを Single Activity に移行しました。

この記事では、Single Activity に移行した背景とつまづいたポイント、Single Activity にしたことによるメリットをまとめます。

Single Activity に移行したきっかけ

Single Activity に移行するきっかけは、大きく 2 つありました。

きっかけ1: Jetpack Navigation の登場

N予備校 Android アプリは、2016 年にリリースされてから 5 年に渡って Multi Activity で開発が行われてきました。

N予備校には、以下のような「教材」「生授業」「Q&A」という機能があるため、それぞれ、MaterialActivity, LessonActivity, QAActivity のような Activity が用意され、それぞれで View を生成していました。

f:id:hiraike32:20211013153306p:plain
N予備校のコンテンツ

そして、これらの各機能の Activity に遷移するには、Drawer からメニューを選択する必要がありました。

f:id:hiraike32:20211013160035p:plain:w300
Drawer による画面遷移

リリース当時の構成としては Multi Activity で良かったのですが、Google IO 2018Android Jetpack が発表されて変化が起こります。

Jetpack の 1 つとして発表された Navigation が Single Activity を推奨していたことです。

Today we are introducing the Navigation component as a framework for structuring your in-app UI, with a focus on making a single-Activity app the preferred architecture.

android-developers.googleblog.com

Navigation の導入は画面遷移をシンプルにするためには必須でした。しかし、その機能を最大限に生かすためには Single Activity へ移行することが必要だったのです。

N予備校でも Navigation を少しずつ取り入れていきましたが、Navigation を使用した画面遷移と、Activity を経由する画面遷移が混在するようになってしまいました。それぞれの画面遷移で、データの受け渡しの方法が異なるので、その分岐が手間になっていました。

きっかけ2: ホーム画面のリニューアル

そこにもう 1 つの大きなきっかけが発生しました。2021年の秋に企画されたホーム画面のリニューアルです。具体的には以下のように画面が刷新されました。

今までのホーム画面 新しいホーム画面
f:id:hiraike32:20211013160142p:plain
f:id:hiraike32:20211013160217p:plain

変更の目的は「ユーザーが学習をすぐに直感的に始められるようにする」というものです。そのために、ホーム画面に各教材のサムネイルの表示や、最新の授業バナーの表示などを行いました。

(変更内容の一覧はこちらでユーザーに周知されました。)

一方で、これらの変更に伴って、今まで「ホーム画面」として表示していた画面は、新たに「マイコース画面」として、Drawer から選択して表示させる必要がありました。

f:id:hiraike32:20211013160742p:plain:w300
マイコースへの導線

しかし、既存の実装では Drawer からは各 Activity を表示するようになっています。つまり今回の変更を既存のまま実現するには、新しく「マイコース画面」を表示するための Activity を増やさなければなりませんでした。

上記の 2 つのきっかけがあり、今後の開発環境への影響も考えた結果、Single Activity に移行する決断をしました。

Single Activity に移行するときに難しかったこと

Single Activity にするときには、Navigation の実装に非常に苦しみました。それぞれ苦しんだ点をまとめます。

Navigation Drawer を活用する

Single Activity に移行するにあたって、Drawer から各画面に遷移するために Navigation Drawer の活用が必須でした。

ただ、ドキュメントが非常に少なくて試行錯誤を重ねて苦労したので、ここに実装方法をまとめておきます。(Navigation のバージョンは 2.3.5 を使用しています。)

まず、Drawer に設定する menu の itemId に遷移したい navigation graph を設定します。

<!-- menu.xml -->
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <group android:checkableBehavior="single">
        <!-- 授業一覧画面に遷移するメニューを作成 -->
        <item
            android:id="@+id/lesson_navigation"
            android:icon="@drawable/ic_drawer_home"
            android:title="@string/title_lessons" />
    </group>
</menu>

次に、itemId に設定した navigation graph を作成して、ホーム画面の navigation graph で参照します。こうすることで、ホーム画面の graph が肥大化することを防げます。

<!-- home_navigation.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/home_navigation"
    app:startDestination="@id/homeFragment">

    <!-- 授業の navigation graph を参照する -->
    <include app:graph="@navigation/lesson_navigation" />

</navigation>

<!-- lesson_navigation.xml -->
<?xml version="1.0" encoding="utf-8"?>
<!-- ここの id を menu.xml の itemId と一致させる -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/lesson_navigation"
    app:startDestination="@id/lessonFragment">

     <!-- ... -->

</navigation>

あとは、Drawer の各アイテムがタップされたときに、Navigation Drawer で画面遷移する処理を実行すれば、指定した navigation graph に遷移することができます。

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return item.onNavDestinationSelected(navController)
}

その他詳しい実装方法は Navigation Drawer に記載されているのご確認ください。ただし今回のように navigation graph の中(home_navigation.xml の中)に 別の graph(lesson_navigation.xml) が入っているという構造(Nested Navigation)における実装方法は見当たらなかったので、上記でコードを紹介しました。

不特定の画面から遷移するために Navigation DeepLink を活用する

同じくドキュメントが見当たらなくて苦労したのが、アプリ内の不特定の画面から URL で遷移するときの実装です。例えばN予備校では、Q&A に投稿された URL から教材を開いたり、教材に掲載された URL から他の教材を開いたりします。

Q&A から教材を開くリンク 教材から別の教材を参照するためのリンク
先生が自習課題となる教材のリンクを貼っています 問題集に回答すると、その問題の理解を深める参考書へのリンクが表示されます
f:id:hiraike32:20211013183257p:plain
f:id:hiraike32:20211013183206p:plain

このように URL から他の画面を開く画面はいくつもあり、そこから開かれる可能性がある画面も 10 以上はあるため、それぞれの経路を navigation graph で設定するのは非常に大変な作業となります。

そこで利用したのが、Navigation DeepLink です。ただし、ドキュメントに書かれている方法とは全く異なる使い方をしました。

まず、遷移したい画面の navigation graph に対して deeplink を設定します。

<!-- material_navigation.xml -->
 <fragment
    android:id="@+id/sectionFragment"
    android:name="com.example.material.sectionFragment">
    <argument
        android:name="sectionId"
        app:argType="long" />
    <!-- deeplinkを設定する。URL に取得したい argument を設定する -->
    <deepLink app:uri="https://to_material_section/{sectionId}" />
</fragment>

もし遷移したい画面が引数を必要としているのであれば、上記のように URL に引数を入れる場所を含めます。deeplink が設定できたら、以下のコードで画面遷移を実行します。

val sectionId = SectionId(1)

val navDeepLinkRequest = NavDeepLinkRequest.Builder
    // deeplink に設定した URL を指定する
    .fromUri("https://to_material_section/$sectionId".toUri())
    .build()

// 画面遷移するときのアニメーションを設定する
val navOptions = NavOptions.Builder()
    .setEnterAnim(R.anim.slide_in)
    .setExitAnim(R.anim.fade_out)
    .setPopExitAnim(R.anim.slide_out)
    .build()

// 画面遷移を実行する
navController.navigate(navDeepLinkRequest, navOptions)

このように Navigation Deeplink を活用することによって、どの画面からでも SectionFragment を開くことができるようになります。

注意点としては、この設定では deeplink はアプリ内独自の URL となっているため、外部のブラウザで https://to_material_section/1 などを叩いてもアプリが開くことはありません。N予備校アプリでは、外部アプリからの DeepLink については別の処理が用意されているので、Navigation Deeplink はアプリ内の遷移にだけ使うようにしました。

上記の 2 点を解決することで、Single Activity への移行を実現することができました。もともと各 Activity がロジックをあまり持っていなかったことも大きな要因になりました。

Single Activity にしてよかったこと

Single Activity に移行してよかったことは大きく 3 点あります。

新しい画面を簡単に追加できるようになった

Single Activity になったことで、新しく作成する画面をどの Activity に置くべきかを考慮する必要がなくなって、非常に楽になりました。今回はホーム画面を入れ替えましたが、もしまたホーム画面を入れ替えることになっても、Fragment を入れ替えるだけで良いので、柔軟に画面追加・修正に対応できるようになりました。

N予備校は、今後も学習をしやすくするための新しい機能や画面が追加されていく予定ですので、その要求に応えやすい構成にできたのは良かったと思います。

Navigation の恩恵を最大限に受けられるようになった

Single Activity になって画面遷移が Navigation に統一されたことで、画面遷移の処理がスッキリまとまりました。また、画面遷移するときの引数の型がチェックされるようになったので、アプリの品質も向上しました。

将来的な話だと、Jetpack Compose に移行していくときにも恩恵を受けられるのではないかと思っています(まだ試していないのではっきりとはわかりませんが...)。

Activity の実装を意識しなくて済むようになった

今まで Activity によっては、バックキーを押したときの挙動がカスタマイズされていたり、画面遷移をするときは Activity の処理を呼びださなければならなかったりしました。表示する Fragment がどの Activity に載っているのかを意識しながら実装するのは非常に厄介でした。

Single Activity に移行することで、Activity の実装は非常にシンプルになって、バックキーの制御などは各 Fragment に任されたので、適切な役割の分担が実現されました。これも、今後の実装に非常に役立っていくと思います。


今回はホーム画面のリニューアルを背景に、Single Activity に移行した経緯をまとめました。Jetpack Navigationについての情報は非常に少ないと感じたので、何かの役に立てば幸いです。

We are hiring!

N予備校 Android チームでは、一緒にアプリ開発を進めていただける方を募集しています。

採用に関係のないカジュアル面談も行っていますので、この記事の内容について、また Android チーム全般の活動について興味がありましたが、ぜひお気軽にお話ししましょう。

カジュアル面談応募フォームはこちら

saiyo.dwango.co.jp

N予備校の他チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com