I'm working on an Android project that has a MainActivity with a bottom navigation bar and a navigation component. The main activity hosts four fragments: Home, Chat, Notification, and Profile. The Home fragment is the destination fragment.
In the Home fragment, I use a ViewModel to fetch data. I call a getData
function in the Home fragment to bring in stories. I also maintain a flag to determine if this function should be called when the fragment changes.
However, I'm encountering an issue where Snackbar or Toast messages are shown multiple times when the user navigates between fragments, causing the fragment to be recreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
MyLogger.v(isFunctionCall = true)
handleInitialization()
}
private fun handleInitialization() {
MyLogger.v(isFunctionCall = true)
initUi()
subscribeToObserver()
if (!isDataLoaded) {
getData()
isDataLoaded=true
}
}
below is my obesever function.
private fun subscribeToObserver() {
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.userStories.onEach { response ->
response.let {
MyLogger.d(msg = "Response coming and it was $response")
when (response) {
is Resource.Success -> {
userData.clear()
response.data?.let {
setData(it)
} ?: run {
setData()
if (isUserStorySnackBarShouldShow){
Helper.showSnackBar(
(requireActivity() as MainActivity).findViewById(R.id.coordLayout),
response.message.toString()
)
}
}
isUserStorySnackBarShouldShow=false
}
is Resource.Loading -> {
isUserStorySnackBarShouldShow=true
}
is Resource.Error -> {
if (isUserStorySnackBarShouldShow){
Helper.showSnackBar(
(requireActivity() as MainActivity).findViewById(R.id.coordLayout),
response.message.toString()
)
isUserStorySnackBarShouldShow=false
}
}
}
}
}.launchIn(this)
// bufferWithDelay is because collector is slow ( MyLoader take some millisecond time to show ui and publisher is fast. So it throw / public item and collector collect item and call loader but loader is not initialize at. So when you access it ui , it throw null pointer exception because binding is not initialize.
AppBroadcastHelper.uploadStories.bufferWithDelay(100).onEach {
MyLogger.v(tagStory, isFunctionCall = true)
when (it.first) {
Constants.StoryUploadState.StartUploading , Constants.StoryUploadState.Uploading ,Constants.StoryUploadState.SavingStory -> {
updateLoader(
it.first,
it.second ?: 0
)
}
Constants.StoryUploadState.UploadingFail, Constants.StoryUploadState.UrlNotGet ->{
MyLogger.e(tagStory, msg = "Something went wrong :- ${it.first.name} occurred !")
hideLoader()
Helper.showSnackBar(
(requireActivity() as MainActivity).findViewById(
R.id.coordLayout
), "Story uploading failed !"
)
}
Constants.StoryUploadState.StoryUploadedSuccessfully -> {
MyLogger.v(
tagStory,
msg = "Loader is show with StoryUploadedSuccessfully state ..."
)
hideLoader()
Helper.showSuccessSnackBar(
(requireActivity() as MainActivity).findViewById(
R.id.coordLayout
), "Story uploaded successfully !"
)
}
}
}.launchIn(this)
homeViewModel.uploadStories.onEach { response ->
when (response) {
is Resource.Success -> {
// Do nothing here
}
is Resource.Loading -> {
MyLogger.v(tagStory, msg = "Story uploading now and now loader is showing !")
showLoader()
}
is Resource.Error -> {
MyLogger.e(
tagStory,
msg = "Some error occurred during post uploading :- ${response.message.toString()}"
)
hideLoader()
Helper.showSnackBar(
(requireActivity() as MainActivity).findViewById(
R.id.coordLayout
),
response.message.toString()
)
}
}
}.launchIn(this)
}
}
}
The Problem:
When I navigate to another fragment and come back to Home, the fragment is recreated (from onCreateView
). This causes my observer to be subscribed again, and the StateFlow holds data that shows the Snackbar again, even though I only want it to show once.
To prevent the Snackbar from being shown multiple times when the fragment is recreated during tab changes.
The way described in the Android documentation, at least recently, is to use a mutable class where there is a mutable Boolean that does not participate in the equals
. For example, defined outside the constructor of a data
class. This can keep track of whether the one-time message has been consumed.
You probably have an error class as part of your sealed interface/class that looks something like this:
data class Error(val message: String, val cause: Exception): Resource
Change it like this:
data class Error(val message: String, val cause: Exception): Resource {
var hasBeenMessagedToUser = false
}
Then in your code that shows the snackbar, only show the snackbar if this property is false. And when you do show the snackbar, change it to true.