iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day12【Start an external Activity編】

学ぶこと

  • Bundle クラスを使用して、ある Fragment から別の Fragmen に引数を渡す方法
  • タイプセーフのために Safe Args Gradle プラグインを使用する方法
  • Share menu item をアプリに追加する方法
  • implicit intent とは何か、それを作成する方法

すること

  • NavDirection クラスを生成する Safe Args プラグインを使用するように AndroidTrivia コードを変更します。

  • 生成された NavDirection クラスの1つを使用して、Game Fragment と game-state Fragment 間でタイプセーフな引数を渡します。

  • "Share" menu item をアプリに追加します。

  • ユーザがゲームの結果に関するメッセージを共有するために使用できるセレクターを起動する implicit intent を作成します。

アプリ概要

前の2つのコードラボで取り組んだ AndroidTrivia アプリは、ユーザーが Android 開発に関する質問に答えるゲームです。ユーザが3つの質問に正しく答えると、ゲームに勝ちます。

このコードラボでは、AndroidTrivia アプリを変更して、ユーザがゲームの結果を他のアプリに送信し、その結果を友達と共有できるようにします。

Safe Args プラグインをセットアップして使用する

ユーザが AndroidTrivia アプリ内からゲームの結果を共有する前に、コードは1つのフラグメントから別のフラグメントにパラメータを渡す必要があります。これらのトランザクションのバグを防ぎ、タイプセーフにするために、Safe Args と呼ばれる Gradle プラグインを使用します。プラグインNavDirection クラスを生成し、これらのクラスをコードに追加します。

このコードラボの後半のタスクでは、生成された NavDirection クラスを使用して、Fragment 間で引数を渡します。

Safe Args プラグインが必要な理由

多くの場合、アプリはフラグメント間でデータを渡す必要があります。あるフラグメントから別のフラグメントにデータを渡す1つの方法は、[Bundle])(https://developer.android.com/reference/android/os/Bundle.html) クラスのインスタンスを使用することです。Android Bundle は key-value ストアです。

key-value ストアは、ディクショナリまたは連想配列とも呼ばれ、一意のキー(文字列)を使用して、そのキーに関連付けられた値をフェッチするデータ構造です。Ex:

Key Value
"name" "Anika"
"favorite_weather" "sunny"
"favorite_color" "blue"

アプリは、Bundle を使用して FrgmentA から FragmentB にデータを渡すことができます。例えば、FragmentA は Bundle を作成し、情報を key-value として保存してから、Bundle を FragmentB に渡します。次に、FragmentB はキーを使用して、Bundle から key-value のペアをフェッチします。この手法は機能しますが、コードがコンパイルされ、アプリの実行時にエラーが発生する可能性があります。

発生する可能性のあるエラーの種類は次の通りです:

  • Type mismatch errors。例えば、FragmentA が文字列を送信し、FragmentB がバンドルから整数を要求した場合、要求はデフォルト値のゼロを返します。ゼロは有効な値なので、この種のタイプの不一致の問題は、アプリのコンパイル時にエラーをスローしません。ただし、ユーザがアプリを実行すると、エラーが原因でアプリが誤動作したりクラッシュしたりする可能性があります。

  • Missing key errors。FragmentB がバンドルに設定されていない引数を要求した場合、操作は null を返します。これはアプリのコンパイル時エラーをスローしませんが、ユーザがアプリを実行した時に深刻な問題を引き起こす可能性があります。

Android Studio でアプリをコンパイルする時にこれらのエラーをキャッチして、アプリを本番環境にデプロイする前にこれらのエラーをキャッチする必要があります。言い換えると、ユーザがそれらに遭遇しないように、アプリ開発中にエラーをキャッチしたいです。

これらの問題を解決するために、Android の Navigation Architecture Component には Safe Args と呼ばれる機能が含まれています。Safe Args は、コードとクラスを生成する Gradle プラグインで、コンパイル時にアプリが実行されるまで表示されないエラーを検出するのに役立ちます。

このコードラボでは、Congratulations 画面の上部に共有アイコンを追加します。Share icon を使用すると、ユーザは結果をメールまたはテキストで共有できます。

プロジェクトに Safe Args を追加する

  1. Android Studio で、プロジェクトレベルの build.gradle ファイルを開きます。

  2. 次に示すように、navigation-safe-args-gradle-plugin 依存関係を追加します。

// Adding the safe-args dependency to the project Gradle file
dependencies {
   ...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

}
  1. アプリレベルの build.gradle ファイルを開きます。

  2. ファイルの先頭で、他の全てのプラグインの後に、androidx.navigation.safeargs プラグインを含む apply plugin ステートメントを追加します。

// Adding the apply plugin statement for safeargs
apply plugin: 'androidx.navigation.safeargs'
  1. プロジェクトを Re-build します。追加のビルドツールのインストールをするように求められたら、それらをインストールします。

プリプロジェクトに、生成された NavDirection クラスが含まれるようになりました。

Safe Args プラグインは、Fragment ごとに NavDirection クラスを生成します。クラスは、全てのアクションからの navigation を表します。

たとえば、GameFragment には GameFragmentDirections クラスが生成されています。GameFragmentDirections クラスを使用して、Game Fragment とアプリ内の他のフラグメントとの間でタイプセーフな引数を渡すことができます。

生成されたファイルを確認するには、generatedJava フォルダーを探します。

注意: NavDirection クラスは編集しないでください。これらのクラスは、プロジェクトがコンパイルされるたびに再生成され、編集内容は失われます。

NavDirection クラスを Game Fragment に追加する

このステップでは、Game Fragment に GameFragmentDirections クラスを追加します。後でこのコードを使用して、GameFragment と game-state Fragment の間で引数を渡します。

  1. java フォルダーにある GameFragment.kt ファイルを開きます。

  2. onCreateView() メソッド内で、geme-won conditional ステートメントを見つけます。NavController.navigate() メソッドに渡されるパラメータを変更します。game-won state の アクション ID を、GameFragmentDirections クラスの actionGameFragmentToGameWinFragmen() メソッドを使用する ID に置き換えます。

条件ステートメントは次のコードのようになります。次のタスクでは、actionGameFragmentToGameWonFragment() メソッドにパラメータを追加します。

// Using directions to navigate to the GameWonFragment
view.findNavController()
        .navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())
  1. 同様に、game-over statement も下記のように変更します。
// Using directions to navigate to the GameOverFragment
view.findNavController()
        .navigate(GameFragmentDirections.actionGameFragmentToGameOverFragment())

引数を追加して渡す

このタスクでは、型付き引数を gameWonFragment に追加し、引数を GameFragmentDirections メソッドに渡します。次に、他の Fragment クラスを同等の NavDirection クラスに置き換えます。

game-won Fragment に引数を追加します

  1. navigation.xml を開きます。navigation graph を開いて、fragment の引数を設定します。

  2. プレビューで gameWonFragment を選択します。

  3. Attributes ペインで、Arguments セクションを開きます。

  4. +icon をクリックして引数を追加します。numQuestions と名前を付け、タイプを Integer に設定して Add をクリックします。この引数は、ユーザが回答した質問の数を表します。

  1. 2つ目の引数を追加します。numCorrect と名前を付け、型を Integer に設定します。この引数は、ユーザが正しく回答した質問の数を表します。

アプリをビルドしようとすると、2つのコンパイルエラーが発生する可能性があります。

No value passed for parameter 'numQuestions'
No value passed for parameter 'numCorrect'

次のステップでこのエラーを修正します。

引数を渡す

このこのステップでは、numQuestions および questionIndex 引数を、GameFragmentDirections クラスから actionGameFragmentToGameWonFragment() メソッドに渡します。

  1. GameFragment.kt Kotlin ファイルを開き、ゲームに勝利した時のステートメントを見つけます。
else {
 // We've won!  Navigate to the gameWonFragment.
 view.findNavController()
      .navigate(GameFragmentDirections
            .actionGameFragmentToGameWonFragment())
}
  1. numQuestions および questionIndex パラメータを actionGameFragmentToGameWonFragment() メソッドに渡します。
// Adding the parameters to the Action
view.findNavController()
      .navigate(GameFragmentDirections
            .actionGameFragmentToGameWonFragment(numQuestions, questionIndex))

質問の総数を numQuestions として渡し、現在試行されている質問を questionIndex として渡します。アプリは、ユーザが全ての質問に正しく回答した場合にのみデータを共有できるように設計します。正しく回答した数は、常に回答された質問の数と同じです。(必要に応じてゲームロジックを変更します。)

  1. GameFragment.kt で、バンドルから引数を取り出し、Toast を使用して引数を表示します。return ステートメントの前の onCreateView() メソッドに次のコードを挿入します。
val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
  1. アプリを実行して確認します。

Fragment クラスを NavDirection クラスに置き換える

"safe argument" を使用すると、navigation code で使用される Fragment class を NavDirection クラスに置き換えることができます。これを行うと、アプリ内のフラグメントでタイプセーフな引数を使用できます。

TitleGragmentGameOverFragment および GameWonFragmentnavigate() メソッドに渡される action ID を変更します。action ID を NavDirection クラスの同等のメソッドに置き換えます。

  1. TitleFragment.kt ファイルを開きます。onCreateView() で、Play ボタンのクリックハンドラー navigate() メソッドを見つけます。TitleFragmentDirections.actionTitleFragmentToGameFragment() をメソッドの引数として渡します。
binding.playButton.setOnClickListener { view: View ->
    view.findNavController()
            .navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
}
  1. GameOverFragment.kt ファイルで Try Again ボタンのクリックハンドラーで、navigate() メソッドの引数として、GameOverFragmentDirections.actionGameOverFragmentToGameFragment() を渡します。
binding.tryAgainButton.setOnClickListener { view: View ->
    view.findNavController()
        .navigate(GameOverFragmentDirections.actionGameOverFragmentToGameFragment())
}
  1. GameWonFragment.kt ファイルの Next Match も下記のように変更します。
binding.nextMatchButton.setOnClickListener { view: View ->
    view.findNavController()
          .navigate(GameWonFragmentDirections.actionGameWonFragmentToGameFragment())
}
  1. アプリを実行

Implicit intent と "share" menu item

Implicit Intens

Navigation Components を使用して、Activity 内の Fragment 間を移動してきました。Android では、インテントを使用して、他のアプリが提供する Activity に移動するこもできます。AndroidTrivia アプリでこの機能を使用して、ユーザがゲームプレイの結果を共有できるようにします。

Intent は、Android Component 間の通信に使用される単純なメッセージオブジェクトです。Implicit intets を使用すると、タスクを処理するアプリまたは Activity を知らなくても Activity を開始できます。例えば、アプリで写真をとる場合は、どのアプリまたは Activty がタスクを実行するかは気にしません。複数の Android アプリが同じ implicit intents を処理できる場合、Android はユーザにセレクターを表示し、ユーザがアプリを選択してリクエストを処理できるようにします。

implicit intent は、実行する処理のタイプを説明する ACTION が必要です。ACTION_VIEW,ACTION_EDIT,ACTION_DIAL などの一般的なアクションは、Intent クラスで定義されています。

Implicit intents の詳細についてはこちら

Congratulations 画面にオプションメニューを追加する

  1. GameWonFragment.kt ファイルを開きます。

  2. onCreateView() メソッド内で、return 前に、setHasOptionsMenu() メソッドを呼び出して true にします。

  setHasOptionsMenu(true)

Implicit intent を構築して呼び出す

ユーザのゲーム結果のメッセージを送信するインテントを作成して呼び出せるようにコードを変更します。いくつかのアプリが ACTION_SEND インテントを処理できるため、ユーザには、情報の送信方法を選択できるセレクターが表示されます。

  1. GameWonFragment クラス内で、onCreateView() メソッドの後に、以下に示すように getShareIntent() というプライベートメソッドを作成します。args の値を設定するコード行は、クラスの onCreateView() で使用されるコード行と同じです。
// Creating our Share Intent
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
}
  1. getShareIntent() メソッドの下に、shareSuccess() メソッドを作成します。このメソッドは、getShareIntent() からインテントを取得し、starActivity() を呼び出して sahre を開きます。
// Starting an Activity with our new Intent
private fun shareSuccess() {
   startActivity(getShareIntent())
}
  1. onCreateOptionsMenu() をオーバーライドして、winner_menu を拡張します。
// Showing the Share Menu Item Dynamically
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
       }
}
  1. menu item を処理するには、onOptionsItemSelected() をオーバーライドします。menu item がクリックされた時に、shareSuccess を呼び出します。
// Sharing from the Menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
}
  1. アプリを実行します。

まとめ

Safe Args: - あるフラグメントから別のフラグメントにデータを渡す時に、キーの欠落や方の不一致によって発生するエラーをキャッチするには、Safe Args と呼ばれる Gradle プラグインを使用します。

  • アプリ内のフラグメントごとに、Safe Args プラグインは対応する NavDirection クラスを生成します。NavDirection クラスを Fragment コードに追加し、そのクラスを使用して、Fragmentと他のフラグメント間で引数を渡します。

  • NavDirection クラスは、全てのアプリのアクションからnavigationを表します。

implicit intents: - implicit intent は、アプリが他のアプリに代わって実行するアクションを宣言します。

sharing functionality: - 成功を友達と共有する場合、Intent アクションは Intent.ACTION_SEND になります。 - オプションメニューをフラグメントに追加するには、Fragment コードで setHasOptionsMenu メソッドを true にします。

  • フラグメントコードで、onCreateOptionsMenu() メソッドをオーバーライドしてメニューを inflate します。

  • onOptionsItemSelected() をオーバーライドして startActivity() を使用し、それを処理できる他のアプリにインテントを送信します。

ユーザーがメニュー項目をタップすると、インテントが発生し、SEND アクションのセレクターが表示されます。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com