android - MVVM带 Backstack 的跨选项卡导航

标签 android tabs mvvmcross

目前是否有一种方法可以在 MVVMCross 中进行选项卡式导航,例如 YouTube 应用程序,每个选项卡都有一个子导航,还是必须由我自己实现?那么如何才是一种干净的方式呢?我应该调整演示者吗?

一个例子: 我有三个选项卡,我可以从一个选项卡导航到另一个选项卡。如果我在第一个选项卡中选择一个元素,它将导航到新的 ViewModel 并替换第一个选项卡,同时我仍然可以切换到其他选项卡。如果我导航回第一个选项卡,我将返回到旧的第一个选项卡。

最佳答案

解决方案架构:

Activity  
  TabLayout  
  ViewPager  
     RootFragment (1 per tab, created/deleted by the viewpager depdending of the selected tab)
         |-- ChildFragmentManager of the RootFragment (= independant navigation stack)
         | Tab1Fragment1  
         | Tab1Fragment2  
         | Tab1Fragment3  
  • 主视图是一个 mvvmcross Activity “HomeContainerActivity”
  • 对于选项卡按钮,我使用了 TabLayout。
  • 内容是链接到选项卡布局的 ViewPager。当选择选项卡时,viewpager 会切换显示的 fragment 。
  • ViewPager 的适配器配有自定义 MvxCachingFragmentPagerAdapter,该适配器实例化 RootTabFragment,而不是指定 MvxViewPagerFragmentInfo 的 fragment ,并使用将成为堆栈根的 fragment 对其进行初始化
  • RootTabFragment 的布局只有一个内容,其中放置了上述 fragment 。

仅此而已。导航到 View 模型会在当前顶部 fragment 中打开它。这意味着标准导航将按预期工作,但后退按钮在您的主要 Activity 中需要一些代码。此外,如果没有自定义 View 呈现器,您就无法通过代码更改选项卡。

主要 Activity 布局:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/chrome"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    >

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <!-- fragment content for popups -->
    <FrameLayout
        android:id="@+id/contentPopup"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="5dp"/>

    <!-- action/tool bar -->
    <!-- must be below all contents -->
    <include layout="@layout/activity_home_toolbar" />

    <!-- tab bar -->
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:layout_gravity="bottom"
        >
            <android.support.design.widget.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
                app:tabIndicator="@null"
                />
    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

使用 TabLayout 设置 ViewPager:

            var fragments = new List<MvxViewPagerFragmentInfo>
            {
                new MvxViewPagerFragmentInfo("Tab1", typeof(Tab1Fragment).FragmentJavaName(), typeof(Tab1Fragment), typeof(Tab1ViewModel)),
                new MvxViewPagerFragmentInfo("Tab2", typeof(Tab2Fragment).FragmentJavaName(), typeof(Tab2Fragment), typeof(Tab2ViewModel)),
            };

            viewPager = FindViewById<ViewPager>(Resource.Id.viewpager);
            viewPager.Adapter = new MvxCachingFragmentStatePagerAdapter2(this, SupportFragmentManager, fragments);

            tabLayout = FindViewById<TabLayout>(Resource.Id.tabs);
            tabLayout.SetupWithViewPager(viewPager);

自定义 fragment 寻呼机适配器:

    [Register("vapolia.MvxCachingFragmentStatePagerAdapter2")]
    public class MvxCachingFragmentStatePagerAdapter2 : MvxCachingFragmentPagerAdapter
    {
        private readonly Context context;

        [Preserve(Conditional=true)]
        protected MvxCachingFragmentStatePagerAdapter2(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) {}

        public MvxCachingFragmentStatePagerAdapter2(Context context, FragmentManager fragmentManager, List<MvxViewPagerFragmentInfo> fragmentsInfo) : base(fragmentManager)
        {
            this.context = context;
            FragmentsInfo = fragmentsInfo;
        }

        public override int Count => FragmentsInfo?.Count() ?? 0;
        public List<MvxViewPagerFragmentInfo> FragmentsInfo { get; }

        public override Fragment GetItem(int position, Fragment.SavedState fragmentSavedState = null)
        {
            var fragment = (RootTabFragment)Fragment.Instantiate(context, typeof(RootTabFragment).FragmentJavaName());
            fragment.FragmentInfo = FragmentsInfo[position];
            return fragment;
        }

        public override int GetItemPosition(Java.Lang.Object @object) => PagerAdapter.PositionNone;
        public override ICharSequence GetPageTitleFormatted(int position) => new Java.Lang.String(FragmentsInfo.ElementAt(position).Title);
        protected override string GetTag(int position) => FragmentsInfo[position].Tag;
        public override void DestroyItem(ViewGroup container, int position, Object objectValue) {} //Disable destroy
    }

根选项卡 fragment :

/// <summary>
    /// Special fragment used as the root content for every tab items.
    /// This enables a specific navigation stack for each tab item (using ChildFragmentManager).
    /// 
    /// Contains only one subfragment
    /// </summary>
    public class RootTabFragment : Fragment
    {
        public MvxViewPagerFragmentInfo FragmentInfo { get; set; }

        [Android.Runtime.Preserve(Conditional=true)]
        protected RootTabFragment(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) {}

        [Android.Runtime.Preserve(Conditional=true)]
        public RootTabFragment()
        {
        }

        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            base.OnCreateView(inflater, container, savedInstanceState);
            return inflater.Inflate(Resource.Layout.home_common_root, container, false);
        }

        public override void OnViewCreated(View view, Bundle savedInstanceState)
        {
            base.OnViewCreated(view, savedInstanceState);
            var fm = ChildFragmentManager;

            var isCacheable = FragmentInfo.FragmentType.IsFragmentCacheable(Mvx.IoCProvider.Resolve<IMvxAndroidCurrentTopActivity>().Activity.GetType());
            var firstFragment = fm.FindFragmentByTag(FragmentInfo.Tag) ?? Fragment.Instantiate(Context, FragmentInfo.FragmentType.FragmentJavaName());

            // ReSharper disable once SuspiciousTypeConversion.Global
            if (firstFragment is IMvxFragmentView mvxFragment)
            {
                if (savedInstanceState == null || !isCacheable)
                {
                    var viewModel = FragmentInfo.ViewModel ?? CreateViewModel();
                    mvxFragment.ViewModel = viewModel;
                }
            }

            var ft = fm.BeginTransaction();
            ft.SetReorderingAllowed(true);
            ft.Replace(Resource.Id.content, firstFragment, FragmentInfo.Tag);
            ft.CommitAllowingStateLoss();
        }

        private IMvxViewModel CreateViewModel()
        {
            MvxBundle mvxBundle = null;
            if (FragmentInfo.ParameterValuesObject != null)
                mvxBundle = new MvxBundle(FragmentInfo.ParameterValuesObject.ToSimplePropertyDictionary());
            var request = new MvxViewModelRequest(FragmentInfo.ViewModelType, mvxBundle, null);

            return Mvx.IoCProvider.Resolve<IMvxViewModelLoader>().LoadViewModel(request, null);
        }
    }

根选项卡布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true"
    >

    <!-- fragment content for navigation -->
    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </FrameLayout>

</FrameLayout>

将进入根选项卡 fragment 的示例 fragment :

    [MvxFragmentPresentation(FragmentContentId = Resource.Id.content, AddToBackStack = false, ActivityHostViewModelType = typeof(HomeContainerActivity), IsCacheableFragment = true)]
    public class SettingsFragment : BaseFragment<SettingsViewModel>, IFragmentWithTitle
    {
        protected override int FragmentLayoutId => Resource.Layout.home_settings;
        public string TitleText => "Settings";
    }

修复了 Activity 中的 onbackpressed:

        public override void OnBackPressed()
        {
            var n = SupportFragmentManager.BackStackEntryCount;
            if (n >= 1)
            {
                base.OnBackPressed();
            }
            else
            {
                //Try to use the current selected tab's backstack
                var currentFragment = (Fragment)viewPager.Adapter.InstantiateItem(null, viewPager.CurrentItem); //Returns the existing fragment
                var fm = currentFragment.ChildFragmentManager;
                n = fm.BackStackEntryCount;
                if (n >= 1)
                {
                    //Must use immediate, so SetCorrectTitle can use the updated backstack to set the correct title
                    fm.PopBackStackImmediate();

                    if (n == 1)
                        SupportActionBar.SetDisplayHomeAsUpEnabled(false); //Set to true to display back button

                    //SetCorrectTitle();
                }
                else
                {
                    base.OnBackPressed();
                }
            }
        }

希望有帮助。

关于android - MVVM带 Backstack 的跨选项卡导航,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49974255/

相关文章:

android - GC_Concurrent Freed 和 Android 内存

Android - 如何让我的应用程序永远不会被垃圾收集?

java - 我应该为 tabhost 中的每个选项卡创建一个新的 Activity/xml 吗?

android - 操作栏选项卡 - 在一个选项卡中有两个 fragment (一个是动态的)

sqlite - 使用 MvvmCross 复制预填充的 SQLite 数据库的首选方法是什么

android - 如何在android中直接打开Gmail邮件编辑器?

java - 在 Textview 中显示中文文本时出现问题

apache-flex - flex 3 中的滚动标签

xamarin.android - 如何使用 MvvmCross 框架使用 MonoDroid TabActivity

xamarin.ios - 如何将 DateTime 属性绑定(bind)到我的 View 模型?