Android Kotlin基礎講座 08.3:フィルタリングとインターネットデータの詳細ビュー

タスク:詳細ページを作成し、ナビゲーションをセットアップする

現在、火星の物件のアイコンがスクロールするグリッドに表示されています。次はそれぞれの物件の詳細を取得できるようにしましょう。
このタスクでは、指定した物件の詳細を表示するための、詳細フラグメントを追加します。詳細フラグメントは大きな画像、価格、物件のタイプ(貸し出し用か販売用か)を表示します。

このフラグメントはユーザーが概要ページの画像をタップすると開きます。これを実現するためには、onClickリスナーをRecyclerViewに追加し、新規フラグメントにナビゲーションさせる必要があります。
これまでのレッスンで行ってきたように、ViewModel内のLiveDataの変更をトリガーとしてナビゲーションさせます。
またナビゲーションコンポーネントのSafe Argsプラグインを使って、概要フラグメントから選択されたMarsPropertyの情報を詳細フラグメントに渡します。

ステップ1:詳細ビューモデルを作成し、詳細レイアウトをアップデートする

概要ビューモデルとフラグメントに対して行ったプロセスと似て、今回は詳細フラグメント用のビューモデルとレイアウトファイルを実装する必要があります。

  1. detail/DetailViewModel.ktを開いてください。network関連のKotlinファイルはnetworkフォルダーに、概要ファイルはoverviewフォルダーにまとめられているように、detailフォルダーには詳細ビューに関連するファイルが含まれています。DetailViewModelクラス(現在は空)がコンストラクタのパラメーターとしてmarsPropertyを取っていることを確認してください。
class DetailViewModel( marsProperty: MarsProperty,
                     app: Application) : AndroidViewModel(app) {
}
  1. クラス定義の中に、選択された火星物件用のLiveDataを追加してください。これを詳細ビューに公開します。いつも通り、MarsProperty自身を保持するためのMutableLiveDataの作成と、publicなLiveDataを公開するパターンに従ってください。
    要求されたら、androidx.lifecycle.LiveDataとandroidx.lifecycle.MutableLiveDataをインポートしてください。
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
   get() = _selectedProperty
  1. init{}ブロックを作成し、選択された火星物件の値をコンストラクタのMarsPropertyを使ってセットしてください。
    init {
        _selectedProperty.value = marsProperty
    }
  1. res/layout/fragment_detail.xmlを開き、デザインビューで表示してください。

    これは詳細フラグメント用のレイアウトファイルです。これには大きい画像用のImageViewと物件のタイプ用のTextViewと価格用のTextViewが含まれています。ConstraintLayoutがScrollViewで包まれていることを確認してください。これによってビューが表示するには大きすぎる画像を取得した場合に、自動的にスクロールされます。例として、ユーザーがlandscapeモードで表示している場合などがあります。
  2. テキストタブで表示してください。レイアウトのトップ、<ScrollView>要素の直前に、詳細ビューモデルとレイアウトを紐づけるための<data>要素を追加してください。
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>
  1. ImageView要素にapp:imageUrl属性を追加してください。それに選択された物件のビューモデルからのimgSrcUrlを設定してください。

    アダプターがapp:imageUrl属性を見ているので、Glideを使って画像を読み込むバインディングアダプターがここでも自動的に使われます。
 app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"

ステップ2:OverviewViewModelにナビゲーションを定義する

ユーザーがOverviewViewModelの写真をタップすると、タップされた物件に関する詳細を表示するフラグメントへのナビゲーションのトリガーとなります。

  1. overview/OverviewViewModel.ktを開いてください。_navigateToSelectedProperty MutableDataプロパティを追加し、immutableなLiveDataで公開してください。

    このLiveDataがnullでない値に変更されると、ナビゲーションがトリガーされます。(すぐ後でこの変数を監視し、ナビゲーションをトリガーするコードを追加します)
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
   get() = _navigateToSelectedProperty
  1. クラスの最後に、_navigateToSelectedPropertyに選択された物件をセットするメソッド、displayPropertyDetails()を追加してください。
fun displayPropertyDetails(marsProperty: MarsProperty) {
   _navigateToSelectedProperty.value = marsProperty
}
  1. _navigateToSelectedPropertyの値をnullにするメソッド、displayPropertyDetailsComplete()を追加してください。これはナビゲーションが完了した状態であることを記録し、ユーザーが詳細ビューから戻ったときに再び画面遷移が起こってしまわないようにするために必要です。
fun displayPropertyDetailsComplete() {
   _navigateToSelectedProperty.value = null
}

ステップ3:GridAdapterとフラグメントにクリックリスナーをセットアップする

  1. overview/PhotoGridAdapter.ktを開いてください。クラスの最後に、marsPropertyパラメーターを使ったラムダ式をとるOnClickListenerクラスを作成してください。このクラスの中で、ラムダのパラメーターにセットされるonClick()関数を定義してください。
class OnClickListener(val clickListener: (marsProperty:MarsProperty) -> Unit) {
     fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty)
}
  1. PhotoGridAdapterのクラス定義の部分までスクロールして戻ってください。privateなOnClickListenerプロパティをコンストラクタに追加してください。
class PhotoGridAdapter( private val onClickListener: OnClickListener ) :
       ListAdapter<MarsProperty,              
           PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
  1. onBindviewHolder()メソッド内で、グリッドアイテムにonClickListenerを追加して、写真をクリックできるようにしてください。getItem()とbind()の間で、クリックリスナーを定義してください。
override fun onBindViewHolder(holder: MarsPropertyViewHolder, position: Int) {
   val marsProperty = getItem(position)
   holder.itemView.setOnClickListener {
       onClickListener.onClick(marsProperty)
   }
   holder.bind(marsProperty)
}
  1. overview/OveriewFragment.ktを開いてください。onCreateView()メソッド内、binding.photoGrid.adapterプロパティを初期化しているコードを以下のコードで置き換えてください。

    このコードはPhotoGridAdapterのコンストラクタにPhotoGridAdapter.onClickListenerオブジェクトを追加しています。そして渡されたMarsPropertyオブジェクトを使ってviewModel.displayPropertyDetails()を呼び出しています。
    これによってナビゲーション用のビューモデル内のLiveDataがトリガーされます。
binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
   viewModel.displayPropertyDetails(it)
})

ステップ4ナビゲーショングラフを修正し、MarsPropertyをParcelableにする

ユーザーが概要ページのグリッドの写真をタップした時に、アプリは詳細フラグメントに遷移し、詳細ビューが情報を表示できるように、選択された物件の詳細を渡すべきです。

現在タップを処理するためのPhotoGridAdapterのクリックリスナーと、ビューモデル内にナビゲーションをトリガーする手段を既に持っています。しかし詳細フラグメントにMarsPropertyオブジェクトが渡されるようにはなっていません。そのためにはナビゲーションコンポーネントのSafe Argsを利用します。

  1. res/navigation/nav_graph.xmlを開いてください。Textタブをクリックし、ナビゲーショングラフのXMLコードを表示してください。
  2. 詳細フラグメント用の<fragment>属性の中に、以下のように<argument>要素を追加してください。
    selectedPropertyというこの属性は、MarsPropertyタイプを持っています。
<argument
   android:name="selectedProperty"
   app:argType="com.example.android.marsrealestate.network.MarsProperty"
   />
  1. アプリをコンパイルしてください。MarsPropertyがParcelableでないため、エラーが発生します。
    Parcelableインターフェースはオブジェクトをシリアライズすることを可能にします。そうすることでオブジェクトのデータがフラグメントやアクティビティ間で受け渡しできるようになります。
    今回の場合は、Safe Argsによって詳細フラグメントにMarsPropetyオブジェクト内のデータが渡されるようにするために、MarsPropetyはParcelableインターフェースを実装する必要があります。幸いなことにKotlinではこのインターフェースを実装するための簡単なショートカットが用意されています。
  2. network/MarsProperty.ktを開いてください。このクラス定義に@Parcelizeアノテーションを追加してください。
    要求されたら、kotlinx.android.parcel.Parcelizeをインポートしてください。

    @ParcelizeアノテーションはこのクラスにParcelableインターフェース内のメソッドを自動的に実装するために、KotlinのAndroid拡張を使っています。これ以外に自身ですることは何もありません。
@Parcelize
data class MarsProperty (
  1. MarsPropetyのクラス定義を変更してParcelableを継承させてください。
    要求されたら、android.os.Parcelableをインポートしてください。

    最終的なMarsPropertyクラスは以下のようになります。
@Parcelize
data class MarsProperty (
       val id: String,
       @Json(name = "img_src") val imgSrcUrl: String,
       val type: String,
       val price: Double) : Parcelable {

ステップ5:フラグメントをつなげる

まだナビゲーションはされません。実際の画面遷移はフラグメントで起こります。このステップでは、概要フラグメントと詳細フラグメント間にナビゲーションを実装するための最終作業を行います。

  1. overview/OverviewFragment.ktを開いてください。onCreateView()内、photoGrid.adaterの初期化文の下に、overviewViewModelのnavigatedToSelectedPropertyを監視するための以下のコードを追加してください。

    要求されたら、androidx.lifecycle.Observerとandroidx.navigation.fragment.findNavControllerをインポートしてください。

    オブザーバーはMarsPropety(ラムダ式内のit)がnullかどうかをテストし、nullである場合はfindNavController()を使ってフラグメントからナビゲーションコントローラーを取得します。
    ビューモデルにLiveDataをnullにリセットさせることを伝えるためにdisplayPropertyDetailsComplete()を呼び出してください。これでアプリがOverviewFragmentに戻った場合に、画面が再び遷移してしまわないようになります。
viewModel.navigateToSelectedProperty.observe(this, Observer {
   if ( null != it ) {   
      this.findNavController().navigate(
              OverviewFragmentDirections.actionShowDetail(it))             
      viewModel.displayPropertyDetailsComplete()
   }
})
  1. detail/DetailFragment.ktを開いてください。onCreateView()メソッド内でbinding.lifecycleOwnerを設定している行のすぐ下に以下のコードを追加してください。このコードはSafe Argsから選択されたMarsPropetyオブジェクトを取得します。

    Kotlinのnot-nullアサーション演算子(!!)が使われていることを確認してください。selectedPropertyがない場合、何か予期せぬことが起こっており、nullポインターを投げさせることになります。(本番コードでは何らかの方法でそのエラーを処理する必要があります)
 val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty
  1. 次に以下のコードを追加してください。これはDetailViewModelFactoryを取得するためのものです。DetailViewModelのインスタンスを取得するためにDetailViewModelFactoryを使います。スターターアプリにはDetailViewModelFactoryの実装が含まれているので、ここでしなければならないのは初期化のみです。
val viewModelFactory = DetailViewModelFactory(marsProperty, application)
  1. 最後に、ファクトリーからDetailViewModelを取得し、全てのパーツをつなげるためのコードを追加してください。
      binding.viewModel = ViewModelProvider(
                this, viewModelFactory).get(DetailViewModel::class.java)
  1. コンパイルしてアプリを起動してください。どれでもいいので、物件の写真をタップしてください。その物件用の詳細フラグメントが表示されます。戻るボタンを押して概要ページに戻ってください。詳細画面がまだまばらであることを確認してください。
    次のタスクで詳細ページに物件データを追加します。