I recently started coding with Jetpack Compose but now I've ran into issue.
My goal is when clicking on the LazyColumn's item it will navigate and display information on the Detail's Screen.
The code worked well, when I was using normal object. But for my college assigment I needed to get data from Firebase and display it using the MVVM pattern.
Here is my code:
#Composable
fun DetailsUI(navController: NavController, articleId: String?,viewModel: ArticleViewModel = hiltViewModel()) {
var expanded = remember { mutableStateOf(false) }
val articleResponse = viewModel.articleState.value
Scaffold(
) {
when (articleResponse) {
is Loading -> ProgressBar()
is Success ->Column(){
var newArticle = articleResponse.data.filter { article -> //the problem area that logs indicated
article.id==articleId
}
ArticleHeader(image =newArticle[0].image)
ArticleTitle(title =newArticle[0].title , author = newArticle[0].author )
ArticleContent(content =newArticle[0].title )
}
is Error -> Utils.printError(articleResponse.message)
}
}
}
ViewModel code:
#HiltViewModel
class ArticleViewModel #Inject constructor(
private val useCases: UseCases
):ViewModel() {
private val _articleState = mutableStateOf<Response<List<Article>>>(Response.Loading)
val articleState: State<Response<List<Article>>> = _articleState
init {
getArticles()
}
private fun getArticles() {
viewModelScope.launch {
useCases.getArticle().collect { response ->
_articleState.value = response
}
}
}
Logs:
2022-06-09 00:43:35.248 12215-12215/com.example.flow E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.flow, PID: 12215
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.get(ArrayList.java:437)
at com.example.flow.DetailScreenKt$DetailsUI$2.invoke(DetailScreen.kt:99)
at com.example.flow.DetailScreenKt$DetailsUI$2.invoke(DetailScreen.kt:92)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$1.invoke(ComposableLambda.jvm.kt:127)
at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$1.invoke(ComposableLambda.jvm.kt:127)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2404)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2585)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source:1)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2547)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:620)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:786)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:105)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:456)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:425)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:34)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:947)
at android.view.Choreographer.doCallbacks(Choreographer.java:761)
at android.view.Choreographer.doFrame(Choreographer.java:693)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock#dde2781, StandaloneCoroutine{Cancelling}#6665026, AndroidUiDispatcher#95fe467]
Perhaps, filtering the articleResponseisn't a best idea, but I didn't want to provide an additional method to retrieve a single article from Firebase, so filtering was an easier solution. Nevertheless, I would be happy to receive the solution that would allow my idea without getting my application crashed.
You are getting a crash because the response does not include the article you're looking for, and your code assumes it is there.
Change this
var newArticle = articleResponse.data.filter {
article.id==articleId
}
to
val newArticle = articleResponse.data.firstOrNull {
article.id==articleId
}
if (newArticle != null) {
// display article
} else {
// display Article not found error
}
Related
I set up navigation, pagination and use flow to connect ui with model. If simplify, my screen code looks like this:
#Composable
MainScreen() {
val listState = rememberLazyListState()
val lazyItems = Pager(PagingConfig(...)) { ... }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
LazyColumn(state = listState) {
items(lazyItems, key = { it.id }) { ... }
}
}
And here is my NavHost code:
NavHost(navController, startDestination = "mainScreen") {
composable("mainScreen") {
MainScreen()
}
}
But when i navigate back to MainScreen from another screen or just opening the drawer, data is loaded from DataSource again and i see noticeable blink of LazyColumn.
How to avoid reloading data?
Your code gives me the following error for cachedIn:
Flow operator functions should not be invoked within composition
You shouldn't ignore such warnings.
During transition Compose Navigation recomposes both disappearing and appearing views many times. This is the expected behavior.
And your code creates a new Pager with a new flow on each recomposition, which is causing the problem.
The easiest way to solve it is using remember: it'll cache the pager flow between recompositions:
val lazyItems = remember {
Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
}
But it'll still be reset during configuration change, e.g. device rotation. The best way to prevent this is moving this logic into a view model:
class MainScreenViewModel : ViewModel() {
val pagingFlow = Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
}
#Composable
fun MainScreen(
viewModel = viewModel<MainScreenViewModel>()
) {
val lazyItems = viewModel.pagingFlow.collectAsLazyPagingItems()
}
im currently trying to implement Server-Side Events for my Ktor-Api. I tried getting the official sse-sample to work i found on https://github.com/ktorio/ktor-samples/tree/1.3.0/other/sse but when i run the server and call the sse-http-request the server throws following Errors:
ERROR ktor.application - Unhandled exception caught for CoroutineName(call-handler)
java.lang.StackOverflowError: null
at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:89)
at java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:72)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:66)
at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:60)
at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:72)
Here is the code sample:
fun main() {
embeddedServer(Netty, port = 8080) { // this: Application ->
val channel = produce { // this: ProducerScope<SseEvent> ->
var n = 0
while (true) {
send(SseEvent("demo$n"))
delay(1000)
n++
}
}.broadcast()
routing {
get("/sse") {
val events = channel.openSubscription()
try {
call.respondSse(events)
} finally {
events.cancel()
}
}
}
}.start(wait = true)
}
data class SseEvent(val data: String, val event: String? = null, val id: String? = null)
suspend fun ApplicationCall.respondSse(events: ReceiveChannel<SseEvent>) {
response.cacheControl(CacheControl.NoCache(null))
respondTextWriter(contentType = ContentType.Text.EventStream) {
for (event in events) {
if (event.id != null) {
write("id: ${event.id}\n")
}
if (event.event != null) {
write("event: ${event.event}\n")
}
for (dataLine in event.data.lines()) {
write("data: $dataLine\n")
}
write("\n")
flush()
}
}
}
Im pretty sure the error comes from the flush command, since i once tried sse using a different approach(without flush) and it kinda worked but i cant follow that approach for different reasons.
I tried to use NetworkBoundResource for my MVVM Model and after i follow some tutorial, i'm having an error look this
10-21 14:15:04.073 31376-31376/com.example.mvvmsecondmodel E/Process: android_os_Process_getProcessNameByPid pid is 31376
10-21 14:15:04.074 31376-31376/com.example.mvvmsecondmodel E/Process: android_os_Process_getProcessNameByPid value is mvvmsecondmodel
10-21 14:15:04.416 31376-31421/com.example.mvvmsecondmodel E/SQLiteLog: (283) recovered 8 frames from WAL file /data/data/com.example.mvvmsecondmodel/databases/movie_db-wal
10-21 14:15:04.640 31376-31376/com.example.mvvmsecondmodel E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.mvvmsecondmodel, PID: 31376
java.lang.IllegalArgumentException: Unable to create call adapter for kotlinx.coroutines.flow.Flow<com.example.mvvmsecondmodel.data.respository.ApiResponse<com.example.mvvmsecondmodel.data.model.MovieResponse$Movie>>
for method ApiService.getMyMovie
at retrofit2.Utils.methodError(Utils.java:52)
at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:105)
at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:66)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:37)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
at retrofit2.Retrofit$1.invoke(Retrofit.java:149)
at java.lang.reflect.Proxy.invoke(Proxy.java:397)
at $Proxy0.getMyMovie(Unknown Source)
at com.example.mvvmsecondmodel.data.remote.ApiService$DefaultImpls.getMyMovie$default(ApiService.kt:11)
at com.example.mvvmsecondmodel.data.respository.MovieRespository$getListMovie$$inlined$networkBoundResource$1.invokeSuspend(NetworkBoundResource.kt:49)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:55)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for kotlinx.coroutines.flow.Flow<com.example.mvvmsecondmodel.data.respository.ApiResponse<com.example.mvvmsecondmodel.data.model.MovieResponse$Movie>>.
Tried:
* retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
* retrofit2.DefaultCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:103)
... 14 more
At first i thought there's red sign at my code so i checked ApiService and Respository class and nothing wrong cause i already follow the tutorial
ApiService:
interface ApiService {
#GET("3/movie/popular")
fun getMyMovie(#Query("api_key") api : String = "32bbbffe944d16d1d2a3ee46cfc6aaa0"
) : Flow<ApiResponse<MovieResponse.Movie>>
}
MovieRespository:
class MovieRespository (val apiService: ApiService, val movieDao: MovieDao) {
fun getListMovie() : Flow<Resource<Movie>> {
return networkBoundResource(
fetchFromLocal = { movieDao.getMyMovie() },
shouldFetchFromRemote = {true},
fetchFromRemote = {apiService.getMyMovie()},
processRemoteResponse = {},
saveRemoteData = {movieDao.insert(
it.results.let {
it.map { data -> Movie.from(data) }
}
)},
onFetchFailed = {_, _ ->}
).flowOn(Dispatchers.IO)
}
You should define your api service as suspend function.
Api Service:
interface ApiService {
#GET("3/movie/popular")
suspend fun getMyMovie(#Query("api_key") api : String = "32bbbffe944d16d1d2a3ee46cfc6aaa0"
) : ApiResponse<MovieResponse.Movie>
}
Movie Repository:
class MovieRepository (val apiService: ApiService, val movieDao: MovieDao) {
fun getListMovie() : Flow<Resource<Movie>> {
return flow {
// do your networkBoundResource functions
}.flowOn(Dispatchers.IO)
}
I can't test the code because you didn't share your NetworkBoundResource class. I hope the code help to fix your problem.
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.
I'm building a very simple application in Kotlin with Vertx and RxJava 2 (RxKotlin), using Kovert REST framework and Retrofit. I have retrofit-vertx adapter and the RxJava2 Retrofit adapter. I can return an arbitrary list from my listUndergroundStations() method, but whenever I try to load from the remote API I get the following error:
Jun 23, 2017 2:16:29 PM uk.amb85.rxweb.api.UndergroundRestController
SEVERE: HTTP CODE 500 - /api/underground/stations - java.io.IOException: java.lang.IllegalStateException: message == null
java.lang.RuntimeException: java.io.IOException: java.lang.IllegalStateException: message == null
at io.reactivex.internal.util.ExceptionHelper.wrapOrThrow(ExceptionHelper.java:45)
at io.reactivex.internal.observers.BlockingMultiObserver.blockingGet(BlockingMultiObserver.java:91)
at io.reactivex.Single.blockingGet(Single.java:2148)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:35)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:13)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:138)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:130)
at nl.komponents.kovenant.NonBlockingDispatcher$ThreadContext.run(dispatcher-jvm.kt:327)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: java.lang.IllegalStateException: message == null
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$enqueue$0(VertxCallFactory.java:90)
at io.vertx.core.impl.FutureImpl.tryFail(FutureImpl.java:170)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleException(HttpClientResponseImpl.java:270)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:259)
at io.vertx.core.http.impl.ClientConnection.handleResponseEnd(ClientConnection.java:361)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:80)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:38)
at io.vertx.core.http.impl.VertxHttpHandler.lambda$channelRead$0(VertxHttpHandler.java:71)
at io.vertx.core.impl.ContextImpl.lambda$wrapTask$2(ContextImpl.java:335)
at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:193)
at io.vertx.core.http.impl.VertxHttpHandler.channelRead(VertxHttpHandler.java:71)
at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:122)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:435)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:293)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:267)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:250)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1228)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1039)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:411)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:248)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:129)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:642)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:565)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:479)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:441)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
... 1 more
Caused by: java.lang.IllegalStateException: message == null
at okhttp3.Response$Builder.build(Response.java:431)
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$null$1(VertxCallFactory.java:109)
at io.vertx.core.http.impl.HttpClientResponseImpl$BodyHandler.notifyHandler(HttpClientResponseImpl.java:301)
at io.vertx.core.http.impl.HttpClientResponseImpl.lambda$bodyHandler$0(HttpClientResponseImpl.java:193)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:257)
... 36 more
I can't for the life of me work out what is causing the IllegalStateException and have googled it to death. I don't think it's Rx related because I get the same error if I make the method return Observable<List<UndergroundLine>> or even get rid of Rx entirely and return Call<List<UndergroundLine>> (adjusting the controller accordingly). However, beyond that, I'm beating my head against a wall! Is anyone able to point out the error of my ways (besides putting a cushion under my head)?
Main Verticle:
class ApiVerticle : AbstractVerticle() {
override fun start(startFuture: Future<Void>?) {
// Initialise injection.
configureKodein()
val apiRouter = configureRouter(vertx)
vertx.createHttpServer()
.requestHandler { apiRouter.accept(it) }
.listen(8080)
}
private fun configureKodein() {
Kodein.global.addImport(Kodein.Module {
import(TflUndergroundService.module)
})
}
private fun configureRouter(vertx: Vertx): Router {
val apiMountPoint = "api"
val routerInit = fun Router.() {
bindController(UndergroundRestController(), apiMountPoint)
}
val router = Router.router(vertx) initializedBy { router ->
router.routerInit()
}
return router
}
}
TflService:
interface TflService {
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
#GET("/Line/{lineName}/StopPoints")
fun getStationsForUndergroundLine(
#Path("lineName") lineName: String
): Observable<UndergroundStation>
#GET("/Line/{lineName}/Arrivals?stopPointId={stationNaptanId")
fun getArrivalsFor(
#Path("lineName") lineName: String,
#Path("stationNaptanId") stationNaptanId: String
) : Observable<Arrival>
}
data class UndergroundLine(val id: String, val name: String)
data class UndergroundStation(val naptanId: String, val commonName: String)
data class Arrival(
val platformName: String,
val towards: String,
val currentLocation: String,
val expectedArrival: LocalDateTime)
object TflUndergroundService {
val module = Kodein.Module {
val vertx: Vertx = Vertx.currentContext().owner()
val client: HttpClient = vertx.createHttpClient()
val jacksonMapper: ObjectMapper = ObjectMapper()
jacksonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.tfl.gov.uk/")
.callFactory(VertxCallFactory(client))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
val tflService: TflService = retrofit.create(TflService::class.java)
bind<TflService>() with instance(tflService)
}
}
ApiKeySecured (Just requires "appid" to be a parameter):
class ApiKeySecured(private val routingContext: RoutingContext) : KodeinGlobalAware {
val user: String = routingContext.request().getParam("appid") ?: throw HttpErrorUnauthorized()
}
The offending REST controller (in Kovert, Promise's are executed on Vertx worker thread):
class UndergroundRestController(val undergroundService: TflService = Kodein.global.instance()) {
fun ApiKeySecured.listUndergroundStations(): Promise<List<UndergroundLine>, Exception> {
//TODO: This is blocking, fix it!??
return task {
undergroundService
.getAllUndergroundLines()
.doOnError { println(it) }
.toList()
.blockingGet()
}
}
}
build.gradle:
mainClassName = "io.vertx.core.Launcher"
def mainVerticleName = "uk.amb85.rxweb.verticles.ApiVerticle"
def configurationFile = "conf/development.json"
run {
args = ["run",
mainVerticleName,
"--launcher-class=$mainClassName",
"-conf $configurationFile"
]
}
There's an issue with retrofit-vertx you are using. OkHttp3's ResponseBuilder requires message to be not null, but VertxCallFactory doesn't set it.
It's fixed in the latest version, but as it's still in development, you have to use snapshot:
repositories {
mavenCentral()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
dependencies {
compile 'com.julienviet:retrofit-vertx:1.0.2-SNAPSHOT'
}
Switching to snapshot dependency fixes the issue you mention in your question, but there's an issue with json mapping, which can be easily fixed by switching code from:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
to:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<List<UndergroundLine>>
And updating your data classes to have default empty constructor to let Jackson instantiate using reflection:
data class UndergroundLine(var id: String = "", var name: String = "")
More on emtpy default constructor for data classes.
But it's another question related to how to parse response from API you're using to Observable and should be asked if you don't find a workaround.