Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

Navigation Architecture Componentコンポーネントを触ってみました

f:id:timers-tech:20180928100225j:plain

こんにちは。Android開発担当の斎藤です。Android JetPack にある Navigation コンポーネント(Navigation Architecture Component)について調査したので結果を報告します。

Navigation とは

  • Jet Pack に含まれるコンポーネントの1つで、Android アプリの画面遷移の実装を簡易に実装するためのもの。
  • (注意)このライブラリは Fragment 間の遷移をメインとしています。

www.youtube.com

JetPack とは

  • Android アプリ作成の高速化を助けるライブラリ群のこと。

Navigation の原則

詳細が知りたい方は 公式 へ。

開発環境

Android Studio 3.2 Canary 14 以上のバージョンが必要です。 Android Studio 3.2 stable 版が最近(2018/09/24) にリリースされましたが、 Navigation Editor が無効化されています。 今回は Android Studio 3.3 Canary 14 を利用しています。

Please note, to maintain high product quality, a couple features (e.g. Navigation Editor) you saw in earlier release channels are not enabled by default in the stable release channel.

リリース時のブログより

事前準備

gradle に下記を追加する

    implementation "android.arch.navigation:navigation-fragment:1.0.0-alpha06"
    implementation "android.arch.navigation:navigation-ui:1.0.0-alpha06"

現時点(2018/09/27) では最新が alpha06 となっています。

実装

大まかな実装を簡単に説明します。

  • 1つの MainActivity で MainFragment -> NextFragment の切り替えをしています。
    • 今までは MainActivity の FrameLayout かなんかで Fragment のコンテナを作り、そのコンテナ内で MainFragment と NextFragment の切り替えを行なっていたと思います。
    <activity>
        <FrameLayout android:id="@+id/container"  />
    </activity>
  • Navigation ではこのコンテナに変わるものが用意されておりそれが、 androidx.navigation.fragment.NavHostFragment というものです。 NavHostFragment はnavigation 用の xml を解析してアプリを遷移させてくれます。
    <activity>
    <fragment android:name="androidx.navigation.fragment.NavHostFragment"
               app:navGraph="@navigation/navmain" />
    </activity>
  • navigation 用の xml というのが下記のようなデータです。これをGUIから作成しようというのが Navigation Editor です。
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navmain"
    app:startDestination="@id/mainFragment">

    <fragment
        android:id="@+id/mainFragment"
        android:name="jp.shts.android.navigationsample.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/action_mainFragment_to_nextFragment"
            app:destination="@id/nextFragment" />
    </fragment>
    <fragment
        android:id="@+id/nextFragment"
        android:name="jp.shts.android.navigationsample.NextFragment"
        android:label="fragment_next"
        tools:layout="@layout/fragment_next" />
</navigation>

f:id:timers-tech:20180928100721p:plain ^Navigation Editor はこんな感じです。

  • 見た目は iOS のストーリーボードに似ているのですが、 Navigation Editor は画面遷移に関する定義のみで、レイアウトに関する定義はできません。

  • 新規アプリの場合は画面遷移のための navigation xml を作成後、各画面の作成、Activity に NavHostFragment を追加 の順に作業していくとやりやすいと感じました。

A 画面から B 画面へ遷移する

実装イメージ

  • 遷移図を作成するため navigation xml を作成します。

    • res/navigation 下に適当な名前のファイルを Navigation resource file で作成します。
    • Navigation resource file の項目がない場合は AndroidStudio のバージョンが古い可能性があるので確認してください。
  • Navigation Editor で作成した Fragment にはテンプレートが書いてありますが、今回のサンプルには不要処理なので一部というかほぼ削除します。

  • Navigation Editor で作成した2つの Fragment を繋げます。 f:id:timers-tech:20180928101000p:plain ^画面の丸を右へ引っ張ると画面遷移を定義できます。

  • Activity に NavHostFragment を定義して^で作成した navigation xml を 設定する。

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

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/navmain" /> <-- 先ほど定義した  navigationxml
</android.support.constraint.ConstraintLayout>
  • MainFragment のボタンをタップした時に画面遷移するリスナーを設定する。
        nextButton.setOnClickListener {
            it.findNavController().navigate(R.id.action_mainFragment_to_nextFragment)
        }
  • action_mainFragment_to_nextFragment は Navigation Editor で画面遷移を定義すると自動で作成されるIDです。自分で変更も可能。

^リスナに設定するのは上から二番目のIDです。

これで画面の切り替えができるようになります。

アニメーションしながらA 画面から B 画面へ遷移する

実装イメージ

  • 前回のA 画面から B 画面へ遷移するのコードをベースとします。
  • Navigation Editor から Main <-> Next 画面遷移時にアニメーションをつけることができます。

  • Navigation Editor で矢印をタップ後の Attributes パネルに Animations の項目が表示されるのでここでアニメーションリソースを選択するだけで画面遷移アニメーションをつけることができます。
    • Enter
      • AからBに遷移するときの B のアニメーション
    • Exit
      • AからBに遷移するときの A のアニメーション
    • Pop Enter
      • BからAに遷移するとき(バックするとき)の A のアニメーション
    • Pop Exit
      • AからBに遷移するとき(バックするとき)の B のアニメーション

A 画面から B 画面へ遷移する(データを渡す)

ちょっとわかりにくいですが、Hello という文字列をBに渡して表示しています。

  • Navigation Editor でデータを受け取りたいクラスを選択して Attributes パネルの Arguments のプラスボタンをタップすると受け取りたいデータを定義できます。
  • XMLを直接編集しても問題はありません。

f:id:timers-tech:20180928102638p:plain

あとは遷移元のFragmentでbundleを作成して渡すだけです。

        nextButton.setOnClickListener {
            val bundle = Bundle().also {
                it.putString("title", "Hello")
            }
            it.findNavController().navigate(R.id.action_mainFragment_to_nextFragment, bundle)
        }

受け取りも今まで通りです。

        text.text = arguments?.getString("title") ?: "Failed"

もっといいやり方

safeargs というライブラリを使えば、これらの処理を型安全に実行できます。

トップレベルのGradleにパスを追加します。

classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'

app配下のGradleでapplyします。

apply plugin: 'androidx.navigation.safeargs'

こうすると MainFragmentDirections というクラスが作成されます。またわざわざ指定しなければならなかった id も Action_mainFragment_to_nextFragment のように必要なパラメータを持ってクラスとして生えるので安全です。

        nextButton.setOnClickListener {
            val action = MainFragmentDirections.Action_mainFragment_to_nextFragment("Hello")
            it.findNavController().navigate(action)
        }

受け取り側は自動作成された NextFragmentArgs クラスから fromBundle します。NextFragmentArgs には Arguments で設定した変数名でメンバ変数が生えているのでこれを参照するだけです。

        text.text = NextFragmentArgs.fromBundle(arguments).title

A 画面から B 画面へ遷移する(リストからデータを渡す)

実装イメージ

よくある形かと思いますが、基本的にはデータの受け渡しは^に同じです。

// OnClickAnimalListener はリストクリック時のコールバックです
class AnimalListFragment : Fragment(), OnClickAnimalListener {
    override fun onClick(name: String) {
        findNavController().navigate(
                AnimalListFragmentDirections
                        .action_animalListFragment_to_animalDetailFragment(name)
        )
    }
}
class AnimalDetailFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        animalName.text = AnimalDetailFragmentArgs.fromBundle(arguments).name
    }
}

注意事項

こういった一覧から詳細へという画面遷移パターンの時、そして詳細へ遷移する時に渡すデータは Parcelable なデータを渡すことが多いと思いますが。 Navigation Editor の Arguments に Parcel なデータを指定できるのですが、 Parcelable を指定しても生成されるソースコードがなぜか String になってしまいます...

f:id:timers-tech:20180928103459p:plain

    public Bundle getArguments() {
      Bundle __outBundle = new Bundle();
      __outBundle.putString("animal", this.animal);
      return __outBundle;
    }

解決できずに今に至ります。なのでこのサンプルでは結局Animal ではなく String で名前だけ渡すようにしました。多分バグだろうと思っています。

ボトムナビゲーションでタブの切り替え

実装イメージ

BottomNavitaion を Activity に設置後に NavigationUI.setupWithNavController を呼ぶだけで簡単に作れてしまいます。

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
...
        app:navGraph="@navigation/navtab" />

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottomNav"
...
        app:menu="@menu/menu" />
        val navController = Navigation.findNavController(this, R.id.my_nav_host_fragment)
        NavigationUI.setupWithNavController(bottomNav, navController)

注意する点として、 Navigation Editor で action を指定する必要はありません。

感想

  • Navigation Editor のこと勘違いしてました。思ったより全然使いやすかったです。生成されるコードも全然読めます。
  • まだalpha版ということでしたが思ったより安定している印象です。

参考にした資料など

https://developer.android.com/topic/libraries/architecture/navigation/

https://inside.dmm.com/entry/2018/05/25/android-navigation

https://docs.google.com/presentation/d/1j3x5G4kOZrg9DOK_whF1KKGZAh-GwGtWuA3lThVl7W8/edit#slide=id.g3ba51d9016_0_28

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる