Implement a LocationService class that eagerly observes location updates and exposes them as a flow. It should also expose the last-known location as a function (or null if there are no known locations). No matter how many observers there are, there should be only one location observer from LocationRepository. When new observers are added, they should receive the last known location. This class should not send the same location multiple times in a row. If observer is slower than the location updates, it should receive only the last known location (we are not interested in intermediate locations).
class LocationService(
locationRepository: LocationRepository,
backgroundScope: CoroutineScope,
) {
fun observeLocation(): Flow<Location> = TODO()
fun currentLocation(): Location? = TODO()
}
There are three ways how this problem can be solved:
Using stateIn.
Using shareIn and replayCache.
Using shareIn and a property to store the last known location.
Try to implement all those solutions.
This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in coroutines/flow/LocationService.kt. You can find there starting code and unit tests.
Once you are done with the exercise, you can check your solution here.
Playground
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LocationService(
locationRepository: LocationRepository,
backgroundScope: CoroutineScope,
) {
fun observeLocation(): Flow<Location> = TODO()
fun currentLocation(): Location? = TODO()
}
interface LocationRepository {
fun observeLocation(): Flow<Location>
}
data class Location(val latitude: Double, val longitude: Double)
class LocationObserverTest {
@Test
fun `should allow observing location`() = runTest {
val locationRepository = FakeLocationRepository()
val locationService = LocationService(locationRepository, backgroundScope)
var locations = listOf<Location>()
locationService.observeLocation().onEach {
locations = locations + it
}.launchIn(backgroundScope)
runCurrent()
assertEquals(0, locations.size)
locationRepository.emitLocation(Location(11.1, 22.2))
runCurrent()
assertEquals(1, locations.size)
}
@Test
fun `should reuse the same connection`() = runTest {
val locationRepository = FakeLocationRepository()
val locationService = LocationService(locationRepository, backgroundScope)
locationService.observeLocation().launchIn(backgroundScope)
locationService.observeLocation().launchIn(backgroundScope)
locationService.observeLocation().launchIn(backgroundScope)
runCurrent()
kotlin.test.assertEquals(1, locationRepository.observersCount())
}
@Test
fun `should provide current location`() = runTest {
val locationRepository = FakeLocationRepository()
val locationService = LocationService(locationRepository, backgroundScope)
assertEquals(null, locationService.currentLocation())
runCurrent()
assertEquals(null, locationService.currentLocation())
val l1 = Location(1.1, 2.2)
locationRepository.emitLocation(l1)
runCurrent()
assertEquals(l1, locationService.currentLocation())
val l2 = Location(3.3, 4.4)
locationRepository.emitLocation(l2)
runCurrent()
assertEquals(l2, locationService.currentLocation())
val l3 = Location(5.5, 6.6)
locationRepository.emitLocation(l3)
runCurrent()
assertEquals(l3, locationService.currentLocation())
}
@Test
fun `should conflate location updates`() = runTest {
val locationRepository = FakeLocationRepository()
val locationService = LocationService(locationRepository, backgroundScope)
var locations = listOf<Location>()
locationService.observeLocation().onEach {
delay(1000)
locations = locations + it
}.launchIn(backgroundScope)
runCurrent()
assertEquals(0, locations.size)
repeat(100) {
locationRepository.emitLocation(Location(it.toDouble(), it.toDouble()))
delay(100)
}
assertEquals(
listOf(
Location(latitude = 0.0, longitude = 0.0),
Location(latitude = 9.0, longitude = 9.0),
Location(latitude = 19.0, longitude = 19.0),
Location(latitude = 29.0, longitude = 29.0),
Location(latitude = 39.0, longitude = 39.0),
Location(latitude = 49.0, longitude = 49.0),
Location(latitude = 59.0, longitude = 59.0),
Location(latitude = 69.0, longitude = 69.0),
Location(latitude = 79.0, longitude = 79.0),
Location(latitude = 89.0, longitude = 89.0)
), locations
)
}
@Test
fun `should emit only distinct locations`() = runTest {
val locationRepository = FakeLocationRepository()
val locationService = LocationService(locationRepository, backgroundScope)
var locations = listOf<Location>()
locationService.observeLocation().onEach {
locations = locations + it
}.launchIn(backgroundScope)
runCurrent()
assertEquals(0, locations.size)
locationRepository.emitLocation(Location(5.5, 6.6))
runCurrent()
assertEquals(1, locations.size)
locationRepository.emitLocation(Location(5.5, 6.6))
runCurrent()
assertEquals(1, locations.size)
locationRepository.emitLocation(Location(5.5, 6.6))
runCurrent()
assertEquals(1, locations.size)
}
}
class FakeLocationRepository : LocationRepository {
private val locationFlow = MutableSharedFlow<Location>()
override fun observeLocation(): Flow<Location> = locationFlow
suspend fun emitLocation(location: Location) {
locationFlow.emit(location)
}
fun observersCount(): Int = locationFlow.subscriptionCount.value
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.