article banner

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 }