Hyzam
Hyzam

Reputation: 183

Compose Snackbar not appearing on repeated error

I am new to jetpack compose and is trying to show an error snackbar whenever the error message I am observing is not null.

Scaffold(scaffoldState = scaffoldState) {
        LaunchedEffect(errorMessage) {
            if (errorMessage != null) {
                scope.launch {
                    scaffoldState.snackbarHostState.showSnackbar(errorMessage)
                }
            }
        }
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
              //some ui components inside here
        }
    }

The issue in the above code is that, the first time the error message changes from null to a particular message it appears fine. However on a repeated user action that produces the same error message it's not coming again.

P.S - I know this is happening due to placing the errorMessage as key inside the LaunchedEffect. My doubt is that, is there a different approach to achieve what I want?

Upvotes: 4

Views: 2219

Answers (4)

Vicky
Vicky

Reputation: 1957

Dirty hack:

So the idea is to restart the side-effect which in this case is LaunchedEffect

Append your error message with Unix timestamp and then remove the time stamp while displaying the error message in the snack bar.

something like this:

errorMessage = errorMessage + System.currentTimeMillis()
LaunchedEffect(errorMessage) {
   if (errorMessage != null) {
        val messageToDisplay = //code to remove the timepstamp 
        scaffoldState.snackbarHostState.showSnackbar(messageToDisplay)
   }
}
 

Upvotes: 0

Sergei S
Sergei S

Reputation: 3097

My solution is to support repeated messages/errors and don't show one message/error multiple times. Recomposition will be called just ones

1.Create a smart delegate that removes the value after reading. It is the main logic

class OnceReadDelegate<T>(private var value: T? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
        val currentValue = value
        value = null
        return currentValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        value = newValue
    }
}

2.Create a Message class that manages our text and type of message

class Message {
    var payload by OnceReadDelegate<String?>()
    var type: MessageType = MessageType.Error

    companion object {
        fun create(text: String?, messageType: MessageType = MessageType.Error) =
            Message().apply {
                payload = text
                this.type = messageType
            }
    }
}

sealed class MessageType {
    data object Regular : MessageType()
    data object Error : MessageType()
}

3.My example of state class with our Message class

data class SettingsPageState(
    val listWidgetState: List<WidgetViewState> = emptyList(),
    val listAgeFormat: List<AgeFormat> = emptyList(),
    val message: Message? = null,
    val isLoading: Boolean = true,
    val isEmpty: Boolean = false
)

4.My example of updatin the state with a new message

mutableState.update { previousState ->
    previousState.copy(message = Message.create(exception?.message))
}

5.And finally Composable function how to how Snackbar

@Composable
fun SettingsScreen() {
    ...

    LaunchedEffect(state.message) {
        val text = state.message?.payload
        if (!text.isNullOrEmpty()) {
            //your method to show snackbar
            onShowSnackbar(text , null)
        }
    }
   ...
}

It is all)

Upvotes: 0

Gast&#243;n Saill&#233;n
Gast&#243;n Saill&#233;n

Reputation: 13159

I see some issues with the answer provided above

First, there is no need of relaunching a coroutine inside a LaunchedEffect (which will execute a suspend function also) , so we can remove the scope.launch from the LaunchedEffect

Second, if we use resetErrorMessage() before .showSnackBar happens, we will emit a null (empty) message. We should reset the error message after the showSnackbar has been executed.

LaunchedEffect(errorMessage) {
   if (errorMessage != null) {
        scaffoldState.snackbarHostState.showSnackbar(errorMessage)
        resetErrorMessage() // reset errorMessage
   }
}

Upvotes: 1

nglauber
nglauber

Reputation: 24044

This is happening because the LaunchedEffect will run again just in case the errorMessage has changed. What you can do is:

LaunchedEffect(errorMessage) {
   if (errorMessage != null) {
        resetErrorMessage() // reset errorMessage
        scope.launch {
            scaffoldState.snackbarHostState.showSnackbar(errorMessage)
        }
   }
}

The resetErrorMessage must set the errorMessage to null, so the LaunchedEffect will run again, but since you're checking if it is not null, nothing will happen. But as soon you receive a new error message, the LaunchedEffect will be executed again.

Upvotes: 6

Related Questions