【问题标题】:Loader unable to retain itself during certain configuration change加载程序在某些配置更改期间无法保留自身
【发布时间】:2013-04-09 08:56:34
【问题描述】:

根据http://developer.android.com/guide/components/loaders.html 的说法,loader 的优点之一是,它能够在配置更改期间保留其数据。

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

但是,它并不适用于所有场景。

我举一个简单的例子。它是一个FragmentActivity,它托管着一个FragmentFragment 本身拥有 AsyncTaskLoader

以下 3 个场景运行良好。

首次启动期间(正常)

1个加载器被创建,loadInBackground被执行一次。

简单旋转时(OK)

没有创建新的加载程序,也没有触发loadInBackground

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

没有创建新的加载程序,也没有触发loadInBackground

但是,在以下场景中。

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

当时调用的是老加载器的onReset。旧的装载机将被销毁。将创建新的加载程序,并再次触发新加载程序的loadInBackground

我期望的正确行为是,不会创建新的加载程序。

loader相关代码如下。我在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;
}

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

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

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

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

【问题讨论】:

  • 旧的加载器将被销毁 - 这是个问题吗(新的Loader 仍然可以使用相同的数据,对吧)?
  • 相同数据不再可用。与旧加载器销毁后一样,它之前从 loadInBackground 加载的数据也被销毁。因此,新的 loader 需要再次运行耗时的 loadInBackground。

标签: android


【解决方案1】:

我的回答其实很直截了当。不要使用 AsyncTaskLoaders。因为您现在已经知道一些关于 AsyncTaskLoaders 的错误。

一个很好的组合是一个可保留的 (setRetainInstance(true) in onActivityCreated()) 片段与 AsyncTask。以同样的方式工作。只需要稍微重构一下代码。


来自 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();            
    }

【讨论】:

  • 我想我知道保留片段+异步任务技术。只是,该技术更繁琐,需要更多的手工工作。在我意识到我需要放弃 AsyncTaskLoader 的所有优点之前,我不想采用这种方法。直到现在,我都不想放弃:) stackoverflow.com/questions/15079948/…
  • 有时手动工作可以让您更好地控制和更好地了解一切是如何运作的。它已经过测试并证明有效,甚至来自@commonsguy 本人。
  • 我能想到的解决方案之一是保留一个实例非 UI 片段来保存数据。在主片段尝试创建/重新连接到 AsyncTaskLoaders 之前的任何时候,它都会首先查找保留的实例片段。如果有,主片段将不会打扰 AsyncTaskLoaders。
  • 如果您可以在我现有的给定代码的顶部添加代码示例,那就太好了。如果我将 500 分(WOW)奖励给你,那就没有异议了:)
【解决方案2】:

尝试改变,

 @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.
    }
}    

 @Override
public void onResume()
{
    super.onResume();

Loader loader = getLoaderManager().getLoader(0); 
if ( loader != null && loader.isReset() ) { 
    getLoaderManager().restartLoader(0, getArguments(), this); 
} else { 
    getLoaderManager().initLoader(0, getArguments(), this); 
} 

}    

【讨论】:

  • 不工作。如果参考上面的测试用例A child activity is launched -&gt; Rotation -&gt; Back button pressed (Wrong),restartLoader永远不会被调用。
  • 好的。如果你把if ( loader != null &amp;&amp; loader.isReset() )改成if ( loader != null &amp;&amp; !loader.isReset() )会怎么样
【解决方案3】:

如果您使用 FragmentManager 的替换片段技术,则会发生此问题。

当您替换/删除 Fragment 时,该 Fragment 会从 Activity 中分离出来,并且由于加载器附加到 Activity,因此加载器将在方向更改期间重新创建。

尝试使用 FragmentManager 的隐藏/显示技术。或许这会对你有所帮助。

【讨论】:

  • 它与替换/删除片段无关。当您从 Fragment 显示新 Activity 时,会出现此问题。
【解决方案4】:

我已经成功地将 AsyncTaskLoader 子类化并对其方法进行了一些调整。

public class FixedAsyncTaskLoader<D> extends AsyncTaskLoader<D> {

    private D result;

    public FixedAsyncTaskLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (result != null) {
            deliverResult(result);
        } else {
            forceLoad();
        }
    }

    @Override
    public void deliverResult(T data) {
        result = data;

        if (isStarted()) {
            super.deliverResult(result);
        }
    }

    @Override
    protected void onReset() {
        super.onReset();
        onStopLoading();

        result = null;
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-19
    • 1970-01-01
    • 1970-01-01
    • 2012-01-15
    • 1970-01-01
    • 2015-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多