Android Kotlin Fundamentals Course 코드랩 하면서 노트. Android Kotlin Fundamentals: 8.1 Getting data from the internet 부터.
dep versions
Just in case.
buildscript {
ext {
// Versions for all the dependencies we plan to use. It's particularly useful for kotlin and
// navigation where the versions of the plugin needs to be the same as the version of the
// library defined in the app Gradle file
version_android_gradle_plugin = '4.0.1'
version_core = "1.3.1"
version_constraint_layout = "1.1.3"
version_glide = "4.8.0"
version_kotlin = "1.3.72"
version_lifecycle = "2.2.0"
version_moshi = "1.8.0"
version_navigation = "1.0.0"
version_retrofit = "2.9.0"
version_recyclerview = "1.0.0"
}
}
Retrofit: A type-safe HTTP client
// build.gradle (project)
buildscript {
ext {
//...
version_retrofit = "2.9.0"
}
}
// build.gradle (module: app)
dependencies {
// ...
implementation "com.squareup.retrofit2:retrofit:$version_retrofit"
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"
}
android {
// ...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
Then, create MarsApiService.kt
.
package com.example.android.marsrealestate.network
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.GET
// private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com/"
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface MarsApiService {
@GET("realestate")
fun getProperties():
Call<String>
}
object MarsApi {
val retrofitService: MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
Call the web service in OverviewViewModel.kt
.
class OverviewViewModel : ViewModel() {
// ...
private fun getMarsRealEstateProperties() {
MarsApi.retrofitService.getProperties().enqueue(
object : Callback<String> {
override fun onFailure(call: Call<String>, t: Throwable) {
_response.value = "Failure: " + t.message
}
override fun onResponse(call: Call<String>, response: Response<String>) {
_response.value = response.body()
}
}
)
}
}
Add INTERNET
permission in app/manifests/AndroidManifest.xml
.
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Moshi: JSON parser library
// build.gradle (project)
buildscript {
ext {
//...
version_moshi = "1.8.0"
}
}
// build.gradle (module: app)
dependencies {
// ...
implementation "com.squareup.moshi:moshi-kotlin:$version_moshi"
// change retrofit2's scalars converter
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
}
Then, sync.
Create the property data class.
// MarsProperty.kt
data class MarsProperty(
val id: String,
// val img_src: String,
// to remap the name
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double
)
Update MarsApiService.kt
with new converter.
/// MarsApiService.kt
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface MarsApiService {
@GET("realestate")
fun getProperties():
Call<List<MarsProperty>>
}
Update the ViewModel.
class OverviewViewModel : ViewModel() {
// ...
private fun getMarsRealEstateProperties() {
MarsApi.retrofitService.getProperties().enqueue(
object : Callback<List<MarsProperty>> {
override fun onFailure(call: Call<List<MarsProperty>>, t: Throwable) {
_response.value = "Failure: " + t.message
}
override fun onResponse(call: Call<List<MarsProperty>>, response: Response<List<MarsProperty>>) {
_response.value =
"Success: ${response.body()?.size} Mars properties retrieved"
}
}
)
}
}
It is a good idea to use coroutines for asynchronous.
// MarsApiService.kt
// Change the function with suspend
interface MarsApiService {
@GET("realestate")
suspend fun getProperties():
List<MarsProperty>
}
Then, replace the ViewModel like the code below using try {}
, catch {}
, and viewModelScope
.
// OverviewViewModel.kt
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getProperties()
_response.value = "Success: ${listResult.size} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
// build.gradle (project)
buildscript {
ext {
//...
version_glide = "4.8.0"
}
}
// build.gradle (module: app)
dependencies {
// ...
implementation "com.github.bumptech.glide:glide:$version_glide"
}
Update the view model.
// OverviewViewModel.kt
private val _property = MutableLiveData<MarsProperty>()
val property: LiveData<MarsProperty>
get() = _property
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getProperties()
if (listResult.size > 0) {
_property.value = listResult[0]
}
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
Add BindingAdapters.kt
.
// BindingAdapters.kt
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide.with(imgView.context)
.load(imgUri)
.into(imgView)
}
}
Update gridview_item.xml
.
<layout ...>
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.property.imgSrcUrl}" />
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
</layout>
Change the layout from FragmentOverviewBinding
to GridViewItemBinding
in the fragment to quick test.
// val binding = FragmentOverviewBinding.inflate(inflater)
val binding = GridViewItemBinding.inflate(inflater)
Add loading and broken image
Add files in res/drawable
. Here, ic_broken_image.xml
and loading_animation.xml
.
Update BindingAdapters.kt
and reflect this file in bindImage()
.
// BindingAdapter.kt
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide.with(imgView.context)
.load(imgUri)
.apply(RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(imgView)
}
}
Update RecyclerView with loaded data
// OverviewViewModel.kt
private val _properties = MutableLiveData<List<MarsProperty>>()
val properties: LiveData<List<MarsProperty>>
get() = _properties
// ...
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
_properties.value = MarsApi.retrofitService.getProperties()
_response.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
Update gridview_item.xml
for the each property.
<layout ...>
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{property.imgSrcUrl}" />
<data>
<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
</data>
</layout>
Change the layout back to FragmentOverviewBinding
in the fragment.
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
Replace <TextView>
to <RecyclerView>
in fragment_overview.xml
.
<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:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
Then, Add the photo grid adapter.
// PhotoGridAdapter.kt
class PhotoGridAdapter : ListAdapter<MarsProperty, PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MarsPropertyViewHolder {
return MarsPropertyViewHolder(GridViewItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: MarsPropertyViewHolder, position: Int) {
val marsProperty = getItem(position)
holder.bind(marsProperty)
}
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
return oldItem.id == newItem.id
}
}
class MarsPropertyViewHolder(private var binding: GridViewItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(marsProperty: MarsProperty) {
binding.property = marsProperty
binding.executePendingBindings()
}
}
}
Update the Binding adapter.
// BindingAdapters.kt
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<MarsProperty>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
Update the fragment.
<!-- fragment_overview.xml -->
<RecyclerView ...
app:listData="@{viewModel.properties}" />
Add the adapter into the fragment.
binding.photosGrid.adapter = PhotoGridAdapter()
// Note: photos_grid is a name of the recycler view
Add error handling in RecyclerView
Add status in the view model.
enum class MarsApiStatus { LOADING, ERROR, DONE }
class OverviewViewModel : ViewModel() {
// ...
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus>
get() = _status
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_properties.value = MarsApi.retrofitService.getProperties()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_properties.value = ArrayList()
}
}
}
}
Update BindingAdapters.kt
.
// BindingAdapters.kt
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, status: MarsApiStatus?) {
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
}
}
}
Then, add the image view for the status.
<!-- fragment_overview.xml -->
<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}" />
다음 챕터: Android Kotlin Fundamentals and detail views with internet data