Update (April 20, 2020): I updated the ViewModel & Activity interaction according to things I’ve learned in the last few months to demonstrate better practices.
For a while now, I’ve been curious about how to do some of the things I’m going to talk about here and that was the impetus for writing this article series.
Some of the things I’d like to showcase are: perfect squares in a grid layout, search box debounce, views drag and drop, fragments/view flick dismissal, MVVM & with ViewModel, LiveData and Coroutines.
This article is the first article in a series of articles that will add incremental value to an open source project that can be found here:
If you want to see the end result for this article, check https://github.com/yoavgray/flickr-kotlin/tree/part-1
It’s also my first Medium article ever so cut me some slack :)
The series will cover small topics and will add incremental value:
I’ll start by saying that I will try to keep things short for brevity, and sometime skip things I wouldn’t in a production app. For example, I’m not going to use Dagger or invest in DI here but I’m happy to add an article about that if there’s a demand. Also, I’m still learning some of these concepts and would love to get some feedback here.
Part 1: Fetch a list of images from Flickr and show them in a staggered grid
Networking and Data Layer
Let’s set up some of infrastructure to be able to fetch Flickr images from the Flickr API. We’ll use Retrofit & OkHttp3 to fetch the images, and Picasso to draw them in a RecyclerView.
First, Get yourself an API Key from https://www.flickr.com/services/apps/create/apply
Then, add these dependencies:
implementation "com.squareup.retrofit2:retrofit:2.7.1"
implementation "com.squareup.retrofit2:converter-gson:2.7.1"
implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
implementation 'com.squareup.picasso:picasso:2.71828'
The Flickr API uses “methods”, so we’re going to use the flickr.photos.search method to fetch photos of dogs, cause I’m sick of cats (sorry) and because later we’ll extend the feature for dynamic searching so it’s a good starting point. Let’s create a static web client and a Retrofit API:
Also, don’t forget to add the “INTERNET” permission in AndroidManifest.xml
:
<uses-permission android:name=”android.permission.INTERNET” />
Next, Let’s create the ViewModel. Again, usually we would want to inject a repository to the view model that has dependencies on a db and/or a networking service but this is a simplified version of a ViewModel that will use an instance of WebClient.kt
. We wouldn’t be able to mock this networking service for testing so I don’t recommend using it in a production app.
Next, we need to add the data layer data classes for the API response:
// The outermost wrapper for the api response
data class PhotosSearchResponse(
val photos: PhotosMetaData
)
data class PhotosMetaData(
val page: Int,
val photo: List<PhotoResponse>
)
data class PhotoResponse(
val id: String,
val owner: String,
val secret: String,
val server: String,
val farm: Int,
val title: String
)
Domain Layer
The Photo.kt
entity data class:
data class Photo(
val id: String,
val url: String,
val title: String
)
And now let’s look at the ViewModel
that will abstract the business logic from our soon to be very “thin” view controller ( PhotosActivity
)
class PhotosViewModel : ViewModel() {
private val mutablePhotosLiveData = MutableLiveData<List<Photo>>()
val photosLiveData: LiveData<List<Photo>> = mutablePhotosLiveData
init {
viewModelScope.launch {
val searchResponse = WebClient.client.fetchImages()
val photosList = searchResponse.photos.photo.map { photo ->
Photo(
id = photo.id,
url = "https://farm${photo.farm}.staticflickr.com/${photo.server}/${photo.id}_${photo.secret}.jpg",
title = photo.title
)
}
mutablePhotosLiveData.postValue(photosList)
}
}
}
To be able to lazily load the view model in PhotosActivity
we need to import the new activity dependency:
implementation "androidx.activity:activity-ktx:1.1.0"
It will throw an error about java compatibility so add this block to your app module app/build.gradle
file (Thanks for this SO response):
android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
Let’s note a few things here:
- You want to use a
viewModelScope
so that if the instance is destroyed, any coroutine that runs in that scope will be cancelled. (Read more here about Structured Concurrency). We also Must use a coroutine scope here to run asuspend
function. - When the ViewModel is initialized, it will fetch the list of photos, and post that value in the mutable
LiveData
. In the activity, we’ll observe on the public immutablephotosLiveData
. - For brevity, I’m not handling any errors here, but if you want to do something simple, Retrofit throws an
HttpException
if anything goes wrong so we can wrap it with a try catch statement. Iif you want to do something fancy, try using these neat Kotlin extensions. You might want to utilize the greatsealed class
feature and create a result type that will wrap anything you fetch from a db or a server and have a mid-layer where you turn your results to a ResultType subtype and allow the UI to handle a network error, a failure, a 404, success, etc. - Usually I like to use a
toEntity()
method on the response and map each data response object to a domain object with certain conditions and mapping, but here I only added amap
transformation between thePhotoResponse
and thePhoto
objects.
Presentation Layer
For simplicity, we’ll have one activity, that holds a RecyclerView
managed by a GridLayoutManager
. I’m not going to expand on RecyclerView
but if you have any questions, feel free to add a comment.
class PhotosActivity : AppCompatActivity() {
private val photosViewModel: PhotosViewModel by viewModels()
private val photosAdapter = PhotosAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photos)
photosRecyclerView.adapter = photosAdapter
photosRecyclerView.layoutManager = GridLayoutManager(this, 3)
photosViewModel.photosLiveData.observe(this,
Observer { list ->
with(photosAdapter) {
photos.clear()
photos.addAll(list)
notifyDataSetChanged()
}
})
}
}
Few things to note:
- Business logic should be abstracted inside
PhotosViewModel
and the activity should do only presentation related actions. - The nice thing about this simple implementation is that the ViewModel
init
block will execute once in the ViewModel lifecycle, so if there’s a rotation and the activity is recreated, it will not fetch from network again, and the live data will emit its “old” value.
The xml layout for the image view is:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
For the purpose of this article, we should use a FrameLayout
instead of a ConstraintLayout
or even not wrap it at all and inflate it differently, but as you’ll see in the next part, I’m laying the groundwork for what’s coming next.
End Result
The end result is intentionally not pretty. There’s no fixed separation between the images in the grid, the images have different sizes, and in Part 2 will fix that