Get fragment's container view id - android

I have a fragment added using
transaction.add(R.id.content, fragment, null);
and I need to start new fragment from this one. But to do this I need to know first fragment's container view id (R.id.content in my case). How can I get this?
I can just use this id directly but I suppose fragment shouldn't know such kind of details about parent activity. For example it will be impossible to use this fragment in another activity in this case.
May be "starting" fragment from another one is a bad practice and all fragment handling logic should be handled by activity itself? But creating nice sequences of fragments starting each other seems quite useful (for example detalView->moreDetailView->evenMoreDetailView).

You can access the container's id by calling
((ViewGroup)getView().getParent()).getId();

I don't know if I exactly understand your question, but you get the Container in onCreateView. I suppose you could stash it in a local variable.
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
mContainer = container;
...
}

I think there's a more standard way of accessing the view rather than using
((ViewGroup) getView().getParent()).getId()
I will assume that you're working with a MainActivity that presents a list fragment, which can then present another list fragment upon clicking an item, and so on. I'm assuming that you've chosen to replace the main view of MainActivity with the contents of the list fragments you present.
Because each list fragment is being hosted in the MainActivity, you can always access the view of the MainActivity.
// Inside of onListItemClick...
FragmentManager fm = getFragmentManager();
Fragment fragment = new MyOtherListFragment();
FrameLayout contentView = (FrameLayout) getActivity().findViewById(R.id.content_view);
fm.beginTransaction()
.replace(contentView.getId(), fragment)
.addToBackStack(null)
.commit();
The above example assumes you have an XML layout resource that you set in the MainActivity, call the XML resource R.layout.activity_main, where there is a FrameLayout with the id R.id.content_view. This is the approach I took. The example I present here is a simpler version from the one that I actually wrote in my app.
Incidentally, my version of IntelliJ (version 1.0.1) warns me that
((ViewGroup) getView().getParent)
may throw a NullPointerException.

Assuming you have Fragment instance mCurrentFragment in Activity class.
You can get Fragment's container View via
int id = mCurrentFragment.getView().getParent().getId();
ViewGroup vg = (ViewGroup) findViewById(id); // Fragment's container View

The Kotlin version
val container = view?.parent as? ViewGroup ?: return
It can be added to a "hand-dandy" extension:
fun Fragment.container(): ViewGroup? {
return view?.parent as? ViewGroup
}
Then get the id
container.id
container().id

Add the new class
import androidx.navigation.NavController
class Navigator {
companion object {
var fragment1_id: Int = 0
var fragment2_id: Int = 0
var navController : NavController? = null
fun goFragment1()
{
navController?.navigate(fragment1_id)
}
fun goFragment2()
{
navController?.navigate(fragment2_id)
}
}
}
In main activity:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
...
val navController = findNavController(R.id.nav_host_fragment_content_main)
Navigator.navController = navController
Navigator.fragment1_id = R.id.nav_fragment1
Navigator.fragment2_id = R.id.nav_fragment2
<navigation xmlns:android...
<fragment
android:id="#+id/nav_fragment1"
...
<fragment
android:id="#+id/nav_fragment2"
Click Listener in any fragment:
fun onClickButton(view: View)
{
Navigator.goFragment1()
}

Related

setFragmentResult doesn't work onClick listener

hello my problem is the following, I have 2 fragments, one receives with SetFragmentResultListener and another sends with setFragmentResult
The problem is that setFragmentResult does not work inside an OnClickListener but it does work outside
Parent
setFragmentResultListener("scannedCode") { requestKey, bundle ->
val result = bundle.getString("code")
Log.i("MYLOG-find","$result")
}
Child - It Work
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_scan, container, false)
setFragmentResult("scannedCode", bundleOf("code" to "pedro"))
return view
}
Child - It doesn't Work
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_scan, container, false)
view.textView2.setOnClickListener {
setFragmentResult("scannedCode", bundleOf("code" to "pedro"))
}
return view
}
As per #ianhanniballake's answer, I went a bit further, because I had a similar problem, and read the documentation he referred (specially, the Receive results in the host activity section). One of the keys in making inter-fragment communication (parent-child) is to use activity's supportFragmentManager methods to perform the information exchange. Example:
Parent fragment (host)
requireActivity().supportFragmentManager
.setFragmentResultListener("whatever_id_you_use", viewLifecycleOwner) {
// Your code for listener
}
Child fragment:
requireActivity().supportFragmentManager
.setFragmentResult("whatever_id_you_use", bundle_with_results)
"whatever_id_you_use" is the requestKey depicted in documentation, which is the index the activity will look for when assigning a listener to its result. Doing this way should keep the listeners in sync with the lifecycle, while simplifying own listener's implementation.
In the case of passing data from Child to Parent fragment, using the childFragmentManager is the key when setting the resultListener on the parent fragment.
Parent fragment (place the code inside onCreate()):
childFragmentManager.setFragmentResultListener("requestKey", this ) { requestKey, bundle ->
val resultReceived = bundle.getString("bundleKey")
// do something with the result
// ...
}
Child fragment (place the code whenever you need to send the result - could be from a click listener, could be once you receive a result from another started activity for result, etc.):
val resultToBeSent = "result"
setFragmentResult("requestKey", bundleOf("bundleKey" to resultToBeSent))
More useful information about the communication between fragments can be found in this medium article:
The modern way to pass data between fragments and of course in the official Android documentation: Communicating with fragments.
When you use the Navigation Component, the previous fragment is stopped when you navigate to a new fragment. As per the Fragment Result API guide:
If you call setFragmentResult() more than once for the same key, and if the listener is not STARTED, the system replaces any pending results with your updated result. If you set a result without a corresponding listener to receive it, the result is stored in the FragmentManager until you set a listener with the same key. Once a listener receives a result and fires the onFragmentResult() callback, the result is cleared. This behavior has two major implications:
Fragments on the back stack do not receive results until they have been popped and are STARTED.
If a fragment listening for a result is STARTED when the result is set, the listener's callback is then fired immediately.
So it is expected that you do not get any calls to your fragment result listener until you pop back to that fragment and it becomes STARTED again.
In my case sending result from pushed fragment to presenting, I need to call: requireActivity().onBackPressed(), then setFragmentResult().

Keeping states of recyclerview in fragment with paging library and navigation architecture component

I'm using 2 components of the jetpack: Paging library and Navigation.
In my case, I have 2 fragment: ListMoviesFragment & MovieDetailFragment
when I scroll a certain distance and click a movie item of the recyclerview, MovieDetailFragment is attached and the ListMoviesFragment is in the backstack. Then I press back button, the ListMoviesFragment is bring back from the backstack.
The point is scrolled position and items of the ListMoviesFrament are reset exactly like first time attach to its activity. so, how to keep states of recyclerview to prevent that?
In another way, how to keep states of whole fragment like hide/show a fragment with FragmentTransaction in traditional way but for modern way(navigation)
My sample codes:
fragment layout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.karaokestar.app.SplashFragment">
<TextView
android:id="#+id/singer_label"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Ca sĩ"
android:textColor="#android:color/white"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="#+id/btn_game_more"
android:layout_centerVertical="true"
android:background="#drawable/shape_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="#dimen/header_margin_bottom_list"
android:textStyle="bold"
android:padding="#dimen/header_padding_size"
android:textAllCaps="true"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list_singers"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Fragment kotlin code:
package net.karaokestar.app
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_splash.*
import net.karaokestar.app.home.HomeSingersAdapter
import net.karaokestar.app.home.HomeSingersRepository
class SplashFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_splash, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val singersAdapter = HomeSingersAdapter()
singersAdapter.setOnItemClickListener{
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
}
list_singers.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
list_singers.setHasFixedSize(true)
list_singers.adapter = singersAdapter
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
}
fun getSingersPagination() : LiveData<PagedList<Singer>> {
val repository = HomeSingersRepository()
val pagedListConfig = PagedList.Config.Builder().setEnablePlaceholders(true)
.setPageSize(Configurations.SINGERS_PAGE_SIZE).setPrefetchDistance(Configurations.SINGERS_PAGE_SIZE).build()
return LivePagedListBuilder(repository, pagedListConfig).build()
}
}
Since you use NavController, you cannot keep the view of the list fragment when navigating.
What you could do instead is to keep the data of the RecyclerView, and use that data when the view is recreated after back navigation.
The problem is that your adapter and the singersPagination is created anew every time the view of the fragment is created. Instead,
Move singersAdapter to a field:
private val singersAdapter = HomeSingersAdapter()
Move this part to onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
Call retainInstance(true) in onAttach. This way even configuration changes won't reset the state.
On fragment's onSaveinstanceState save the layout info of the recyclerview:
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_LAYOUT, myRecyclerView.getLayoutManager().onSaveInstanceState());
}
and on onActivityCreated, restore the scroll position:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
myRecyclerView.getLayoutManager().onRestoreInstanceState(
savedInstanceState.getParcelable(KEY_LAYOUT));
}
}
Try the following steps:
Initialize your adapter in onCreate instead of onCreateView. Keep the initialization one time, and attach it in onCreateView or onViewCreated.
Don't return a new instance of your pagedList from getSingersPagination() method everytime, instead store it in a companion object or ViewModel (preferred) and reuse it.
Check the following code to get a rough idea of what to do:
class SingersViewModel: ViewModel() {
private var paginatedLiveData: MutableLiveData<YourType>? = null;
fun getSingersPagination(): LiveData<YourType> {
if(paginatedLiveData != null)
return paginatedLiveData;
else {
//create a new instance, store it in paginatedLiveData, then return
}
}
}
The causes of your problem are:
You are attaching a new adapter each time, so it jumps to top.
You are creating new paged list each time, so it jumps to the top, thinking the data is all new.
The sample fragment code you posted does not correspond to the problem description, I guess it's just an illustration of what you do in your app.
In the sample code, the actual navigation (the fragment transaction) is hidden behind this line:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
The key is how the details fragment is attached.
Based on your description, your details fragment is probably attached with FragmentTransaction.replace(int containerViewId, Fragment fragment). What this actually does is first remove the current list fragment and then add the detail fragment to the container. In this case, the state of the list fragment is not kept. When you press the back button, the onViewCreated of the list fragment will run again.
To keep the state of your list fragment, you should use FragmentTransaction.add(int containerViewId, Fragment fragment) instead of replace. This way, the list fragment remains where it is and it gets "covered" by the detail fragment. When you press the back button, the onViewCreated will not be called, since the view of the fragment did not get destroyed.
Whenever the fragment back again, it get the new PagedList, this cause the adapter present new data thus you will see the recycler view move to top.
By moving the creation of PagedList to viewModel and check to return the exist PagedList instead of create new one will solve the problem. Of course, depends on your app business create new PagedList might require but you can control it completely. (ex: when pull to refresh, when user input data to search...)
Things to keep in mind when dealing with these issues:
In order to let OS handle the state restoration of a view automatically, you must provide an id. I believe that this is not the case (because you must identify the RecyclerView in order to bind data).
You must use the correct FragmentManager. I had the same issue with a nested fragment and all that i had to do was to use ChildFragmentManager.
The SaveInstanceState() is triggered by the activity. As long as activity is alive, it won't be called.
You can use ViewModels to keep state and restore it in onViewCreated()
Lastly, navigation controller creates a new instance of a fragment every time we navigate to a destination. Obviously, this does not work well with keeping persistent state. Until there is an official fix you can check the following workaround which supports attaching/detaching as well as different backstacks per tab. Even if you do not use BottomNavigationView, you can use it to implement an attaching/detaching mechanism.
https://github.com/googlesamples/android-architecture-components/blob/27c4045aa0e40d402bbbde16d7ae0c9822a34447/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt
I had the same problem with my recycle view using the paging library. My list list would always reload and scroll to the top when up navigation button is clicked from my details fragment. Picking up from #Janos Breuer's point I moved the initialisation of my view model and initial list call(repository) to the fragment onCreate() method which is called only once in the fragment lifecycle.
onCreate() The system calls this method when creating the fragment. You should initialize essential components of the fragment that you want to retain when the fragment is paused or stopped, then resumed.
Sorry to be late. I had a similar problem and I figured out a solution (maybe not the best). In my case I adapted the orientation changes.
What you need is to save and retrieve is the LayoutManager state AND the last key of the adapter.
In the activity / fragment that is showing your RecyclerView declare 2 variables:
private Parcelable layoutState;
private int lastKey;
In onSaveInstanceState:
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if(adapter != null) {
lastKey = (Integer)adapter.getCurrentList().getLastKey();
outState.putInt("lastKey",lastKey);
outState.putParcelable("layoutState",dataBinding.customRecyclerView
.getLayoutManager().onSaveInstanceState());
}
}
In onCreate:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//.... setContentView etc...
if(savedInstanceState != null) {
layoutState = savedInstanceState.getParcelable("layoutState");
lastKey = savedInstanceState.getInt("lastKey",0);
}
dataBinding.customRecyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(lastKey != 0) {
dataBinding.customRecyclerView.scrollToPosition(lastKey);
Log.d(LOG_TAG, "list restored; last list position is: " +
((Integer)adapter.getCurrentList().getLastKey()));
lastKey = 0;
if(layoutState != null) {
dataBinding.customRecyclerView.getLayoutManager()
.onRestoreInstanceState(layoutState);
layoutState = null;
}
}
}
});
}
And that's it. Now your RecyclerView should restore properly.
Well... You can check if the view is initialized or not (for kotlin)
Initialize view variable
private lateinit var _view: View
In onCreateView check if view is initialized or not
if (!this::_view.isInitialized) {
return inflater.inflate(R.layout.xyz, container, false)
}
return _view
And in onViewCreated just check for it
if (!this::_view.isInitialized) {
_view = view
// ui related methods
}
This solution is for onreplace()....but if data is not changing on back press you may use add() method of fragment.
Here oncreateview will be called but your data won't be reloaded.
Simple steps...
Give id to all the views which you need to maintain state in fragment back stack. Like Scrollview, root layout, Recyclerview...
The properties which should hold values, initialize in onCreate() of the fragment. Like adapter, count...except view references.
If you are setting observers use lifecycleowner as this#yourFragment instead of viewLifecycleOwner.
That's it. Fragment onCreate() is called only once so setting properties on onCreate() will be there in memory till the fragment onDestroy() is called(not onDestroyView).
All the view referencing code should be there after onCreateView() and before onDestroyView().
BTW If possible you can save the properties in ViewModel which will be there even when fragment configuration changes where all the instance variables will be destroyed.
hope this link will help:
PagingDataAdapter.refresh() not working after fragment navigation
This so simple, only use this method cachedIn()
fun getAllData() {
viewModelScope.launch {
_response.value = repository.getPagingData().cachedIn(viewModelScope)
}
}

Why is fragment calling OnCreate / OnCreateView when using fragment transactions with FrameLayout?

The main page of my application has a FrameLayout.
I'm instantiating two fragments when the activity starts, and I'm trying to use a menu button to swap between the fragment.
scanHistoryFrag = new HistoryFragment();
scanFrag = new ScanFragment();
I never replace these objects - I use the same ones throughout the lifecycle of the application. However, when I swap them in my FrameLayout...
private void ChangeFragment(Android.Support.V4.App.Fragment fragment)
{
Android.Support.V4.App.FragmentTransaction ft = SupportFragmentManager.BeginTransaction();
ft.Replace(Resource.Id.fragmentContainer, fragment);
ft.Commit();
}
OnCreate and OnCreateView are called on the Fragment again... which means any adjustments I made post creation on that fragment are overwritten with initial values again. I can't seem to find any explanation for why this is happening or how I might avoid it.
The ChangeFragment method is being called by OnOptionsItemSelected, as I'm using a menu button to toggle them.
I never replace these objects - I use the same ones throughout the lifecycle of the application.
Initialization of a subclass of Fragment just create a instance of this class object, the constructor of this class will be called, but it will not go through the lifecycle of Fragment unless this Fragment is added, for more information, you can refer to Fragments. To understand it easier, I personal think the instance saves the data state of this Fragment class, but the events of lifecycle handle the view state of this Fragment.
which means any adjustments I made post creation on that fragment are overwritten with initial values again.
Yes, you're right. To avoid overwritting with initial values again, we can cache the fragment's view in OnCreateView for example like this:
private View rootView;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
if (rootView == null)
{
//first time creating this fragment view
rootView = inflater.Inflate(Resource.Layout.fragmentlayout1, container, false);
//Initialization
//TODO:
}
else
{
//not first time creating this fragment view
ViewGroup parent = (ViewGroup)rootView.Parent;
if (parent != null)
{
parent.RemoveView(rootView);
}
}
return rootView;
}

How do I find a view inside a fragment?

I am trying to add a fragment, then find a view inside said fragment, and add a view into it. However I keep getting a NullPointerException on this statement
FrameLayout container2 = (FrameLayout) fragment.getActivity().findViewById(R.id.content_frame);
Here is my code. Can someone tell me how to fix this please? thanks
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment fragment = new FragmentNavigationDrawer();
ViewGroup decor = (ViewGroup) getActivity().getWindow().getDecorView();
View child = decor.getChildAt(0);
decor.removeView(child);
fragmentTransaction.add(decor.getId(), fragment);
fragmentTransaction.commit();
FrameLayout container2 = (FrameLayout) fragment.getActivity().findViewById(R.id.content_frame);
container2.addView(child);
Just use a getter. Set a tag on your fragment so you can access it later, then either call getView() on your fragment to return its root view, or use a getter to access a specific View:
public class MainActivity extends AppCompatActivity {
//In onCreate
if (getFragmentManager().findFragmentByTag(FragmentNavigationDrawer.TAG) == null) {
getFragmentManager()
.beginTransaction()
.add(android.R.id.content, new FragmentNavigationDrawer(), FragmentNavigationDrawer.TAG)
.commit();
}
//Later, when you want to add said View:
FragmentNavigationDrawer frag =
(FragmentNavigationDrawer) getFragmentManager().findFragmentByTag(FragmentNavigationDrawer.TAG)
//Return the root view:
View fragRootView = frag.getView();
//Return a specific view:
frag.getUpdatableViewGroup().addView(newViewToAdd):
}
For your Fragment:
public class FragmentNavigationDrawer extends Fragment {
public static final String TAG = FragmentNavigationDrawer.class.getSimpleName();
FrameLayout updatableViewGroup;
//Can do this inside onCreateView() whilst inflating your Fragment's Views
//That's up to you.
#Override
public void onViewCreated (View view, Bundle savedInstanceState) {
updateableViewGroup = view.findViewById(R.id.updateable_view_group);
}
public FrameLayout getUpdatableViewGroup() {
return updateableViewGroup;
}
Be conscious of the Activity and Fragment life cycles however, and be careful not to attempt to access the Fragment's Views until they have finished inflating - onStart() of your Activity and later should be ok.
Please see the javadoc for FragmentTransaction.commit(). It says it will schedule a change to the fragment back stack. It doesn't happen immediately. It looks like you're expecting the fragment and its views to be instantly available.
Also, I'm really confused why you're reaching in a decor view to make changes. Usually you call out a view by id in the host activity's layout and make changes inside it.

Getting Parent ViewPager View from inside Fragment

I am trying to access the parent viewpager from inside a fragment, but i have no idea how to do that.
I need to switch the currentItem on the ViewPager after a onClick event inside the fragment.
Any ideas?
EDIT:
I want access to the parent view(ViewPager View) so that i can change the currentItem which is visible, from inside one of my fragments.
From fragment, call getActivity() which will gives you the activity in which the fragment is hosted. Then call findViewById(ViewPagerId) to get the ViewPager.
ViewPager vp=(ViewPager) getActivity().findViewById(ViewPagerId);
Edit: I must add, even though Eldhose answer's works, I would defend my approach. Because the less the fragment knows about the Activity containing it, the better. By doing this, you can get the parent view without depending on IDs, and you can still get information from it even if it isn't a ViewPager.
You can get the in the Fragment's onCreateView method.
The container param is the parent View, in that case, a ViewPager.
In Java:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ViewPager pager = (ViewPager) container;
//.... Rest of your method
}
Kotlin Version:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup,
savedInstanceState: Bundle) {
val pager = container as ViewPager
//.... Rest of your method
}
The methods onCreateView and onViewCreated and onAttach are called too early.
The view is definitely attached to its parent view in the fragment's onResume method. This is a good place to then use getView().getParent()
Another way that helped me :
List<Fragment> fragment = getActivity().getSupportFragmentManager().getFragments();
for (Fragment f:fragment) {
if (f instanceof CreatCheckInFragment){
((ParentFragment) f).viewPager.arrowScroll(View.FOCUS_LEFT);
}
}

Categories

Resources