Listeners for NavigationUI, set on NavigationViewOptions not working - mapbox-android

I'm using mapbox-android-navigation and navigation-ui, version 0.11.1, to display turn-by-turn for routes I'm creating. Everything is working great, except I'm not getting callbacks from the navigation UI.
Specifically, I've set listeners on my NavigationViewOptions object as directed. But the listeners never get called back. Moreover, it appears like the listeners are being ignored if you follow the code into NavigationLauncher#startNavigation
Here is my code to launch turn-by-turn:
private fun launchTurnByTurn() {
val navigationListener = object: NavigationListener {
override fun onNavigationFinished() = Timber.i("onNavigationFinished()")
override fun onNavigationRunning() = Timber.i("onNavigationRunning()")
override fun onCancelNavigation() = Timber.i("onCancelNavigation()")
}
val routeListener = object: RouteListener {
override fun allowRerouteFrom(offRoutePoint: Point?): Boolean {
Timber.i("allowRerouteFrom()")
return true
}
override fun onFailedReroute(errorMessage: String?) = Timber.i("onFailedReroute()")
override fun onRerouteAlong(directionsRoute: DirectionsRoute?) = Timber.i("onRerouteAlong()")
override fun onOffRoute(offRoutePoint: Point?) = Timber.i("TC onOffRoute")
}
val simulateRoute = true
// Create a NavigationViewOptions object to package everything together
val options = NavigationViewOptions.builder()
.directionsRoute(routesMap?.currentRoute)
.shouldSimulateRoute(simulateRoute)
.navigationListener(navigationListener)
.routeListener(routeListener)
.build()
NavigationLauncher.startNavigation(this, options)
}
My question is, should these listeners be called, or has this callback feature just not been implemented yet?

Looks like you're trying to pass NavigationViewOptions into NavigationLauncher.startNavigation when it accepts NavigationLauncherOptions.
See the clarification here: https://github.com/mapbox/mapbox-navigation-android/issues/781#issuecomment-374328736

Related

How to handle tap events for an interactive watch faces with androidx.wear?

What is the correct way of handling tap events with the new androidx.wear.watchface libraries? I was doing this with the now deprecated android.support:wearable libraries without any problem with Java (using onTapCommand). Now, everything seems to be quite different, especially since the documentation and examples seem to be available only in Kotlin. Also, I can't find any example which properly shows how the functions are used.
The documentation mentions setTapListener, TapListener. And then there are the functions onTap, onTapEvent and onTapCommand. This seems very confusing.
Could somebody put here a small example? Or point me to a working example on the internet?
Any help much appreciated!
Implementing this seemed to work for me
https://developer.android.com/reference/androidx/wear/watchface/WatchFace.TapListener
My code:
class WatchFaceRenderer(...): Renderer.CanvasRenderer(...), WatchFace.TapListener {
override fun onTapEvent(tapType: Int, tapEvent: TapEvent, complicationSlot: ComplicationSlot?) {
if (tapType == TapType.UP)
// do something
invalidate()
}
}
class Service : WatchFaceService() {
override suspend fun createWatchFace(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository
): WatchFace {
val renderer = WatchFaceRenderer(
context = applicationContext,
surfaceHolder = surfaceHolder,
watchState = watchState,
currentUserStyleRepository = currentUserStyleRepository,
canvasType = CanvasType.SOFTWARE,
)
return WatchFace(WatchFaceType.ANALOG, renderer).setTapListener(renderer)
}
}

Is CoroutineScope(SupervisorJob()) runs in Main scope?

I was doing this code lab
https://developer.android.com/codelabs/android-room-with-a-view-kotlin#13
and having a question
class WordsApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob())
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
val repository by lazy { WordRepository(database.wordDao()) }
}
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
this is the code I found, as you can see, it is directly calling scope.launch(...)
my question is that:
isn't all the Room operations supposed to run in non-UI scope? Could someone help me to understand this? thanks so much!
Is CoroutineScope(SupervisorJob()) runs in Main scope?
No. By default CoroutineScope() uses Dispatchers.Default, as can be found in the documentation:
CoroutineScope() uses Dispatchers.Default for its coroutines.
isn't all the Room operations supposed to run in non-UI scope?
I'm not very familiar specifically with Room, but generally speaking it depends if the operation is suspending or blocking. You can run suspend functions from any dispatcher/thread. deleteAll() and insert() functions in the example are marked as suspend, therefore you can run them from both UI and non-UI threads.

ViewModel not cleared on DialogFragment onDestroy

Lifecycle v 2.3.1
Short Story: ViewModel is not cleared when DialogFragment onDestroy is called. That's it.
In my MainActivity, I instantiate and show a DialogFragment on button press:
private fun showConfirmDialog(message: String) {
MyConfirmDialog.getInstance(message)
.show(supportFragmentManager, MyConfirmDialog.TAG)
}
my dialog fragment:
class MyConfirmDialog: DialogFragment() {
companion object {
val TAG:String = MyConfirmDialog::class.java.simpleName
const val ARGS_MESSAGE = "message"
fun getInstance(message:String): MyConfirmDialog {
val dialog = MyConfirmDialog()
dialog.message = message
val b = Bundle()
b.putString(ARGS_MESSAGE, message)
dialog.arguments = b
return dialog
}
}
private lateinit var message:String
private val vm: MyConfirmDialogViewModel by activityViewModels()
private lateinit var binding: MyConfirmDialogBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
message = requireArguments().getString(ARGS_MESSAGE, "")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = MyConfirmDialogBinding.inflate(inflater, container, false)
with(binding) {
btnDone.setOnClickListener {
vm.doTheThing(message)
}
btnCancel.setOnClickListener {
dismiss()
}
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
/**
* Observer immediately fires on second fresh instance of this dialog.
* regardless of whether observed with this or viewLifecycleOwner. Thus before
* it can even be shown, this second unique dialog is immediately dismissed.
* The observer shouldn't receive an update until after btnDone is clicked.
*/
vm.doTheThing.observe(viewLifecycleOwner, {
Log.e(TAG, "observed doTheThing")
dismiss()
})
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "onDestroy")
//VM NOT CLEARED
}
}
I call showConfirmDialog a second time from my MainActivity, a second dialog is instantiated, but the VM is still happily sitting resident and NOT cleared, despite the first dialog confirming onDestroy having been called.
Thus is appears I can never use a DialogFragment with a ViewModel, if said dialog will be instantiated more than once.
This feels like a bug in Android lifecycle 2.3.1
I think I found my own answer. I was, in fact, instantiating the VM like this:
private val vm: MyConfirmDialogViewModel by activityViewModels()
Had to change to:
private val vm: MyConfirmDialogViewModel by viewModels()
Apparently, the choice of delegate is absolutely critical. Using the first delegate (by activityViewModels()) was automatically scoping the VM lifetime to the lifetime of the Activity instead of the Dialog. Thus even when the dialog was destroyed the VM was not cleared.
I thought scoping the VM's lifecycle was handled when registering the observer (e.g. observe(this, {}) vs. observe(viewLifecycleOwner, {})), but apparently the choice of these two targets has only to do with whether you intend to override onCreateDialog. To me, it's a confusing mess.
It would certainly be a better world if I didn't have to know absolutely everything about lifecycles and all their intricacies just to use the SDK properly.

How to return single object from Reactive REST API in Kotlin Coroutine style

I am trying to convert a REST service from the Spring 5 Reactive style to an async Kotlin Coroutine style.
I followed several different guides/tutorials on how this should work but I must be doing something wrong.
I get a compile error trying to turn a single object into a Flow, whereas the guides I'm following dont seem to do this at all.
Any pointers or otherwise very appreciated!
Router:
#Bean
fun mainRouter(handler: EobHandler) = coRouter {
GET("/search", handler::search)
GET("/get", handler::get)
}
Handler:
suspend fun search(request: ServerRequest): ServerResponse {
val eobList = service.search()
return ServerResponse.ok().bodyAndAwait(eobList)
}
suspend fun get(request: ServerRequest): ServerResponse
val eob = service.get()
return ServerResponse.ok().bodyAndAwait(eob); // compile error about bodyAndAwait expecting a Flow<T>
}
Service:
override fun search(): Flow<EOB> {
return listOf(EOB()).asFlow()
}
//
override suspend fun get(): EOB? {
return EOB()
}
If curious, here are some of the guides I've based my code on:
https://www.baeldung.com/spring-boot-kotlin-coroutines
https://docs.spring.io/spring/docs/5.2.0.M1/spring-framework-reference/languages.html#how-reactive-translates-to-coroutines
https://medium.com/#hantsy/using-kotlin-coroutines-with-spring-d2784a300bda
I was able to get this to compile by changing
return ServerResponse.ok().bodyAndAwait(eob);
to
return eob?.let { ServerResponse.ok().bodyValueAndAwait(it) } ?: ServerResponse.notFound().buildAndAwait()
guess it's something to do with type-safety of Kotlin - I was not returning a nullable object I think

Reactor switchifempty does not behave as expected in junit test

I am writing tests for the method provide below.
`
class ScrapedRecipeCache #Autowired constructor(private val cache: RecipeScrapingCacheService,
private val recipeService: RecipeService) : ScrapedRecipeProvider {
override fun provide(request: ScrapingRequest): Flux<ScrapedRecipe> =
cache.retrieve(request.link)
.doOnNext { println(it) }
.flatMap { (link, _, recipeHash, error) ->
recipeService.findByHash(recipeHash)
.map { ScrapedRecipe(it, link, error)}
.switchIfEmpty(cache.remove(request.link).then(Mono.empty()))
}
.flux()
}
`
The test looks as follows:
private val recipeFetched = Recipe("Tortellini", RecipeDifficulty.EASY, 15.0)
val cacheContents = RecipeScrapingResource("www.google.com", ScrapingOrigin.JAMIE_OLIVER, recipeFetched.hash,
mutableListOf(
pl.goolash.core.Exception("aa", ErrorType.WARNING, LocalDateTime.MIN)
))
val request = ScrapingRequest("www.google.com", ScrapingOrigin.JAMIE_OLIVER, 4)
#BeforeEach
fun setUp() {
given(cache.retrieve("www.google.com")).willReturn(Mono.just(cacheContents))
given(recipeService.findByHash(recipeFetched.hash)).willReturn(Mono.just(recipeFetched))
}
#Test
#DisplayName("Then return data fetched from service and don't delete cache")
fun test() {
cacheFacade.provide(request)
.test()
.expectNext(ScrapedRecipe(recipeFetched, "www.google.com", cacheContents.error!!))
.expectComplete()
.verify()
BDDMockito.verify(cache, BDDMockito.never()).remove(request.link)
}
The test fails because cache.remove(request.link) is called. To my understanding (or from what I managed to gather from documentation) switchIfEmpty, should only be fired when recipeService.findByHash returns Mono.empty(). However the debugger shows that it returns mocked value of Mono.just(fetchedRecipe).
The interesting thing is that when I replace
.switchIfEmpty(cache.remove(request.link).then(Mono.empty()))
with
.switchIfEmpty(Mono.just(1).doOnNext{println("weeee")}.then(Mono.empty()))
Then weee is not printed hence it behaves as expected, that is switchIfEmpty is not fired.
Furthermore the tested issue runs properly in integration test and does not clear the cache.
Reactor version : 3.1.0-RC1
Other notable details: Spring Boot 2.0.0-M4, Mockito-core:2.10, junit 5, project is written in kotlin
The question is, does anybody see anything wrong with this? Because I have spent two days over and still have no clue why this behaves so bizzarely.
Finally I found out how to make this work.
In order to remedy it:
override fun provide(request: ScrapingRequest): Flux<ScrapedRecipe> =
cache.retrieve(request.link)
.flatMap { (link, _, recipeHash, error) ->
recipeService.findByHash(recipeHash)
.map { ScrapedRecipe(it, link, error) }
.switchIfEmpty(Mono.just(1)
.flatMap { cache.remove(request.link) }
.then(Mono.empty()))
}
.flux()
You can see how using flatMap to execute the asynch work does the job, even if this is not the neatest implementation, it revealed to me quite an interesting mechanism hidden here.

Resources