Reputation: 13
I'm making an android app with Kotlin and compose and I'm using GSON to parse some JSON that I retrieve from an API. The data is then put into a list and returned from the function. I have a composable that should be drawn for each item in the list but the function that returns the list is on a coroutine. Here is what I've tried to do to draw them but the app just crashes.
val mutableBricksList = remember { mutableStateOf(listOf<LegoPart>()) }
Button(onClick = {
GlobalScope.launch(Dispatchers.IO) {
val fetchedLegoBricks =
fetchLegoSetBricks("https://rebrickable.com/api/v3/lego/sets/${setNumber}/parts/?key=${BuildConfig.API_KEY}")
if (fetchedLegoBricks != null) {
withContext(Dispatchers.Main) {
mutableBricksList.value = fetchedLegoBricks
}
}
}
}) {
Text("Load Bricks")
}
mutableBricksList.value.forEach { legoPart ->
Brick(
brickNumber = legoPart.part.partNum,
brickName = legoPart.part.name,
imageURL = legoPart.part.partImgUrl
)
println(legoPart.part.name)
}
In case you want it here is the function that returns the list
suspend fun fetchLegoSetBricks(url: String): List<LegoPart>? = withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()
try {
val response = client.newCall(request).execute()
val responseBody = response.body?.string()
// Parse JSON using Gson library
val gson = Gson()
val legoResponse = gson.fromJson(responseBody, LegoResponse::class.java)
val legoPartsList = legoResponse.results
legoPartsList
} catch (e: Exception) {
println("Error fetching LEGO parts: $e")
null
}
}
The project is on a public GitHub repo at https://github.com/IAMGeeCee/BrickBox if you want to see all the code.
I've tried using LaunchedEffect and having it not be when the button is pressed and just when the composable is drawn but that didn't work either.
EDIT: Here is the logcat for when it crashes on the code above
Process: com.brickbox, PID: 17658
java.lang.NullPointerException: Parameter specified as non-null is null: method com.brickbox.BricksBackEndKt.Brick, parameter brickNumber
at com.brickbox.BricksBackEndKt.Brick(Unknown Source:2)
at com.brickbox.ScreensKt.SetInfoScreen(Screens.kt:198)
at com.brickbox.ScreensKt$SetInfoScreen$4.invoke(Unknown Source:12)
at com.brickbox.ScreensKt$SetInfoScreen$4.invoke(Unknown Source:10)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:192)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2556)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2827)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3314)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3265)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:940)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1155)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:127)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:583)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:551)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:41)
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:1397)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1408)
at android.view.Choreographer.doCallbacks(Choreographer.java:1008)
at android.view.Choreographer.doFrame(Choreographer.java:934)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1382)
at android.os.Handler.handleCallback(Handler.java:959)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.app.ActivityThread.main(ActivityThread.java:8501)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@8c66dc4, androidx.compose.ui.platform.MotionDurationScaleImpl@b86a4ad, StandaloneCoroutine{Cancelling}@94f03e2, AndroidUiDispatcher@c9fb373]
EDIT 2: I got it working with
val mutableBricksList = remember { mutableStateListOf<LegoPart>() }
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
val fetchedLegoBricks =
fetchLegoSetBricks("https://rebrickable.com/api/v3/lego/sets/${setNumber}/parts/?key=${BuildConfig.API_KEY}")
if (fetchedLegoBricks != null) {
fetchedLegoBricks.forEach{legoPart ->
mutableBricksList.add(legoPart)
}
}
}
}) {
Text("Load Bricks")
}
mutableBricksList.forEach { legoPart ->
Brick(
brickNumber = legoPart.id.toString(),
brickName = legoPart.part.name,
imageURL = legoPart.part.partImgUrl
)
println(legoPart.part.name)
}
Upvotes: 0
Views: 52
Reputation: 15614
In absence of the error log and strack trace I can only surmise the problem lies in your coroutine handling.
It should look something like this:
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
val fetchedLegoBricks =
fetchLegoSetBricks("https://rebrickable.com/api/v3/lego/sets/${setNumber}/parts/?key=${BuildConfig.API_KEY}")
if (fetchedLegoBricks != null) {
mutableBricksList.value = fetchedLegoBricks
}
}
}) {
Text("Load Bricks")
}
Never use GlobalScope
, that defies the principles of structured concurrency. You will always want to manage the lifecycle of your scopes (which cannot be done with GlobalScope). In Android you do not even have to do that yourself because you can easily obtain an appropriate scope in the following locations:
lifecycleScope
viewModelScope
rememberCoroutineScope()
Furthermore, switiching dispatchers isn't necessary here. fetchLegoSetBricks
already is made main-safe by moving to the IO dispatcher, so you can safely execute the suspend function as-is.
The rest of the code looks ok-ish, without further information about what exactly failed there is nothing more to do.
That said, your UI shouldn't make API calls in the first place. This is something your view model should initiate, either by doing it itself or, in a layered architecture, by calling some function in the data layer.
You should also be more explicit about how the results are processed. Are they only displayed once in the UI and vanish the next time some UI action is taken (like the next request)? Should they be accumulated over the session? Should they be persisted in a database so they can be retrieved in later sessions? This also shouldn't lie in the responsibility of the UI, but it will affect your Unidirectional Data Flow. Wherever the data eventually comes from, it should be readily available in the view model so the UI can just simply use it. Any requests for new data are formulated as fire-and-forget calls of a view model's function, without a return value. Instead, when the request completes, the view model will provide fresh data in the form of an updatable Flow, best to be exposed as a StateFlow. The UI just needs to observe that data for changes (by using collectAsStateWithLifecycle) to always be up-to-date.
Upvotes: 0