🌊

Android Kotlin Fundamentalsで学ぶ その3

2020/11/03に公開

はじめに

この記事はGoogleが提供しているCodelabの中のAndroidを作りながら学ぶAndroid Kotlin Fundamentalsコースで学習した内容を自分用に残していくものです。間違っていることなどあればコメントをいただけるとありがたいです!

この記事について

その3では、Lesson3について残していきます。
このレッスンでは、アプリ内のナビゲーションを作成する方法を説明しています。Fragmentを作成し、ナビゲーションを追加する流れで説明されています。ナビゲーションでは戻る操作(バックスタック)の宛先の変更や、外部アクティビティの呼び出し方法も学びます。

3-1 Create a fragment

Android Trivia プロジェクトのダウンロード

ここからダウンロードしてください。
その中の AndroidTrivia-Starter を開いてください。

Fragment

Fragment とは、アクティビティの動作やUIの一部として動作する。1つのアクティビティで複数のフラグメントを組み合わせてマルチペインUIの作成や複数のアクティビティでフラグメントを再利用できる。

  • 独自のライフサイクルがある
  • 独自の入力イベントを受け取れる
  • アクティビティの実行中にフラグメントの追加・削除ができる
  • UIはxmlファイルで記述

3-2 Define navigation paths

Navigationライブラリは画面遷移をいい感じに行なってくれる

  • Projectレベルのgradleで,
build.gradle(project)
ext {
        ...
        navigationVersion = "2.3.0"
        ...
    }
  • アプリレベルのgradleで,
build.gradle(app)
dependencies {
  ...
  implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
  implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
  ...
}

を追記する-> sync now

resディレクトリに対し、オプション->new->Android Resource File->ファイル名'navigationと指定するとres/navigation/navigation.xml`が追加される。

FragmentにNavigationを追加

actiovity_main.xml
<fragment
    ...
    app:navGraph="@navigation/navigation"
    app:defaultNavHost="true"
    ...
/>

app:navGraph属性に@navigation/navigationを指定することでナビゲーショングラフリソースにあることを宣言
app:defaultNavHost属性をtrueにすることで、このナビゲーションホストがデフォルトのホストになり、[戻る]ボタンが傍受できる。

navigation.xml で、プレビューをみながらフラグメントのアクションなど追加できる。
見てわかるのがすごくありがたい

ハンドラを追加

アクションを起こすビューに対し、ハンドラを追加する。
今回はPLAYボタンに追加するので、

TitleFragment.kt
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // データバインディングの設定
        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater, R.layout.fragment_title, container, false)
	// playボタンのハンドラを追加
        binding.playButton.setOnClickListener{view: View ->
            view.findNavController().navigate(R.id.action_titleFragment_to_gameFragment)
        }

        return binding.root
    }

こんな感じ

バックスタックの管理をする

いわゆる[戻る]ボタンを押したらタイトルに戻るようにする。
ここではpopUpTo, popUpToInclusive属性を使って行う

  • popUpTo: 対象のFragmentから上にあるバックスタックを全て破棄して、次のFragmentをスタックする
  • popUpToInclusive:
    • true: app:popUpToに指定したFragmentも含めてバックスタックを破棄して、新たなFragmentをスタックする
    • false: 指定したFragmentは残したままFragmentをスタックする。

App barの追加

NavigationUIを使って実装する

  • 戻るアクションの実装
    Imgur
MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
	...
        // app bar の実装
        val navController = this.findNavController(R.id.myNavHostFragment)
        NavigationUI.setupActionBarWithNavController(this, navController)
    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = this.findNavController(R.id.myNavHostFragment)
        return navController.navigateUp()
    }
  • オプションボタンの作成
    Imgur

レイアウトは新たなレイアウトリソースファイル(リソースタイプ: Men)が必要

オプションボタンの表示はonCreateView()内で
setHasOptionsMenu(true)
を定義する。

オプションボタンのonClickハンドラは

option_handler_sample
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.option_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return NavigationUI.onNavDestinationSelected(item, requireView().findNavController())
                || super.onOptionsItemSelected(item)
    }

とする。

よくあるハンバーガアイコンを押したら出てくるやつ
画面左端から右にスワイプしても出てくるよ

close open
Imgur Imgur

マテリアルデザインの追加

アプリレベルのgradleで、

build.gradle(app)
dependencies {
    ...
    implementation "com.google.android.material:material:$supportlibVersion"
    ...
}

を追加

drawer menuと drawer layoutを作成

こちらも新たなレイアウトリソースファイル(navdrawer_menu)を作成
オプションボタンと同様、メニューレイアウトで作成し、アイテムを追加することで項目を増やしていく。

MainActivity.kt
    // Drawerレイアウトを表すメンバ変数を定義
    private lateinit var drawerLayout: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
	... 
        NavigationUI.setupActionBarWithNavController(this, navController)

        // navigation drawer の実装
        drawerLayout = binding.drawerLayout
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)


    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = this.findNavController(R.id.myNavHostFragment)
        return NavigationUI.navigateUp(navController, drawerLayout)
    }

こんな感じ

3-3 Start an external Activity

フラグメント間の引数の受け渡し

NavDirectionを使う。{key: value}の形で扱うことができる。

build.gradle(project)
dependencies {
   ...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

}
build.gradle(app)
apply plugin: 'androidx.navigation.safeargs'

これらを追記することで、プロジェクト全体にNavDirectionが含まれるようになった。
*一度アプリをビルドしないと適用されないかも。。

GameFragmentからGameWonFragmentGameOverFragmentへ問題数、回答数を送る。

まず、引数の設定をする。
navigation.xmlで引数の設定をする。Designでもできるけど、コードで書くとこうなる。
これはGameWonFragmentに引数を追加した時のやつ

navigation.xml
    <fragment
        android:id="@+id/gameWonFragment"

        <action
            android:id="@+id/action_gameWonFragment_to_gameFragment"
            app:destination="@id/gameFragment" />
        <argument  // 引数1
            android:name="numQuestions"
            app:argType="integer" />
        <argument  // 引数2
            android:name="numCorrect"
            app:argType="integer" />
    </fragment>

次にGameFragmentで遷移させていたコードを変更する。

GameFragment.kt
                if (answers[answerIndex] == currentQuestion.answers[0]) {
		...
                    } else {
                        // We've won!  Navigate to the gameWonFragment.
                        view.findNavController().navigate(R.id.action_gameFragment_to_gameWonFragment)
                    }
                } else {
                    // Game over! A wrong answer sends us to the gameOverFragment.
                    view.findNavController().navigate(R.id.action_gameFragment_to_gameOverFragment)
                }

GameFragment.kt
                if (answers[answerIndex] == currentQuestion.answers[0]) {
		...
                    } else {
                        // We've won!  Navigate to the gameWonFragment.
			view.findNavController().navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment(numQuestions, questionIndex))
                    }
                } else {
                    // Game over! A wrong answer sends us to the gameOverFragment.
                    view.findNavController().navigate(GameFragmentDirections.actionGameFragmentToGameOverFragment())
                }

にする。

GameWonFragmentで引数を受け取る

GameWonFragment.kt
class GameWonFragment : Fragment() { // この行はそのまま
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val args = GameWonFragmentArgs.fromBundle(requireArguments())  // ここがBundleからの引数を受け取るためのコード
	...
        }
        return binding.root
    }
}

共有機能を追加する

インテントを使って他のアプリのアクティビティに移動することができる。

  • 共有するためにインテントを作成
GameWonFragment.kt
    private fun getShareIntent(): Intent{
        val args = GameWonFragmentArgs.fromBundle(requireArguments())
        val shareIntent = Intent(Intent.ACTION_SEND)
        shareIntent.setType("text/plain")
                .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
        return shareIntent
    }

IntentACTION_SENDインテントを使うことで、メッセージの送信を行うことができる。
また、setTypeメソッドでタイプの指定をし、putExtraで送信するメッセージを指定する。

  • 共有の開始を呼び出す
GameWonFragment.kt
    private fun shareSuccess(){
        startActivity(getShareIntent())
    }
  • 共有ボタンの表示
GameWonFragment.kt
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.winner_menu, menu)
        if(getShareIntent().resolveActivity(requireActivity().packageManager) == null){
            menu.findItem(R.id.share).isVisible = false
        }
    }
  • メニューから共有を呼び出す
GameWonFragment.kt
override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
}

こんな感じに共有先が出てくれる
Imgur

共有先のアクティビティは自動で見つけてくれるみたい。すごい。。
他にも、Sharesheetを使用することで、好きな人と情報を共有することもできるみたい。
Android側から提供されているこういった機能はとても気が利いている

まとめ

今回は、FragmentやNavigationの使い方について学びました。Fragmentをうまく使うことでUIの充実度がかなり増すことがわかりました。また、NavigationでFragmentを正しく遷移することができれば快適なアプリケーションを作成することができるとわかりました。このレッスンで抑えるべきこととして、Navigationによる遷移の宛先や、バックスタックで管理する画面など、遷移元、遷移先の関係を十分理解することだと思いました。遷移の辻褄が合わないと快適なUIを作ることができないと実感しました。この章は自分なりにもっと噛み砕いて理解していきたいと思います。

Discussion