【问题标题】:How do I save application state between Activities with embedded fragments in the backstack?如何在 Backstack 中嵌入片段的活动之间保存应用程序状态?
【发布时间】:2014-08-04 19:12:10
【问题描述】:

在使用嵌入在活动中的片段时,关于恢复应用程序状态的正确方法,我发现了大量相互矛盾的信息。请让我知道我的架构是否有问题,因为这是完全可能的。我的测试天气应用程序的架构如下。

主要活动“ReportsActivity”包含片段“ReportsFragment”(这是未来10天天气报告的列表) ReportsFragment 有一个 onItemClickListener,它启动一个新的 Activity“WeatherDetailActivity”,并向它传递一个 Intent,其中包含一些我用来填充 Weather Detail UI 的 JSON 数据。然后,此数据将显示在 WeatherDetailActivity 管理的片段上。

我的问题是,当用户按下后退按钮时,ReportsFragment 已被销毁,因此它会贯穿其整个生命周期。我已经尝试了一些我在网上找到的技术来从包中加载活动的数据,但无论我到目前为止尝试了什么,Intents 的 Extras 在 ReportsActivity 的 onCreate 方法中都是空的。 (注意:我需要这样做的原因是避免每次打开从 Weather Underground 获取天气数据的主 Activity 时触发 API 调用)。

我正在努力确定构建这个应用程序的最佳方式是什么:我是否应该有一个单一的活动来推送和弹出它管理的片段?还是每个管理自己的片段的多个活动是标准做法?

目前我正在尝试将应用程序状态保存到意图中。我正在尝试从我的 AsyncTask 中保存 onPostExecute 中的状态,因此在我从 API 调用中获取结果后,我在主线程上:

 @Override
protected void onPostExecute(Report[] result){
    if (result != null){

        ArrayList<String>reportsArrayList = new ArrayList<String>();
        Gson jsonArray = new GsonBuilder().setPrettyPrinting().create();
        for (int x = 0; x < result.length; x++){

            reportsArrayList.add(jsonArray.toJson(result[x], Report.class));
        }

        mExtras.putStringArrayList(ReportsActivity.ReportsActivityState.KEY_ACTIVITY_REPORTS,reportsArrayList);
    }
}

然后我尝试从 ReportsActivity 的 onCreate 方法恢复状态:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_reports);

    if (savedInstanceState == null) {

        Intent intent = getIntent();

        mFragment = ReportsFragment.newInstance(intent
                .getStringArrayListExtra(ReportsActivityState.KEY_ACTIVITY_REPORTS));

        getFragmentManager().beginTransaction()
                .add(R.id.container, mFragment).commit();
    }
}

在所有情况下,我试图从意图中获取的 StringArrayListExtra 返回 null。

这很可能是我试图以 iOS 思维方式解决 Android 问题,但是否有一种简单的方法可以将主要活动恢复到我推送详细视图之前的状态?

【问题讨论】:

    标签: java android android-intent android-fragments android-asynctask


    【解决方案1】:

    我认为看看 EventBus 是值得的。

    基本上你可以定义任何类型的对象持有者,例如:

    class WeatherData {
        List<String> reports;
        public WeatherData(List<String> reports) {
            this.reports = reports;
        }
    }
    

    现在,在您希望记住状态的 Activity 或 Fragment 中,或将某些状态传递给另一个 Activity 或 Fragment 时,请执行以下操作:

    // this removes all the hazzle of creating bundles etc
    EventBus.getDefault().postSticky(new WeatherData(reports)); 
    

    您希望在代码中的任何位置了解最新的 WeatherData:

    WeatherData weatherData = EventBus.getDefault().getSticky(WeatherData.class);
    

    EventBus 也有很好的事件处理方法(按钮点击、长时间运行进程的完成等)

    图书馆可以在这里找到:https://github.com/greenrobot/EventBus

    这里还有更多示例:http://awalkingcity.com/blog/2013/02/26/productive-android-eventbus/

    一些不使用3.零件库的建议:

    1) 在您的片段 onCreate 方法中调用 setRetainInstance(true),这应该做的是在实例之间保留公共变量。

    虽然它似乎不适用于后堆栈上的片段:Understanding Fragment's setRetainInstance(boolean)

    2) 将片段数据传递给您的 Activity,例如读取/更新 ((YourActivity)getActivity()).someFragmentBundle,可能将其保存在 Activity 的 onSaveInstanceState 中并在 onCreate 中检索。也就是说,让您的 Activity 保存实例之间的数据。

    3) 您还可以持久化数据,将其保存到文件或使用 SharedPreferences http://developer.android.com/training/basics/data-storage/index.html

    此方法的优点是即使在您的应用程序完全关闭后也可以恢复数据。

    架构问题

    免责声明:主观意见

    我通常会说保持 Activity 尽可能“苗条”,包含一系列相关的片段。

    因此,拥有多个 Activity 很好,但它们应该各自管理一组(或单个)与当前 Activity 相关的相关片段。

    【讨论】:

    • 这看起来很棒,感谢您的回答。我一定会试一试的。但我特别在寻找不需要第三方库的解决方案,这样我就可以在省去麻烦并使用第三方库之前更好地理解 Android 设计模式。您是否碰巧知道使用 Android 标准库实现此目的的方法?
    • 已尝试提出一些建议。还尝试记录您的片段的方法,我感觉您认为可能不会调用 onCreate 。看看这里并向下滚动一点:developer.android.com/guide/components/fragments.html.
    【解决方案2】:

    我突然想到要查看 Google 提供的一个我经常忽略的 Android Studio 模板。从谷歌自己的模板中,可以清楚地看出,主从活动/片段的首选方法是让每个片段由它们自己的活动管理(正如我在上面试图实现的那样)。

    (我应该注意到,我能够成功地实现我想要使用具有多个片段的单个 Activity 并自定义动画并强制显示和隐藏向上按钮。)

    PersonListActivity.java

     public class PersonListActivity extends Activity
            implements PersonListFragment.Callbacks {
    
        /**
         * Whether or not the activity is in two-pane mode, i.e. running on a tablet
         * device.
         */
        private boolean mTwoPane;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_person_list);
    
            if (findViewById(R.id.person_detail_container) != null) {
                // The detail container view will be present only in the
                // large-screen layouts (res/values-large and
                // res/values-sw600dp). If this view is present, then the
                // activity should be in two-pane mode.
                mTwoPane = true;
    
                // In two-pane mode, list items should be given the
                // 'activated' state when touched.
                ((PersonListFragment) getFragmentManager()
                        .findFragmentById(R.id.person_list))
                        .setActivateOnItemClick(true);
            }
    
            // TODO: If exposing deep links into your app, handle intents here.
        }
    
        /**
         * Callback method from {@link PersonListFragment.Callbacks}
         * indicating that the item with the given ID was selected.
         */
        @Override
        public void onItemSelected(String id) {
            if (mTwoPane) {
                // In two-pane mode, show the detail view in this activity by
                // adding or replacing the detail fragment using a
                // fragment transaction.
                Bundle arguments = new Bundle();
                arguments.putString(PersonDetailFragment.ARG_ITEM_ID, id);
                PersonDetailFragment fragment = new PersonDetailFragment();
                fragment.setArguments(arguments);
                getFragmentManager().beginTransaction()
                        .replace(R.id.person_detail_container, fragment)
                        .commit();
    
            } else {
                // In single-pane mode, simply start the detail activity
                // for the selected item ID.
                Intent detailIntent = new Intent(this, PersonDetailActivity.class);
                detailIntent.putExtra(PersonDetailFragment.ARG_ITEM_ID, id);
                startActivity(detailIntent);
            }
        }
    }
    

    PersonListFragment.java

      public class PersonListFragment extends ListFragment {
    
        /**
         * The serialization (saved instance state) Bundle key representing the
         * activated item position. Only used on tablets.
         */
        private static final String STATE_ACTIVATED_POSITION = "activated_position";
    
        /**
         * The fragment's current callback object, which is notified of list item
         * clicks.
         */
        private Callbacks mCallbacks = sDummyCallbacks;
    
        /**
         * The current activated item position. Only used on tablets.
         */
        private int mActivatedPosition = ListView.INVALID_POSITION;
    
        /**
         * A callback interface that all activities containing this fragment must
         * implement. This mechanism allows activities to be notified of item
         * selections.
         */
        public interface Callbacks {
            /**
             * Callback for when an item has been selected.
             */
            public void onItemSelected(String id);
        }
    
        /**
         * A dummy implementation of the {@link Callbacks} interface that does
         * nothing. Used only when this fragment is not attached to an activity.
         */
        private static Callbacks sDummyCallbacks = new Callbacks() {
            @Override
            public void onItemSelected(String id) {
            }
        };
    
        /**
         * Mandatory empty constructor for the fragment manager to instantiate the
         * fragment (e.g. upon screen orientation changes).
         */
        public PersonListFragment() {
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            // TODO: replace with a real list adapter.
            setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(
                    getActivity(),
                    android.R.layout.simple_list_item_activated_1,
                    android.R.id.text1,
                    DummyContent.ITEMS));
        }
    
        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
    
            // Restore the previously serialized activated item position.
            if (savedInstanceState != null
                    && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
                setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
            }
        }
    
        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
    
            // Activities containing this fragment must implement its callbacks.
            if (!(activity instanceof Callbacks)) {
                throw new IllegalStateException("Activity must implement fragment's callbacks.");
            }
    
            mCallbacks = (Callbacks) activity;
        }
    
        @Override
        public void onDetach() {
            super.onDetach();
    
            // Reset the active callbacks interface to the dummy implementation.
            mCallbacks = sDummyCallbacks;
        }
    
        @Override
        public void onListItemClick(ListView listView, View view, int position, long id) {
            super.onListItemClick(listView, view, position, id);
    
            // Notify the active callbacks interface (the activity, if the
            // fragment is attached to one) that an item has been selected.
            mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            if (mActivatedPosition != ListView.INVALID_POSITION) {
                // Serialize and persist the activated item position.
                outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
            }
        }
    
        /**
         * Turns on activate-on-click mode. When this mode is on, list items will be
         * given the 'activated' state when touched.
         */
        public void setActivateOnItemClick(boolean activateOnItemClick) {
            // When setting CHOICE_MODE_SINGLE, ListView will automatically
            // give items the 'activated' state when touched.
            getListView().setChoiceMode(activateOnItemClick
                    ? ListView.CHOICE_MODE_SINGLE
                    : ListView.CHOICE_MODE_NONE);
        }
    
        private void setActivatedPosition(int position) {
            if (position == ListView.INVALID_POSITION) {
                getListView().setItemChecked(mActivatedPosition, false);
            } else {
                getListView().setItemChecked(position, true);
            }
    
            mActivatedPosition = position;
        }
    }
    

    PersonDetailActivity.java

        public class PersonDetailActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_person_detail);
    
            // Show the Up button in the action bar.
            getActionBar().setDisplayHomeAsUpEnabled(true);
    
            // savedInstanceState is non-null when there is fragment state
            // saved from previous configurations of this activity
            // (e.g. when rotating the screen from portrait to landscape).
            // In this case, the fragment will automatically be re-added
            // to its container so we don't need to manually add it.
            // For more information, see the Fragments API guide at:
            //
            // http://developer.android.com/guide/components/fragments.html
            //
            if (savedInstanceState == null) {
                // Create the detail fragment and add it to the activity
                // using a fragment transaction.
                Bundle arguments = new Bundle();
                arguments.putString(PersonDetailFragment.ARG_ITEM_ID,
                        getIntent().getStringExtra(PersonDetailFragment.ARG_ITEM_ID));
                PersonDetailFragment fragment = new PersonDetailFragment();
                fragment.setArguments(arguments);
                getFragmentManager().beginTransaction()
                        .add(R.id.person_detail_container, fragment)
                        .commit();
            }
        }
    
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            int id = item.getItemId();
            if (id == android.R.id.home) {
                // This ID represents the Home or Up button. In the case of this
                // activity, the Up button is shown. For
                // more details, see the Navigation pattern on Android Design:
                //
                // http://developer.android.com/design/patterns/navigation.html#up-vs-back
                //
                navigateUpTo(new Intent(this, PersonListActivity.class));
                return true;
            }
            return super.onOptionsItemSelected(item);
        }
    }
    

    PersonDetailFragment.java

        public class PersonDetailFragment extends Fragment {
        /**
         * The fragment argument representing the item ID that this fragment
         * represents.
         */
        public static final String ARG_ITEM_ID = "item_id";
    
        /**
         * The dummy content this fragment is presenting.
         */
        private DummyContent.DummyItem mItem;
    
        /**
         * Mandatory empty constructor for the fragment manager to instantiate the
         * fragment (e.g. upon screen orientation changes).
         */
        public PersonDetailFragment() {
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            if (getArguments().containsKey(ARG_ITEM_ID)) {
                // Load the dummy content specified by the fragment
                // arguments. In a real-world scenario, use a Loader
                // to load content from a content provider.
                mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
            }
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false);
    
            // Show the dummy content as text in a TextView.
            if (mItem != null) {
                ((TextView) rootView.findViewById(R.id.person_detail)).setText(mItem.content);
            }
    
            return rootView;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-30
      • 2023-03-18
      • 1970-01-01
      • 2017-05-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多