Navigation Drawer: how make fragments persistent (keep alive) while switching (not rotating) - android-fragments

With Fragment:setRetainInstance(true); the fragment is not re-instantiated on a phones orientation change.
And of course i want my fragments to be kept alive while switching from one fragment to another.
But the Android Studio 4 provides a wizard-template with only
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
From hours of debugging and searching the net if think it would need to inherent from the class FragmentNavigator so i can overwrite FragmentNavigator:naviagte where a new fragment gets created via final Fragment frag = instantiateFragment(.. and then is added with ft.replace(mContainerId, frag);
So i could find my old fragment and use ftNew.show and ftOld.hide instead.
Of course this is a stupid idea, because this navigate method is full of other internal stuff.
And i have no idea where that FrameNavigator is created.
I can retrieve it in the MainActivity:OnCreate with
NavigatorProvider navProvider = navController.getNavigatorProvider ();
Navigator<NavDestination> navigator = navProvider.getNavigator("fragment");
But at that time i could only replace it with my derived version. And there is no replaceNavigtor method but only a addNavigator method, which is called where ?
And anyways this all will be far to complicated and therefore error prone.
Why is there no simple option to keep my fragments alive :-(
In older Wizard-Templates there was the possibility of
#Override
public void onNavigationDrawerItemSelected(int position) {
Fragment fragment;
switch (position) {
case 1:
fragment = fragment1;
break;
case 2:
fragment = fragment2;
break;
case 3:
fragment = fragment3;
break;
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if(mCurrentFragment == null) {
ft.add(R.id.container, fragment).commit();
mCurrentFragment = fragment;
} else if(fragment.isAdded()) {
ft.hide(mCurrentFragment).show(fragment).commit();
} else {
ft.hide(mCurrentFragment).add(R.id.container, fragment).commit();
}
mCurrentFragment = fragment;
}
but i have no idea how to do this with the Android 4.0 template where my MainActivity is only derived as:
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration mAppBarConfiguration;
Ideas welcome :'(

Hi there & sorry for my late answer! I had a similar problem with navigation drawers and navigation component. I tried around a little and found a working solution, which might be helpful for others too.
The key is the usage of a custom FragmentFactory in the FragmentManager of the MainActivity. See the code for this below:
public class StaticFragmentFactory extends FragmentFactory {
private myNavHostFragment1 tripNavHostFragment;
private myNavHostFragment2 settingsNavHostFragment;
#NonNull
#Override
public Fragment instantiate(#NonNull ClassLoader classLoader, #NonNull String className) {
if (MyNavHostFragment1.class.getName().equals(className)) {
if (this.myNavHostFragment1 == null) {
this.myNavHostFragment1 = new MyNavHostFragment1();
}
return this.myNavHostFragment1 ;
} else if (MyNavHostFragment2.class.getName().equals(className)) {
if (this.myNavHostFragment2 == null) {
this.myNavHostFragment2 = new MyNavHostFragment2();
}
return this.myNavHostFragment2;
}
return super.instantiate(classLoader, className);
}
}
The FragmentFactory survives the navigation between different fragments using the NavigationComponent of AndroidX. To keep the fragments alive, the FragmentFactory stores an instance of the fragments which should survive and returns this instance if this is not null. You can find a similar pattern when using a singleton pattern in classes.
You have to register the FragmentFactory in the corresponding activity by calling
this.getSupportFragmentManager().setFragmentFactory(new StaticFragmentFactory())
Please note also that I'm using nesten fragments here, so one toplevel fragment (called NavHostFragmen here) contains multiple child fragments. All fragments are using the same FragmentFactory of their parent fragments. The custom FragmentFactory above returns the result of the super class method, when the fragment to be instantiated is not known to keep alive.

Related

Persistent header fragment (disable animation) in Android TV (leanback)

Anyone knows how to achieve the question in the title? The objective is to avoid the animation that results in the Headers bar disappearing as the Leanback app zooms in on the Row Item once a Header has been clicked.
setHeadersState of BrowseSupportFragment doesn't help. Perhaps something to do with hijacking startHeadersTransitionInternal during OnHeaderClickedListener? If so, any idea how to correctly implement it?
So the problem with this one is that the transition is handled by the method startHeadersTransitionInternal which is package private. Because of this, you can't override it in most situations. However, since it's only package private and not private private, there's a little hack around this that you can do.
First, make a package in your app with the same package name as BrowseSupportFragment. Then make a class in that package that extends BrowseSupportFragment and override the offending method with no implementation. That'd look something like this:
package android.support.v17.leanback.app; // Different for AndroidX
public class HackyBrowseSupportFragment extends BrowseSupportFragment {
#Override
void startHeadersTransitionInternal(boolean withHeaders) {
// Do nothing. This avoids the transition.
}
}
Then, instead of extending BrowseSupportFragment, you'd extend HackyBrowseSupportFragment.
One thing to note that I found with this is that the back button will no longer refocus the headers from one of the rows, so you'll have to do that manually. Other than that, seems to work just fine.
Following #MichaelCeley 's response and based on the original startHeadersTransitionInternal method from BrowseSupportFragment, this implementation that keeps the backstack and listeners works, too.
#Override
void startHeadersTransitionInternal(final boolean withHeaders) {
if (getFragmentManager().isDestroyed()) {
return;
}
if (!isHeadersDataReady()) {
return;
}
new Runnable() {
#Override
public void run() {
if (mBrowseTransitionListener != null) {
mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
}
if (mHeadersBackStackEnabled) {
if (!withHeaders) {
getFragmentManager().beginTransaction()
.addToBackStack(mWithHeadersBackStackName).commit();
} else {
int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
if (index >= 0) {
FragmentManager.BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
getFragmentManager().popBackStackImmediate(entry.getId(),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
}
}
}.run();
}

Android TabLayout Fragment with MVP

I'm implementing an MVP app in which the Views are fragments loaded in Activities. Each Activity had 1 fragment to display. I have to change my implementation and add the TabLayout which will now display the fragments. I've tried different ways of passing the fragment to the adapter but all makes my app crash and I can't understand the error. My last try, I'm passing an arraylist of fragments(1 for now) to the adapter. At the base, I'm following google samples MVP todo app, but I need to implement this tab layout. Please, this is for my major project, I looked everywhere and this is my last resort.
public class HomeActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
Toolbar mHomeToolbar = (Toolbar) findViewById(R.id.toolbar); // Set to the corresponding Toolbar in the UI.
setSupportActionBar(mHomeToolbar);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); // Set to the corresponding Drawer Layout in the UI.
ActionBarDrawerToggle mToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mHomeToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
mDrawerLayout.addDrawerListener(mToggle); // Set mToggle as Drawer's toggle button and listen to actions.
mToggle.syncState();
NavigationView mDrawerNavigationView = (NavigationView) findViewById(R.id.nav_view); // Set the corresponding Navigation View in the UI.
mDrawerNavigationView.setNavigationItemSelectedListener(this); // Add listener on Navigation's items.
HomeFragment homeFragment = (HomeFragment) getSupportFragmentManager().findFragmentById(R.id.Quests_Frame); // Set to corresponding Fragment View in the UI.
if (homeFragment == null) {
homeFragment = HomeFragment.newInstance();
FragmentLoader.loadFragmentInActivity(getSupportFragmentManager(), homeFragment, R.id.Quests_Frame); // Display fragment in Activity.
}
repo = QuestsRepository.getInstance(QuestsDataSource.getINSTANCE());
mHomePresenter = new HomePresenter(repo , homeFragment);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
TabPagerAdapter adapter = new TabPagerAdapter(getSupportFragmentManager());
adapter.addFragment(homeFragment);
viewPager.setAdapter(adapter);
}
The adapter class:
public class TabPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final int tabCount = 3;
public TabPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
mFragmentList.get(position);
default:
return null;
}
}
public void addFragment(Fragment fragment) {
mFragmentList.add(fragment);
}
#Override
public int getCount() {
return tabCount;
}
}
For what you want to achieve, you won't be using the FragmentLoader Class. Remove it (just for the tabs Activities). And the getSupportFragmentManager line.
In Home Activity, how you set up the tabLayout and Viewpager, it's fine.
Remove addFragment line.
Add the following after setAdapter:
mTabLayout.setupWithViewPager(mViewPager);
In the tabPagerAdapter, just create the object presenter and fragment there.
In the getItem method, case 0, you can have:
HomeFragment homeFragment = HomeFragment.newInstance();
homePresenter = new HomePresenter(repo, homeFragment);
return homeFragment;
Oh and in the TabPagerAdapter, you can pass your repo argument there for creating your presenter.
I hope I was clear. Let me know if you have any issues.

Android - Reload a Fragment that is Part of ViewPager

I need help with reloading a Fragment that is part of a ViewPager. Here is my current setup:
ReportActivity extends FragmentActivity and uses ViewPager to host 4 Tabs
TAB 1 to 4. Each Tab is a Fragment with seperate layout.
ViewPager uses TabsPagerAdapter to switch between the tabs and this works as expected.
TAB1Fragment displays a graph using a static dataset. Now I have added a spinner to dynamically change the dataset and I am looking for ways to reload the Fragment so it can re-display the graph with the correct dataset. The following ints correspond to the values of the Spinner.
static final int REPORT_PERIOD_DAY = 0;
static final int REPORT_PERIOD_WEEK = 1;
static final int REPORT_PERIOD_MONTH = 2;
static final int REPORT_PERIOD_YEAR = 3;
I have a method that correctly calculates the correct data based on the the int passed in, what I am struggling with is how to create an Intent or any other method that will recreate TAB1Fragment and passes it an int parameter.
Here is where I want to re-create the Fragment
mReportPeriodSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
//How can I re-load the Fragment here?
reloadFragment(REPORT_PERIOD_DAY);
break;
case 1:
reloadFragment(REPORT_PERIOD_WEEK);
break;
case 2:
reloadFragment(REPORT_PERIOD_MONTH);
break;
case 3:
reloadFragment(REPORT_PERIOD_YEAR);
break;
}
}
Please can you give me an idea of how to write the code that goes into the method reloadFragment(Int period)
I would recommend re-configuring the existing fragment, rather than constructing a new fragment with the new report period.
I'm guessing that you currently configure your graph inside TAB1Fragment's onCreateView. If you move that configuration code to onResume, then you can call the same function when the user selects something in the spinner.
public class TAB1Fragment extends Fragment {
int reportPeriod;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = ...
reportPeriod = REPORT_PERIOD_DAY; // default report period
return rootView;
}
#Override
public void onResume() {
super.onResume();
configureGraph();
}
private void configureGraph() {
//TODO put all the code that draws your graphs here, using reportPeriod
}
public void reloadFragment(int reportPeriod) {
this.reportPeriod = reportPeriod;
configureGraph();
}
}
If this approach really doesn't work for you, and you really need a new fragment each time, then this question explains how to get the tag for a fragment so that you can tell the FragmentManager to replace it:
Replace Fragment inside a ViewPager

Structuring a MonoTouch.Dialog application

From the examples at Xamarin.com you can build basic M.T. Dialog apps, but how do you build a real life application?
Do you:
1) Create a single DialogViewController and tree every view/RootElement from there or,
2) Create a DialogViewController for every view and use the UINavigationController and push it on as needed?
Depending on your answer, the better response is how? I've built the example task app, so I understand adding elements to a table, click it to go to the 'next' view for editing, but how to click for non-editing? How to click a button, go next view if answer is number 1?
Revised:
There is probably no one right answer, but what I've come up with seems to work for us. Number 2 from above is what was chosen, below is an example of the code as it currently exists. What we did was create a navigation controller in AppDelegate and give access to it throughout the whole application like this:
public partial class AppDelegate : UIApplicationDelegate
{
public UIWindow window { get; private set; }
//< There's a Window property/field which we chose not to bother with
public static AppDelegate Current { get; private set; }
public UINavigationController NavController { get; private set; }
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
Current = this;
window = new UIWindow (UIScreen.MainScreen.Bounds);
NavController = new UINavigationController();
// See About Controller below
DialogViewController about = new AboutController();
NavController.PushViewController(about, true);
window.RootViewController = NavController;
window.MakeKeyAndVisible ();
return true;
}
}
Then every Dialog has a structure like this:
public class AboutController : DialogViewController
{
public delegate void D(AboutController dvc);
public event D ViewLoaded = delegate { };
static About about;
public AboutController()
: base(about = new About())
{
Autorotate = true;
about.SetDialogViewController(this);
}
public override void LoadView()
{
base.LoadView();
ViewLoaded(this);
}
}
public class About : RootElement
{
static AboutModel about = AboutVM.About;
public About()
: base(about.Title)
{
string[] message = about.Text.Split(...);
Add(new Section(){
new AboutMessage(message[0]),
new About_Image(about),
new AboutMessage(message[1]),
});
}
internal void SetDialogViewController(AboutController dvc)
{
var next = new UIBarButtonItem(UIBarButtonSystemItem.Play);
dvc.NavigationItem.RightBarButtonItem = next;
dvc.ViewLoaded += new AboutController.D(dvc_ViewLoaded);
next.Clicked += new System.EventHandler(next_Clicked);
}
void next_Clicked(object sender, System.EventArgs e)
{
// Load next controller
AppDelegate.Current.NavController.PushViewController(new IssuesController(), true);
}
void dvc_ViewLoaded(AboutController dvc)
{
// Swipe location: https://gist.github.com/2884348
dvc.View.Swipe(UISwipeGestureRecognizerDirection.Left).Event +=
delegate { next_Clicked(null, null); };
}
}
Create a sub-class of elements as needed:
public class About_Image : Element, IElementSizing
{
static NSString skey = new NSString("About_Image");
AboutModel about;
UIImage image;
public About_Image(AboutModel about)
: base(string.Empty)
{
this.about = about;
FileInfo imageFile = App.LibraryFile(about.Image ?? "filler.png");
if (imageFile.Exists)
{
float size = 240;
image = UIImage.FromFile(imageFile.FullName);
var resizer = new ImageResizer(image);
resizer.Resize(size, size);
image = resizer.ModifiedImage;
}
}
public override UITableViewCell GetCell(UITableView tv)
{
var cell = tv.DequeueReusableCell(skey);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, skey)
{
SelectionStyle = UITableViewCellSelectionStyle.None,
Accessory = UITableViewCellAccessory.None,
};
}
if (null != image)
{
cell.ImageView.ContentMode = UIViewContentMode.Center;
cell.ImageView.Image = image;
}
return cell;
}
public float GetHeight(UITableView tableView, NSIndexPath indexPath)
{
float height = 100;
if (null != image)
height = image.Size.Height;
return height;
}
public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
{
//base.Selected(dvc, tableView, path);
tableView.DeselectRow(indexPath, true);
}
}
#miquel
The current idea of a workflow is an app that starts with a jpg of the Default.png that fades into the first view, with a flow control button(s) that would move to the main app. This view, which I had working previous to M.T.D. (MonoTouch.Dialog), which is a table of text rows with an image. When each row is clicked, it moves to another view that has the row/text in more detail.
The app also supports in-app-purchasing, so if the client wishes to purchase more of the product, then switch to another view to transact the purchase(s). This part was the main reason for switching to M.T.D., as I thought M.T.D. would be perfect for it.
Lastly there would be a settings view to re-enable purchases, etc.
PS How does one know when the app is un-minimized? We would like to show the fade in image again.
I have been asking myself the same questions. I've used the Funq Dependency Injection framework and I create a new DialogViewController for each view. It's effectively the same approach I've used previously developing ASP.NET MVC applications and means I can keep the controller logic nicely separated. I subclass DialogViewController for each view which allows me to pass in to the controller any application data required for that particular controller. I'm not sure if this is the recommended approach but so far it's working for me.
I too have looked at the TweetStation application and I find it a useful reference but the associated documentation specifically says that it isn't trying to be an example of how to structure a MonoTouch application.
I use option 2 that you stated as well, it works pretty nicely as you're able to edit the toolbar options on a per-root-view basis and such.
Option 2 is more feasible, as it also gives you more control on each DialogViewController. It can also helps if you want to conditionally load the view.

Firebase+RecyclerView: RecyclerView not displaying on application start

I'm using the Android Studio provided class for a tabbed activity that uses Action Bar Tabs with ViewPager. Inside this activity, I'm trying to initialize a RecyclerView with data from a Firebase database.
Problem: On the app's first run, the RecyclerView is empty as shown below.
If I close and reopen the application from within the emulator, my RecyclerView gets populated as it should, as shown below.
Any ideas as to why this might be happening? I have a theory but I haven't been able to find a solution. After trying to read the FragmentPagerAdapter page, I got the impression that the fragments must be static (I don't know what the implications of this might be, so if anyone can shed some light on this it would be appreciated). On the app's first run, it initializes the RecyclerView. It then adds the data from the Firebase database but since the RecyclerView has already been initialized it is empty and is never properly updated. I tried calling the notify... methods to no avail.
StudentFragment's onCreateView method:
private View view;
private Context c;
private RecyclerView mRecyclerView;
private LinearLayoutManager manager;
private Firebase mFirebaseRef;
private FirebaseRecyclerAdapter<Student, ViewHolder> firebaseRecyclerAdapter;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_students, container, false);
mFirebaseRef = new Firebase("<your Firebase link here>");
c = getContext();
//Initializes Recycler View and Layout Manager.
mRecyclerView = (RecyclerView) view.findViewById(R.id.studentRecyclerView);
manager = new LinearLayoutManager(c);
mRecyclerView.setHasFixedSize(true);
firebaseRecyclerAdapter =
new FirebaseRecyclerAdapter<Student, ViewHolder>(
Student.class,
R.layout.single_student_recycler,
ViewHolder.class,
mFirebaseRef
) {
#Override
protected void populateViewHolder(ViewHolder viewHolder, Student student, int i) {
viewHolder.vFirst.setText(student.getFirst());
viewHolder.vLast.setText(student.getLast());
viewHolder.vDue.setText(Double.toString(student.getCurrentlyDue()));
viewHolder.vRadio.setButtonTintList(ColorStateList.valueOf(Color.parseColor(student.getColor())));
Log.d(TAG, "populateViewHolder called");
}
};
mRecyclerView.setAdapter(firebaseRecyclerAdapter);
mRecyclerView.setLayoutManager(manager);
return view;
}
ViewHolder:
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView vFirst;
public final TextView vLast;
public final TextView vDue;
public final RadioButton vRadio;
public ViewHolder(View itemView) {
super(itemView);
vFirst = (TextView) itemView.findViewById(R.id.recycler_main_text);
vLast = (TextView) itemView.findViewById(R.id.recycler_sub_text);
vRadio = (RadioButton) itemView.findViewById(R.id.recycler_radio_button);
vDue = (TextView) itemView.findViewById(R.id.recycler_due_text);
}
Homescreen's onCreate method:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_homescreen);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
Firebase context is set on another application that starts as soon as the Homescreen activity starts. Any help will be appreciated.
Edit: I was digging through the FirebaseUI GitHub page, which is where the problem most likely lies, and found another user with the exact same problem. It seems that onBindViewHolder isn't called after notifyItemInserted in the FirebaseRecyclerAdapter class. Now to fix it...
In my case this was caused by mRecyclerView.setHasFixedSize(true); If you comment out this line of code the list loads properly. I got my solution from this discussion: https://github.com/firebase/FirebaseUI-Android/issues/204
Let me try, as you say on the question title,
RecyclerView not displaying on application start
so, the
Initializes Recycler View and Layout Manager.
should be declared on the onStart
#Override
public void onStart() {
super.onStart();
mFirebaseRef = new Firebase("<your Firebase link here>");
firebaseRecyclerAdapter = ...
//and so on
Hope it helps!
firebaseRecyclerAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
int friendlyMessageCount = firebaseRecyclerAdapter.getItemCount();
int lastVisiblePosition =
linearLayoutManager.findLastCompletelyVisibleItemPosition();
// If the recycler view is initially being loaded or the
// user is at the bottom of the list, scroll to the bottom
// of the list to show the newly added message.
if (lastVisiblePosition == -1 ||
(positionStart >= (friendlyMessageCount - 1) &&
lastVisiblePosition == (positionStart - 1))) {
linearLayoutManager.scrollToPosition(positionStart);
}
}
});
recyclerListIdeas.setAdapter(firebaseRecyclerAdapter);
** Just add Recyclerview.AdapterDataObserver() . worked for me ! hope it helps :)**
i had the same issue, check the documentation:
https://codelabs.developers.google.com/codelabs/firebase-android/#6
fixed it by adding a data observer:
mFirebaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
int friendlyMessageCount = mFirebaseAdapter.getItemCount();
int lastVisiblePosition =
mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
// If the recycler view is initially being loaded or the
// user is at the bottom of the list, scroll to the bottom
// of the list to show the newly added message.
if (lastVisiblePosition == -1 ||
(positionStart >= (friendlyMessageCount - 1) &&
lastVisiblePosition == (positionStart - 1))) {
mMessageRecyclerView.scrollToPosition(positionStart);
}
}
});

Resources