android - pie - ¿Realmente deberíamos llamar a getLoaderManager(). InitLoader en onActivityCreated, lo que hace que se llame dos veces a onLoadFinished?
android pie (2)
Google nos recomienda llamar a getLoaderManager().initLoader(0, null, this);
dentro de Fragment''s onActivityCreated
http://developer.android.com/reference/android/content/AsyncTaskLoader.html
Sin embargo, esto produce el siguiente problema: se llamará dos veces a onLoadFinished durante los cambios de configuración (Rotación)
Podemos simular el problema de la siguiente manera.
Código
package org.yccheok.gui;
import android.content.Context;
import android.os.Bundle;
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;
import com.actionbarsherlock.app.SherlockFragment;
public class HomeMenuFragment extends SherlockFragment implements LoaderManager.LoaderCallbacks<HomeMenuFragment.Infos> {
private static class InfosLoader extends AsyncTaskLoader<Infos> {
private Infos infos = null;
public InfosLoader(Context context) {
super(context);
}
@Override
public Infos loadInBackground() {
Log.i(TAG, "loadInBackground");
this.infos = Infos.newInstance();
return infos;
}
/**
* Handles a request to cancel a load.
*/
@Override
public void onCanceled(Infos infos) {
super.onCanceled(infos);
}
/**
* 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.infos != null) {
Log.i(TAG, "deliverResult");
deliverResult(this.infos);
}
if (takeContentChanged() || this.infos == null) {
Log.i(TAG, "forceLoad");
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.infos = null;
}
}
static class Infos {
private Infos() {
}
public static Infos newInstance() {
return new Infos();
}
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.i(TAG, "onActivityCreated");
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Infos> onCreateLoader(int arg0, Bundle arg1) {
return new InfosLoader(this.getSherlockActivity());
}
@Override
public void onLoadFinished(Loader<Infos> arg0, Infos arg1) {
Log.i(TAG, "onLoadFinished! -> " + arg1);
}
@Override
public void onLoaderReset(Loader<Infos> arg0) {
}
public void reloadAfterOpenFromCloud() {
this.getLoaderManager().getLoader(0).onContentChanged();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.home_menu, container, false);
return v;
}
private static final String TAG = HomeMenuFragment.class.getSimpleName();
}
Explotación florestal
I/HomeMenuFragment(14776): onActivityCreated
I/HomeMenuFragment(14776): forceLoad
I/HomeMenuFragment(14776): loadInBackground
I/HomeMenuFragment(14776): onLoadFinished! -> org.yccheok.gui.HomeMenuFragment$Infos@4195ad58
[Rotation happens right here]
I/HomeMenuFragment(14776): onActivityCreated
I/HomeMenuFragment(14776): onLoadFinished! -> org.yccheok.gui.HomeMenuFragment$Infos@4195ad58
I/HomeMenuFragment(14776): onLoadFinished! -> org.yccheok.gui.HomeMenuFragment$Infos@4195ad58
Según Android: LoaderCallbacks.OnLoadFinished se llama dos veces , una de las soluciones propuestas es llamar a initLoader
en onResume
.
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.i(TAG, "onActivityCreated");
//getLoaderManager().initLoader(0, null, this);
}
@Override
public void onResume()
{
super.onResume();
Log.i(TAG, "onResume");
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
Aquí está el registro. Ahora se ve bien después de mover initLoader
a onResume
.
Explotación florestal
I/HomeMenuFragment(15468): onActivityCreated
I/HomeMenuFragment(15468): onResume
I/HomeMenuFragment(15468): forceLoad
I/HomeMenuFragment(15468): loadInBackground
I/HomeMenuFragment(15468): onLoadFinished! -> org.yccheok.gui.HomeMenuFragment$Infos@4195aed0
I/HomeMenuFragment(15468): onActivityCreated
I/HomeMenuFragment(15468): onResume
I/HomeMenuFragment(15468): onLoadFinished! -> org.yccheok.gui.HomeMenuFragment$Infos@4195aed0
me preguntaba
- ¿Por qué funciona la solución propuesta?
- ¿Es esto un error? ¿Debemos presentar un error a Google con respecto a este comportamiento? Tal vez hay un boleto que se está presentando a Google, pero no puedo encontrarlo.
Intente eliminar el resultado de entrega de su onStartLoading
. LoaderManager
ya devuelve valores existentes cuando se llama a initLoader
para un cargador que ya ha regresado.
¿Por qué la solución propuesta está funcionando?
Si llamamos a getLoaderManager()
en onActivityCreated()
, inicializamos la variable Fragment.mLoaderManager
.
Como resultado tenemos la llamada mLoaderManager.doReportStart()
en Fragment.performStart()
en FragmentActivity.onStart()
:
void performStart() {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
mChildFragmentManager.execPendingActions();
}
mCalled = false;
onStart();
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onStart()");
}
if (mChildFragmentManager != null) {
mChildFragmentManager.dispatchStart();
}
if (mLoaderManager != null) {
mLoaderManager.doReportStart();
}
}
Es causa de la primera llamada de onLoadFinished()
.
Más adelante en FragmentActivity.onStart()
hemos llamado a lm.finishRetain()
(ver fragmento de código):
if (mAllLoaderManagers != null) {
LoaderManagerImpl loaders[] = new LoaderManagerImpl[mAllLoaderManagers.size()];
mAllLoaderManagers.values().toArray(loaders);
if (loaders != null) {
for (int i=0; i<loaders.length; i++) {
LoaderManagerImpl lm = loaders[i];
lm.finishRetain();
lm.doReportStart();
}
}
}
Es causa de la segunda llamada de onLoadFinished()
.
DE ACUERDO. Ahora considere el caso cuando llamamos a getLoaderManager().initLoader(0, null, this)
en onResume()
:
Si lo hacemos de esta manera, no tenemos mLoaderManager.doReportStart()
ni lm.finishRetain()
después de onActivityCreated()
, sino que tenemos onLoadFinished()
llamada initLoader()
durante initLoader()
:
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn''t already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
Puede ver la llamada a info.callOnLoadFinished()
en este fragmento:
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
Creo que está claro :)