MVVM Flickr Android app with common dev use cases— Part 1

Yoav Gray
5 min readJan 27, 2020

--

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 a suspend 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 immutable photosLiveData.
  • 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 great sealed 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 a map transformation between the PhotoResponse and the Photo 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

End result of Part 1

That’s it for Part 1. Part 2 is going to be super short and will load the photos into squares. If I missed something important, please leave a comment. Got questions? Follow me on Medium at Yoav Gray or find me on Twitter.

--

--

Yoav Gray

Android Eng @Uber | Previously @Autolist (Acquired by @CarGurus)