I'm trying to adapt Google's LocationsUpdatesForegroundService example into Kotlin to use in my app. Now, everything is going fine, until I need to make a reference to a service equal to null. That doesn't 'cause any problems within the Java code it originates from but, when I try to implement it in Kotlin, even if use null!!, I get a KotlinNullPointerException when I try to run the app and the app crashes. I'm not quite sure how to avoid this or set it in a different way. I've spent a few hours on this and sometime browsing StackOverFlow without really being able to find a solution for it. If anyone could help me, it'd be greatly appreciated. I've enclosed the link to the code I'm going off of here:
https://github.com/android/location-samples/blob/master/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java#L127
...as well as the necessary code I'm using below.
Relevant code from my Main Activity:
private var lservice : LocService = null!! // A reference to the service to get location updates
private var bound = false // Tracks the bound state of the service
// Monitors the state of the connection to the service.
private val mServiceConnection = object:ServiceConnection {
override fun onServiceConnected(name:ComponentName, service: IBinder) {
val binder : LocService.LocalBinder = service as LocService.LocalBinder
lservice = binder.getService()
bound = true
}
override fun onServiceDisconnected(name: ComponentName) {
lservice = null!!
bound = false
}
}
My service class, which may or may not be necessary for helping to debug this error:
class LocService : Service() {
private val PACKAGE_NAME = "com.example.localization"
private val TAG = LocService::class.java!!.getSimpleName()
val ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"
private val EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME + ".started_from_notification"
// To return a current instance of the service
private val binder = LocalBinder()
// To check if the bounded activity has actually gone away
// and not unbound as part of an orientation change
private var changingConfig = false
private lateinit var fusedLocClient: FusedLocationProviderClient // For FusedLocationProvider API
private lateinit var locRequest : LocationRequest // Parameters for FusedLocationProvider
// Callback for changes in location
private lateinit var locCallback: LocationCallback
private lateinit var serviceHandler : Handler
private lateinit var notificationManager : NotificationManager // Notification Manager
private lateinit var loc : Location // The current location
// The identifier for the notification displayed for the foreground service
private val NOTIFICATION_ID = 12345678
// Set up when the service is created
override fun onCreate()
{
// An instance of Fused Location Provider Client
fusedLocClient = LocationServices.getFusedLocationProviderClient(this)
// Obtains location callback
locCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
super.onLocationResult(locationResult)
loc = locationResult!!.getLastLocation() // Obtains last location
// Send location information to any broadcast receivers
val intention = Intent(ACTION_BROADCAST)
intention.putExtra("Coordinates", locationResult!!.getLastLocation())
intention.putExtra("Address", getAddress(locationResult))
intention.putExtra("Time", SimpleDateFormat("MM/dd/yyyy 'at' HH:mm").format(Date()))
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intention)
// Change notification content if the service is running in the foreground
if (serviceIsRunningInForeground(this#LocService))
{
notificationManager.notify(NOTIFICATION_ID, getNotification())
}
}
}
// Create location request and get the last location
getLastLocation()
buildLocReq()
// Creates a HandlerThread, which is an extension of Thread and works
// with a Looper, meaning it's meant to handle multiple jobs in the background
// thread. The Looper is what keeps the thread alive. Notification Manager
// is there to notify the user of the notification service
val handlerThread = HandlerThread(TAG)
handlerThread.start()
serviceHandler = Handler(handlerThread.getLooper())
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
// Called whenever the client starts the service using startService()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val startedFromNotification = intent!!.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, false)
return START_NOT_STICKY // Don't recreate the service after it's killed
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
changingConfig = true
}
// Called when the client comes to the foreground and binds
// with this service. The service will stop being a foreground
// service when that happens
override fun onBind(intent: Intent): IBinder {
stopForeground(true)
changingConfig = false
return binder
}
// Called when the client returns to the foreground
// and binds once again with this service. The service will
// stop being a foreground service when that happens
override fun onRebind(intent: Intent?) {
stopForeground(true)
changingConfig = false
super.onRebind(intent)
}
// Called when the client unbinds with the service. If it's called
// with a configuration change, do nothing. Otherwise, make the service
// a foreground service
override fun onUnbind(intent: Intent?): Boolean {
if (!changingConfig && requestingLocationUpdates(this))
{
startForeground(NOTIFICATION_ID, getNotification())
}
return true
}
// Called when service is destroyed
override fun onDestroy() {
serviceHandler.removeCallbacksAndMessages(null)
}
inner class LocalBinder : Binder()
{
fun getService() : LocService
{
return this#LocService
}
}
// For obtaining location request
private fun buildLocReq()
{
// Create a location request to store parameters for the requests
locRequest = LocationRequest.create()
// Sets priority, interval, and --smallest displacement-- for requests
locRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locRequest.interval = 5000
// locRequest.smallestDisplacement = 10f
}
private fun getLastLocation() {
try
{
fusedLocClient.getLastLocation()
.addOnCompleteListener(object:OnCompleteListener<Location>
{
override fun onComplete(#NonNull task:Task<Location>) {
if (task.isSuccessful() && task.getResult() != null)
{
loc = task.getResult() as Location
}
else
{
Log.w(TAG, "Failed to get location.")
}
}
})
}
catch (unlikely:SecurityException) {
Log.e(TAG, "Lost location permission." + unlikely)
}
}
fun requestLocationUpdates()
{
setRequestingLocationUpdates(this, true)
startService(Intent(getApplicationContext(), LocService::class.java))
try
{
fusedLocClient.requestLocationUpdates(locRequest, locCallback, Looper.myLooper())
} catch (unlikely:SecurityException)
{
setRequestingLocationUpdates(this, false)
Log.e(TAG, "Lost location permission. Couldn't request updates. " + unlikely)
}
}
// Obtain address via GeoCoder class
private fun getAddress(locResult: LocationResult?): String {
var address = ""
var geoCoder = Geocoder(this, Locale.getDefault())
var loc1 = locResult!!.locations.get(locResult.locations.size-1)
try {
var addresses:ArrayList<Address> = geoCoder.getFromLocation(loc1.latitude, loc1.longitude, 1) as ArrayList<Address>
address = addresses.get(0).getAddressLine(0)
} catch (e: IOException) {
e.printStackTrace()
}
return address
}
private fun getNotification(): Notification {
val intent = Intent(this, LocService::class.java)
val text = getLocationText(loc)
val builder = NotificationCompat.Builder(this)
.setContentText(text)
.setOngoing(true)
.setPriority(Notification.PRIORITY_HIGH)
.setTicker(text)
.setWhen(System.currentTimeMillis())
return builder.build()
}
// Checks to see if the service is running in the foreground or not
fun serviceIsRunningInForeground(context: Context) : Boolean
{
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Integer.MAX_VALUE))
{
if (javaClass.getName().equals(service.service.getClassName()))
{
if (service.foreground)
{
return true
}
}
}
return false
}
val KEY_REQUESTING_LOCATION_UPDATES = "requesting_locaction_updates"
// Returns true if the requesting location updates, else false
fun requestingLocationUpdates(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(KEY_REQUESTING_LOCATION_UPDATES, false)
}
// Stores the location updates state in SharedPreferences
fun setRequestingLocationUpdates(context: Context, requestingLocationUpdates: Boolean)
{
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(KEY_REQUESTING_LOCATION_UPDATES, requestingLocationUpdates).apply()
}
// Returns the coordinates as a string for the notifications
fun getLocationText(loc: Location) : String
{
if (loc == null) {
return "Unknown Location"
} else {
return "Latitude: " + loc.longitude.toString() + " | Longitude: " + loc.longitude.toString()
}
}
}
Here's the error:
11-01 00:27:36.923 15995-15995/com.example.localization E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.localization, PID: 15995
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.localization/com.example.localization.MainActivity}: kotlin.KotlinNullPointerException
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2327)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: kotlin.KotlinNullPointerException
at com.example.localization.MainActivity.<init>(MainActivity.kt:40)
at java.lang.Class.newInstance(Native Method)
at android.app.Instrumentation.newActivity(Instrumentation.java:1067)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2317)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
You declare lservice as:
private var lservice: LocService
That means it's not nullable; Kotlin won't allow you to set it to null.
(In particular, note that null!! will always throw an exception: the !! operator tells the compiler to treat an expression as non-null, or to throw an exception if it is. And since null obviously is null, you're guaranteed an exception!)
If you want to allow the service to be null, you'll have to declare it as:
private var lservice: LocService?
The ? in the type means that it's nullable. As a result, you'll be able to set it to null without any exception. However, you'll need to check whether it's null when you use it, to prevent a NullPointerException there.
Nullability is pretty basic to Kotlin. It's all explained in the Kotlin language docs.
Related
I am trying to retrieve the base url from my proto datastore to be used to initialize my ktor client instance I know how to get the data from the datastore but I don't know how to block execution until that value is received so the client can be initialized with the base url
So my ktor client service asks for a NetworkURLS class which has a method to return the base url
Here is my property to retrieve terminalDetails from my proto datastore
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
Normally when I want to get the values I would do something like this
private fun getTerminalDetailsFromStore() {
try {
viewModelScope.launch(Dispatchers.IO) {
localRepository.getTerminalDetails.collect {
_terminalDetails.value = it
}
}
} catch(e: Exception) {
Log.d("AdminSettingsViewModel Error", e.message.toString()) // TODO: Handle Error Properly
}
}
but in my current case what I am looking to do is return terminalDetails.backendHost from a function and that where the issue comes in I know I need to use a coroutine scope to retrieve the value so I don't need to suspend the function but how to a prevent the function returning until the coroutine scope has finished?
I have tried using async and runBlocking but async doesn't work the way I would think it would and runBlocking hangs the entire app
fun backendURL(): String = runBlocking {
var url: String = "localhost"
val job = CoroutineScope(Dispatchers.IO).async {
repo.getTerminalDetails.collect {
it.backendHost
}
}
url
}
Can anyone give me some assistance on getting this to work?
EDIT: Here is my temporary solution, I do not intend on keeping it this way, The issue with runBlocking{} turned out to be the Flow<T> does not finish so runBlocking{} continues to block the app.
fun backendURL(): String {
val details = MutableStateFlow<TerminalDetails>(TerminalDetails.getDefaultInstance())
val job = CoroutineScope(Dispatchers.IO).launch {
repo.getTerminalDetails.collect {
details.value = it
}
}
runBlocking {
delay(250L)
}
return details.value.backendHost
}
EDIT 2: I fully fixed my issue. I created a method with the same name as my val (personal decision) which utilizes runBlocking{} and Flow<T>.first() to block while the value is retrieve. The reason I did not replace my val with the function is there are places where I need the information as well where I can utilize coroutines properly where I am not initializing components on my app
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
fun getTerminalDetails(): TerminalDetails = runBlocking {
cxt.terminalDetails.data.first()
}
Here is a breakdown of how the current third party SDK implementation works.
class Handler(val context: Context) {
val device = Controller.getInstance(context,Listener())
fun connectBT(BTDevice:BluetoothDevice){
device.connectBT(BTDevice)
}
}
and then the Listener implementation
class Listener: BBDeviceController.BBDeviceControllerListener{
override fun onBTConnected(device: BluetoothDevice?) {
println("Device Connected")
// Send back to function that device is connect
}
}
This is a straightforward example, but the idea is, when you press a button it will call connectBT() and then contain the result like so:
val handler = Handler(this)
val res = handler.connectBT(btDevice)
I know you can use suspendCoroutine on the function handler.connectBT() however the issue is how do I get the listeners result from the SDK to return back to the main function that called it?
When using suspendCoroutine, you need to call resume/resumeWithException/etc on the continuation object. You can store/pass this object anywhere, for example to your listener:
class Handler(val context: Context) {
val listener = Listener()
val device = Controller.getInstance(context, listener)
suspend fun connectBT(BTDevice:BluetoothDevice){
suspendCoroutine<Unit> { continuation ->
listener.continuation = continuation
device.connectBT(BTDevice)
}
}
}
class Listener: BBDeviceController.BBDeviceControllerListener{
var continuation: Continuation<Unit>? = null
override fun onBTConnected(device: BluetoothDevice?) {
println("Device Connected")
if (continuation != null) {
continuation?.resume(Unit)
continuation = null
}
}
}
i was wondering if it is possible to send messages from the backend (for example a running task that receives information from an external system) to the UI. In my case it needs to be a specific session (no broadcast) and only on a specific screen
plan B would be polling the backend frequently but i was hoping to get something more "realtime"
I was trying to work something out like this, but i keep getting a NotSerializableException.
#Push
class StorageAccess : Screen(), MessageListener {
#Inject
private lateinit var stationWSService: StationWebSocketService
#Inject
private lateinit var notifications: Notifications
#Subscribe
private fun onInit(event: InitEvent) {
}
#Subscribe("stationPicker")
private fun onStationPickerValueChange(event: HasValue.ValueChangeEvent<StorageUnit>) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
val prevValue = event.prevValue
if (prevValue != null) {
stationWSService.remove(current.userSession.id)
}
val value = event.value ?: return
stationWSService.listen(current.userSession.id, value, this)
}
override fun messageReceived(message: String) {
val current = AppUI.getCurrent()
current.access {
notifications.create().withCaption(message).show()
}
}
#Subscribe
private fun onAfterDetach(event: AfterDetachEvent) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
stationWSService.remove(current.userSession.id)
}
}
-- The callback interface
interface MessageListener : Serializable {
fun messageReceived(message: String);
}
-- The listen method of my backend service
private val listeners: MutableMap<String, MutableMap<UUID, MessageListener>> = HashMap()
override fun listen(id: UUID, storageUnit: StorageUnit, callback: MessageListener) {
val unitStationIP: String = storageUnit.unitStationIP ?: return
if (!listeners.containsKey(unitStationIP))
listeners[unitStationIP] = HashMap()
listeners[unitStationIP]?.set(id, callback)
}
The Exception i get is NotSerializableException: com.haulmont.cuba.web.sys.WebNotifications which happens during adding the listener to the backend: stationWSService.listen(current.userSession.id, value, this)
as far as i understand this is the place where the UI sends the information to the backend - and with it the entire status of the class StorageAccess, including all its members.
is there an elegant solution to this?
regards
There is an add-on that solves exactly this problem: https://github.com/cuba-platform/global-events-addon
I'm attempting to use Kotlin's Flow class as a message queue to transfer data from a producer (a camera) to a set of workers (image analyzers) running on separate coroutines.
The producer in my case is a camera, and will run substantially faster than the workers. Back pressure should be handled by dropping data so that the image analyzers are always operating on the latest images from the camera.
When using channels, this solution works, but seems messy and does not provide an easy way for me to translate the data between the camera and the analyzers (like flow.map).
class ImageAnalyzer<Result> {
fun analyze(image: Bitmap): Result {
// perform some work on the image and return a Result. This can take a long time.
}
}
class CameraAdapter {
private val imageChannel = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
private val imageReceiveMutex = Mutex()
// additional code to make this camera work and listen to lifecycle events of the enclosing activity.
protected fun sendImageToStream(image: CameraOutput) {
// use channel.offer to ensure the latest images are processed
runBlocking { imageChannel.offer(image) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
runBlocking { imageChannel.close() }
}
/**
* Get the stream of images from the camera.
*/
fun getImageStream(): ReceiveChannel<Bitmap> = imageChannel
}
class ImageProcessor<Result>(workers: List<ImageAnalyzer<Result>>) {
private val analysisResults = Channel<Result>(capacity = Channel.RENDEZVOUS)
private val cancelMutex = Mutex()
var finished = false // this can be set elsewhere when enough images have been analyzed
fun subscribeTo(channel: ReceiveChannel<Bitmap>, processingCoroutineScope: CoroutineScope) {
// omit some checks to make sure this is not already subscribed
processingCoroutineScope.launch {
val workerScope = this
workers.forEachIndexed { index, worker ->
launch(Dispatchers.Default) {
startWorker(channel, workerScope, index, worker)
}
}
}
}
private suspend fun startWorker(
channel: ReceiveChannel<Bitmap>,
workerScope: CoroutineScope,
workerId: Int,
worker: ImageAnalyzer
) {
for (bitmap in channel) {
analysisResults.send(worker.analyze(bitmap))
cancelMutex.withLock {
if (finished && workerScope.isActive) {
workerScope.cancel()
}
}
}
}
}
class ExampleApplication : CoroutineScope {
private val cameraAdapter: CameraAdapter = ...
private val imageProcessor: ImageProcessor<Result> = ...
fun analyzeCameraStream() {
imageProcessor.subscribeTo(cameraAdapter.getImageStream())
}
}
What's the proper way to do this? I would like to use a ChannelFlow instead of a Channel to pass data between the camera and the ImageProcessor. This would allow me to call flow.map to add metadata to the images before they're sent to the analyzers. However, when doing so, each ImageAnalyzer gets a copy of the same image instead of processing different images in parallel. Is it possible to use a Flow as a message queue rather than a broadcaster?
I got this working with flows! It was important to keep the flows backed by a channel throughout this sequence so that each worker would pick up unique images to operate on. I've confirmed this functionality through unit tests.
Here's my updated code for posterity:
class ImageAnalyzer<Result> {
fun analyze(image: Bitmap): Result {
// perform some work on the image and return a Result. This can take a long time.
}
}
class CameraAdapter {
private val imageStream = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
private val imageReceiveMutex = Mutex()
// additional code to make this camera work and listen to lifecycle events of the enclosing activity.
protected fun sendImageToStream(image: CameraOutput) {
// use channel.offer to enforce the drop back pressure strategy
runBlocking { imageChannel.offer(image) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
runBlocking { imageChannel.close() }
}
/**
* Get the stream of images from the camera.
*/
fun getImageStream(): Flow<Bitmap> = imageChannel.receiveAsFlow()
}
class ImageProcessor<Result>(workers: List<ImageAnalyzer<Result>>) {
private val analysisResults = Channel<Result>(capacity = Channel.RENDEZVOUS)
private val cancelMutex = Mutex()
var finished = false // this can be set elsewhere when enough images have been analyzed
fun subscribeTo(flow: Flow<Bitmap>, processingCoroutineScope: CoroutineScope): Job {
// omit some checks to make sure this is not already subscribed
return processingCoroutineScope.launch {
val workerScope = this
workers.forEachIndexed { index, worker ->
launch(Dispatchers.Default) {
startWorker(flow, workerScope, index, worker)
}
}
}
}
private suspend fun startWorker(
flow: Flow<Bitmap>,
workerScope: CoroutineScope,
workerId: Int,
worker: ImageAnalyzer
) {
while (workerScope.isActive) {
flow.collect { bitmap ->
analysisResults.send(worker.analyze(bitmap))
cancelMutex.withLock {
if (finished && workerScope.isActive) {
workerScope.cancel()
}
}
}
}
}
fun getAnalysisResults(): Flow<Result> = analysisResults.receiveAsFlow()
}
class ExampleApplication : CoroutineScope {
private val cameraAdapter: CameraAdapter = ...
private val imageProcessor: ImageProcessor<Result> = ...
fun analyzeCameraStream() {
imageProcessor.subscribeTo(cameraAdapter.getImageStream())
}
}
It appears that, so long as the flow is backed by a channel, the subscribers will each get a unique image.
My application (typical REST server that calls other REST services internally) has two main classes to perform the bootstrapping procedure.
There is the Application.kt class that is supposed to configure the vertx instance itself and to register certain modules (jackson kotlin integration for example):
class Application(
private val profileSetting: String? = System.getenv("ACTIVE_PROFILES"),
private val logger: Logger = LoggerFactory.getLogger(Application::class.java)!!
) {
fun bootstrap() {
val profiles = activeProfiles()
val meterRegistry = configureMeters()
val vertx = bootstrapVertx(meterRegistry)
vertx.deployVerticle(ApplicationBootstrapVerticle(profiles)) { startup ->
if (startup.succeeded()) {
logger.info("Application startup finished")
} else {
logger.error("Application startup failed", startup.cause())
vertx.close()
}
}
}
}
In addition there is a ApplicationBootstrapVerticle.kt class that is supposed to deploy the different verticles in a defined order. Some of them in sequence, some of them in parallel:
class ApplicationBootstrapVerticle(
private val profiles: List<String>,
private val logger: Logger = LoggerFactory.getLogger(ApplicationBootstrapVerticle::class.java)
) : AbstractVerticle() {
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}
private fun initializeApplicationConfig(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
ApplicationConfigVerticle(profiles),
it.completer()
)
}
}
private fun initializeScheduledJobs(): CompositeFuture {
val stationsJob = Future.future<String>()
val capabilitiesJob = Future.future<String>()
return CompositeFuture.all(stationsJob, capabilitiesJob).also {
vertx.deployVerticle(
StationQualitiesVerticle(),
stationsJob.completer()
)
vertx.deployVerticle(
VideoCapabilitiesVerticle(),
capabilitiesJob.completer()
)
}
}
private fun initializeRestEndpoints(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
RestEndpointVerticle(dispatcherFactory = RouteDispatcherFactory(vertx)),
it.completer()
)
}
}
}
I am not sure if this is the supposed way to bootstrap an application, if there is any. More important though, I am not sure if I understand the Future.compose mechanics correctly.
The application starts up successfully and I see all desired log messages except the
Application startup finished
message. Also the following code is never called in case of successs:
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
In case of an failure though, for example when my application configuration files (yaml) cannot be parsed because there is an unknown field in the destination entity, the log message
Application startup failed
appears in the logs and also the code above is invoked.
I am curious what is wrong with my composed futures chain. I thought that the handler would be called after the previous futures succeeded or one of them failed but I think it's only called in case of success.
Update
I suppose that an invocation of startFuture.complete() was missing. By adapting the start method, it finally worked:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture.complete()
startFuture
}.setHandler(
startFuture.completer()
)
}
I am not sure though, if this is the supposed way to handle this future chain.
The solution that worked for me looks like this:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.setHandler { ar ->
if(ar.succeeded()) {
logger.info("Http server started")
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}