I'm getting an NPE and I think I'm not understandi...
# compose
c
I'm getting an NPE and I think I'm not understanding how snapshot state works? In essence I want to show a modal dialog as part of my state. Code inside the thread.
Copy code
class SignInViewState {
    var email by mutableStateOf("")
    var password by mutableStateOf("")
    var modalError: ModalError? by mutableStateOf(null)
}

data class ModalError(
    val text1: String,
    val text2: String,
)
My compose screen includes this
Copy code
if (viewModel.state.modalError != null) {
    Dialog(onDismissRequest = { viewModel.state.modalError = null }) {
        MyModal(
            viewModel.state.modalError!!.text1,
            viewModel.state.modalError!!.text2)
    }
}
And this works-ish. My modal does not show. Then I update my modal error in my state, and the pop up shows! But then when I click outside of the dialog I get a crash. My on dismiss request tries to null out the error, but I get an NPE on
viewModel.state.modalError!!.text1
r
As a workaround, try assigning
modalError
to a local variable and using that. Not sure about the root cause, but that should get around it
c
Hm.
Copy code
if (viewModel.state.modalError != null) {
    Dialog(onDismissRequest = { viewModel.state.modalError = null }) {
        MyModal(
            viewModel.state.modalError?.text1.orEmpty(),
            viewModel.state.modalError?.text2.orEmpty())
    }
}
stops the issue from happening, but now I really feel like I don't know what's going on. Can anyone identify what's going on?
a
I think it's just that the dismiss of the dialog isn't immediate (there's an animation) and during the animation the content of the dialog still have to be shown. This kind of problem also exists when you are using
Crossfade
,
AninatedVisibility
, etc. Using
val error = remember { viewModel.state.modalError!! }
to solve the problem.
c
Interesting. This is kind of breaking my mind because I'm trying to figure out where else this would break, and I'm now also confused a bit by why adding an additional val error would help.
a
It's the
remember
that helps. It remembers the initial value so even if
viewModel.state.modalError
becomes null the remembered value doesn't.
c
@Albert Chang do you think I'm just better off adding an additional field like "showModal"?
Copy code
class SignInViewState {
    var email by mutableStateOf("")
    var password by mutableStateOf("")
    var showModal by mutableStateOf(false)
    var modalError: ModalError? by mutableStateOf(null)
}
Adding a
val error = remember { viewModel.state.modalError!! }
almost seems too "clever" and I'm not sure it's really self explanatory on what's actually going on.
d
This is a case of needing to retain the data for the exit animation. To achieve that, you could either do
val error = remember { viewModel.state.modalError!! }
inside the dialog content, so that the error isn't retained after the content is disposed. Or, you could retain that data in your ViewModel, and wait for the
onDisposed
signal to do the cleanup. Either way works. It's a matter of personal preference.
c
Thanks everyone. This was driving me a little insane so I really appreciate it. Readability and intent is really important for me and my team. Following Doris' first suggestion I think I could change this
Copy code
if (viewModel.state.modalError != null) {
    Dialog(onDismissRequest = { viewModel.state.modalError = null }) {
        MyModal(
            viewModel.state.modalError!!.text1,
            viewModel.state.modalError!!.text2)
    }
}
to this
Copy code
if (viewModel.state.modalError != null) {
    Dialog(onDismissRequest = { viewModel.state.modalError = null }) {
        val error = remember { viewModel.state.modalError!! }
        MyModal(error.text1, error.text2)
    }
}
@Doris Liu I am not following your second option "you could retain that data in your ViewModel". How would that look?
d
Re: "you could retain that data in your ViewModel" . How would that look? You'd need to have a separate show/hide Boolean in the state, similar to what you proposed above. It could be used to determine when to dismiss the dialog. The actual data for the error is only reset after the dialog has been disposed:
Copy code
if (viewModel.state.showError) {    
    Dialog(onDismissRequest = { viewModel.state.showError = false }) {
        MyModal(
            viewModel.state.modalError!!.text1,
            viewModel.state.modalError!!.text2)
        DisposableEffect(viewModel) {
            // retain data until disposed
            onDisposed { viewModel.state.modelError = null } 
        }
    }
}
On another look, your original code below
Copy code
if (viewModel.state.modalError != null) {
    Dialog(onDismissRequest = { viewModel.state.modalError = null }) {
...
should have the Dialog removed from the tree when
modalError = null
and hence the if-condition evaluates to false. Is the dialog dismissed through something other than a gesture? Could you post the full NPE stack strace?
c
Oh. This is an interesting update. Let me post the full stacktrace!
Copy code
2021-06-27 19:10:17.368 10948-10948/com.rollertoaster.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: <http://com.rollertoaster.app|com.rollertoaster.app>, PID: 10948
java.lang.NullPointerException
at com.rollertoaster.app.login.SignInFragment$onCreateView$1$2$1$2.invoke(SignInFragment.kt:102)
at com.rollertoaster.app.login.SignInFragment$onCreateView$1$2$1$2.invoke(SignInFragment.kt:100)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2156)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2399)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2574)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2567)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(SnapshotState.kt:523)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2560)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2536)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:613)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:763)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:102)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:446)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:415)
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:970)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:727)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Clicking through the link in the stacktrace puts my cursor on
Copy code
viewModel.state.modalError!!.text1,
Which is basically the thing that was blowing my mind of how this got past the if statement in the first place (to Doris' point "and hence the if-condition evaluates to false")
d
This NPE seems strange. @Colton Idle could you isolate the issue in a minimal repro case and file a bug so we can investigate it?
c
Will do so first thing tomorrow. 👍
l
You meant first thing on Monday?
c
Time zones are hard
😅 1