Blog Infos
Author
Published
Topics
, ,
Published
Photo by Nastia Petruk on Unsplash
Motivation

I’ve recently gone through several interesting tech interviews and one of the new challenges for me was a multithreading interview. It was a one-hour live-coding session where I had to solve typical Android multithreading issues. Even though I have experience with threads and coroutines, it was still a challenge. The main reason for this was my lack of practical experience in solving such tasks.

This article will be helpful for mid-level and junior developers who haven’t had extensive experience with multithreading or whose tasks have mainly involved simple GET/POST requests in a coroutine launch block (nothing wrong with that!). In this article, I present a more complex example to help you improve your skills and better prepare for similar interviews.

En avant!

Initial Implementation

The entire interview consisted of a series of tasks centered around multithreading and code optimization in a single example. One of the requirements was that all code should be written in a single class and function, so let’s focus on the key concepts. The code can always be refactored later.

Here is the initial code.

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val asyncImageView = findViewById<ImageViewAsync>(R.id.asyncImageView)
asyncImageView.setUrl(
"Some image url"
)
}
}
https://gist.github.com/55f55f44a2d2e0c2c908b319f42c81d

As you can see, this implementation attempts to download an image on the main thread, which is problematic. Afterward, the image is set to the imageView. Not good. The first task was to identify and resolve these issues.

Base Implementation
private val scope = CoroutineScope(Dispatchers.IO)
fun setUrl(url: String) {
scope.launch {
val bitmap = loadBitmap(url)
withContext(Dispatchers.Main) {
setImageBitmap(bitmap)
}
}
}

Initially, I downloaded the image on a background thread and then set the bitmap on the main thread (since the UI can only be updated from the main thread). My first thought was, “Ha, that was much easier than I imagined!” But, of course, it was just the beginning…

What if we want to change the previous image?

The next task was to stop an ongoing download process and start a new one. This can easily be done by canceling the current job.

private var job: Job? = null
fun setUrl(url: String) {
job?.cancel()
job = scope.launch {
// ...
}
}

It’s important to note, that even though we trigger cancellation, it only takes effect within a suspended block. In our case, this happens when we switch contexts using the withContext function, as I mentioned during the interview.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

With the advent of Android 15, edge-to-edge design has become the default configuration. Consequently, applications must be capable of accommodating window insets, including the system status bar and navigation bar, as well as supporting drawing…
Watch Video

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility GmbH

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android develop ...
Seven Principles Mob ...

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility ...

Jobs

Let’s Add Some Cache

Another task was to implement a cache. I chose ConcurrentHashMap because it allows for fast access to entries, and we have unique keys in the form of URLs. Additionally, using ConcurrentHashMap ensures thread safety, which is important in a multithreaded environment. Now, when we attempt to download the same image, it will be retrieved from the cache without needing to re-download it.

One important aspect of the code below is that we double-check and save the bitmap only if the image wasn’t downloaded previously.

val hashMap = ConcurrentHashMap<String, Bitmap?>()
job = scope.launch {
val bitmap = if (hashMap.containsKey(url)) {
hashMap[url]
} else {
val bitmap = loadBitmap(url)
if (!hashMap.containsKey(url)) {
hashMap[url] = bitmap
bitmap
} else {
hashMap[url]
}
}
withContext(Dispatchers.Main) {
setImageBitmap(bitmap)
}
}

It was my original solution, I kept it unchanged. But after a brief discussion with ChatGPT, I realized that there still could be a race condition if multiple threads reach the same point simultaneously. As a better solution, I was recommended to use the following method, which enhances atomicity and reduces the number of lines:

val bitmap = hashMap.computeIfAbsent(url) {
    loadBitmap(url)
}

However, there is still one issue that my interviewer politely brought to my attention: what if two or more requests try to download the same image simultaneously? The answer is that if hashMap is empty, all threads will initiate their own downloading processes, leading to multiple requests for the same image.

Let’s add some cache but now with async

This was the most challenging part for me, but I eventually found a solution. We can use coroutines with the async builder to handle future results. By saving the Deferred value in our cache, we can return it whenever we need the same image.

Now, there’s no need to wait for the image to download each time. We simply retrieve the existing Deferred object and wait until the image becomes available. This approach not only reduces redundant network calls but also improves the overall efficiency of our application.

val hashMap = ConcurrentHashMap<String, Deferred<Bitmap?>>()
//...
val bitmap = async { loadBitmap(url) }
//...
bitmap?.await()?.let { setImageBitmap(it) }
Retry Policies

The next task was to add retry policies. Initially, my mind was overwhelmed by the previous challenge, and I started overthinking the solution. Then my interviewer pointed out, “You are thinking too much,” and at that moment, the simplest approach to adding the retry functionality became clear to me.

var counter = 0
val bitmap = async {
while (counter < 3) {
try {
return@async loadBitmap(url)
} catch (e: Exception) {
e.printStackTrace()
}
counter++
}
null
}
Restricting the Number of Threads

The final question was about how to restrict the number of threads used for these operations. Fortunately, this one was quite straightforward.

private val scope = CoroutineScope(newFixedThreadPoolContext(3, "Pool"))
Conclusion

As you can see, it wasn’t a particularly difficult interview, and I was pleased that I found solutions and implemented them for each of the tasks on my own.

I hope this article has been useful and that some readers have gained new insights. Good luck with your interviews!

P.S. Any suggestions for improvement are always welcome!

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu