I am trying to implement tabs through MvvmCross in Xamarin. I came across MvxTabActivity in Android and MvxTabBarViewController in IOS. Both are working well. The problem is MvxTabActivity is obselete. Are there any alternatives for MvxTabActivity?

I found another way to implement this, which uses TabLayout and a ViewPager. The solution asks to use fragments within a fragment. I have pasted the code for this approach. The problem here is on swiping the tabs, all the data in previous tabs is lost. I tried using RetainInstance = true, that gave following exception : "Can't retain fragements that are nested in other fragments."

Product Detail Activity :

[Activity(Label = "ProductDetailView")]
public class ProductDetailView : MvxAppCompatActivity<ProductDetailViewModel>
{
    private FrameLayout _mainFrame;

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.product_detail_view);

        if (FindViewById<FrameLayout>(Resource.Id.frame_Detail) != null)
        {
            var frag = new NutritionCategoryView();
            frag.ViewModel = ViewModel.NutritionCategoryModel;
            var trans = SupportFragmentManager.BeginTransaction();
            trans.Replace(Resource.Id.frame_Detail, frag);
            trans.AddToBackStack(null);
            trans.Commit();
        }
    }
}

Nutrition Category View Fragment :

   public class NutritionCategoryView : MvxFragment
{
    public NutritionCategoryViewModel vm
    {
        get { return (NutritionCategoryViewModel) ViewModel; }
    }

    private TabLayout _tablayout;
    private ViewPager _viewPager;


    public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        this.EnsureBindingContextIsSet(inflater);
        var view = this.BindingInflate(Resource.Layout.nutrition_category_view, container, false);

        SetViewPager(view);

        return view;
    }

    private void SetViewPager(View view)
    {
        _viewPager = view.FindViewById<Android.Support.V4.View.ViewPager>(Resource.Id.viewpager);

        if (_viewPager != null)
        {
            var fragments = new List<CategoryTabsAdapter.FragmentInfo>
            {
                new CategoryTabsAdapter.FragmentInfo
                {
                    FragmentType = typeof(CategoryView),
                    Title = "Proximates",
                    ViewModel = vm.Category1
                },
                new CategoryTabsAdapter.FragmentInfo
                {
                    FragmentType = typeof(CategoryView),
                    Title = "Minerals",
                    ViewModel = vm.Category2
                },
                new CategoryTabsAdapter.FragmentInfo
                {
                    FragmentType = typeof(CategoryView),
                    Title = "Fats",
                    ViewModel = vm.Category3
                },
                new CategoryTabsAdapter.FragmentInfo
                {
                    FragmentType = typeof(CategoryView),
                    Title = "Vitamins",
                    ViewModel = vm.Category4
                }
            };

            _viewPager.Adapter = new CategoryTabsAdapter(Activity, ChildFragmentManager, fragments);
        }

        _tablayout = view.FindViewById<TabLayout>(Resource.Id.sliding_tabs);
        _tablayout.SetBackgroundColor(Android.Graphics.Color.Black);
        _tablayout.SetupWithViewPager(_viewPager);
    }
}

Category Tabs Adapter :

public class CategoryTabsAdapter : FragmentStatePagerAdapter
{
    private readonly Context _context;

    public IEnumerable<FragmentInfo> Fragments { get; private set; }

    public CategoryTabsAdapter(Context context, FragmentManager fragmentManager, IEnumerable<FragmentInfo> fragments) : base(fragmentManager)
    {
        _context = context;
        Fragments = fragments;
    }

    public override int Count
    {
        get { return Fragments.Count(); }
    }

    public override Fragment GetItem(int position)
    {
        var fragmentInfo = Fragments.ElementAt(position);

        var fragment = Fragment.Instantiate(_context, Java.Lang.Class.FromType(fragmentInfo.FragmentType).Name);
        ((MvxFragment)fragment).ViewModel = fragmentInfo.ViewModel;
        return fragment;
    }

    public override ICharSequence GetPageTitleFormatted(int position)
    {
        return new Java.Lang.String(Fragments.ElementAt(position).Title);
    }

    public class FragmentInfo
    {
        public string Title { get; set; }

        public Type FragmentType { get; set; }

        public IMvxViewModel ViewModel { get; set; }
    }
}

Category View Fragment

public class CategoryView : MvxFragment<CategoryViewModel>
{
    public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        this.EnsureBindingContextIsSet(inflater);
        var view = this.BindingInflate(Resource.Layout.category_view, container, false);

        //Exception caused here :
        //RetainInstance = true;

        return view;
    }
}

I am new to Xamarin and MvvmCross, so could come up with this much research only. Any solution for either approaches would be of great help.

P.S. This is my first question on Stackoverflow.

upvote
  flag
Possible duplicate of Tabs in the actionbar with mvvmcross – Demitrian
upvote
  flag
With that, the tabs are visible, but the data is lost on swiping between 3 or more tabs. – Pratik Arya

2 Answers 11

I think you need to use the MvxCachingFragmentStatePagerAdapter which is in the MvvmCross.Droid.Support.V4 Nuget package. Then hook it up to your TabLayout with SetupWithViewPager().

        var viewPager = view.FindViewById<ViewPager>(Resource.Id.viewpager);
        if (viewPager != null)
        {
            var fragments = new List<MvxCachingFragmentStatePagerAdapter.FragmentInfo>
            {
                new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
                    "TitleA",
                    typeof (YourFragmentA),
                    typeof (YourViewModelA)),
                new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
                    "TitleB",
                    typeof (YourFragmentB),
                    typeof (YourViewModelB)),
                new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
                    "TitleC",
                    typeof (YourFragmentC),
                    typeof (YourViewModelC))
            };

            viewPager.Adapter = new MvxCachingFragmentStatePagerAdapter(Activity, ChildFragmentManager, fragments);
            viewPager.OffscreenPageLimit = fragments.Count;
            var tabLayout = view.FindViewById<TabLayout>(Resource.Id.tabs);
            tabLayout.SetupWithViewPager(viewPager);
        }
upvote
  flag
Using above code, while setting up the view pager, gives : Android.Support.V4.App.Fragment+InstantiationException. Exception Message : Unable to instantiate fragment nutritionrecommendor.droid.views.fragments.CategoryView: make sure class name exists, is public, and has an empty constructor that is public – Pratik Arya
upvote
  flag
Have you made sure class name exists, is public, and has an empty constructor that is public? Otherwise I will need to see your implementation of the code above and of the fragments it instantiates. – c.lamont.dev
upvote
  flag
Yes, I checked that as well. When I use 2 tabs, there is no data loss while swiping. But as soon as the number of tabs is increased, there is the data loss. – Pratik Arya

Your tabs are being recreated each time because you are using FragmentStatePagerAdapter instead of FragmentPagerAdapter. From the docs, FragmentPagerAdapter:

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.

FragmentStatePagerAdapter:

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.

So use FragmentPagerAdapter, but I think it will have to be at the Activity level, and not a fragment within a fragment.

Not the answer you're looking for? Browse other questions tagged or ask your own question.