Exercise: LocationService
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
}