How to Block Fragment Re-Creation When BottomNavigationView's Tabs Selected? - kotlin

I am trying to use a new Navigation structure on my sample project.
I used BottomNavigationView in activity.xml, and it launches with NavigationController.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launcher)
val navController = Navigation.findNavController(this, R.id.navHostFragment)
NavigationUI.setupWithNavController(bottomNavigation, navController)
}
That's great so far, but every time I click on the tabs, the relative fragments are recreated every time.
How can I prevent this behavior?
I don't want to create new fragments each time.
I just want to use the first created fragments.
Note: I didn't use setOnNavigationItemSelectedListener() or any other listeners. The navigation structure itself regenerates the fragments.

You can prevent creation of new fragment every time by saving the last created fragment instance.
You need to create fragment stack list : val mFragmentStacks: MutableList<Stack<Fragment>>
You need to save fragmnet instance according to tab position : mFragmentStacks[currentStackIndex].push(fragment)
Check first the stack has any entry then attach the last fragment otherwise create new fragment.
if (!mFragmentStacks[index].isEmpty()) {
val fragment = mFragmentStacks[currentStackIndex].peek()
} else {
val fragment = DemoFragment()
mFragmentStacks[currentStackIndex].push(fragment)
}

To avoid the recreation fragment, you can check if there is an instance of this one on the backstack.
You can use the tag of backtask to search after for especific fragment instances

Related

Push notifications with back stack (Pending Intents, Kotlin)

I'm facing a problem with passing the launch URL from one activity to another, without creating a new Intent for my MainActivity.
I have a webview, which is work with OneSignal push notifications. I wanted to modify the grouping notifications content.
If there's a way to get the result I want (modifying notifications group layout for OneSignal) That would be awesome. I'll simply use the One Signal default action and that would be the best solution for me.
If I have to implement it on that way:
https://developer.android.com/training/notify-user/group
The problem is, when I start a new child activity of the MainActivity, I don't use the "StartActivity / StartActivityForResults" functions.
This is the extension of OSRemoteNotificationReceivedHandler (OneSignal class)
It's outside of my MainActivity class.
class NotificationServiceExtension : OSRemoteNotificationReceivedHandler {
#RequiresApi(Build.VERSION_CODES.N)
override fun remoteNotificationReceived(
context: Context,
notificationReceivedEvent: OSNotificationReceivedEvent
) {
val notification = notificationReceivedEvent.notification
val bigText = Html.fromHtml(notification.body, FROM_HTML_MODE_LEGACY).toString()
var smallText = Html.fromHtml(notification.additionalData["cleantitle"] as String, FROM_HTML_MODE_LEGACY).toString()
val summaryStatistics = Html.fromHtml(notification.additionalData["setSummaryText"] as String, FROM_HTML_MODE_LEGACY).toString()
if (smallText == "test") {
smallText = Html.fromHtml(notification.additionalData["smalltitle"] as String, FROM_HTML_MODE_LEGACY).toString()
}
else{
val name = Html.fromHtml(notification.additionalData["text"] as String, FROM_HTML_MODE_LEGACY).toString()
smallText += " from $name"
}
val smallContent = RemoteViews("com.webviewapp.mywebviewapp", R.layout.small_layout_notification)
val sum = RemoteViews("com.webviewapp.mywebviewapp", R.layout.summary_layout_notification)
val bigContent = RemoteViews("com.webviewapp.mywebviewapp", R.layout.large_notification_layout)
bigContent.setTextViewText(R.id.notification_title, smallText)
bigContent.setTextViewText(R.id.notification_content, bigText)
smallContent.setTextViewText(R.id.notification_title, smallText)
sum.setTextViewText(R.id.notification_title, summaryStatistics)
notificationReceivedEvent.complete(null)
var bp: Bitmap? = null
try {
bp =Picasso.get().load(notification.largeIcon).get()
smallContent.setImageViewBitmap(R.id.noti_pic, bp)
bigContent.setImageViewBitmap(R.id.noti_pic, bp)
}
catch(e:Exception){
print(e)
}
try {
val fid = notification.additionalData["fid"] as String
notificationId = fid.toInt()
}
catch(e:java.lang.Exception){
notificationId += Date().time.toInt()
}
val notificationOpenActivity = Intent(context.applicationContext, MainActivity::class.java)
.putExtra("launchURL", notification.additionalData["pushURL"] as String)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
val resultPendingIntent: PendingIntent? = TaskStackBuilder.create(context.applicationContext).run {
// Add the intent, which inflates the back stack
addNextIntentWithParentStack(notificationOpenActivity)
// Get the PendingIntent containing the entire back stack
getPendingIntent(0,
PendingIntent.FLAG_UPDATE_CURRENT)
}
val receivedNotification = NotificationCompat.Builder(context.applicationContext, NOTIFICATION_GROUP)
.setSmallIcon(R.drawable.myIcon)
.setColor(ContextCompat.getColor(context.applicationContext,R.color.blue_primary))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) // to expand button
.setAutoCancel(true)
.setVibrate(longArrayOf(500, 500, 500))
.setCustomBigContentView(bigContent)
.setCustomContentView(smallContent)
.setCustomHeadsUpContentView(sum)
.setChannelId(NOTIFICATION_CHANNEL)
.setGroup(NOTIFICATION_GROUP)
.setGroupSummary(false)
.setContentIntent(resultPendingIntent)
.build()
val summary = NotificationCompat.Builder(context.applicationContext, NOTIFICATION_GROUP)
.setSmallIcon(R.drawable.myIcon)
.setColor(ContextCompat.getColor(context.applicationContext,R.color.blue_primary))
//.setContentTitle(summaryStatistics.toString())
.setContentTitle(summaryStatistics)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(NotificationCompat.InboxStyle()
.setBigContentTitle(summaryStatistics)
.addLine(summaryStatistics)
.setSummaryText(summaryStatistics))
.setAutoCancel(true)
.setChannelId(NOTIFICATION_CHANNEL)
.setGroup(NOTIFICATION_GROUP)
.setGroupSummary(true)
.build()
NotificationManagerCompat.from(context.applicationContext).apply {
notify(notificationId, receivedNotification)
notify(SUMMARY_ID, summary)
}
}
}
And the notifications work good as I want. The problem is, How do I pass from the child activity the extra parameter to it's parent if I didn't create that child from the parent? I simply want to load it's URL into my webview, but also keep the back stack.
Also, how can I make sure I don't create multiple MainActivity if I won't use that child?
Thanks in advance.
Notification Back Stack
Android's documentation page "Start an Activity from a Notification" covers the back stack use case under the "Regular activity" suggestion.
Regular activity
This is an activity that exists as a part of your app's normal UX flow. So when the user arrives in the activity from the notification, the new task should include a complete back stack, allowing them to press Back and navigate up the app hierarchy.
I see you are using the addNextIntentWithParentStack method on TaskStackBuilder already in your code so looks like you may have already followed that page.
However there is one thing wrong with Google's docs here, the requestCode sent to getPendingIntent should be a unique value for your app.
Example:
getPendingIntent(
1234, // NOTE: Change this to a unique requestCode for your app
PendingIntent.FLAG_UPDATE_CURRENT
)
I have filed an docs issue with Google on this.
Lastly, since I didn't see this in your question make sure you have correctly added android:parentActivityName to your Activity in your AndroidManifest.xml per Android's "Define your app's Activity hierarchy"
OneSignal Details
Notification Tracking
Note that calling notificationReceivedEvent.complete(null) means OneSignal won't know anything about your notification you're displaying with NotificationManagerCompat. This changes a few things:
Click counts won't be sent to OneSignal
Notification won't be restored. (notifications are automatically cleaned when the app is "force stopped", device is rebooted, or app is updated)
Notification Groups
OneSignal can already do the grouping and summary you have in your code. Just set the "Group Key" on the dashboard, or android_group if you are sending the notification with the REST API.
Back stack
OneSignal doesn't allow you to control the back stack, it simply just always resumes that app and leaves the back stack un-effected. However you can disable this default behavior with com.onesignal.NotificationOpened.DEFAULT in your AndroidManifest.xml and use your own startActivity from the OneSignal.setNotificationOpenedHandler.

Kotlin StateFlow multiple subscriptions after returning from another Fragment

I'm trying to implement StateFlow in my app with MVVM architecture between ViewModel and Fragment.
In ViewModel:
...
private val _eventDataState = MutableStateFlow<EventDataState>(EventDataState.Loading)
val eventDataState: StateFlow<EventDataState> = _eventDataState
...
In Fragment:
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.eventDataState.collect { eventDataState ->
when (eventDataState) {
is EventDataState.Loading -> {}
is EventDataState.Success -> onEventDataUpdated(data = eventDataState)
is EventDataState.Deleted -> onEventDataDeleted()
}
}
}
}
viewModel.getEventData()
}
...
All seems to work well:
2022-04-15 17:57:20.352 5251-5251/ ... E/EVENT: Data state: Success(...)
But when I switch to another Fragment through Activity's SupportFragmentManager and then hit Back button I get multiple responses from StateFlow:
2022-04-15 17:57:30.358 5251-5251/ ... E/EVENT: Data state: Success(...)
2022-04-15 17:57:30.362 5251-5251/ ... E/EVENT: Data state: Success(...)
So the more times I switch to another Fragment and back - the more copies of response I get from StateFlow (this is not initial StateFlow value).
My guess is that when another Fragment are called the first one destroys its view but subscription to Flow is still exists, and when returning back from another Fragment the first one calls its onViewCreated and so creating another copy of subscription to this Flow. Nevertheless I was unable to find any solution to my case on stackoverflow or any other resources.
What have I missed?
You should use viewLifecycleOwner.lifecycleScope.launch in fragments. viewLifecycleOwner.lifecycleScope and all its jobs will be cancelled when the view is destroyed.

Double fragment instance when it comes from background

I've got the problem with creating multiple fragment instances in OnCreate. When i close the app using Home button and I will return to the app, the fragment instance is created one more time. How can I prevent this?
fragment = FragmentMain.newInstance(intent.extras?.getSerializable(DATA_MAIN)).also {
supportFragmentManager.beginTransaction()
.add(frameLayout.id, it, FragmentMain::class.java.simpleName)
.addToBackStack(FragmentMain::class.java.simpleName)
.commit()
}
This is expected behavior, as Android recreates the fragments after process death that are added to the fragment manager.
You're just also adding a 2nd new fragment on top of the one created by Android, which, you probably don't want to do.
fragment = when {
savedInstanceState == null -> FragmentMain.newInstance(intent.extras?.getSerializable(DATA_MAIN)).also {
supportFragmentManager.beginTransaction()
.add(frameLayout.id, it, FragmentMain::class.java.simpleName)
.addToBackStack(FragmentMain::class.java.simpleName)
.commit()
}
else -> supportFragmentManager.findFragmentByTag(FragmentMain::class.java.simpleName)
}

NavController in Activity & Fragment Issue

In my Activity I setup the NavController in OnCreate:
navController = findNavController(this, R.id.NavHostFragment)
In my Fragments, I setup the NavController in onViewCreated:
navController = Navigation.findNavController(view)
The Activity only navigates to a few Fragments (e.g. SitesFragment, ContactsFragment, TasksFragment etc) from the NavigationView when item is clicked as below:
R.id.nav_sites_fragment -> {
navController.popBackStack(R.id.sitesFragment, true)
navController.navigate(R.id.sitesFragment)
}
In the Fragments, click events (in RecyclerViews mainly heading to other Fragments, such as SiteFragment, ContactFragment, TaskFragment etc) are handled as below:
if (!navController.popBackStack(R.id.siteFragment, false)) {
// not in BackStack
navController.navigate(R.id.action_contactFragment_to_siteFragment)
}
The problem is, Fragments from Fragment actions remain in the backstack even when I popBackStack in the Activity actions..
I think my understanding of backstack is not correct as I'm not sure what is going on - but that maybe I have created two separate instances of NavController?
EDIT: Although I have looked at this post How to clear navigation Stack after navigating to another fragment in Android, I am still finding same behaviour. PopBackStack in Activity is not clearing Fragments added to the backstack..
For example:
SITES -> SITE -> CONTACT -> CONTACTS should remove first three fragments, but backpress still returns to CONTACT..
Ok, so I found this post https://github.com/android/architecture-components-samples/issues/767.
I modified the code a bit and in my Activity I have the following function:
private fun navigateWithClearStack(destination: Int) {
val navController = findNavController(R.id.NavHostFragment)
val navHostFragment: NavHostFragment = supportFragmentManager.findFragmentById(R.id.NavHostFragment) as NavHostFragment
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.business_navigation)
graph.startDestination = destination
navController.graph = graph
}
Then, where I handle the NavigationView clicks:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_sites_fragment -> navigateWithClearStack(R.id.sitesFragment)
R.id.nav_projects_fragment -> navigateWithClearStack(R.id.projectsFragment)
R.id.nav_contacts_fragment -> navigateWithClearStack(R.id.contactsFragment)
R.id.nav_tasks_fragment -> navigateWithClearStack(R.id.tasksFragment)
R.id.nav_profile_fragment -> makeToast("Todo: Profile Fragment")
R.id.nav_settings_fragment -> makeToast("Todo: Settings Fragment")
}
business_drawer_layout.closeDrawer(GravityCompat.START)
return true
}
So as users Navigate through app, Fragments are added to the BackStack (using popUpTo to deal with duplicates), but when user clicks shortcut back to one of 'starting' fragments, the graph is replaced, thus clearing the BackStack.. Which I think is kind of neat.

how to have loading in Kotlin

my MainActivity contains a ViewPager that loads 4 fragments, each fragment should load lots of data from the server.
so when my app wants to be run for the first time, it almost takes more than 3 seconds and the other times(for example, if you exit the app but not clean it from your 'recently app' window and reopen it) it takes almost 1 second.
while it is loading, it shows a white screen.
is there any way instead of showing a white screen till data become ready, I show my own image?
something like the splash page?
If you do long-running actions on the main thread, you risk getting an ANR crash.
Your layout for each fragment should have a loading view that is initially visible, and your data view. Something like this:
(not code)
FrameLayout
loading_view (can show a progress spinner or something, size is match parent)
content_view (probably a RecyclerView, initial visibility=GONE, size is match parent)
/FrameLayout
You need to do your long running action on a background thread or coroutine, and then swap the visibility of these two views when the data is ready to show in the UI.
You should not be directly handling the loading of data in your Fragment code, as Fragment is a UI controller. The Android Jetpack libraries provide the ViewModel class for this purpose. You would set up your ViewModel something like this. In this example, MyData could be anything. In your case it's likely a List or Set of something.
class MyBigDataViewModel(application: Application): AndroidViewModel(application) {
private val _myBigLiveData = MutableLiveData<MyData>()
val myBigLiveData: LiveData<MyData>() = _myBigLiveData
init {
loadMyBigData()
}
private fun loadMyBigData() {
viewModelScope.launch { // start a coroutine in the main UI thread
val myData: MyData = withContext(Dispatchers.Default) {
// code in this block is done on background coroutine
// Calculate MyData here and return it from lambda
// If you have a big for-loop, you might want to call yield()
// inside the loop to allow this job to be cancelled early if
// the Activity is closed before loading was finished.
//...
return#withContext calculatedData
}
// LiveData can only be accessed from the main UI thread so
// we do it outside the withContext block
_myBigLiveData.value = myData
}
}
}
Then in your fragment, you observe the live data to update the UI when it is ready. The below uses the fragment-ktx library, which you need to add to your project. You definitely should read the documentation on ViewModel.
class MyFragment: Fragment() {
// ViewModels should not be instantiated directly, or they won't be scoped to the
// UI life cycle correctly. The activityViewModels delegate handles instantiation for us.
private val model: MyBigDataViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.myBigLiveData.observe(this, Observer<MyData> { myData ->
loading_view.visibility = View.GONE
content_view.visibility = View.VISIBLE
// use myData to update the view content
})
}
}