カテゴリー
Kotlin言語

ゼロから始めるAndroidアプリ(Kotlin編)Unit3 – 2:フラグメント6(ナビゲーションアクションの実装)

Android Developers > スタートガイド > Kotlin と Android をゼロから学ぶ > Unit3 – ナビゲーションUnit3-2 の内容をまとめたものです(翻訳ではありません)


ナビゲーションアクションを実装する

アプリにナビゲーションアクションを設定しますね

画面の移動ができればアプリ完成ですね!

  • LetterListFragment にある RecyclerView の項目をタップすると WordListFragment を起動するようにします
  • NavHost に表示するデスティネーションを letterListFragment から wordListFragment に切り替えます
  • LetterListFragment の RecyclerView をタップしたときの処理は LetterAdapter に記述されるので、ナビゲーションアクションもここに記述します

LetterAdapter.kt の書き換え

LetterAdapter から WordListFragment を起動するようにコードを記述します。次のようにします

onClickListener ですね

  1. LetterAdapter.kt を開きます
  2. ボタンの onClickListener() の内容を削除します
  3. 生成したナビゲーションアクションを取得します
  4. navigate() メソッドでアクションを実行します
  5. onClickListener() に記述するコードは次のようになります
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())
holder.view.findNavController().navigate(action)

ナビゲーション用のクラスやメソッドはナビゲーションコンポーネントが自動で用意してくれるの

  • アクションを取得するときに使うクラスや関数は、NavGraph に記述した内容から、プロジェクトをビルドするときに自動生成されたものです
  • 自動生成されるクラスや関数の名前は、おおよそ何に使うものかわかるようになっています
  • LetterListFragmentDirections は letterListFragment デスティネーションから始まるすべてのナビゲーションを含んでいます
  • actionLetterListFragmentToWordListFragment() 関数は wordListFragmentへ移動するためのアクションです
  • 変数 action にナビゲーションアクションへの参照を取得したら、NavController の navigate() を使って表示するデスティネーションの切り替えを行います
    • findNavController() は NavController への参照を取得するメソッドです
    • navigate() は指定されたアクションによって NavHost に表示するデスティネーションを切り替えるメソッドです

MainActivity の設定

さいごに navController の設定をしますね

navController プロパティの作成

プロパティを作成して…

  • MainActivity.kt を開きます
  • クラスの最初に navController プロパティを設定します
  • 初期化はあとから onCreate() で行うので lateinit をつけておきます
private lateinit var navController: NavController

navController プロパティの設定

onCreate の中で初期化します

  • onCreate() の中で navController プロパティを初期化します
  • nav_host_fragment(FragmentContainerView の ID)への参照を使って navControllerプロパティを設定します
  • 次のようにします
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController

アプリバー(アクションバー)の設定

NavController を使うときは、アプリバーの設定はこんな風にしておくの

  • フラグメントを表示しているときにもアプリバー(アクションバー)がちゃんと表示されるように設定します
  • onCreate() の中で setupActionBarWithNavController() を呼んで navController を渡します
setupActionBarWithNavController(navController)

UPボタンをサポートする

UPボタンも使えるようにしますね

  • onSupportNavigateUp() メソッドをオーバーライドで実装します
  • XML で defaultNavHost を true に設定した上でこのメソッドを用意しておくと UP(戻る)ボタンを処理できるようになります
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}
  • navigateUp() 関数が失敗(falseを返す)した場合、親クラスの実装 super.onSupportNavigateUp() が呼ばれます
  • navigateUp() が true を返したときは、|| の右側は実行されません
  • || 演算子は true の条件が得られるまで右に向かって実行され、true が得られた時点で評価を終了します
  • これは short-circuit evaluation(短絡評価)と呼ばれます。便利なプログラミングの小技として知られています

今回の修正をしたあとの MainActivity クラス

MainActivity クラスはこんなかんじになりました

class MainActivity : AppCompatActivity() {

    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }

    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp() || super.onSupportNavigateUp()
    }

}
  • 以上で、単語アプリのナビゲーションはインテントを使わずにフラグメントで行われるようになりました
  • インテントを使わないので、インテントのエクストラを使っていたデータの引き渡しも動作しません
  • フラグメントでは、代わりに arguments プロパティを使ってデータを引き渡します

フラグメントに渡された引数を取得する

フラグメントに渡すデータなので arguments 使っちゃいますね

  • WordListFragment にコードを移植したときに、インテントの letter エクストラから値を取得するのに activity?.intent を参照しました
  • この方法でも確かに参照はできますが、アプリの規模が大きくなるとフラグメントがどのアクティビティに表示されているのか判定がしにくく、あまりいい方法とは言えません
  • また、ナビゲーションに NavGraph と Safe Args を使う今回のような場合では、そもそもインテントが存在しないのでインテントエクストラは利用できません
  • フラグメントへのデータの引き渡しは arguments プロパティを使います。Safe Args を使うことで型安全が保証されており、使い方も簡単です。また onViewCreated() が呼ばれるまで待つ必要もありません

arguments プロパティの使い方

  • WordListFragment.kt を開きます
  • WordListFragment クラスに letterId プロパティ(選択された文字を保存)を作成します
  • このプロパティは nullable にする必要はなく、lateinit としておくことができます
private lateinit var letterId: String
  • onCreate() をオーバーライドで作成します
    • onCreateView() , onViewCreate() ではありません
  • 次のようなコードを追加します
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}

拡張関数の let() を使っています

  • arguments の指定は任意なので null のこともあります。let() にラムダ式を渡すことで、arguments が null の場合に letterId に null が渡されないようにする処理を簡潔に記述しています
    • このコードは arguments が null でない場合に実行されます。null でない it を使って処理されます
    • arguments が null の場合、ラムダ式は実行されません
arguments?.let {
    letterId = it.getString(LETTER).toString()
}

let() と 拡張関数

  • 拡張関数・・・オブジェクトに用意されていない独自メソッド(関数)をユーザーがその場で簡易に追加するような機能
  • 関数 let ・・・拡張関数です。変数(オブジェクト)に対して使います。その変数(it)を使った処理をラムダ式を使って記述します
    • 変数.let{ itを使った処理 } のように記述します
    • 最後に処理された値が戻り値になります
    • 変数?.let{ 処理 } のように変数名に ? をつけると、変数が null のときは処理を実行せずに null を返します(セーフコール演算子)

arguments と Bundle

  • let() を使ったコードを Android Studio で編集すると、it というパラメータが使えることに気付くように「it:Bundle」というヒントが表示されます(コードの中に表示されますが、単なるヒントであってアプリのコードの一部ではありません)
  • Bundle はアクティビティやフラグメントなどのクラスの間でデータを渡すのに使うコレクション(データの入れ物)で、キーと値がペアになっています
  • インテントのエクストラ(extras)やフラグメントの arguments はどちらも Bundle です

arguments から得た文字を設定する

インテントを参照してる部分を書き換えますね

  • WordListFragment クラスの RecyclerView にアダプタを設定するのにインテントのエクストラを使っている部分があります
  • これを arguments から取得した letterId を使うように書き換えます

書き換えの手順

  1. WordListFragment.kt を開いて onViewCreated() に移動します
  2. activity?.intent?.extras?.getString(LETTER).toString() を letterId に置き換えます
  3. 次のようにします
recyclerView.adapter = WordAdapter(letterId, requireContext())

ここまでのまとめ

あともうちょっとです

書き換えをした WordListFragment クラスをまとめるとこんな感じになります

WordListFragment クラス

class WordListFragment : Fragment() {

    companion object {
        val LETTER = "letter"
        val SEARCH_PREFIX = "https://www.google.com/search?q="
    }

    private var _binding: FragmentWordListBinding? = null
    private val binding get() = _binding!!

    private lateinit var letterId: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        arguments?.let {
            letterId = it.getString(LETTER).toString()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentWordListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val recyclerView = binding.recyclerView
        recyclerView.layoutManager = LinearLayoutManager(requireContext())
        //recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())
        recyclerView.adapter = WordAdapter(letterId, requireContext())

        recyclerView.addItemDecoration(
            DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
        )
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

アプリを実行してみます

  • メニューから Run を選択して、ビルド、実行してみます
  • インテントを使わずに、すべて単一のアクティビティの中で2つの画面(フラグメント)の間をナビゲートできるようになっていることを確認します

うん、ちゃんと動いてるみたい

フラグメントのラベルを設定する

アプリバーのラベルは NavGraph で設定するの

  • アクティビティ版の単語アプリを、フラグメントを使うように変更できました
  • ナビゲーションコンポーネントとフラグメントを使うことで、アクティビティのときに表示されていたアプリバー(アクションバー)のタイトル文字が消えてしまったので、最後にこれを修正します
  • NavGraph に設定したフラグメント(デスティネーション)には label プロパティがあります。ここに設定されたタイトルが、フラグメントの親になるアクティビティーのアプリバー(アクションバー)に表示されます
  • ラベルに設定するタイトル(文字列)はハードコードせずに strings.xml に登録したものを使うようにします

ラベルの設定手順

strings.xml の設定

  • strings.xml を開きます
  • app 名の後に次の定数を追加します
<string name="word_list_fragment_label">Words That Start With {letter}</string>

letter は フラグメントの arguments に設定した letter を参照します

nav_graph.xml の設定

  • NavGraph 上のデスティネーション(フラグメント)にラベルを設定します
  • nav_graph.xml を開きます
  • コンポーネントツリーの中で letterListFragment を選択します
  • Attributes ペインの中にある label を app_name という文字列に設定します(app_name は strings.xml に登録してあるアプリ名の定数です)
    • id: letterListFragment
    • label: @string/app_name
    • name: 空
  • コンポーネントツリーで wordListFragment を選択します
  • label に word_list_fragment_label を設定します
    • id: wordListFragment
    • label: @string/word_list_fragment_label
    • name: 空

ビジュアルエディタを使えば簡単ですね

アプリを実行してみます

  • メニューから Run を選択して、ビルド、実行してみます
  • アクティビティ版と同じように動作することを確認します
  • それぞれの画面に対応したフラグメントが、たったひとつのアクティビティの上で動作するようになりました

今回のチュートリアルのアプリ、これで完成です。おつかれさまでした!

まとめ

このチュートリアルのまとめです。完成したプロジェクトが必要なときは こちら からダウンロードしてくださいね(Code > Download ZIP)

  • フラグメントは再利用が可能なUI部品で、アクティビティに埋め込んで使うことができます
  • フラグメントのライフサイクルは、アクティビティのライフサイクルとは異なります。view の設定は onCreateView() でなく、onViewCreated() で行います
  • FragmentContainerView はフラグメントをアクティビティに埋め込むのに使います。また、フラグメント間の移動(ナビゲート)の管理もします

ナビゲーションコンポーネント(Navigation Component)

  • FragmentContainerView の navGraph 属性を設定すると、ひとつのアクティビティ内で複数のフラグメントをナビゲートできるようになります
  • NavGraph のビジュアルエディタを使うと、ナビゲーションアクションを追加したり、デスティネーション間で引数を渡したりできます
  • インテントを使ったナビゲーションではエクストラ(extras)を使ってデータを渡します。ナビゲーションコンポーネントではナビゲーションアクション用のクラスやメソッドを自動生成するのに Safe Args プラグインを使っており、arguments は型安全が保証されています

フラグメントの使用例

  • ナビゲーションコンポーネントを使うことで、ひとつのアクティビティ内ですべてのレイアウトを管理できるようになります。また、すべてのナビゲーションがフラグメント間で行われるようになります
  • フラグメントを使うことで、タブレットの master-detail レイアウトや同一アクティビティ内での複数タブ(multiple tabs)のようなよくあるレイアウトパターンが可能になります

参考