Jetpack Compose + Firebase Crash java.lang.ArrayIndexOutOfBoundsException at androidx.compose.runtime.SlotTableKt.groupSize(SlotTable.kt:2558)

Viewed 541

I am working on an application using firbase firestore realtime database. The application once the user logs in for the first time will check if the user has a username. If not, it will display a popup to allow user name to change. User can also change the username via the drawer menu, which will also display the same popup.

This what my viewmodel looks like:

class MainViewModel : ViewModel() {
    private val _user = FirebaseAuth.getInstance().currentUser
    private val _isLoggedIn = MutableLiveData(FirebaseAuth.getInstance().currentUser != null)
    val profile = UserProfile(_user.uid)

    var isLoaded: LiveData<Boolean> = profile.isLoaded
    var isLoggedIn: LiveData<Boolean> = _isLoggedIn

    init {
        getInstance().addAuthStateListener() {
            _isLoggedIn.value = it.currentUser != null
        }
    }

    fun updateUsername(username: String) {
        profile.updateUsername(username)
    }

    fun logOut(){
        FirebaseAuth.getInstance().signOut()
    }

}

My Profile class looks like this:

class UserProfile(private val userId: String) {

    private val _isLoaded = MutableLiveData(false)
    private val _username = MutableLiveData("")

    var isLoaded: LiveData<Boolean> = _isLoaded
    var username: LiveData<String> = _username

    init {
        if (userId != "") {
            Firebase.firestore.collection("Users").document(userId)
                .addSnapshotListener { snapshot, e ->
                    if (e != null)
                        return@addSnapshotListener
                    if (snapshot != null) {
                        if (snapshot.exists()) {
                            snapshot.getString("username")?.let { setUsername(it) }
                            if (_isLoaded.value == false)
                                _isLoaded.value = true
                        } else {
                            val data = hashMapOf("username" to "")
                            FirebaseFirestore.getInstance().collection("Users").document("")
                                .set(data)
                        }
                    }
                }
        }
    }

    private fun setUsername(newUsername: String) {
        _username.value = newUsername
    }

    fun updateUsername(newUsername: String) {
        if (userId != "") {
            val data = hashMapOf("username" to newUsername)
            FirebaseFirestore.getInstance().collection("Users").document(userId)
                .set(data, SetOptions.merge())
        }
    }
}

And my Main composable looks like this:

fun Main(mainViewmodel: MainViewModel = viewModel(), navHostController: NavHostController) {
    var fabShape = CircleShape
    val state = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    val isLoggedIn: Boolean by mainViewmodel.isLoggedIn.observeAsState(true)
    val isLoaded: Boolean by mainViewmodel.isLoaded.observeAsState(false)
    val username: String by mainViewmodel.profile.username.observeAsState("")
    var showUsernameChange: Boolean by remember (username){ mutableStateOf(false)}
    if (!isLoggedIn)
        navHostController.navigate(Screen.Login.route)
    Scaffold(
        scaffoldState = state,
        topBar = {
            TopAppBar(
                title = { Text(text = "AppName") },
                navigationIcon = {
                    IconButton(onClick = {
                        if (state.drawerState.isClosed) scope.launch { state.drawerState.open() }
                        else scope.launch { state.drawerState.close() }
                    }) {
                        Icon(Icons.Filled.AccountCircle, null)
                    }
                }

            )
        },
        bottomBar = {
            BottomAppBar(cutoutShape = fabShape) {
                BottomNavigationItem(
                    selected = true,
                    onClick = { /*TODO*/ },
                    icon = { Icon(Icons.Filled.Home, null) })
                BottomNavigationItem(
                    selected = false,
                    onClick = { /*TODO*/ },
                    icon = { Icon(Icons.Filled.Favorite, null) })
                BottomNavigationItem(
                    selected = false,
                    onClick = { /*TODO*/ },
                    icon = { Icon(Icons.Filled.Search, null) })
                BottomNavigationItem(
                    selected = false,
                    onClick = { /*TODO*/ },
                    icon = { Icon(Icons.Filled.Settings, null) })
            }
        },
        drawerShape = RoundedCornerShape(
            topStart = 10.dp,
            topEnd = 10.dp,
            bottomStart = 10.dp,
            bottomEnd = 10.dp,
        ),
        drawerContent = {
            Drawer(username = username, onLogoutClick = {mainViewmodel.logOut()}, onUsernameChangeClick = {
                showUsernameChange = true
                scope.launch { state.drawerState.close() }
            })
        },
        drawerGesturesEnabled = true,
        isFloatingActionButtonDocked = true,
        floatingActionButtonPosition = FabPosition.Center,
        floatingActionButton = {
            FloatingActionButton(
                onClick = { /*TODO*/ },
                shape = fabShape,
                content = { Icon(Icons.Filled.Add, null) })
        }
    ) {
        if (isLoaded) {
            content(mainViewmodel = mainViewmodel)
            val hasUsername: Boolean by remember(username) { mutableStateOf(username.isNotEmpty())}
            if (!hasUsername || showUsernameChange) {
                var cacheUsername: String by remember { mutableStateOf(username) }
                    changeUsername(
                        cacheUsername,
                        onNameChange = { cacheUsername = it },
                        onDismiss = {if(showUsernameChange){
                            showUsernameChange = false
                        } },
                        onDoneClick = {
                            mainViewmodel.updateUsername(cacheUsername)
                        })
                }
        } else {
            loading()
        }

    }
}

Finally, my change user name composable looks like this:

fun changeUsername(username: String, onNameChange : (String) -> Unit, onDoneClick : () -> Unit, onDismiss: () -> Unit){
    val error : Boolean = username.isNullOrEmpty() || username.length > 64 || username.length < 4
    Dialog(onDismissRequest = { onDismiss() }) {
        Card() {
            Column(modifier = Modifier.padding(4.dp)) {
                Text(text = "Update Username", modifier = Modifier.padding(4.dp), style = MaterialTheme.typography.subtitle2)
                TextField(value = username,
                    onValueChange = { onNameChange(it) },
                    modifier = Modifier.padding(4.dp),
                    isError = error,
                    placeholder = { Text("Username") })
                TextButton(
                    onClick = { onDoneClick() }, modifier = Modifier
                        .padding(4.dp)
                        .align(Alignment.End),
                    enabled = !error
                ) {
                    Text("Done")
                }
            }

        }

    }

}

The Problem Whenever the user updates the username, we update the firebase database. Whenever the firebase database is updated we get a new document snapshot. This results in a crash, with the following signature:

 java.lang.ArrayIndexOutOfBoundsException: length=5120; index=-2
        at androidx.compose.runtime.SlotTableKt.groupSize(SlotTable.kt:2558)
        at androidx.compose.runtime.SlotTableKt.access$groupSize(SlotTable.kt:1)
        at androidx.compose.runtime.SlotReader.groupSize(SlotTable.kt:596)
        at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2215)
        at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2499)
        at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2625)
        at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:406)
        at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:724)
        at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:100)
        at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:437)
        at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:411)
        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:112)
        at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:43)
        at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:72)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:964)
        at android.view.Choreographer.doCallbacks(Choreographer.java:790)
        at android.view.Choreographer.doFrame(Choreographer.java:721)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Whats going on here? How can we fix it?

0 Answers
Related