fragment onAttach(context :Context) is not called - android

I saw many people use fragmentManager for activity to fragment call. So I tried that too. When I click toolbar first, I got the error like this.
kotlin.UninitializedPropertyAccessException: lateinit property mActivity has not been initialized
even though I override and initialize that method, can't see the log when starting onAttach. Why I can't initialize activity? Myfragment is DaggerFragment.
Main button click
binding.toolbar.setOnClickListener {
currentFramgnet = MainPictureFragment().instance
changeFragment(this#MainActivity, binding.fragmentContainer, currentFramgnet!!)
(currentFramgnet as MainPictureFragment).changeDataset("button OnClick")
}
Main changeFragment
fun changeFragment(activity: AppCompatActivity, view: View, fragment: Fragment) {
val fragmentManager = activity.supportFragmentManager
fragmentManager
.beginTransaction()
.replace(view.id, fragment)
.commit()
}
Fragment changeDataset
fun changeDataset(mes : String){
showLog("changeDataset : "+mes)
showToast(mActivity,"11111")
}
Fragment onAttach
private lateinit var mActivity :Activity
override fun onAttach(context: Context) {
super.onAttach(context)
showLog("onAttach")
if (context is Activity) mActivity = context
}
updated : I logged this and found that the changeDataset is called before onAttach()
D/TAG: changeDataset : button OnClick
D/TAG: onAttach

Your problem is you try to access Context before fragment transaction execute because you use commit(). Easy way to fix it is using commitNow().

Related

Fragment instances are kept in viewPager2 when host fragment removed

Have one activity app, where I have HomeFragment from which I open HostFragment, and HostFragment has ViewPager2 with 3 fragment items TabFragment1, TabFragment2 and TabFragment3.
When I open the HostFragment, and then go back, my tabFragments' instances are not removed. Below is the logs from lifecycles
navController.navigate(directionToHostFragment)
HostFragment OnStart HostFragment{c37542c}
TabFragment1 OnStart TabFragment1 {f41b08}
navController.popBackStack()
HostFragment OnPause HostFragment{c37542c}
HostFragment OnStop{c37542c}
HostFragment OnDestroy {c37542c}
navController.navigate(directionToHostFragment)
Blockquote
HostFragment OnStart{2670c03}
TabFragment1 OnStart{f45b87d}
navController.popBackStack()
HostFragment OnPause{2670c03}
HostFragment OnStop{2670c03}
HostFragment onDestroy{2670c03}
From ids you can see that there is a new TabFragment1 instance and old one haven't destroyed(I have logs in onPause(), onStop() etc.).
Some code snippets:
Adapter for viewPager2 I used -
class LessonTabsAdapter(fragmentActivity: FragmentActivity, val pages: List<BaseFragment>) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount() = pages.size
override fun createFragment(position: Int) = pages[position]
}
Some part from TabFragment1
class TabFragment1 : BaseFragment(R.layout.fragment_tab_1) {
private var mediaPlayer: MediaPlayerClass? = null
private lateinit var player: SimpleExoPlayer
private val viewModel by sharedViewModel<SomeViewModel>()
private val binding by viewDataBinding(FragmentTab1Binding::bind)
//Overriden onViewCreated(..) and some private methods
}
So any hints how to deal with this problem?
When you open the HostFragment the ViewPager2 creates a fragment in the lifecycle provided. In this case you are passing activity as the parameter so, it is creating the fragments in Activity's lifecycle. You can check the same by printing the activity?.supportFragmentManager?.fragments list. If you want to couple the ViewPager2's childs with HostFRagment, you need to pass the fragments instance to the FragmentStateAdapter. Check the function 2 that I have added. The best way to implement the ViewPager2 is by using function 3.
1..
public FragmentStateAdapter(#NonNull FragmentActivity fragmentActivity) {
this(fragmentActivity.getSupportFragmentManager(),fragmentActivity.getLifecycle());
}
2.//
public FragmentStateAdapter(#NonNull Fragment fragment) {
this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
3.//
public FragmentStateAdapter(#NonNull FragmentManager fragmentManager, #NonNull Lifecycle lifecycle) {
mFragmentManager = fragmentManager;
mLifecycle = lifecycle;
super.setHasStableIds(true);
}

Launch BottomSheetDialogFragment in UITest

When I'm trying to write UITests for the app, I've encountered a problem:
when using espresso to click on the element to launch the BottomSheetDialog fragment, the dialog fragment would just not showing up.
when explicitly using launchFragment to launch the dialog fragment, it would throw an error, because in Hilt it requires using launchFragmentInHiltContainer.
However, the dialog fragment is a childFragment with a scoped view model, it can't be launched using launchFragmentInHiltContainer.
Here's the minimal example of the app:
FragmentA:
#AndroidEntryPoint
class FragmentA : Fragment() {
private val vm: ViewModelA by viewModels()
...
// in between the lifecycle of onResume() and onStop() the click listener will launch FragmentB
button.setOnClickListener {
FragmentB.show(childFragmentManager, "")
}
}
Then FragmentB as a dialog fragment, extends BottomSheetDialogFragment
#AndroidEntryPoint
class FragmentB : BottomSheetDialogFragment() {
private val vm: ViewModelA by viewModels({ requireParentFragment() })
...
}
ViewModelA is a singleton and all its constructors fields are injected using Hilt.
When doing UITest
// applied HiltRule
#Test
fun testcaseA() {
// this would success and I can test everything in FragmentA
launchFragmentInHiltContainer<FragmentA>()
// then launch the dialog FragmentB
onView(withId(buttonId))
.perform(click())
// it appears in RootView, the below will pass
onView(withText(bottomSheetDialogTitle)).inRoot(isDialog())
// how should I perform more tests on the elements in Fragment B?
}
What I have tried:
By using UIAutomater to check if the dialog has showed up, and the result is: the dialog did show up, but it is not visible
device.wait(
Until.findObject(
By.text("Title of the BottomSheetDialog")
), 500
)
Also I found this issue: https://github.com/robolectric/robolectric/issues/5158 , but it is reported on Robolectric, seems not related to my problem since I'm not using it.
Stacktrace when trying to launch BottomSheetDialogFragment directly using launchFragmentInHiltContainer:
Fragment is not a child Fragment, it is directly attached to
dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper
Question:
How to launch a BottomSheetDialogFragment(Fragment B) from Fragment A in Espresso to test against FragmentB's UI elements?
You can modify launchFragmentInHiltContainer as below and launch your dialog fragment (Fragment B) individually. Then you can test Fragment B's UI elements.
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
fragmentArgs: Bundle? = null,
#StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
crossinline action: Fragment.() -> Unit = {}
) {
val startActivityIntent = Intent.makeMainActivity(
ComponentName(
ApplicationProvider.getApplicationContext(),
HiltTestActivity::class.java
)
).putExtra(
"androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
themeResId
)
ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
Preconditions.checkNotNull(T::class.java.classLoader),
T::class.java.name
)
fragment.arguments = fragmentArgs
if (fragment is DialogFragment){
fragment.show(activity.supportFragmentManager, "")
} else {
activity.supportFragmentManager
.beginTransaction()
.add(android.R.id.content, fragment, "")
.commitNow()
}
fragment.action()
}
}

onActivityCreated is deprecated, how to properly use LifecycleObserver?

Google deprecate fragment’s onActivityCreated() on Android and recommend to use LifeCycleObserver:
To get a callback specifically when a Fragment activity's
* {#link Activity#onCreate(Bundle)} is called, register a
* {#link androidx.lifecycle.LifecycleObserver} on the Activity's
* {#link Lifecycle} in {#link #onAttach(Context)}, removing it when it receives the
* {#link Lifecycle.State#CREATED} callback.
So I try to make it in recommended way, but only state I can observe in Logcat is just State: INITIALIZED.
private lateinit var lifecycleObserver: LifecycleObserver
override fun onAttach(context: Context) {
super.onAttach(context)
hostActivity = context as HostActivity
lifecycleObserver = object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
Logger.tag("SOME-TAG")d("State: ${lifecycle.currentState}")
if(lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
Logger.tag("SOME-TAG").d("CREATED")
hostActivity.lifecycle.removeObserver(lifecycleObserver)
}
}
}
hostActivity.lifecycle.addObserver(lifecycleObserver)
}
What is wrong in code above?
UPDATE 1: Looks like I forgot to use hostActivity.lifecycle.currentState and checked fragment's lifecycle instead of Activities lifecycle.
UPDATE 2: Suggested by Google approach not worked for
1 Host activity and 2 fragments when you click back button from one to another, cause onAttach never called, but onActivityCreated called.
As per the changelog here
The onActivityCreated() method is now deprecated. Code touching the
fragment's view should be done in onViewCreated() (which is called
immediately before onActivityCreated()) and other initialization code
should be in onCreate(). To receive a callback specifically when the
activity's onCreate() is complete, a LifeCycleObserver should be
registered on the activity's Lifecycle in onAttach(), and removed once
the onCreate() callback is received.
You can do something like this in your fragment class:
class MyFragment : Fragment(), LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreated() {
// ... Your Logic goes here ...
}
override fun onAttach(context: Context) {
super.onAttach(context)
activity?.lifecycle?.addObserver(this)
}
override fun onDetach() {
activity?.lifecycle?.removeObserver(this)
super.onDetach()
}
}
All I needed was onActivityCreated(...), hence I did implement an observer that:
Automatically removes itself (using .removeObserver(...)).
Then calls passed callback (update()).
I did it in next way:
class MyActivityObserver(
private val update: () -> Unit
) : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
owner.lifecycle.removeObserver(this)
update()
}
}
and use it in fragments onAttach (or another lifecycle method) like:
myActivity.lifecycle.addObserver(MyActivityObserver {
myOnActivityCreated()
})
You can consider the Lifecycle.State as the nodes in a graph and Lifecycle.Event as the edges between these nodes.
So you will never reached the State.Created on your ON_CREATE function.
Solution
class YourFragment : Fragment(), LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onCreated(){
Log.i("tag","reached the State.Created")
}
override fun onAttach(context: Context) {
super.onAttach(context)
lifecycle.addObserver(this)
}
override fun onDetach() {
super.onDetach()
lifecycle.removeObserver(this)
}
}
For more details
https://developer.android.com/topic/libraries/architecture/lifecycle#lc
The best way to solve the issue is to use lifecycleScope which is present in the activity lifecycle. Below is the code snippet
override fun onAttach(context: Context) {
super.onAttach(context)
activity?.lifecycleScope?.launchWhenCreated {
setupActionbar()
}
}
How does it work? launchWhenXxx runs the launch block when it automatically reaches the specified state(in this case it is Created) and if the lifecycle goes to the destroyed state it cancels the launched coroutine automatically. Internally lifecycleScope uses Dispatchers.Main.immediate and hence there is no penalty of thread switching
Pros of this approach are following:
You don't have to manually maintain registering and deregistering of the observer
No need to overwrite two lifecycle methods
You have to latest activity and fragment dependencies to use lifecycleScope attached to the lifecycle
onActivityCreated is deprecated in API level 28.
use onViewCreated for code touching the view created by
onCreateView and onCreate for other initialization. To get a
callback specifically when a Fragment activity's onCreate is called,
register a androidx.lifecycle.LifecycleObserver on the Activity's
Lifecycle in onAttach, removing it when it receives the CREATED
callback.
The annotation #OnLifecycleEvent is deprecated too.
This annotation required the usage of code generation or reflection,
which should be avoided. Use DefaultLifecycleObserver or
LifecycleEventObserver instead.
So, to fix the issue with the deprecated onActivityCreated and OnLifecycleEvent annotation you should do the following:
Implement DefaultLifecycleObserver.
Register your class as observer in onAttach().
Override onCreate(owner: LifecycleOwner) and move your code from onActivityCreated in it.
De-register the observer when the CREATE event is received in onCreate()
See Kotlin and Java examples below:
Kotlin:
class YourFragment : Fragment(), DefaultLifecycleObserver {
override fun onAttach(context: Context) {
super.onAttach(context)
// Register your class as observer
activity?.lifecycle?.addObserver(this)
}
override fun onCreate(owner: LifecycleOwner) {
super<DefaultLifecycleObserver>.onCreate(owner)
// Remove the observer
activity?.lifecycle?.removeObserver(this)
//Move here your code from onActivityCreated(savedInstanceState: Bundle?)
}
}
Java:
public class YourFragment extends Fragment implements DefaultLifecycleObserver {
public void onAttach(#NonNull Context context) {
super.onAttach(context);
// Register your class as observer
if (getActivity() != null) {
getActivity().getLifecycle().addObserver(this);
}
}
#Override
public void onCreate(#NonNull LifecycleOwner owner) {
DefaultLifecycleObserver.super.onCreate(owner);
// Remove the observer
if (getActivity() != null) {
getActivity().getLifecycle().removeObserver(this);
}
//Move here your code from onActivityCreated(savedInstanceState: Bundle?)
}
IMPORTANT: Note that onActivityCreated is called after onCreateView, but DefaultLifecycleObserver.onCreate is called before onCreateView. So, if until now you were using in onActivityCreated something initialised in onCreateView, you'll have to move it somewhere else. E.g. in onViewCreated().

Fragment Backstack Toolbar title

Is there an efficient way to automatically set the toolbar's title when adding/replacing fragments as well as popping fragments from the backstack?
I have implemented this abstract method in my BaseFragment class:
abstract fun header() : String
override fun onResume() {
super.onResume()
(activity as SSBaseActivity).header.text = header()
}
and I modify the header in each Fragment that inherits from my BaseFragment class and displays the value in onResume but I noticed that when I press back, the last title set isn't being replaced from the fragment currently in the stack.
You could do this by using an OnBackStackChangedListener in your Activity:
supportFragmentManager.addOnBackStackChangedListener {
val fragment = supportFragmentManager.findFragmentById(R.id.yourFragmentFrame)
if (fragment is BaseFragment) {
header.text = fragment.header()
}
}

Which Fragment Manager is needed for TimePickerDialogue Fragment kotlin

I am trying to implement a simple TimePickerDialogue fragment, which displays when a button in pressed in a layout which is also a fragment. The Android developer guide shows this in Java:
public void showTimePickerDialog(View v) {
DialogFragment newFragment = new TimePickerFragment();
newFragment.show(getSupportFragmentManager(), "timePicker");
}
and says:
The show() method requires an instance of FragmentManager and a unique name for the fragment.
When I converted this to Kotlin there is no getSupportFragmentManager method offered. What should I use instead?
class AlertsFragment : Fragment() {
fun showTimePickerDialog(v: View) {
val newFragment = TimePickerFragment()
newFragment.show(FragmentManager(), "timePicker") // WHAT FRAGMENTMGR???
}
}
I am importing android.support.v4.app.Fragment
My MainActivity will display the TimePickerDialogue fragment as well as the Fragment that has a button to open the TimePickerDialogue. MainActivity has a tablayout using fragments.
Does anything need to be changed in MainActivity to make the TimePickerDialogue show() function work in the AlertsFragment?
class MainActivity : FragmentActivity(){
private lateinit var pagerAdapter: TabPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
pagerAdapter = TabPagerAdapter(supportFragmentManager)
pagerAdapter.addFragments(WorkoutGridFragment(), "Workouts")
pagerAdapter.addFragments(AlertsFragment(), "Reminders")
pagerAdapter.addFragments(AboutFragment(), "About")
// customViewPager is the viewpager created in the activity_main xml layout
customViewPager.adapter = pagerAdapter
// set up the viewpager with the tablayout
customTabLayout.setupWithViewPager(customViewPager)
}
}
Fragments don't have a SupportFragmentManager field or getter. This applies whether you write it in Kotlin or Java.
Activities, however, do. So call the activity and get the supportFragmentManager:
fun showTimePickerDialog(v: View) {
val newFragment = TimePickerFragment()
newFragment.show(activity.supportFragmentManager, "timePicker")
}
Also, if this is what you were reading in the docs, you'll see this:
Also make sure that your activity that displays the time picker extends FragmentActivity instead of the standard Activity class. (emphasis mine)
Which implies the showTimePickerDialog method is defined in an Activity. And don't get this wrong, I'm not saying you have to define it in an activity, but since that's what they do in the docs, they can call the SupportFragmentManager directly. But if you call it from a fragment or anywhere outside an activity, you need an activity instance.

Categories

Resources