在某些配置更改期间,Loader无法保留自身

Che*_*eng 20 android

根据http://developer.android.com/guide/components/loaders.html,有关加载器的一个好处是,它能够在配置更改期间保留其数据.

在配置更改后重新创建时,它们会自动重新连接到最后一个加载器的光标.因此,他们不需要重新查询他们的数据.

但是,它在所有情况下都不能很好地工作.

我采取以下简单的例子.它是一个FragmentActivity,主持一个Fragment.它Fragment本身拥有AsyncTaskLoader.

以下3种情况非常有效.

首次启动时(OK)

创建了1个加载程序,并loadInBackground执行一次.

在简单旋转期间(OK)

没有创建新的加载器,loadInBackground也没有被触发.

启动子活动,按下后退按钮(OK)

没有创建新的加载器,loadInBackground也没有被触发.

但是,在以下场景中.

启动子活动 - >旋转 - >按下后退按钮(错误)

那时,旧的装载机onReset被称为.旧装载机将被销毁.将创建新的加载器loadInBackground并再次触发新的加载器 .

我期待的正确行为是,不会创建新的加载器.

加载器相关代码如下.我在Android 4.1模拟器下运行代码.

package com.example.bug;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MainFragment extends Fragment implements LoaderManager.LoaderCallbacks<Integer> {
    private static class IntegerArrayLoader extends AsyncTaskLoader<Integer> {

        private Integer result = null;

        public IntegerArrayLoader(Context context) {
            super(context);
            Log.i("CHEOK", "IntegerArrayLoader created!");
        }

        @Override
        public Integer loadInBackground() {
            Log.i("CHEOK", "Time consuming loadInBackground!");
            this.result = 123456;
            return result;
        }

        /**
         * Handles a request to cancel a load.
         */
        @Override 
        public void onCanceled(Integer integer) {
            super.onCanceled(integer);
        }

        /**
         * Handles a request to stop the Loader.
         * Automatically called by LoaderManager via stopLoading.
         */
        @Override 
        protected void onStopLoading() {
            // Attempt to cancel the current load task if possible.
            cancelLoad();
        }

        /**
         * Handles a request to start the Loader.
         * Automatically called by LoaderManager via startLoading.
         */
        @Override        
        protected void onStartLoading() {
            if (this.result != null) {
                deliverResult(this.result);
            }

            if (takeContentChanged() || this.result == null) {
                forceLoad();
            }
        }

        /**
         * Handles a request to completely reset the Loader.
         * Automatically called by LoaderManager via reset.
         */
        @Override 
        protected void onReset() {
            super.onReset();

            // Ensure the loader is stopped
            onStopLoading();

            // At this point we can release the resources associated with 'apps'
            // if needed.
            this.result = null;
        }        
    }

    @Override
    public Loader<Integer> onCreateLoader(int arg0, Bundle arg1) {
        Log.i("CHEOK", "onCreateLoader being called");
        return new IntegerArrayLoader(this.getActivity());
    }

    @Override
    public void onLoadFinished(Loader<Integer> arg0, Integer arg1) {
        result = arg1;

    }

    @Override
    public void onLoaderReset(Loader<Integer> arg0) {
        // TODO Auto-generated method stub

    }

    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_main, container, false);
        return v;
    }

    // http://stackoverflow.com/questions/11293441/android-loadercallbacks-onloadfinished-called-twice
    @Override
    public void onResume()
    {
        super.onResume();

        if (result == null) {
            // Prepare the loader.  Either re-connect with an existing one,
            // or start a new one.
            getLoaderManager().initLoader(0, null, this);
        } else {
            // Restore from previous state. Perhaps through long pressed home
            // button.
        }
    }    

    private Integer result;
}
Run Code Online (Sandbox Code Playgroud)

完整的源代码可以从https://www.dropbox.com/s/n2jee3v7cpwvedv/loader_bug.zip下载

这可能与1个未解决的Android错误有关:https://code.google.com/p/android/issues/detail? id = 20791 & can = 5& cfpec = ID%20Type%20Status%20Owner%20Summary%20Stars

https://groups.google.com/forum/?fromgroups=#!topic/android-developers/DbKL6PVyhLI

我想知道,这个bug有什么好的解决方法吗?

Kev*_*Tan 6

我的回答实际上非常直截了当.不要使用AsyncTaskLoaders.因为有关AsyncTaskLoaders的一些错误,你现在就知道了.

一个很好的组合是使用AsyncTask保留(onActivityCreated())片段中的setRetainInstance(true).以同样的方式工作.只需稍微重构一下代码.


来自OP的消息

虽然作者没有提供任何代码示例,但这是最接近的可行解决方案.我不使用作者提出的解决方案.相反,我仍然依赖于AsyncTaskLoader所有必要的加载任务.解决方法是,我将依赖一个额外的保留片段来确定是否应该重新连接/创建加载器.这是整个想法的骨架代码.只要我知道,到目前为止工作得很好.

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    ...

    dataRetainedFragment = (DataRetainedFragment)fm.findFragmentByTag(DATE_RETAINED_FRAGMENT);
    // dataRetainedFragment can be null still...
}

@Override
public void onResume() {
    ...
    if (this.data == null) {
        if (dataRetainedFragment != null) {
            // Re-use!
            onLoadFinished(null, dataRetainedFragment);
        } else {
            // Prepare the loader.  Either re-connect with an existing one,
            // or start a new one.
            getLoaderManager().initLoader(0, null, this);
        }
    } else {
    }
}

@Override
public void onLoadFinished(Loader<Data> arg0, Data data) {
    this.data = data;

    if (this.dataRetainedFragment == null) {
        this.dataRetainedFragment = DataRetainedFragment.newInstance(this.data);
        FragmentManager fm = getFragmentManager();
        fm.beginTransaction().add(this.dataRetainedFragment, DATE_RETAINED_FRAGMENT).commitAllowingStateLoss();            
    }
Run Code Online (Sandbox Code Playgroud)