I am making an Android app in Kotlin (following MVVM as much as I can if it matters) and my app structure is as follows:
1 Activity (MainActivity) which contains a FrameLayout and a BottomNavigationView.
The FrameLayout is filled dynamically with Fragments per BottomNavigationView clicks. One Fragment then opens an another fragment on click. That latter fragment is structured as follows:
1 NavigationView (navigationQuestions) and 1 FrameLayout (frameQuestion)
The FrameLayout should change Fragments based on NavigationView clicks.
Those containing fragments contain a textview and a listview.
I have implemented all of the above without many issues.
The problems arise when I need to communicate backwards between the last child fragment and its parent fragment because I need to, based on the item in the listview that's been clicked, change the color of the navigationview text that opened that fragment and change the color of that listview entry. I have tried calling the parentFragment but I can't access it's variables, tried with bundles, but they always seem to be null etc.
Also, I can't seem to maintain the state the last fragment is in when I change to an another one with the navigationview.
I am changing fragments like this:
navigationQuestions.setNavigationItemSelectedListener {
val transaction = this.activity?.supportFragmentManager!!.beginTransaction()
val index : Int = it.title.toString().toInt()-1
transaction.replace(R.id.frameQuestion, fragmentQuestions[index])
transaction.commit()
return#setNavigationItemSelectedListener true
}
fragmentQuestions is a MutableList that I create on the start of the class and fill when I fill the navigationView. The reason I did this is because each time I pressed the navigationView, a new instance of that Fragment was created, which isn't really what I want, so this solves it.
I have tried saving the state of the fragment with various override combinations including onPause(), onInstanceSaved, onViewDestroyed() etc, but my Bundle always remains null.
So, the question is, is there an efficient way to, on listView click, color the navigationView entry that belongs to the parent fragment and keep the current fragment saved so that when I switch to an another navigationview fragment and back, it remains the way it was?
I am using onCreateView in all my Fragment classes and this is the listview onitemclicklistener:
answersList.setOnItemClickListener { parent, view, position, id ->
if(position == question.correct-1) {
(view as TextView).setTextColor(resources.getColor(R.color.greenanswer, null))
parent.isEnabled = false
}
else {
(view as TextView).setTextColor(resources.getColor(R.color.redanswer, null))
parent.isEnabled = false
}
}
I have tried accessing the navigationview with something like
(parent.parent.parent.parent as ViewGroup).get(0)
But I quickly realized that I can't access it that way :(
The look of the navigationview and the framelayout is:
image
Any help & tips? I can provide more detailed code of any part necessary, didn't want to overwhelm the question with code which isn't needed as there is a lot of code.
Thanks in advance :)
you can pass data to the parent fragment by calling
setFragmentResult(
REQUEST_KEY_FRAGMENT,
bundleOf(EXTRA_DATA to "but your data here")
)
and on the parent activity u should listen to the result as following :
setFragmentResultListener(REQUEST_KEY_FRAGMENT) { key, bundle ->
bundle.getString(EXTRA_DATA)?.let {
}
}
you should have this dependency in your project:
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
Related
I have a parent Fragment that has ViewPager which holds another Fragments(child). Some child Fragment can have list. If the user press a back button the list will scroll up if can scroll vertically, another press on back button will move the ViewPager to first item (Fragment).
While I can create an approach like this on child Fragment.
if (adapter.currentList.isNotEmpty() && recyclerView.canScrollVertically(-1))
recyclerView.smoothScrollToPosition(0)
else {
try {
val parentFragment: HomeFragment = parentFragment as HomeFragment
parentFragment.onBackPressed()
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
}
}
And a public method on parent Fragment like this
fun onBackPressed() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
requireActivity().onBackPressed()
}
I am not sure if this is the best thing to do since I read that Fragments should better not communicate directly with each other, instead communication should be handle by the host Activity or shared ViewModel. But doing so seems an overkill and I do not feel the use of LiveData just for this case.
I would go with the SharedViewModel approach, its kinda built for this. Your child Fragments are effecting the ViewPager that belongs to the ParentFragment. They are effectively part of the same View but the child Fragments don't have access to the View that contains them. So using a SharedViewModel can bridge the gap.
What I would do is make a well defined interface that manipulates the ViewPager and its Adapter as you see fit for your application. That interface should then be accessible through the SharedViewModel.
In the interface you could have a function called navigateToFragment(Class fragmentClass); In this function you would then need to find a fragment via its class inside the Adapter, and then use the ViewPager to go to it. Ie navigateToFragment(HomeFragment.class);
The code is in the Android Studio template.
Android Studio - Create new project - Navigation Drawer Activity
There are an Activity and three Fragments (HomeFragment, GalleryFragment, SlideshowFragment) in the template project. I use an AppBarConfiguration object to manage three Fragment. Now I create a function in each Fragment.
My title may be a bit vague. My problem is how to execute the method in the current fragment by clicking the button in Activity.
Now my solution is as follows.
In each fragment. Get the activity object and find the button. Then set the click method. Here is my Kotlin code.
activity?.findViewById<FloatingActionButton>(R.id.fab)?.setOnClickListener { view ->
myFunction()
}
It works fine. But I think this is not elegant enough. And there are some problems with this. If I only set the click event of one fragment, when I switch to another fragment, the click event just now will be executed after I click the button.
I guess the reason for the above problem is that the click event of the activity is bound by the fragment.
My other solution is as follows.
Create an ActivityViewModel and set a LiveData value. Change value after clicking the button. Get ActivityViewModel in fragment and observe the value in each fragment. Then execute the method when the value is changed
My third solution is as follows.
Get each fragment object and determine whether it is visible. Then perform the function of the visible fragment. Here is my Kotlin code.
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
var fragment1 = navHostFragment!!.childFragmentManager.fragments[0] as HomeFragment
var fragment2 = navHostFragment!!.childFragmentManager.fragments[1] as GalleryFragment
var fragment3 = navHostFragment!!.childFragmentManager.fragments[2] as SlideshowFragment
if(fragment1.isVisiable){
fragment1.myFunction()
}else if(fragment2.isVisiable){
fragment2.myFunction()
}else if(fragment3.isVisiable){
fragment3.myFunction()
}
But I still think the methods above are not elegant enough.
Can I use the interface to achieve the above functions? And how to achieve it?
I am not a native English speaker. Sorry for my bad English.
Thanks.
I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link
I have multiple fragment, one bottom navigation view on bottom of Main Activity. The problem appears when I want to set selected item on Bottom Navigation View when fragment pop back from stack, setSelectedItemId always trigger OnBackStackChangedListener, thus make loop event. Here are the code
fragmentManager.addOnBackStackChangedListener {
var f : Fragment = fragmentManager.findFragmentById(R.id.frame)
if(f is HomeFragment){
bottomNavigation.selectedItemID = R.id.navigation_home
}
}
I've check from the documentation at developer.android.com and various post on StackOverflow or even forum and I don't find any proper solution for my case.
Any solution? thanks
I'm a beginner in Android, so I apologize for the mistakes and I'd appreciate any constructive criticism.
I'm writing a basic application with a ListView of images, and when the user clicks on an item in the list, I want to display that image in a ViewPager, where the user can swipe back and forth to browse the whole list of images. Afterwards when the user presses the back button, I want to switch back to the ListView.
I manage the business logic in the MainActivity, which uses MainActivityFragment for the ListView and ImageHolderFragment for ViewPager.
The simplified code so far is as follows:
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListItems = new ArrayList<>();
mListItemAdapter = new ListItemAdapter(this, R.layout.list_item, R.id.list_item_name, mListItems);
mListView = (ListView) findViewById(R.id.list_view_content);
mListView.setAdapter(mListItemAdapter);
mDeletedListItems = new ArrayList<>();
mViewPager = (ViewPager) getLayoutInflater().inflate(R.layout.image_display, null, true);
mImageAdapter = new ImageAdapter(getSupportFragmentManager(), mListItems);
mViewPager.setAdapter(mImageAdapter);
mViewPager.setOffscreenPageLimit(3);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mViewPager.setCurrentItem(position);
setContentView(mViewPager); // TODO: this is very wrong!
}
});
loadImages();
noContentText = (TextView) findViewById(R.id.no_content_text);
if (mListItems.isEmpty()) {
noContentText.setText(R.string.no_images);
} else {
mImageAdapter.notifyDataSetChanged();
}
}
Although this does work to some extent, meaning that it manages to display the ViewPager when an item in the list is clicked, there are two things about it ringing the alarm bells:
I've read that calling setContentView() for the second time in the same class is pretty much a sin. Nobody explained me why.
The back button doesn't work in this case. When it's pressed, the application is terminated instead of going back to the list view. I believe this is connected to the first point.
I would appreciate any help, explanations if my idea is completely wrong, and if my case is hopeless, I'd like to see a successful combination of ListView and ViewPager with transitions between each other.
Your activity already has R.layout.activity_main set as content view, which rightly displays the list view - that's what the responsibility of this activity is as you defined it. If we want to change what's shown on the screen, we should use a different instance of a building block (activity or fragment) to display the view pager images.
To say the least, imagine if you wanted to change the view to a third piece of functionality or UI, or a fourth... it would be a nightmare to maintain, extend and test as you're not separating functionality into manageable units. Fields that are needed in one view are mixed with those needed in another, your class file would grow larger and larger as each view brings its click listeners, callbacks, etc., you'd also have to override the back button so it does what you want - it's just not how the Android framework was designed to help you. And what if you wanted to re-use UI components in different contexts whilst tapping in to the framework's activity lifecycle callbacks? That's why fragments were introduced.
In your case, the list view could continue to run in your MainActivity and in your click listener, onItemClick you could start a new activity that will hold a viewPager:
Intent i = new Intent(MainActivity.this, MyLargePhotoActivityPager.class);
i.putExtra(KEY_POSITION, position);
// pass the data too
startActivityForResult(i, REQUEST_CODE);
Notice how you could pass the position to this activity as an int extra, in order for that second activity to nicely set the viewPager to the position that the user clicked on. I'll let you discover how to build the second activity and put the ViewPager there. You also get back button functionality assuming your launch modes are set accordingly, if needed. One thing to note is that when you do come back to the list View, you'd probably want to scroll to the position from the view pager, which is why you could supply that back as a result via a request code. The returned position can be supplied back to the list view.
Alternatively, you could use the same activity but have two fragments (see the link further above) and have an equivalent outcome. In fact, one of your fragments could store the list view, and the second fragment could be a fullscreen DialogFragment that stores a viewPager, like a photo gallery (some details here).
Hope this helps.
I've read that calling setContentView() for the second time in the
same class is pretty much a sin. Nobody explained me why.
Well, you kind of get an idea as to why.
When you use setContentView() to display another 'screen' you do no have a proper back stack.
You also keep references to Views (like mListView) that are not visible anymore and are therefore kind of 'useless' after you setContentView() for the second time.
Also keep in mind orientation changes or your app going to the background - you'll have to keep track of the state that your Activity was in which is way more complicated than it has to be if you have one Activity that does two different things.
You won't be arrested for doing things like you do right now, but it's just harder to debug and keep bug free.
I'd suggest using two different Activities for the two different things that you want to do, or use one Activity and two Fragments, swapping them back and forth.
If you insist on having it all in one Activity you need to override onBackPressed() (called when the user presses the back button) and restore the first state of your Activity (setContentView() again, pretty much starting all over).