I am trying to use a TabLayout with different fragments and have started with AndroidStudio's automatically generated code for the tabbed layout. I have not changed how the placeholder fragment is created, displayed, handled etc.: The fragment is handled by a FragmentPagerAdapter, which is used by a ViewPaper, which in turn is used to setup the TabLayout.
The layout already included a FAB. Its onClick looks like this:
fab.setOnClickListener { view ->
val currentFragment: Fragment = sectionsPagerAdapter.getItem(viewPager.currentItem)
when (viewPager.currentItem) {
0 -> doSomething()
1 -> (currentFragment as PlaceholderFragment).fabOnClick()
else -> doSomethingElse()
}
}
Eventhough the above code makes sure that fabOnClick() is only called on the currently visible fragment, when I am trying to get a context using requireContext() in the PlaceholderFragment, java throws the following exception:
java.lang.IllegalStateException: Fragment PlaceholderFragment{660c58b} (08f94c5f-64b3-4a50-a1d4-2f3a6c7b491c)} not attached to a context.
For some reason, the context is available in e.g. onResume() in the PlaceholderFragment:
override fun onResume() {
super.onResume()
// Works fine
Toast.makeText(requireContext(), "placeholder", Toast.LENGTH_LONG).show()
}
fun fabOnClick() {
// Throws exception
Toast.makeText(requireContext(), "placeholder", Toast.LENGTH_SHORT).show()
}
I found this thread, Fragment not attached to a context, in which the solution was to commit a fragment transaction but all of this seems to be handled automatically in this case.
I just fixed an error somewhat similar to you in my code. With the same error you are getting.
The idea is that I was initializing a string variable (NOT in MainActivity) with getResources().
However, the variable was not being initialized with context.getResources()
I was able to initialize this variable inside MainActivity and make it static so that I could just copy the value into the variable NOT inside MainActivity.
So, I think you should search for any variables that you are initializing with getResources and see if you are using a context or not.
Related
I have a progress dialog which is a singleton because I want it to be shown only once if it gets called multiple times, and doesn't stack on itself. but somewhere in my code, it gets called simultaneously from two places and I get fragment already added exception.
I'm checking if the dialog is not added to activity then I call Dialog.show() but since the method gets called simultaneously from two places, before the first one is added to activity the other one is passed throw if statement and that causes the problem.
I want two thread-safe the function so that multiple threads cant call it simultaneously.
as you can see I've tried synchronizing it by #Synchronized annotation but it didn't work
class ProgressDialogFragment private constructor() : DialogFragment() {
companion object {
private var dialogInstance: DialogFragment? = null
#JvmStatic
#Synchronized
fun showDialog(fragmentManager: FragmentManager) {
if (dialogInstance == null) {
dialogInstance = ProgressDialogFragment().apply {
setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen)
isCancelable = false
}
}
if (!dialogInstance!!.isAdded && fragmentManager.findFragmentByTag("progress_dialog") == null) {
dialogInstance!!.show(fragmentManager, "progress_dialog")
}
}
as said in the documentation, the DialogFragment#show()
Display the dialog, adding the fragment to the given FragmentManager. This is a convenience for explicitly creating a transaction, adding the fragment to it with the given tag, and committing it.
FragmentTransactions commits are async, to make sure it is committed before you move on just call executePendingTransactions() :
if (!dialogInstance!!.isAdded && fragmentManager.findFragmentByTag("progress_dialog") == null) {
dialogInstance!!.show(fragmentManager, "progress_dialog")
fragmentManager.executePendingTransactions()
}
As for the #Synchronized it's unnecessary in this situation because this function can only run in the UI Thread.
I have a simple CalculatorActivity with consecutively shown Fragments :
InputFragment //requests two input values for calclation
ResultFragment // show the result of the calculation
To keep it simple lets assume, I want to calculate the sum of two numbers.
I start the CalculatorActivity which immediately loads the InputFragment via:
fun replaceFragment(resId : Int, newFragment : Fragment){
this.supportFragmentManager?.beginTransaction()?.replace(resId, newFragment)?.addToBackStack(null)?.commit()
}
resId is the resource ID for the container to inflate the fragment in.
newFragment is the new Fragment to be inflated. Here in this first step I call in CalculatorActivity's onCreate():
replaceFragment(R.id.container, InputFragment.getInstance())
SO FAR this works fine.
But when I enter the required numbers in InputFragment, how can I forward the values from InputFragment back to the calling CalculatorActivity, so that it can continue and inflate the ResultFragment into the very same container to show the result of calculation?
You can always get the enclosing activity from any fragment (by using getActivity), then cast it to the proper type and use your own setValues method on it. Check out this: https://developer.android.com/training/basics/fragments/communicating
You can have the value as a variable in the activity and from the fragment do
CalculatorActivity myActivity = (CalculatorActivity)getActivity()
myActivity.setInputValues(fragmentInputValues)
myActivity.doSomethingWithTheValues()
If you are learning and this is just for fun, then feel free to do the direct cast to activity and access it's public methods. However, I would never do that in real world practice.
Best practice would be to use an interface implementation on the parent activity.
example:
INTERFACE CLASS
interface IBaseActivityCallback {
fun onInputValuesReady(myObjectOfValues: MyObject)
}
ACTIVITY CLASS
class CalculatorActivity : AppCompatActivity(), IBaseActivityCallback {
override fun onInputValuesReady(myObjectOfValues: MyObject){
//handle inflate or usage of values
}
}
FRAGMENT CLASS
lateinit var baseActivityCallback: IBaseActivityCallback
override fun onAttach(context: Context) {
super.onAttach(context)
//NOTE* wrap in try/catch if context can be something else
baseActivityCallback = context as IBaseActivityCallback
}
fun someButtonClicked(){
baseActivityCallback.onInputValuesReady(myObjectToPass)
}
I figured I don't need to explain creating an object to hold variables or buttons that were clicked etc.. so I just kept that part generic for you.
I'm having a problem with memory leak in EnterTransitionCoordinator while using shared element transitions. Below you can see the app structure:
It has 2 screens, first is an Activity with DrawerLayout and few Fragments inside. One of them consists a list of photos and clicking specific photo triggers shared element transition to Fragment from ViewPager located in another Activity. I'm using custom SharedElementCallback when exiting and reentering these two Activitys for mapping a correct View for shared element transition. I based my code on this great blog post: https://android.jlelse.eu/dynamic-shared-element-transition-23428f62a2af
The problem is, that after swiping between ViewPager's items, Fragments are being destroyed, but the View used for shared element transition is being kept in Activity's ActivityTransitionState, specifically in EnterTransitionCoordinator. The same when reentering to Activity with DrawerLayout and then opening another Fragment. References to Views used for shared element transitions are still kept int Activitys even though Fragments were destroyed, which causes a memory leak.
My question: Is there a good way to avoid this memory leak?
I discovered that there's a method clearState() in EnterTransitionCoordinator, which should be called in Activity.onStop(). But since the Activity is not yet being stopped, Views from Fragments are being leaked. As a temporary workaround, I'm clearing that state manually on Fragment.onDestroyView() by calling this method with reflection. Below you can see the code:
/**
* Works only for API < 28
* https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces
*/
fun Fragment.clearEnterTransitionState() {
try {
getActivityTransitionState()
?.getEnterTransitionCoordinator()
?.invokeClearStateMethod()
} catch (e: Exception) {
// no-op
}
}
private fun Fragment.getActivityTransitionState() =
Activity::class.java.getField("mActivityTransitionState", requireActivity())
private fun Any.getEnterTransitionCoordinator() = javaClass.getField("mEnterTransitionCoordinator", this)
private fun Any.invokeClearStateMethod() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
javaClass.superclass?.invokeClearStateMethod(this)
} else {
javaClass.invokeClearStateMethod(this)
}
}
private fun <T> Class<T>.getField(name: String, target: Any): Any? =
getDeclaredField(name).run {
isAccessible = true
get(target)
}
private fun <T> Class<T>.invokeClearStateMethod(target: Any) {
getDeclaredMethod("clearState").apply {
isAccessible = true
invoke(target)
}
}
I found a case when architecture components ViewModel isn't retained - in short it goes as follows:
Activity is started and ViewModel instance is created
Activity is put to background
Device screen is rotated
Activity is put back to foreground
ViewModel's onCleared method is called and new object is created
Is it normal behavior of Android that my ViewModel instance is getting destroyed in this case? If so, is there any recommended solution of keeping its state?
One way I can think of is saving it once onCleared is called, however, it would also persist the state whenever activity is actually finishing. Another way could be making use of onRestoreInstanceState but it's fired on every screen rotation (not only if the app is in background).
Any silver bullet to handle such case?
Yes #tomwyr, this was a bug from an android framework. Bug details
The fix is available in 28.0.0-alpha3 and AndroidX 1.0.0-alpha3
But if you don't want to update to above versions now itself, Then you can solve like this (I know this is a bad solution but I didn't see any other good way)
In your activity override onDestroy method and save all the required fields to local variables before calling super.onDestroy. Now call super.onDestroy then Initialize your ViewModel again and assign the required fields back to your new instance of ViewModel
about isFinishing
Below code is in Kotlin:
override fun onDestroy() {
val oldViewModel = obtainViewModel()
if (!isFinishing) { //isFinishing will be false in case of orientation change
val requiredFieldValue = oldViewModel.getRequiredFieldValue()
super.onDestroy
val newViewModel = obtainViewModel()
if (newViewModel != oldViewModel) { //View Model has been destroyed
newViewModel.setRequiredFieldValue(requiredFieldValue)
}
} else {
super.onDestroy
}
}
private fun obtainViewModel(): SampleViewModel {
return ViewModelProviders.of(this).get(SampleViewModel::class.java)
}
AFAIK, ViewModel's only purpose is to survive and keep the data (i.e. "save the state") while its owner goes through different lifecycle events. So you don't have to "save the state" yourself.
We can tell from this that it's "not normal behavior". onCleared() is only called after the activity is finished (and is not getting recreated again).
Are you creating the ViewModel using the ViewModelProvider, or are you creating the instance using the constructor?
In your activity, you should have something like:
// in onCreate() - for example - of your activity
model = ViewModelProviders.of(this).get(MyViewModel.class);
// then use it anywhere in the activity like so
model.someAsyncMethod().observe(this, arg -> {
// do sth...
});
By doing this, you should get the expected effect.
For others that may not be helped by previous answers like me, the problem could be that you haven't set up your ViewModelProvider properly with a factory.
After digging around I solved my similiar problem by adding the following method to my Activities:
protected final <T extends ViewModel> T obtainViewModel(#NonNull AppCompatActivity activity, #NonNull Class<T> modelClass) {
ViewModelProvider.AndroidViewModelFactory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication());
return new ViewModelProvider(activity, factory).get(modelClass);
}
And then I did this in my Fragments:
protected final <T extends ViewModel> T obtainFragmentViewModel(#NonNull FragmentActivity fragment, #NonNull Class<T> modelClass) {
ViewModelProvider.AndroidViewModelFactory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(fragment.getApplication());
return new ViewModelProvider(fragment, factory).get(modelClass);
}
I already had some abstract super classes for menu purposes so I hid the methods away there so I don't have to repeat it in every activity. That's why they are protected. I believe they could be private if you put them in every activity or fragment that you need them in.
To be as clear as possible I would then call the methods to assign my view model in onCreate() in my activity and it would look something like this
private MyViewModel myViewModel;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myViewModel = obtainViewModel(this, MyViewModel.class);
}
or in fragment
private MyViewModel myViewModel;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() != null) {
myViewModel = obtainFragmentViewModel(getActivity(), MyViewModel.class);
}
}
Change support library/compileSDK/targetSDK to 28.
I had similar issue with multi-window. When switching to split screen, my viewModel is recreated. Support library 28 fixed my problem. (My lifecycle version is 1.1.1)
Scenario
I got multiple fragments that are going to call DialogFragment#1 which further got 3 Buttons and each button opens another DialogFragment#2.
What I want here is to get a String value that user selected via DialogFragment#2 and send it back to callingFragment(can be any one out of 6) to set textView Text.
I can do this by keeping different DialogFragment classes for each 6 fragments but I want to keep 1 class for DialogFragment #1 and 1 for DialogFragment #2 and use is for each fragment because the functionality is same for all...
Possible Solutions I tried but no success
1 - Tried to get callingFragment Name but when I try to call public Method of fragment its not working
callingFragment.setText(text)
callingFragment should be 1 of any 6 fragments...
2 - OnActivityResult as well but no success...
Actually this can be implemented as the following:
class MainActivity : AppCompatActivity(){
private lateinit var currentFragment: CurrentFragment?
private val host by lazy {
supportFragmentManager.findFragmentById(R.id.main_nav_fragment) as NavHostFragment
}
override fun showQuantityDialog() {
currentFragment= host.childFragmentManager.primaryNavigationFragment as CurrentFragment
navController.navigate(ViewProductFragmentDirections.actionViewProductFragmentToQuantityDialogFragment())
}
// This is a callback from your dialog
override fun setQuantity(quantity: Int) {
viewProductFragment?. ... set what ever you want
viewProductFragment = null
}
}
So you save a reference to the current fragment before you open the dialog.