I have an activity with a product list fragment and many other fragments and I am trying to use architecture component navigation controller.
The problem is: it replaces the (start destination) product list fragment and I don't want the list to be reloaded when user click back button.
How to make the fragment transaction as add not replace?
Android navigation component just replace but you want to add fragment instead of replace like dialog you can use this but need to min. "Version 2.1.0" for navigation component.
Solution
and you can see "Dialog destinations"
I faced the same problem, while waiting on add and other options for fragment transactions I implemented this work around to preserve the state when hitting back.
I just added a check if the binding is present then I just restore the previous state, the same with the networking call, I added a check if the data is present in view model then don't do the network refetching. After testing it works as expected.
EDIT:
For the recycler view I believe it will automatically return to the same sate the list was before you navigated from the fragment but storing the position in the onSavedInstanceSate is also possible
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel =
ViewModelProviders.of(this, mViewModelFactory).get(SearchResultsViewModel::class.java)
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//some stuff
root
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//reload only if search results are empty
if (viewModel.searchResults.isEmpty()) {
args.searchKey.let {
binding.toolbarHome.title = it
viewModel.onSearchResultRequest(it)
}
}
}
You have to override NavHostFragment's createFragmentNavigator method and return YourFragmentNavigator.
YourFragmentNavigator must override FragmentNavigator's navigate method.
Copy and paste FragmentNavigator's navigate method to your YourFragmentNavigator.
In navigate method, change the line ft.replace(mContainerId, frag); with
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
The solution will look like this:
class YourNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<...> {
return YourFragmentNavigator(...)
}}
....
class YourFragmentNavigator(...) : FragmentNavigator(...) {
override fun navigate(...){
....
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
....
}}
in your xml use YourNavHostFragment.
I was facing the same issue but in my case I updated my code to use livedata and viewmodel.
when you press back the viewmodel is not created again and thus your data is retained.
make sure you do the api call in init method of viewmodel, so that it happens only once when viewmodel is created
just copy the FragmentNavigator's code (300 lines) and replace replace() with add(). this is the best solution for me at the moment.
#Navigator.Name("fragment")
public class CustomFragmentNavigator extends
Navigator<...> {
...
public NavDestination navigate(...) {
...
ft.add(mContainerId, frag);
...
}
...
}
#Rainmaker is right in my opinion, I did the same thing.
We can also save the recycler view position/state in onSaveInstanceState
in order to return to the same recycler view position when navigating back to the list fragment.
You can use these classes as your custom NavHostFragment and Navigator
NavHostFragment
class YourNavHostFragment : NavHostFragment() {
override fun onCreateNavHostController(navHostController: NavHostController) {
/**
* Done this on purpose.
*/
if (false) {
super.onCreateNavHostController(navHostController)
}
val containerId = if (id != 0 && id != View.NO_ID) id else R.id.nav_host_fragment_container
navController.navigatorProvider += YourFragmentNavigator(requireContext(), parentFragmentManager, containerId)
navController.navigatorProvider += DialogFragmentNavigator(requireContext(), childFragmentManager)
}
}
Navigator
#Navigator.Name("fragment")
class YourFragmentNavigator(private val context: Context, private val fragmentManager: FragmentManager, private val containerId: Int) : Navigator<YourFragmentNavigator.Destination>() {
private val savedIds = mutableSetOf<String>()
/**
* {#inheritDoc}
*
* This method must call
* [FragmentTransaction.setPrimaryNavigationFragment]
* if the pop succeeded so that the newly visible Fragment can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation pops the Fragment
* asynchronously, so the newly visible Fragment from the back stack
* is not instantly available after this call completes.
*/
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state")
return
}
if (savedState) {
val beforePopList = state.backStack.value
val initialEntry = beforePopList.first()
// Get the set of entries that are going to be popped
val poppedList = beforePopList.subList(
beforePopList.indexOf(popUpTo),
beforePopList.size
)
// Now go through the list in reversed order (i.e., started from the most added)
// and save the back stack state of each.
for (entry in poppedList.reversed()) {
if (entry == initialEntry) {
Log.i(TAG, "FragmentManager cannot save the state of the initial destination $entry")
} else {
fragmentManager.saveBackStack(entry.id)
savedIds += entry.id
}
}
} else {
fragmentManager.popBackStack(popUpTo.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
state.pop(popUpTo, savedState)
}
override fun createDestination(): Destination {
return Destination(this)
}
/**
* Instantiates the Fragment via the FragmentManager's
* [androidx.fragment.app.FragmentFactory].
*
* Note that this method is **not** responsible for calling
* [Fragment.setArguments] on the returned Fragment instance.
*
* #param context Context providing the correct [ClassLoader]
* #param fragmentManager FragmentManager the Fragment will be added to
* #param className The Fragment to instantiate
* #param args The Fragment's arguments, if any
* #return A new fragment instance.
*/
#Suppress("DeprecatedCallableAddReplaceWith")
#Deprecated(
"""Set a custom {#link androidx.fragment.app.FragmentFactory} via
{#link FragmentManager#setFragmentFactory(FragmentFactory)} to control
instantiation of Fragments."""
)
fun instantiateFragment(context: Context, fragmentManager: FragmentManager, className: String, args: Bundle?): Fragment {
return fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
}
/**
* {#inheritDoc}
*
* This method should always call
* [FragmentTransaction.setPrimaryNavigationFragment]
* so that the Fragment associated with the new destination can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation commits the new Fragment
* asynchronously, so the new Fragment is not instantly available
* after this call completes.
*/
override fun navigate(entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state")
return
}
for (entry in entries) {
navigate(entry, navOptions, navigatorExtras)
}
}
private fun navigate(entry: NavBackStackEntry, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
val backStack = state.backStack.value
val initialNavigation = backStack.isEmpty()
val restoreState = (navOptions != null && !initialNavigation && navOptions.shouldRestoreState() && savedIds.remove(entry.id))
if (restoreState) {
// Restore back stack does all the work to restore the entry
fragmentManager.restoreBackStack(entry.id)
state.push(entry)
return
}
val destination = entry.destination as Destination
val args = entry.arguments
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
frag.arguments = args
val ft = fragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
#IdRes val destId = destination.id
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && backStack.last().destination.id == destId)
val isAdded = when {
initialNavigation -> {
true
}
isSingleTopReplacement -> {
// Single Top means we only want one instance on the back stack
if (backStack.size > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
fragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
ft.addToBackStack(entry.id)
}
false
}
else -> {
ft.addToBackStack(entry.id)
true
}
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key, value)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
if (isAdded) {
state.push(entry)
}
}
override fun onSaveState(): Bundle? {
if (savedIds.isEmpty()) {
return null
}
return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds))
}
override fun onRestoreState(savedState: Bundle) {
val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS)
if (savedIds != null) {
this.savedIds.clear()
this.savedIds += savedIds
}
}
/**
* NavDestination specific to [FragmentNavigator]
*
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [setClassName].
*
* #param fragmentNavigator The [FragmentNavigator] which this destination will be associated
* with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method.
*/
#NavDestination.ClassType(Fragment::class)
open class Destination
constructor(fragmentNavigator: Navigator<out Destination>) : NavDestination(fragmentNavigator) {
/**
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [setClassName].
*
* #param navigatorProvider The [NavController] which this destination
* will be associated with.
*/
//public constructor(navigatorProvider: NavigatorProvider) : this(navigatorProvider.getNavigator(FragmentNavigator::class.java))
#CallSuper
public override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
val className = array.getString(R.styleable.FragmentNavigator_android_name)
if (className != null) setClassName(className)
}
}
/**
* Set the Fragment class name associated with this destination
* #param className The class name of the Fragment to show when you navigate to this
* destination
* #return this [Destination]
*/
fun setClassName(className: String): Destination {
_className = className
return this
}
private var _className: String? = null
/**
* The Fragment's class name associated with this destination
*
* #throws IllegalStateException when no Fragment class was set.
*/
val className: String
get() {
checkNotNull(_className) { "Fragment class was not set" }
return _className as String
}
override fun toString(): String {
val sb = StringBuilder()
sb.append(super.toString())
sb.append(" class=")
if (_className == null) {
sb.append("null")
} else {
sb.append(_className)
}
return sb.toString()
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is Destination) return false
return super.equals(other) && _className == other._className
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + _className.hashCode()
return result
}
}
/**
* Extras that can be passed to FragmentNavigator to enable Fragment specific behavior
*/
class Extras internal constructor(sharedElements: Map<View, String>) :
Navigator.Extras {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* The map of shared elements associated with these Extras. The returned map
* is an [unmodifiable][Map] copy of the underlying map and should be treated as immutable.
*/
val sharedElements: Map<View, String>
get() = _sharedElements.toMap()
/**
* Builder for constructing new [Extras] instances. The resulting instances are
* immutable.
*/
class Builder {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* Adds multiple shared elements for mapping Views in the current Fragment to
* transitionNames in the Fragment being navigated to.
*
* #param sharedElements Shared element pairs to add
* #return this [Builder]
*/
fun addSharedElements(sharedElements: Map<View, String>): Builder {
for ((view, name) in sharedElements) {
addSharedElement(view, name)
}
return this
}
/**
* Maps the given View in the current Fragment to the given transition name in the
* Fragment being navigated to.
*
* #param sharedElement A View in the current Fragment to match with a View in the
* Fragment being navigated to.
* #param name The transitionName of the View in the Fragment being navigated to that
* should be matched to the shared element.
* #return this [Builder]
* #see FragmentTransaction.addSharedElement
*/
fun addSharedElement(sharedElement: View, name: String): Builder {
_sharedElements[sharedElement] = name
return this
}
/**
* Constructs the final [Extras] instance.
*
* #return An immutable [Extras] instance.
*/
fun build(): Extras {
return Extras(_sharedElements)
}
}
init {
_sharedElements.putAll(sharedElements)
}
}
private companion object {
private const val TAG = "YourFragmentNavigator"
private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
}
}
Usage
In your activity/fragment your FragmentContainerView should look like this.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/navHost"
android:name="in.your.android.core.platform.navigation.YourNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
After searching a bit, it's not possible, but the problem itself can be solved with viewmodel and livedata or rxjava.
So fragment state is kept after transactions and my product list will not reload each time
Related
I am getting this leak after several rotation and app being in background. Here is stack trace which I m not able to understand the cause. Also 32474006 bytes retained object is very much. I have 10 same leak.
32474006 bytes retained by leaking objects
Displaying only 1 leak trace out of 10 with the same signature
Signature: 329ec5b3be0cfe3ed2fc888129f5a6be93fb9
┬───
│ GC Root: Global variable in native code
│
├─ android.app.LoadedApk$ServiceDispatcher$DeathMonitor instance
│ Leaking: UNKNOWN
│ ↓ LoadedApk$ServiceDispatcher$DeathMonitor.this$0
│ ~~~~~~
├─ android.app.LoadedApk$ServiceDispatcher instance
│ Leaking: UNKNOWN
│ ↓ LoadedApk$ServiceDispatcher.mContext
│ ~~~~~~~~
╰→ com.ics.homework.ui.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.MainActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
key = 8bcc50f8-ea3f-47d9-8dc3-904042a58df4
watchDurationMillis = 60220
retainedDurationMillis = 55216
====================================
0 LIBRARY LEAKS
Cause of Leak
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener {
....
override fun onStart() {
super.onStart()
findChromeCustomTabsNavigator(navController).bindCustomTabsService()
}
....
}
I have tried to implement Chrome Custom Tab using This Tutorial
#Navigator.Name("chrome")
class ChromeCustomTabsNavigator(
private val context: Context
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
/**
* Initialized when `findChromeCustomTabsNavigator().bindCustomTabsService()` is called.
*/
private var session: CustomTabsSession? = null
private val urisInProgress = mutableMapOf<Uri, Long>()
private var connection :CustomTabsServiceConnection?= null
/**
* Prevent the user from repeatedly launching Chrome Custom Tabs for the same URL. Throttle
* rapid repeats unless the URL has finished loading, or this timeout has passed (just in
* case something went wrong with detecting that the page finished loading).
* Feel free to change this value with [Fragment.findChromeCustomTabsNavigator.throttleTimeout()]
* if you feel the need, or for testing purposes.
* Defaults to two seconds.
*/
#SuppressWarnings("WeakerAccess")
var throttleTimeout: Long = 2000L
private val upIconBitmap: Bitmap by lazy {
AppCompatResources.getDrawable(context, R.drawable.ic_baseline_keyboard_backspace_24)?.toBitmap()!!
}
override fun createDestination() =
Destination(this)
override fun navigate(
destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras?
): NavDestination? {
// The Navigation framework enforces the destination URL being non-null
val uri = args?.getParcelable<Uri>(KEY_URI)!!
if (!shouldAllowLaunch(uri)) return null
buildCustomTabsIntent(destination).launchUrl(context, uri)
return null // Do not add to the back stack, managed by Chrome Custom Tabs
}
override fun popBackStack() = true // Managed by Chrome Custom Tabs
private fun buildCustomTabsIntent(destination: Destination): CustomTabsIntent {
val builder = CustomTabsIntent.Builder()
val params = CustomTabColorSchemeParams.Builder()
session?.let { builder.setSession(it) }
builder.setColorScheme(destination.colorScheme)
if (destination.toolbarColor != 0) {
params.setToolbarColor(ContextCompat.getColor(context, destination.toolbarColor))
}
if (destination.navigationBarColor != 0) {
params.setNavigationBarColor(ContextCompat.getColor(context, destination.navigationBarColor))
}
builder.setDefaultColorSchemeParams(params.build())
builder.setStartAnimations(context, destination.enterAnim, destination.popEnterAnim)
builder.setExitAnimations(context, destination.popExitAnim, destination.exitAnim)
builder.setShowTitle(destination.showTitle)
if (destination.upInsteadOfClose) {
builder.setCloseButtonIcon(upIconBitmap)
}
if (destination.addDefaultShareMenuItem) {
builder.setShareState(CustomTabsIntent.SHARE_STATE_ON)
}
val customTabsIntent = builder.build()
// Adding referrer so websites know where their traffic came from, per Google's recommendations:
// https://medium.com/google-developers/best-practices-for-custom-tabs-5700e55143ee
customTabsIntent.intent.putExtra(
Intent.EXTRA_REFERRER, Uri.parse("android-app://" + context.packageName)
)
return customTabsIntent
}
private fun shouldAllowLaunch(uri: Uri): Boolean {
urisInProgress[uri]?.let { tabStartTime ->
// Have we launched this URI before recently?
if (System.currentTimeMillis() - tabStartTime > throttleTimeout) {
// Since we've exceeded the throttle timeout, continue as normal, launching
// the destination and updating the time.
Timber.w("Throttle timeout for $uri exceeded. This means ChromeCustomTabsNavigator failed to accurately determine that the URL finished loading. If you see this error frequently, it could indicate a bug in ChromeCustomTabsNavigator.")
} else {
// The user has tried to repeatedly open the same URL in rapid succession. Let them chill.
// The tab probably just hasn't opened yet. Abort opening the tab a second time.
urisInProgress.remove(uri)
return false
}
}
urisInProgress[uri] = System.currentTimeMillis()
return true
}
/**
* Boilerplate setup for Chrome Custom Tabs. This should suffice for most apps using Chrome
* Custom Tabs with the Navigation component. It warms up Chrome in advance to save a few
* milliseconds, and sets a [CustomTabsSession] for the [ChromeCustomTabsNavigator] so that
* [CustomTabsSession.mayLaunchUrl] can be called from application code.
*/
fun bindCustomTabsService() {
connection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
client.warmup(0L)
session = client.newSession(customTabsCallback)
//context.unbindService(this)
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
CustomTabsClient.bindCustomTabsService(context, CUSTOM_TAB_PACKAGE_NAME, connection!!)
}
fun unBindCustomTabsService(){
if(connection !=null) return
context.unbindService(connection!!)
}
/**
* Possibly pre-load one or more URLs. Note that
* per https://developer.chrome.com/multidevice/android/customtabs#pre-render-content,
* mayLaunchUrl should only be used if the odds are at least 50% of the user clicking
* the link.
* #see [CustomTabsSession.mayLaunchUrl] for more details on mayLaunchUrl.
*/
fun mayLaunchUrl(url: Uri, extras: Bundle? = null, otherLikelyBundles: List<Bundle>? = null) {
session?.mayLaunchUrl(url, extras, otherLikelyBundles)
}
val customTabsCallback: CustomTabsCallback by lazy {
object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
when (navigationEvent) {
NAVIGATION_ABORTED, NAVIGATION_FAILED, NAVIGATION_FINISHED -> {
// Navigation has finished. Remove the indication that page has not finished
// loading, so we will allow the user to try to open the same page again.
with(urisInProgress.entries) {
remove(first())
}
}
}
}
}
}
companion object {
private const val TAG = "ChromeTabsNavigator"
private const val CUSTOM_TAB_PACKAGE_NAME = "com.android.chrome"
const val KEY_URI = "uri"
}
#NavDestination.ClassType(Activity::class)
class Destination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
var colorScheme: Int = 1
#ColorRes
var toolbarColor: Int = 0
#ColorRes
var navigationBarColor: Int = 0
#AnimRes
var enterAnim: Int = 0
#AnimRes
var exitAnim: Int = 0
#AnimRes
var popEnterAnim: Int = 0
#AnimRes
var popExitAnim: Int = 0
var showTitle: Boolean = false
var upInsteadOfClose: Boolean = false
var addDefaultShareMenuItem: Boolean = false
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.withStyledAttributes(attrs, R.styleable.ChromeCustomTabsNavigator, 0, 0) {
colorScheme = getInt(R.styleable.ChromeCustomTabsNavigator_colorScheme, 0)
toolbarColor = getResourceId(R.styleable.ChromeCustomTabsNavigator_toolbarColor, 0)
navigationBarColor =
getResourceId(R.styleable.ChromeCustomTabsNavigator_navigationBarColor, 0)
enterAnim = getResourceId(R.styleable.ChromeCustomTabsNavigator_enterAnim, 0)
exitAnim = getResourceId(R.styleable.ChromeCustomTabsNavigator_exitAnim, 0)
popEnterAnim = getResourceId(R.styleable.ChromeCustomTabsNavigator_popEnterAnim, 0)
popExitAnim = getResourceId(R.styleable.ChromeCustomTabsNavigator_popExitAnim, 0)
showTitle = getBoolean(R.styleable.ChromeCustomTabsNavigator_showTitle, false)
upInsteadOfClose =
getBoolean(R.styleable.ChromeCustomTabsNavigator_upInsteadOfClose, false)
addDefaultShareMenuItem =
getBoolean(R.styleable.ChromeCustomTabsNavigator_addDefaultShareMenuItem, false)
}
}
}
}
/**
* From https://proandroiddev.com/add-chrome-custom-tabs-to-the-android-navigation-component-75092ce20c6a
*/
class EnhancedNavHostFragment : NavHostFragment() {
#SuppressLint("RestrictedApi")
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
context?.let { navController.navigatorProvider += ChromeCustomTabsNavigator(it) }
}
}
Extension Function
fun Fragment.findChromeCustomTabsNavigator(): ChromeCustomTabsNavigator =
findNavController().navigatorProvider.getNavigator(ChromeCustomTabsNavigator::class.java)
fun AppCompatActivity.findChromeCustomTabsNavigator(navController: NavController): ChromeCustomTabsNavigator =
navController.navigatorProvider.getNavigator(ChromeCustomTabsNavigator::class.java)
Note I am binding service in main activity because I have to use custom tab within drawer as well as some fragments too.
EnhancedNavHostFragment- Added Inside main Activity Layout
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="com.ics.homework.utils.EnhancedNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbar"
app:navGraph="#navigation/main_nav_graph" />
This looks like a leak in the Android Framework code, which you can figure out by looking at the sources for LoadedApk.java.
When a separate process connects to a service in your process, a ServiceDispatcher.DeathMonitor is created. This is used to notify LoadedApk if the connected process dies.
The leak here is happening because the service gets destroyed, but somehow the native reference to the DeathMonitor isn't released. That seems to imply that IBinder.unleakToDeath() isn't called by LoadedApk.
You should try to repro on the latest Android release and see if the bug still exists. If yes, file a bug in AOSP.
Looks like the leak is caused by the line:
navController.addOnDestinationChangedListener(this)
Here you're passing an instance of your activity to the controller but never removing it when the activity is destroyed, hence the leak.
I would recommend adding the listener instead in onResume and removing it in onPause.
I'm developing a timetable application and I have a fragment in it, which displays the users subjects and lessons in a RecyclerView, like this:
It works fine, but only for the first time. The moment I open the fragment for the second time (or just simply reload it) I get this weird bug: It starts filling in random circles with that blue-ish color as if there were lessons on that particular day. For example.: Let's say the user added two subjects, Calculus I and Databases, and a lesson to Databases on Wednesday. As soon as the fragment gets opened for the second time, there'll be a filled-in circle next to Calculus I's Wednesday as well.
I think there must be a problem somewhere in the SubjectsViewHolder class, but just in case I missed something, here's the whole RecyclerView's adapter file:
// Each constant value represents a view type
private const val VIEW_TYPE_NOT_EMPTY = 0
private const val VIEW_TYPE_EMPTY = 1
/**
* A [RecyclerView.Adapter] subclass.
* This class serves as an adapter for RecyclerViews
* which were created to display lessons.
*
* #property subjectsList An [ArrayList] containing all [Subject] objects to display
* #property lessonsList An [ArrayList] containing lessons of subjects
* #property listener Fragments that use this class must implement [OnSubjectClickListener]
*/
class SubjectsRecyclerViewAdapter(
private var subjectsList: ArrayList<Subject>?,
private var lessonsList: ArrayList<Lesson>?,
private val listener: OnSubjectClickListener
) : RecyclerView.Adapter<SubjectsRecyclerViewAdapter.ViewHolder>() {
/**
* This interface must be implemented by activities/fragments that contain
* this RecyclerViewAdapter to allow an interaction in this class to be
* communicated to the activity/fragment.
*/
interface OnSubjectClickListener {
fun onSubjectClick(subject: Subject)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
VIEW_TYPE_EMPTY -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.no_subject_list_item, parent, false)
EmptySubjectViewHolder(view)
}
VIEW_TYPE_NOT_EMPTY -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.subject_list_items, parent, false)
SubjectViewHolder(view)
}
else -> throw IllegalStateException("Couldn't recognise the view type")
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val list = subjectsList
val lessonsList = lessonsList
when (getItemViewType(position)) {
VIEW_TYPE_EMPTY -> {
// We are not putting any data into the empty view, therefore we do not need to do anything here
}
VIEW_TYPE_NOT_EMPTY -> {
if (list != null && lessonsList != null) {
val subject = list[position]
val days = arrayListOf<Int>()
for (lesson in lessonsList) {
if (lesson.subjectId == subject.id) {
days.add(lesson.day)
}
}
holder.bind(subject, days, listener)
}
}
}
}
override fun getItemCount(): Int {
val list = subjectsList
return if (list == null || list.size == 0) 1 else list.size
}
override fun getItemViewType(position: Int): Int {
val list = subjectsList
return if (list == null || list.size == 0) VIEW_TYPE_EMPTY else VIEW_TYPE_NOT_EMPTY
}
/**
* Swap in a new [ArrayList], containing [Subject] objects
*
* #param newList New list containing subjects
* #return Returns the previously used list, or null if there wasn't one
*/
fun swapSubjectsList(newList: ArrayList<Subject>?): ArrayList<Subject>? {
if (newList === subjectsList) {
return null
}
val numItems = itemCount
val oldList = subjectsList
subjectsList = newList
if (newList != null) {
//notify the observers
notifyDataSetChanged()
} else {
//notify the observers about the lack of a data set
notifyItemRangeRemoved(0, numItems)
}
return oldList
}
/**
* Swap in a new [ArrayList], containing [Lesson] objects
*
* #param newList New list containing lessons
* #return Returns the previously set list, or null if there wasn't one
*/
fun swapLessonsList(newList: ArrayList<Lesson>?): ArrayList<Lesson>? {
if (newList === lessonsList) {
return null
}
val list = lessonsList
val numItems = if (list == null || list.size == 0) 0 else list.size
val oldList = lessonsList
lessonsList = newList
if (newList != null) {
notifyDataSetChanged()
} else {
//notify the observers about the lack of a data set
notifyItemRangeRemoved(0, numItems)
}
return oldList
}
open class ViewHolder(override val containerView: View) :
RecyclerView.ViewHolder(containerView), LayoutContainer {
open fun bind(
subject: Subject,
days: ArrayList<Int>,
listener: OnSubjectClickListener
) {
}
}
private class SubjectViewHolder(override val containerView: View) : ViewHolder(containerView) {
override fun bind(
subject: Subject,
days: ArrayList<Int>,
listener: OnSubjectClickListener
) {
containerView.sli_name.text = subject.name
for (day in days) {
when (day) {
1 -> {
containerView.sli_sunday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_sunday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
2 -> {
containerView.sli_monday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_monday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
3 -> {
containerView.sli_tuesday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_tuesday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
4 -> {
containerView.sli_wednesday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_wednesday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
5 -> {
containerView.sli_thursday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_thursday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
6 -> {
containerView.sli_friday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_friday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
7 -> {
containerView.sli_saturday.setBackgroundResource(R.drawable.has_lesson_on_day)
containerView.sli_saturday.setTextColor(
ContextCompat.getColor(
containerView.context,
R.color.colorBackgroundPrimary
)
)
}
}
}
containerView.sli_linearlayout.setOnClickListener {
listener.onSubjectClick(subject)
}
}
}
// We do not need to override the bind method since we're not putting any data into the empty view
private class EmptySubjectViewHolder(override val containerView: View) :
ViewHolder(containerView)
}
It is seem to be Recycler Views are not getting cleaned before reuse them.
you must clear/reset your days color.. e.g here
containerView.sli_name.text = subject.name
<HERE CLEAR/RESET YOUR DAYS COLOR FOR ALL THE 7 DAYS>
for (day in days) {
when (day) { ... }
I think you have to use database here if user enter two subjects you have to save local database next time he opens fragment he can see saved subjects
I changed:
private val map = HashMap<Int, AuthorizationContentView>()
on
private val map = SparseArray<AuthorizationContentView>()
But how can I fix the situation here?
val view = map.getOrPut(position) {
AuthorizationContentView(context = context)
}
The getOrPut is an extension function in MutableMap You can do the same for SparseArray as well using your own custom extension function. That's how convenient Kotlin is :)
/**
* Returns the value for the given key. If the key is not found in the SparseArray,
* calls the [defaultValue] function, puts its result into the array under the given key
* and returns it.
*/
public inline fun <V> SparseArray<V>.getOrPut(key: Int, defaultValue: () -> V): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}
Recently, I was creating a test app to familiarize myself with RecyclerViewand the Android Palette library when I came across this semantic error in my fragment that deals with Palette. When I take a picture in the fragment, it saves the photo in the File for the current orientation, for the landscape orientation but when I rotate my phone back to portrait, the File resets back to null. I have discovered this based off my Log tests and reading stack traces.
Currently I've wrapped the null absolute path in a null check to prevent further errors but I'm not sure how to proceed. Below is my Kotlin file.
class PicFragment : Fragment() {
private var imgFile: File? = null
private lateinit var cameraPic: ImageView
private lateinit var cycleLayout: View
private var swatchIndex: Int = 0
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View? = inflater?.inflate(R.layout.camera_fragment, container, false)
// init
val cameraButton: ImageButton = view!!.findViewById(R.id.click_pic)
val colorCycler: ImageButton = view.findViewById(R.id.color_clicker)
cameraPic = view.findViewById(R.id.camera_pic)
cycleLayout = view.findViewById(R.id.color_selector)
val swatchDisplay: ImageView = view.findViewById(R.id.main_color)
val swatchName: TextView = view.findViewById(R.id.main_color_name)
// restoring the picture taken if it exists
if(savedInstanceState != null){
val path: String? = savedInstanceState.getString("imageFile")
swatchIndex = savedInstanceState.getInt("swatchIndex")
if(path != null) {
val bm: Bitmap = BitmapFactory.decodeFile(path)
cameraPic.setImageBitmap(bm)
animateColorSlides(cycleLayout, duration = 500)
}
}
// taking the picture (full size)
cameraButton.setOnClickListener { _ ->
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (intent.resolveActivity(context.packageManager) != null){
imgFile = createFileName()
val photoURI = FileProvider.getUriForFile(context, "com.github.astronoodles.provider", imgFile)
grantUriPermissions(intent, photoURI)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(intent, 3)
}
}
// Palette Button (click to go through color values)
colorCycler.setOnClickListener { _ ->
if(cameraPic.drawable is BitmapDrawable){
val img: Bitmap = (cameraPic.drawable as BitmapDrawable).bitmap
Palette.from(img).generate { palette ->
val swatches = palette.swatches
Log.d(MainActivity.TAG, "Swatch Size: ${swatches.size}")
Log.d(MainActivity.TAG, "Counter: $swatchIndex")
val hexCode = "#${Integer.toHexString(swatches[swatchIndex++ % swatches.size].rgb)}"
swatchName.text = hexCode
animateColorDrawableFade(context, swatchDisplay, hexCode)
}
} else Log.e(MainActivity.TAG, "No bitmap found! Cannot cycle images...")
}
return view
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString("imageFile", imgFile?.absolutePath)
outState?.putInt("swatchIndex", swatchIndex)
}
/**
* Animates the color of an ImageView using its image drawable
* #author Michael + StackOverflow
* #since 6/24/18
* #param ctx Context needed to load the animations
* #param target Target ImageView for switching colors
* #param hexCode The hex code of the colors switching in
*/
private fun animateColorDrawableFade(ctx: Context, target: ImageView, hexCode: String){
val fadeOut = AnimationUtils.loadAnimation(ctx, android.R.anim.fade_out)
val fadeIn = AnimationUtils.loadAnimation(ctx, android.R.anim.fade_in)
fadeOut.setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {}
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
target.setImageDrawable(ColorDrawable(Color.parseColor(hexCode)))
target.startAnimation(fadeIn)
}
})
target.startAnimation(fadeOut)
}
/**
* Helper method for animating a layout's visibility from invisible and visible
* #author Michael
* #param layout The layout to animate
* #param duration The length of the alpha animation.
*/
private fun animateColorSlides(layout: View, duration: Long){
layout.alpha = 0f
layout.visibility = View.VISIBLE
layout.animate().alpha(1f).setListener(null).duration = duration
}
/**
* Creates an unique name for the file as suggested here using a SimpleDateFormat
* #author Michael
* #returns A (temporary?) file linking to where the photo will be saved.
*/
private fun createFileName(): File {
val timeStamp: String = SimpleDateFormat("yyyyMd_km", Locale.US).format(Date())
val jpegTitle = "JPEG_${timeStamp}_"
val directory: File = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
try {
return File.createTempFile(jpegTitle, ".png", directory)
} catch (e: IOException) {
e.printStackTrace()
}
return File(directory, "$jpegTitle.jpg")
}
/**
* Grants URI permissions for the file provider to successfully save the full size file. <br>
* Code borrowed from https://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps
* #param intent The intent to send the photo
* #param uri The URI retrieved from the FileProvider
* #author Michael and Leszek
*/
private fun grantUriPermissions(intent: Intent, uri: Uri){
val intentHandleList: List<ResolveInfo> = context.packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
intentHandleList.forEach {
val packageName: String = it.activityInfo.packageName
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(requestCode == 3 && resultCode == Activity.RESULT_OK){
val bitmap: Bitmap = BitmapFactory.decodeFile(imgFile!!.absolutePath)
cameraPic.setImageBitmap(bitmap)
animateColorSlides(cycleLayout, duration = 2000)
}
}
}
I also have my WRITE_EXTERNAL_STORAGE permission in the manifest if that helps.
Thanks.
From the Android activity lifecycle documentation, this is the relevant part:
If you override onSaveInstanceState(), you must call the superclass implementation if you want the default implementation to save the state of the view hierarchy
Which will give you something like this:
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString("imageFile", imgFile?.absolutePath)
outState?.putInt("swatchIndex", swatchIndex)
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(outState)
}
I have a simple activity with a BottomNavigationView. I'm using fragments to implement the contents of the activity for the different pages.
When the user presses the back button, it's supposed to go back to the previously looked at page. The problem is, when you repeatedly switch back and forth between the pages (fragments), this entire history is recorded. Take this example:
A -> B -> A -> B -> C -> A -> C
Pressing the back button would result in the reverse, but instead I want this behaviour (I noticed it in the Instagram app):
C -> A -> B -> Exit App
So every fragment should only have one entry in the backstack. How do I do this? I do I remove the previous transactions for a fragment from the stack?
Is this at all possible using a FragmentManager? Or do I have to implement my own?
My Activity with the BottomNavigationView:
class ActivityOverview : AppCompatActivity() {
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
// "Home" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_home))
return#OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
// "Dashboard" menu item pressed
return#OnNavigationItemSelectedListener true
}
R.id.navigation_settings -> {
// "Settings" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_settings))
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_overview)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.menu.findItem(R.id.navigation_home).setChecked(true)
// Set initial fragment
setActiveFragment(resources.getString(R.string.tag_fragment_home))
}
override fun onBackPressed() {
// > 1 so initial fragment addition isn't removed from stack
if (fragmentManager.backStackEntryCount > 1) {
fragmentManager.popBackStack()
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = if (fragmentManager.findFragmentByTag(tag) != null) {
// Fragment is already initialized
if (fragmentManager.findFragmentByTag(tag).isVisible) {
// Fragment is visible already, don't add another transaction
null
} else {
// Fragment is not visible, add transaction
fragmentManager.findFragmentByTag(tag)
}
} else {
// Fragment is not initialized yet
when (tag) {
resources.getString(R.string.tag_fragment_home) -> FragmentHome()
resources.getString(R.string.tag_fragment_settings) -> FragmentSettings()
else -> null
}
}
if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.container_fragment, fragment, tag)
transaction.addToBackStack(null)
transaction.commit()
}
}
}
At this point I'm pretty sure it doesn't work with FragmentManager, so I created a class to implement a stack that doesn't allow duplicates:
class NoDuplicateStack<T> {
val stack: MutableList<T> = mutableListOf()
val size: Int
get() = stack.size
// Push element onto the stack
fun push(p: T) {
val index = stack.indexOf(p)
if (index != -1) {
stack.removeAt(index)
}
stack.add(p)
}
// Pop upper element of stack
fun pop(): T? {
if (size > 0) {
return stack.removeAt(stack.size - 1)
} else {
return null
}
}
// Look at upper element of stack, don't pop it
fun peek(): T? {
if (size > 0) {
return stack[stack.size - 1]
} else {
return null
}
}
}
I then integrated this class into my activity:
class ActivityOverview : AppCompatActivity() {
val fragmentsStack = NoDuplicateStack<String>()
val fragmentHome = FragmentHome()
val fragmentSettings = FragmentSettings()
val fragmentHistory = FragmentHistory()
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = ...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
override fun onBackPressed() {
if (fragmentsStack.size > 1) {
// Remove current fragment from stack
fragmentsStack.pop()
// Get previous fragment from stack and set it again
val newTag = fragmentsStack.pop()
if (newTag != null) {
setActiveFragment(newTag)
}
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = when (tag) {
resources.getString(R.string.tag_fragment_home) -> fragmentHome
resources.getString(R.string.tag_fragment_settings) -> fragmentSettings
resources.getString(R.string.tag_fragment_history) -> fragmentHistory
else -> null
}
if (fragment != null && !fragment.isVisible) {
fragmentManager.beginTransaction()
.replace(R.id.container_fragment, fragment, tag)
.commit()
fragmentsStack.push(tag)
}
}
}
I also faced the same problem, I did this which uses the system stack
val totalFragments = supportFragmentManager.backStackEntryCount
if (totalFragments != 0) {
val removed = supportFragmentManager.getBackStackEntryAt(totalFragments - 1)
poppedFragments.add(removed.name!!)
for (idx in totalFragments - 1 downTo 0) {
val fragment = supportFragmentManager.getBackStackEntryAt(idx)
if (!poppedFragments.contains(fragment.name)) {
supportFragmentManager.popBackStack(fragment.name, 0)
return
}
}
finish()
return
}
super.onBackPressed()
and then added this while launching the fragment
if (poppedFragments.contains(tag)) {
poppedFragments.remove(tag)
}