Skip to content

Commit

Permalink
Replaced Channel with Flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
syrop committed Aug 8, 2019
1 parent 5f9245a commit 2512790
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 88 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.3.41'
ext.kotlin_version = '1.3.50-eap-54'

repositories {
google()
Expand Down
14 changes: 7 additions & 7 deletions compass/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,24 @@ dependencies {

// Kotlin, Kodein
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.kodein.di:kodein-di-generic-jvm:6.2.1'
implementation 'org.kodein.di:kodein-di-conf-jvm:6.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC'
implementation 'org.kodein.di:kodein-di-generic-jvm:6.3.3'
implementation 'org.kodein.di:kodein-di-conf-jvm:6.3.3'

// Jetpack, AndroidX
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha02'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha02'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha02'
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0-alpha06'
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0-alpha06'
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0-beta02'
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0-beta02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'com.google.android.material:material:1.1.0-alpha08'
implementation 'com.google.android.material:material:1.1.0-alpha09'

// Play Services, Firebase
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.firebase:firebase-core:17.0.0'
implementation 'com.google.firebase:firebase-core:17.0.1'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'

// Testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,28 @@ package pl.org.seva.compass.compass
import androidx.lifecycle.*
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.map
import pl.org.seva.compass.location.LocationChannelFactory
import kotlinx.coroutines.flow.map
import pl.org.seva.compass.location.DestinationModel
import pl.org.seva.compass.location.LocationFlowFactory
import pl.org.seva.compass.location.toDirection
import pl.org.seva.compass.main.channelLiveData
import pl.org.seva.compass.main.flowLiveData
import pl.org.seva.compass.rotation.RotationChannelFactory
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.*

class CompassViewModel(
rotationChannelFactory: RotationChannelFactory,
locationChannelFactory: LocationChannelFactory,
locationFlowFactory: LocationFlowFactory,
liveDataContext: CoroutineContext = EmptyCoroutineContext) : ViewModel() {

private val mutableDestination by lazy { MutableLiveData<DestinationModel?>() }
@ExperimentalCoroutinesApi
val rotation by channelLiveData(liveDataContext) { rotationChannelFactory.getRotationChannel() }

@ExperimentalCoroutinesApi
val direction by channelLiveData(liveDataContext) {
locationChannelFactory.getLocationChannel().map { location ->
withContext(Dispatchers.Default) {
val distance = distance(location)
val bearing = bearing(location)
DirectionModel(distance.await(), bearing.await())
}
}
val direction by flowLiveData(liveDataContext) {
locationFlowFactory.getLocationFlow().map { it.toDirection(destinationLocation) }
}
val destination get() = mutableDestination as LiveData<DestinationModel?>

Expand All @@ -59,28 +55,4 @@ class CompassViewModel(
}
mutableDestination.value = destination
}

private fun CoroutineScope.distance(location: LatLng) = async {
val dLat = Math.toRadians(destinationLocation.latitude - location.latitude)
val dLon = Math.toRadians(destinationLocation.longitude - location.longitude)
val radLatLoc = Math.toRadians(location.latitude)
val radLatDest = Math.toRadians(destinationLocation.latitude)
val a = sin(dLat / 2).pow(2) +
sin(dLon / 2) * sin(dLon / 2) * cos(radLatLoc) * cos(radLatDest)
val c = 2 * asin(sqrt(a))
RADIUS_KM * c
}

private fun CoroutineScope.bearing(location: LatLng) = async {
val longDiff = destinationLocation.longitude - location.longitude
val y = sin(longDiff) * cos(destinationLocation.latitude)
val x = cos(location.latitude) *
sin(destinationLocation.latitude) - sin(location.latitude) *
cos(destinationLocation.latitude) * cos(longDiff)
((Math.toDegrees(atan2(y, x)) + 360 ) % 360).toFloat()
}

companion object {
const val RADIUS_KM = 6371.0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* If you like this program, consider donating bitcoin: bc1qncxh5xs6erq6w4qz3a7xl7f50agrgn3w58dsfp
*/

package pl.org.seva.compass.compass
package pl.org.seva.compass.location

import com.google.android.gms.maps.model.LatLng

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import androidx.navigation.navGraphViewModels
import kotlinx.android.synthetic.main.fr_destination_picker.*
import pl.org.seva.compass.R
import pl.org.seva.compass.compass.CompassViewModel
import pl.org.seva.compass.compass.DestinationModel
import pl.org.seva.compass.main.extension.*
import pl.org.seva.compass.main.init.KodeinVMFactory

Expand Down
52 changes: 52 additions & 0 deletions compass/src/main/java/pl/org/seva/compass/location/Direction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2019 Wiktor Nizio
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If you like this program, consider donating bitcoin: bc1qncxh5xs6erq6w4qz3a7xl7f50agrgn3w58dsfp
*/

package pl.org.seva.compass.location

import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.*
import kotlin.math.*

suspend fun LatLng.toDirection(destination: LatLng) = withContext(Dispatchers.Default) {
val distance = distance(this@toDirection, destination)
val bearing = bearing(this@toDirection, destination)
DirectionModel(distance.await(), bearing.await())
}

private fun CoroutineScope.distance(location: LatLng, destination: LatLng) = async {
val dLat = Math.toRadians(destination.latitude - location.latitude)
val dLon = Math.toRadians(destination.longitude - location.longitude)
val radLatLoc = Math.toRadians(location.latitude)
val radLatDest = Math.toRadians(destination.latitude)
val a = sin(dLat / 2).pow(2) +
sin(dLon / 2) * sin(dLon / 2) * cos(radLatLoc) * cos(radLatDest)
val c = 2 * asin(sqrt(a))
RADIUS_KM * c
}

private fun CoroutineScope.bearing(location: LatLng, destination: LatLng) = async {
val longDiff = destination.longitude - location.longitude
val y = sin(longDiff) * cos(destination.latitude)
val x = cos(location.latitude) *
sin(destination.latitude) - sin(location.latitude) *
cos(destination.latitude) * cos(longDiff)
((Math.toDegrees(atan2(y, x)) + 360 ) % 360).toFloat()
}

private const val RADIUS_KM = 6371.0
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
* If you like this program, consider donating bitcoin: bc1qncxh5xs6erq6w4qz3a7xl7f50agrgn3w58dsfp
*/

package pl.org.seva.compass.compass
package pl.org.seva.compass.location

data class DirectionModel(val distance: Double, val degrees: Float)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.LatLng
import pl.org.seva.compass.compass.DestinationModel
import pl.org.seva.compass.main.init.instance
import java.lang.Exception

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

open class LocationChannelFactory(ctx: Context) {
open class LocationFlowFactory(ctx: Context) {

private val client = LocationServices.getFusedLocationProviderClient(ctx)
private val request = LocationRequest.create().apply {
Expand All @@ -47,22 +46,21 @@ open class LocationChannelFactory(ctx: Context) {
private var lastLocation: LatLng? = null

@ExperimentalCoroutinesApi
open fun getLocationChannel(): ReceiveChannel<LatLng> =
Channel<LatLng>(Channel.CONFLATED).also { channel ->
val lastLocationJob = GlobalScope.launch {
lastLocation = getLastLocation()?.also { channel.sendBlocking(it) }
}
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
lastLocationJob.cancel()
lastLocation = result.lastLocation.toLatLng().also { channel.sendBlocking(it) }
}
open fun getLocationFlow() = callbackFlow {
val lastLocationJob = GlobalScope.launch {
lastLocation = getLastLocation()?.also { offer(it) }
}
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
lastLocationJob.cancel()
lastLocation = result.lastLocation.toLatLng().also { offer(it) }
}

client.requestLocationUpdates(request, callback, Looper.myLooper())
channel.invokeOnClose { client.removeLocationUpdates(callback) }
}

client.requestLocationUpdates(request, callback, Looper.myLooper())
awaitClose { client.removeLocationUpdates(callback) }
}

private suspend fun getLastLocation(): LatLng? = lastLocation ?:
suspendCancellableCoroutine { continuation ->
client.lastLocation.addOnSuccessListener {
Expand Down
11 changes: 11 additions & 0 deletions compass/src/main/java/pl/org/seva/compass/main/Coroutine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ package pl.org.seva.compass.main
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

Expand All @@ -40,3 +42,12 @@ fun <T> channelLiveData(
}
}
}

fun <T> flowLiveData(
context: CoroutineContext = EmptyCoroutineContext,
block: () -> Flow<T>): Lazy<LiveData<T>> = lazy {
liveData(context) {
val flow = block()
flow.collect { emit(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
import pl.org.seva.compass.compass.CompassViewModel
import pl.org.seva.compass.location.LocationChannelFactory
import pl.org.seva.compass.location.LocationFlowFactory
import pl.org.seva.compass.main.Permissions
import pl.org.seva.compass.rotation.RotationChannelFactory
import java.util.*
Expand All @@ -45,7 +45,7 @@ class KodeinModuleBuilder(private val ctx: Context) {
bind<Bootstrap>() with singleton { Bootstrap() }
bind<Geocoder>() with singleton { Geocoder(ctx, Locale.getDefault()) }
bind<Permissions>() with singleton { Permissions() }
bind<LocationChannelFactory>() with singleton { LocationChannelFactory(ctx) }
bind<LocationFlowFactory>() with singleton { LocationFlowFactory(ctx) }
bind<RotationChannelFactory>() with singleton { RotationChannelFactory(ctx) }
bind<CompassViewModel>() with provider { CompassViewModel(instance(), instance()) }
}
Expand Down
46 changes: 25 additions & 21 deletions compass/src/test/java/pl/org/seva/compass/CompassViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.lifecycle.Observer
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.*
Expand All @@ -32,9 +33,9 @@ import org.mockito.Mockito.`when`

import org.mockito.Mockito.mock
import pl.org.seva.compass.compass.CompassViewModel
import pl.org.seva.compass.compass.DestinationModel
import pl.org.seva.compass.compass.DirectionModel
import pl.org.seva.compass.location.LocationChannelFactory
import pl.org.seva.compass.location.DestinationModel
import pl.org.seva.compass.location.DirectionModel
import pl.org.seva.compass.location.LocationFlowFactory
import pl.org.seva.compass.rotation.RotationChannelFactory

@ObsoleteCoroutinesApi
Expand Down Expand Up @@ -69,30 +70,35 @@ class CompassViewModelTest {
lastDistance = distance
}

val mockLocationChannelFactory: LocationChannelFactory = mock(LocationChannelFactory::class.java)
val mockLocationFlowFactory: LocationFlowFactory = mock(LocationFlowFactory::class.java)
val mockRotationChannelFactory: RotationChannelFactory = mock(RotationChannelFactory::class.java)

val locationChannel = Channel<LatLng>(Channel.CONFLATED)
var locationClosed = false
val locationFlow = channelFlow {
offer(HOME)
var lat = HOME.latitude
try {
while (true) {
delay(INTERVAL)
lat += LATITUDE_STEP
offer(LatLng(lat, HOME.longitude))
}
}
finally {
locationClosed = true
}
}.flowOn(Dispatchers.IO)

val rotationChannel = Channel<Float>(Channel.CONFLATED)

`when`(mockLocationChannelFactory.getLocationChannel()).thenReturn(locationChannel)
`when`(mockLocationFlowFactory.getLocationFlow()).thenReturn(locationFlow)
`when`(mockRotationChannelFactory.getRotationChannel()).thenReturn(rotationChannel)

launch(Dispatchers.IO) {
locationChannel.send(HOME)
var lat = HOME.latitude
while (true) {
delay(INTERVAL)
lat += LATITUDE_STEP
locationChannel.send(LatLng(lat, HOME.longitude))
}
}

val liveDataJob = Job()

val vm = CompassViewModel(
mockRotationChannelFactory,
mockLocationChannelFactory,
mockLocationFlowFactory,
coroutineContext + liveDataJob)
vm.setDestination(DestinationModel(HOME, ""))

Expand All @@ -105,12 +111,10 @@ class CompassViewModelTest {
delay(STABILIZE_DELAY)
progress()
progress()
assertFalse(locationChannel.isClosedForReceive)
assertFalse(locationChannel.isClosedForSend)
assertFalse(locationClosed)
liveDataJob.cancel()
delay(STABILIZE_DELAY)
assertTrue(locationChannel.isClosedForReceive)
assertTrue(locationChannel.isClosedForSend)
assertTrue(locationClosed)
}

companion object {
Expand Down

0 comments on commit 2512790

Please sign in to comment.