概要
Retrofitによりインターネット接続
指定のURLよりJSONを取得しmoshiにてデータの中のidとurlをリスト化して取得
取得したリストのurlをフラグメント中のリサイクラービューのバインドデータにセット
カスタムバインディングアダプタを作成し取得したリストのurlをリサイクラービューへ渡す
リサイクラービューはListAdapterを使用
ListAdapterのonBindViewHolderでスクロールした位置のURL文字列をxmlレイアウトのバインディングデータにセット
カスタムバインディングアダプタを作成しurl文字列をインプットしてImageViewを出力
画像のダウンロードと表示はCoilライブラリを利用
環境設定
Retrofitを追加する
合わせてライブラリで必要になるJava8言語機能を追加する
Projectのbuild.gradleのリポジトリ確認
googleとjcenterがあること
buildscript {
….
repositories {
google()
jcenter()
}
….
}
appのbuild.gradleにライブラリを追加しJava8機能のサポート確認
android {
…
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = ‘1.8’
}
}
…
dependencies {
// Retrofit
implementation “com.squareup.retrofit2:retrofit:2.9.0”
implementation “com.squareup.retrofit2:converter-scalars:2.9.0”
}
Retrofit,moshi,Coilを追加
合わせてライブラリで必要になるJava8言語機能を追加する
Projectのbuild.gradleのリポジトリ確認
googleとjcenterがあること
buildscript {
….
repositories {
google()
jcenter()
mavenCentral()
}
….
}
appのbuild.gradleにライブラリを追加
合わせてJava8機能のサポート確認
plugins {
id ‘com.android.application’
id ‘kotlin-android’
id ‘kotlin-kapt’
}
android {
…
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
権限の追加
manifests/AndroidManifest.xmlの タグの直前に以下を追加
<uses-permission android:name=”android.permission.INTERNET” />
サンプル
レイアウトxml
画面遷移なし
1つのアクティビティに1つのフラグメントのみ
リサイクラービューのレイアウトはイメージビュー1つのみ。grid_view_item.xmlに記載。
ViewModelのMutableLiveDataを用いる。
フラグメント上のRecyclerViewとImageViewはBindingAdapters.ktにて定義したカスタムバインディングアダプタを使用
app:listData→@BindingAdapter(“listData”)
app:marsApiStatus→@BindingAdapter(“marsApiStatus”)
app:imageUrl→@BindingAdapter(“imageUrl”)
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/overviewFragment"
android:name="com.example.android.marsphotos.overview.OverviewFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_overview" />
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.marsphotos.overview.OverViewFragment">
<!--
app:listData要素は、カスタムバインディングアダプタ
BindingAdapters.ktで定義した
@BindingAdapter("listData")の関数(bindRecyclerView)
から戻されるイメージデータ
入力する値はviewModelのLiveData
-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:listData="@{viewModel.photos}"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
<!--
app:marsApiStatus要素は、、カスタムバインディングアダプタ
BindingAdapters.ktで定義した
@BindingAdapter("listData")の関数(bindRecyclerView)
から戻されるイメージデータ
入力する値はviewModelのLiveData
-->
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
<ImageView
android:id="@+id/mars_image"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:padding="2dp"
app:imageUrl="@{photo.imgSrcUrl}"
tools:src="@tools:sample/backgrounds/scenic"/>
</layout>
カスタムバインディングアダプタ
MainActivity.ktと同じフォルダにBindingAdapterアノテーションと関数だけのファイルを作成し、カスタムバインディングアダプターを定義する。
書式は、@BindingAdapter(カスタムバインディングアダプタ名)
fun 任意の関数名(
アウトプットオブジェクトの変数名 : xmlレイアウトオブジェクトの型,
インプットデータ : xmlレイアウトの右辺のデータ型 ){
処理内容
}
関数内のアウトプットオブジェクトはxmlレイアウトのビューオブジェクト
インプットデータはxmlレイアウトの右辺で指定した値
処理内容はCoilで画像を取得したり、オブジェクトの表示・非表示の切替たり、RecyclerViewへのデータ受渡など様々。
import android.view.View
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.example.android.marsphotos.network.MarsPhoto
import com.example.android.marsphotos.overview.MarsApiStatus
import com.example.android.marsphotos.overview.PhotoGridAdapter
/**
* カスタムバインディングアダプタの作成
* MainActivity.ktと同じフォルダにアノテーションと関数だけの
* ファイルを作成しカスタムバインディングアダプターを定義する。
*
* 書式は、@BindingAdapter(カスタムバインディングアダプタ名)
* fun 任意の関数名(
* アウトプットオブジェクトの変数名 : xmlレイアウトオブジェクトの型,
* インプットデータ : xmlレイアウトの右辺のデータ型 ){
* 処理内容
* }
*/
// grid_view_item.xmlで定義したapp:imageUrlのバインディングアダプタ
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
// URLが存在する場合は画像を取得しImageViewに表示する
imgUrl?.let {
// URL文字列をUriに変換しその上にスキームhttpsを付加
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
// Coilで画像を取得してImageViewにセットする(たったこれだけ!)
imgView.load(imgUri){
placeholder(R.drawable.loading_animation) // 処理中の画像
error(R.drawable.ic_broken_image) // エラー時の画像
}
}
}
// fragment_overview.xmlで定義したapp:listDataのバインディングアダプタ
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
// 出力するrecyclerViewのadapterをListAdapter型の
// PhotoGridAdapterにキャストし変数に代入
// ※フラグメント側で binding.photosGrid.adapter = PhotoGridAdapter()
// としているので、ここでもadapterの型をPhotoGridAdapterとしておく
val adapter = recyclerView.adapter as PhotoGridAdapter
// RecycleViewに表示するListデータ(data)を登録
adapter.submitList(data)
}
// fragment_overview.xmlで定義したapp:marsApiStatusのバインディングアダプタ
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
// statusの値に応じてViewを表示・非表示を切替しイメージを表示する
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
}
}
アクティビティ
レイアウトxmlで指定したフラグメントを起動するだけ
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
フラグメント
ビューモデルとしてOverviewViewModelを定義
フラグメント側はデータバインディングを設定
RecyclerViewのadapterを別途作成したListAdapter型のPhotoGridAdapterとする。
※RecyclerViewの別例はRecyclerView.Adapter型を使用した
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.marsphotos.databinding.FragmentOverviewBinding
import com.example.android.marsphotos.databinding.GridViewItemBinding
class OverviewFragment : Fragment() {
// ビューモデル OverviewViewModelを定義
private val viewModel: OverviewViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// fragment_overview.xmlにデータバインディングを設定
val binding = FragmentOverviewBinding.inflate(inflater)
// lifecycleOwnerはフラグメントとする
binding.lifecycleOwner = this
// xmlレイアウトの<data>タグ部分との紐づけ
// バインディングデータの設定
binding.viewModel = viewModel
// リサイクラービューの@+id/photos_grid"に対して
// adapterをPhotoGridAdapterに設定する
binding.photosGrid.adapter = PhotoGridAdapter()
return binding.root
}
}
ビューモデル
データバインディング用のMutableLiveDataを設定
init処理にてインターネットからJSONデータを取得する
データの取得はMarsApiService.ktに定義したMarsApiオブジェクト(retrofit)を使う。
import android.util.Log
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.load
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch
import com.example.android.marsphotos.network.MarsPhoto
// 定数を定義したenumクラス
enum class MarsApiStatus { LOADING, ERROR, DONE }
class OverviewViewModel : ViewModel() {
// データバインディングのMutableLiveData変数を定義
// ステータスはenumで定義した定数の型とする
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
private val _photos = MutableLiveData<List<MarsPhoto>>()
val photos: LiveData<List<MarsPhoto>> = _photos
init {
// データ取得
getMarsPhotos()
}
// JSONデータ取得メソッド
private fun getMarsPhotos() {
// launchによりコルーチン(子スレッド)を起動してデータを取得する
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
// MarsApiオブジェクト(retrofitService)を
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
// エラー処理
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
}
}
}
ライブラリ関連 (retrofit,moshi)
ライブラリ関連で使用するオブジェクトをまとめたktファイルを作成(MarsApiService.kt)
moshiの設定とretrofitの設定をそれぞれ作成。
retrofitの設定はデータ変換にmoshiを利用するためMoshiConverterFactoryを使用する。
retrofitのオブジェクトは、interfaceクラス(MarsApiService)を指定して作成。この時「by lazy」で遅延初期化する(実際に利用される時に初期化)
interfaceクラスはURLのエンドポイントの指定とGETなどのリクエスト方法を定義。
合わせてinterfaceクラス内に任意の名前の関数を定義し取得するデータの型を指定。
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
// ベースアドレス
private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com"
// Moshiの設定
// (項目名変換のMoshiアノテーションを動作させるためKotlinJsonAdapterFactoryを追加)
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
// Retrofitの設定
private val retrofit = Retrofit.Builder()
// ConverterFactoryにMoshiConverterFactoryを指定
// Converterのcreate引数はMoshiビルダーのインスタンスをセット
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
// interface
interface MarsApiService {
// getリクエストを実行。ベースURLに続いての「photos」がurlになる
@GET("photos")
// 任意の名前の関数を定義し取得するデータの型を指定する
// MoshiコンバータによりJsonデータをMarsPhoto型のリストとして取得
// suspendとする事でコルーチンからの呼出しが可能
suspend fun getPhotos(): List<MarsPhoto>
}
// retrofitのオブジェクトを作成
// ktファイルのロード時は「by lazy」で遅延初期化(実際に利用される時に初期化)
// シングルトンオブジェクトとして1つのみインスタンス化される
object MarsApi {
val retrofitService : MarsApiService by lazy {
// Retrofitビルダーのcreateメソッドに
// MarsApiServiceインターフェイスを渡す
retrofit.create(MarsApiService::class.java)
}
}
データクラス
moshiのデータ取得で使用する項目名を定義する。
基本はJSONの項目名と同じ名前の変数名とする
ただし、JSON側の項目名がキャメルケースでない場合は@Jsonにより別名で紐づけを定義(KotlinJsonAdapterFactoryでビルドする必要あり)
import com.squareup.moshi.Json
/*
JSONのサンプルデータ
[{"id":"424905","img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"},
{"id":"424906","img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"},
*/
data class MarsPhoto(
// 基本はJSONの項目名と同じ名前の変数名とする。
// ただしJSON側の項目名がキャメルケースでない場合は@Jsonにより別名で紐づけを定義する
val id: String,
@Json(name = "img_src") val imgSrcUrl: String
)
リストアダプタ
リサイクラービューのアダプタとしてListAdapterクラスを利用
書式は
class クラス名 : ListAdapter(C: DiffUtils.ItemCallback)
A:Object は表示するデータ
(バインドデータよりカスタムバインディングアダプタで設定済み → adapter.submitList(data))
B:クラス内でViewHolderを定義しそのクラス名をセット
ViewHolderは2つの抽象メソッドでも使用する
C:DiffUtils.ItemCallback は A:Objectの差分確認方法を実装したItemCallback
クラス内にcompanion objectを作成しDiffUtil.ItemCallbackを作成
2つの抽象メソッドが必要
ListAdapterクラス内には、前述のViewHolderとDiffUtil.ItemCallbackオブジェクトの他にonCreateViewHolderとonBindViewHolderを実装する。
onCreateViewHolder→ViewHolderをリターン
onBindViewHolder→画面スクロール時に現在のposのデータをViewHolderに渡す
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.android.marsphotos.databinding.GridViewItemBinding
import com.example.android.marsphotos.network.MarsPhoto
/**
* リサイクラービューのアダプタ ListAdapter
* ListAdapterはLiveDataの変更されたレコードを読取しバインドデータを戻す
*
* RecyclerViewの例はRecyclerView.Adapter型だったけれど
* この例題はRecyclerView.Adapter型の派生クラスのListAdapterを使用
*
* 書式は
* class クラス名 : ListAdapter<A: Object, B: RecyclerView.ViewHolder>(C: DiffUtils.ItemCallback<A>)
* - A:Object は表示するデータ
* (バインドデータよりカスタムバインディングアダプタで設定済み → adapter.submitList(data))
* - B:クラス内でViewHolderを定義しそのクラス名をセット
* ViewHolderは2つの抽象メソッドでも使用する
* - C:DiffUtils.ItemCallback<A> は A:Objectの差分確認方法を実装したItemCallback
* クラス内にcompanion objectを作成しDiffUtil.ItemCallbackを作成
* 2つの抽象メソッドが必要
*
* ListAdapterクラス内には、前述のViewHolderとDiffUtil.ItemCallbackオブジェクトの他に
* onCreateViewHolderとonBindViewHolderを実装する。
* onCreateViewHolder→ViewHolderをリターン
* onBindViewHolder→画面スクロール時に現在のposのデータをViewHolderに渡す
*/
class PhotoGridAdapter : ListAdapter<
MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder
>(DiffCallback) {
// 2つの要素を比較するObject
// ListAdapterのコンストラクタにて必要
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
}
// ビューホルダの定義 型はRecyclerView.ViewHolder(binding.root)
// 引数はレイアウトファイル(grid_view_item.xml)のバインディングデータ
// ListAdapterのコンストラクタにて必要
class MarsPhotoViewHolder(
private var binding: GridViewItemBinding
) : RecyclerView.ViewHolder(binding.root) {
// 後述のonBindViewHolderよりコールされる
// ViewModelのinit処理で取得したLiveDataからカレントデータを受取り、
// ビュー側(binding)にidとurlの入った文字列データをセットする
fun bind(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
// bind関数が呼ばれた時点で画面反映させる
binding.executePendingBindings()
}
}
// 抽象メソッド1
// 前述したビューホルダ(MarsPhotoViewHolder)をCreateして戻す
override fun onCreateViewHolder(
parent: ViewGroup, // ViewGroup(リサイクラービューのレイアウトxmlのコンテキスト)
viewType: Int // 異なるビュータイプがある時に利用
): PhotoGridAdapter.MarsPhotoViewHolder {
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
}
// 抽象メソッド2
// 画面スクロール時に現在のposのデータを受渡する
override fun onBindViewHolder(
holder: PhotoGridAdapter.MarsPhotoViewHolder,
position: Int) {
// 現在のposのデータを取得
val marsPhoto = getItem(position)
// 取得データをMarsPhotoViewHolderのbindメソッドに渡す
holder.bind(marsPhoto)
}
}
アダプタにRecyclerView.Adapterを使ったものはRecyclerView (RecyclerView.Adapter)を参照