简体   繁体   English

方向更改时LoaderManager的行为

[英]LoaderManager behavior when Orientation Changes

This is the hierarchy of my app: 这是我的应用程序的层次结构:

等级制度

The 3 fragments inside the ViewPager contain a FrameLayout: ViewPager中的3个片段包含一个FrameLayout:

  • A loading spinner, visible while searching for data to fill the ListView. 一个加载微调器,在搜索数据以填充ListView时可见。
  • A LinearLayout showing a message, visible when no data is found. 显示消息的LinearLayout,当找不到数据时可见。
  • And a ListView. 还有一个ListView。

The first and second fragments get their data from a ContentProvider, using a CursorLoader. 第一个和第二个片段使用CursorLoader从ContentProvider获取数据。 And everything is working fine, except when the following situation occurs: 一切正常,除非出现以下情况:

  1. Stop the app, pressing the home button, while on landscape mode. 在横向模式下,停止应用程序,按“主页”按钮。
  2. change to portrait mode, and start the app again.(actually, if I stay in landscape the error still exists, because when I resume the app, Android recreates the activity in portrait and then rotates it. But since i'm going to show you the LOG, let's avoid logging one extra lifecycle). 更改为纵向模式,然后再次启动该应用程序。(实际上,如果我仍然停留在风景中,则该错误仍然存​​在,因为当我恢复该应用程序时,Android会以纵向模式重新创建活动然后旋转它。您的日志,让我们避免记录一个额外的生命周期)。

When the previous situation happens. 当以前的情况发生时。 The first and second fragment stay in the loadingSpinner, never show the listview. 第一个和第二个片段保留在loadingSpinner中,从不显示listview。 Let's see the Fragment1's code (the second fragment is pretty much the same): 让我们看一下Fragment1的代码(第二个片段几乎相同):

public class FragmentOne extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

private LinearLayout emptyMsgContainer;
private ListView listView;
private ProgressBar loadingSpinner;
private Details mActivity;

FragmentOneListAdapter mAdapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("onCreateView()", "FragmentOne");
    View view = inflater.inflate(R.layout.fragment_one_list_fragment, container, false);
    emptyMsgContainer = (LinearLayout)view.findViewById(R.id.empty_message_container_1);
    listView = (ListView)view.findViewById(R.id.listView_1);
    loadingSpinner = (ProgressBar)view.findViewById(R.id.loading_spinner_1);

    return view;
}

@Override
public void onActivityCreated(Bundle icicle) {
    super.onActivityCreated(icicle);
    Log.d("onActivityCreated()", "FragmentOne");

    mAdapter = new FragmentOneListAdapter(getActivity(), null, 0);

    listView.setAdapter(mAdapter);
    listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    listView.setOnItemClickListener(mListListener);

    // executes initLoader and logs at the same time
    Log.d("onActivityCreated()", getActivity().getSupportLoaderManager().initLoader(1, null, this).toString());
}

@Override
public void onResume() {
    super.onResume();
    Log.d("onResume()", "FragmentOne");
    // used to communicate direclty with the MainActivity
    MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");
    mActivity = (Details)parentFragment.getDetailsListener();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri = ...;  
        String selection = "...";
        String[] selectionArgs = new String[] { ... };
        CursorLoader loader =  new CursorLoader(getActivity(), uri, null, selection, selectionArgs, null);
        Log.d("onCreateLoader()", loader.toString());
        return loader;
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            Log.d("onLoadFinished()", loader.toString());
    if(data.getCount() == 0) {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    } else {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
    myCursor = null;
}

private OnItemClickListener mListListener = new OnItemClickListener() {

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mActivity.ShowStoredDetails(position, 1);
    }

};

}

I logged Activity and Fragment's lifecycle callbacks, onCreateLoader and onLoadFinished to try to figure out what is going on. 我记录了Activity和Fragment的生命周期回调,onCreateLoader和onLoadFinished,以试图弄清楚发生了什么。 First let's open the app in portrait: 首先,让我们以纵向打开该应用程序:

23:04:00.089: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:00.139: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:00.139: D/onCreate()(8240): <!> ...  68<!> MainFragment
23:04:00.139: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:00.169: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:00.179: D/onattach()(8240): <!> ...  51<!> FragmentOne
23:04:00.189: D/onCreate()(8240): <!> ...  57<!> FragmentOne
23:04:00.189: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:00.199: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:00.199: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
23:04:00.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:00.209: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:00.209: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:00.219: D/onattach()(8240): <!> ...  50<!> FragmentTwo
23:04:00.219: D/onCreate()(8240): <!> ...  56<!> FragmentTwo
23:04:00.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:00.229: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:00.239: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:00.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo
23:04:00.479: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

The Loader with id=1 doesn't exist, it is created. id = 1的装载程序不存在,已创建。 onCreateLoader() is called and after that onLoadFinished(). 在调用onCreateLoader()之后,再调用onLoadFinished()。 The ListView is filled and working fine. ListView已满并且可以正常工作。 Now let's rotate to landscape: 现在让我们旋转到风景:

23:04:26.999: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:27.009: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:27.009: D/onPause()(8240): <!> ...  46<!> MainActivity
23:04:27.019: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:04:27.019: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:04:27.019: D/onStop()(8240): <!> ...  125<!> MainFragment
23:04:27.019: D/onStop()(8240): <!> ...  51<!> MainActivity
23:04:27.019: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:04:27.019: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:04:27.029: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:04:27.029: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:04:27.039: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:04:27.069: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:27.079: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:27.169: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:27.199: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:27.199: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:27.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:27.239: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:27.239: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:27.239: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:27.249: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:27.249: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}
23:04:27.249: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:27.259: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:27.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo

The MainActivity is destroyed and also de view hierarchy of the Fragments, but the fragments instances remain the same (MainFragment uses setRetainInstance(true)). MainActivity被销毁,并且也查看了片段的层次结构,但是片段实例保持不变(MainFragment使用setRetainInstance(true))。 The MainActivity is recreated, the MainFragment attached to it, the FragmentOne's view hierarchy is created again and the ListView is filled with the same Loader id=1, it already exists so only onLoadFinished() is called. 重新创建MainActivity,将其连接到MainFragment,再次创建FragmentOne的视图层次结构,并用相同的Loader id = 1填充ListView,它已经存在,因此仅调用onLoadFinished()。 Now let's stop the app pressing the home button: 现在,让我们停止按下主屏幕按钮的应用程序:

23:04:59.639: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:59.649: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:59.659: D/onPause()(8240): <!> ...  46<!> MainActivity
23:05:00.059: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:05:00.069: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:05:00.069: D/onStop()(8240): <!> ...  125<!> MainFragment
23:05:00.069: D/onStop()(8240): <!> ...  51<!> MainActivity

Everything is stopped. 一切都停止了。 Finally let's resume the app: 最后,让我们恢复该应用程序:

23:05:47.489: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:05:47.489: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:05:47.499: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:05:47.499: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:05:47.499: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:05:47.509: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:05:47.509: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:05:47.539: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:05:47.569: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:05:47.569: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:05:47.579: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:05:47.579: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:05:47.589: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:05:47.589: D/onStart()(8240): <!> ...  101<!> MainFragment
23:05:47.589: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:05:47.599: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:05:47.599: D/onStart()(8240): <!> ...  31<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  41<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  107<!> MainFragment
23:05:47.599: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:05:47.599: D/onResume()(8240): <!> ...  94<!> FragmentTwo

Activity and Fragments lifecycle completes. 活动和片段生命周期完成。 The Activity is recreated, the MainFragment attaches to it. 重新创建活动,MainFragment附加到该活动。 FragmentOne's view hierarchy is created. FragmentOne的视图层次结构已创建。 But this time, the LoaderManager doesn't contain a Loader with id=1 anymore. 但是这次,LoaderManager不再包含id = 1的Loader。 When initLoader is executed, onCreateLoader() is called (the "id" parameter it receives is 1), but onLoadFinished() is not called and the loadingSpinner stays visible. 执行initLoader时,将调用onCreateLoader()(它收到的“ id”参数为1),但不会调用onLoadFinished(),并且loadingSpinner保持可见。

From the log you can compare the first time the app is executed (Loader id=1 doesn't exist)... 从日志中,您可以比较第一次执行应用程序(不存在Loader id = 1)...

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

...with the second time the loader id=1 doesn't exist: ...第二次不存在加载程序ID = 1:

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}

The first time onActivityCreated (initLoader) returns a Loader with id=1, but the second time it returns id=0. 第一次onActivityCreated(ini​​tLoader)返回id = 1的Loader,但是第二次返回id = 0。 I can only suppose that's the reason onLoadFinished() is not called the second time. 我只能假设这是第二次不调用onLoadFinished()的原因。 As far as I know a LoaderManager is supposed to retain its state when orientation changes. 据我所知,当方向改变时,LoaderManager应该保持其状态。 Any ideas of what is happening here? 对这里发生的事情有任何想法吗?

EDIT 编辑

I should have mentioned I'm using a support library: 我应该提到我正在使用支持库:

import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;

Ok, I've found similar cases: Loader restarts on orientation change . 好的,我发现了类似的情况: 加载程序在方向更改时重新启动

And it seems to be a bug in the SupportLibrary, related with my implementation of nested Fragments. 这似乎是SupportLibrary中的一个错误,与我对嵌套Fragments的实现有关。

To make it work, I had to change the location of the LoaderCallbacks interface and the initLoader() , from FragmentOne and FragmentTwo to the MainActivity. 为了使其工作,我必须将LoaderCallbacks接口和initLoader()的位置从FragmentOne和FragmentTwo更改为MainActivity。 It's a little bit messy, because i had to create some interfaces, but it does the work. 有点混乱,因为我必须创建一些接口,但是可以完成工作。

I'll explain in case someone finds himself in this situation: 如果有人发现自己处于这种情况,我将进行解释:

First, I created two interfaces: 首先,我创建了两个接口:

ListenerFragments interface, is implemented in the MainActivity and is used from FragmentOne and FragmentTwo to register themselves in the MainActivity as fragments that are going to be using loaders: ListenerFragments接口是在MainActivity中实现的,用于FragmentOne和FragmentTwo中,以便将自己注册为要使用加载程序的片段:

public interface ListenerFragments {
    public void setFragmentOne(FragmentsUICallbacks callbacks);
    public void setFragmentTwo(FragmentsUICallbacks callbacks);
    public void prepareLoader(int id);
}

The second interface, is implemented in FragmentOne and FragmentTwo. 第二个接口在FragmentOne和FragmentTwo中实现。 And consist of methods that are going to change the Fragment's UI, swapping the cursor and making the FrameLayout childs ( ListView , LoadingSpinner ...) visible or not. 并且由一些方法组成,这些方法将更改Fragment的UI,交换光标并使FrameLayoutListViewLoadingSpinner ...)可见或不可见。 Also, this is the interface we are going to be passing to the MainActivity's setFragmentOne() and setFragmentTwo() , so it can modify the UI when onLoadFinished() and onLoaderReset() are called: 此外,这是我们将要传递给在MainActivity的接口setFragmentOne()setFragmentTwo()所以当它可以修改UI onLoadFinished()onLoaderReset()被调用:

public interface FragmentsUICallbacks {
        public void emptyCursor();
        public void assignCursor(Cursor data);
    public void clearCursorReferences();
}

The MainActivity is implementing ListenerFragments and LoaderCallbacks<Cursor> interfaces: MainActivity正在实现ListenerFragmentsLoaderCallbacks<Cursor>接口:

public class MainActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>, ListenerFragments {
    private FragmentsUICallbacks fragmentOneCallbacks;
    private FragmentsUICallbacks fragmentTwoCallbacks;

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri;
        String selection;
        String[] selectionArgs;
        switch(id) {
            case 1:
                uri = ...;  
                selection = "...";
                selectionArgs = new String[] { ... };
                return new CursorLoader(this, uri, null, selection, selectionArgs, null);
            case 2:
                ...
        }
        return null;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        switch(loader.getId()) {
            case 1:
                if(data.getCount() == 0) {
                    fragmentOneCallbacks.emptyCursor(); 
                } else {
                    fragmentOneCallbacks.assignCursor(data);
                }
                break;
            case 2:
                ...
            }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        switch(loader.getId()) {
            case 1:
                fragmentOneCallbacks.clearCursorReferences();
                break;
            case 2:
                ...
        }
    }

    @Override
    public void setFragmentOne(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentOneCallbacks = callbacks;
    }

    @Override
    public void setFragmentTwo(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentTwoCallbacks = callbacks;
    }

    @Override
    public void prepareLoader(int id) {
        getSupportLoaderManager().initLoader(id, null, this);
    }
}

The code is pretty straightforward. 该代码非常简单。 The tricky part comes in FragmentOne's onResume() : 棘手的部分来自FragmentOne的onResume()

public class FragmentOne extends Fragment implements FragmentsUICallbacks {

    ...

    @Override
    public void onResume() {
        super.onResume();
        MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

        ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
        listenerFragments.setFragmentOne(this);
        listenerFragments.prepareLoader(1);
    }

    public void emptyCursor() {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    }

    public void assignCursor(Cursor data) {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }

    public void clearCursorReferences() {
        mAdapter.swapCursor(null);
        myCursor = null;
    }

}

We need to get a reference to the ListenerFragment interface's methods the MainActivity is implementing, in order to inform it FragmentOne is going to be starting a loader. 我们需要获得MainActivity正在实现的ListenerFragment接口的方法的引用,以告知FragmentOne将要启动加载程序。 We get that reference through the MainFragment, Why? 我们通过MainFragment获得该引用,为什么呢? because we can't get it directly from FragmentOne.onAttach(Activity activity) , since it is only called the first time the app is started, and the fragment is neither destroyed nor detached, when orientation changes the fragment goes from onDestroyView() to onCreateView() . 因为我们不能直接从FragmentOne.onAttach(Activity activity)获取它,因为它仅在应用程序首次启动时才被调用,并且该片段既不被破坏也不分离,当方向改变时,该片段从onDestroyView()变为onCreateView() onAttach() is not called. 不调用onAttach()

On the other hand, MainFragment, is not destroyed either ( setRetainInstance(true) ), but it is detached from the old MainActivity and attached again to the new MainActivity when orientation change completes. 另一方面,MainFragment也不会被销毁( setRetainInstance(true) ),但是它将与旧的MainActivity分离,并在方向更改完成后再次附加到新的MainActivity上。 We use onAttach() to hold the reference and we create a getter method so the fragments inside the ViewPager can get that reference: 我们使用onAttach()来保存引用,并创建一个getter方法,以便ViewPager的片段可以获取该引用:

public class MainFragment extends Fragment implements OnClickListener {

    private ListenerFragments listenerFragments;

    @Override
    public void onAttach(Activity myActivity) {
        super.onAttach(myActivity);
        this.listenerFragments = (ListenerFragments)myActivity;
    }

    public ListenerFragments getListenerFragments() {
            return listenerFragments;
    }

}

Knowing that, we can get back to FragmentOne.onResume() , where we get a reference to the MainFragment: 知道了这一点,我们可以回到FragmentOne.onResume() ,在这里我们得到对MainFragment的引用:

MainFragment parentFragment = (MainFragment)
    getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

We use the MainFragment getter method we created to get the get access to the MainActivity methods: 我们使用创建的MainFragment getter方法来获取对MainActivity方法的访问权限:

    ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
    listenerFragments.setFragmentOne(this);
    listenerFragments.prepareLoader(1);

and that's basically it. 基本上就是这样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM