Різниця між initLoader та restartLoader у LoaderManager


129

Я повністю втрачений щодо відмінностей між функціями initLoaderта restartLoaderфункціями LoaderManager:

  • Вони обоє мають однаковий підпис.
  • restartLoader також створює завантажувач, якщо його не існує ("Запускає новий або перезапускає наявний навантажувач у цьому менеджері").

Чи існує якесь співвідношення між двома методами? Чи дзвонить restartLoaderзавжди initLoader? Чи можу я зателефонувати restartLoaderбез дзвінка initLoader? Чи безпечно дзвонити initLoaderдвічі, щоб оновити дані? Коли я повинен використовувати одне з двох і чому ?

Відповіді:


202

Щоб відповісти на це запитання, вам потрібно зануритися в LoaderManagerкод. Хоча сама документація для LoaderManager недостатньо чітка (або не виникало б цього питання), документація для LoaderManagerImpl, підкласу абстрактного LoaderManager, є набагато просвітнішою.

initLoader

Зателефонуйте ініціалізувати певний ідентифікатор за допомогою навантажувача. Якщо для цього ідентифікатора вже є асоційований навантажувач, він залишається незмінним і будь-які попередні зворотні дзвінки замінюються на щойно надані. Якщо наразі немає навантажувача для ідентифікатора, створюється і запускається новий.

Ця функція, як правило, повинна використовуватися при ініціалізації компонента, щоб забезпечити створення завантажувача, на який він покладається. Це дозволяє йому повторно використовувати дані наявних навантажувачів, якщо такі вже є, так що, наприклад, коли активність заново створюється після зміни конфігурації, їй не потрібно повторно створювати завантажувачі.

restartLoader

Заклик повторно створити навантажувач, пов’язаний з певним ідентифікатором. Якщо в даний час навантажувач пов'язаний з цим ідентифікатором, він буде скасований / зупинений / знищений, як це доречно. Буде створений новий навантажувач із заданими аргументами, і його дані будуть доставлені вам, коли вони стануть доступними.

[...] Після виклику цієї функції будь-які попередні навантажувачі, пов’язані з цим ідентифікатором, будуть вважатися недійсними, і ви більше не отримуватимете від них оновлення даних.

В основному є два випадки:

  1. Навантажувач з ідентифікатором не існує: обидва способи створять новий завантажувач, тому різниці там немає
  2. Навантажувач з ідентифікатором вже існує: initLoaderзамінить лише зворотні дзвінки, передані як параметр, але не скасовує та не зупиняє завантажувач. Для CursorLoaderцього означає, що курсор залишається відкритим і активним (якщо це було так до initLoaderвиклику). `restartLoader, з іншого боку, скасує, зупинить і знищить завантажувач (і закриє базове джерело даних, як курсор) і створить новий завантажувач (який також створить новий курсор і повторно запустить запит, якщо завантажувач буде CursorLoader).

Ось спрощений код для обох методів:

initLoader

LoaderInfo info = mLoaders.get(id);
if (info == null) {
    // Loader doesn't already exist -> create new one
    info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
   // Loader exists -> only replace callbacks   
   info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}

restartLoader

LoaderInfo info = mLoaders.get(id);
if (info != null) {
    LoaderInfo inactive = mInactiveLoaders.get(id);
    if (inactive != null) {
        // does a lot of stuff to deal with already inactive loaders
    } else {
        // Keep track of the previous instance of this loader so we can destroy
        // it when the new one completes.
        info.mLoader.abandon();
        mInactiveLoaders.put(id, info);
    }
}
info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);

Як ми бачимо у випадку, якщо завантажувача не існує (info == null), обидва способи створять новий завантажувач (info = createAndInstallLoader (...)). Якщо завантажувач вже існує, initLoaderвін замінює лише зворотні дзвінки (info.mCallbacks = ...), тоді як restartLoaderінактивує старий завантажувач (він буде знищений, коли новий завантажувач завершить свою роботу), а потім створить новий.

Таким чином, сказано, що зараз зрозуміло, коли використовувати, initLoaderколи використовувати restartLoaderта чому має сенс використовувати два методи. initLoaderвикористовується для ініціалізації завантажувача. Якщо немає, створюється новий, якщо він уже існує, він буде повторно використаний. Ми завжди використовуємо цей метод, БІЛЬШЕ нам потрібен новий завантажувач, оскільки запит для запуску змінився (не основні дані, а фактичний запит, як у SQL-операторі для CursorLoader), і в цьому випадку ми зателефонуємо restartLoader.

Життєвий цикл " Діяльність / фрагмент " не має нічого спільного з рішенням про використання того чи іншого методу (і немає необхідності відслідковувати дзвінки, використовуючи прапор одного пострілу, як запропонував Саймон)! Це рішення приймається виключно виходячи з "потреби" нового навантажувача. Якщо ми хочемо запустити той самий запит, який ми використовуємо initLoader, якщо ми хочемо запустити інший запит, який ми використовуємо restartLoader.

Ми завжди могли використовувати, restartLoaderале це було б неефективно. Після обертання екрана або якщо користувач відходить від програми та повертається пізніше до тієї ж діяльності, ми зазвичай хочемо показати той самий результат запиту, і тому ми restartLoaderзайво створимо завантажувач та відкинемо базовий (потенційно дорогий) результат запиту.

Дуже важливо зрозуміти різницю між завантаженими даними та "запитом" для завантаження цих даних. Припустимо, ми використовуємо CursorLoader, який запитує таблицю для замовлень. Якщо до цієї таблиці додано нове замовлення, CursorLoader використовує onContentChanged (), щоб повідомити інтерфейс користувача про оновлення та показати нове замовлення (не потрібно використовувати restartLoaderв цьому випадку). Якщо ми хочемо відображати лише відкриті замовлення, нам потрібен новий запит, і ми будемо використовувати restartLoaderдля повернення нового CursorLoader, що відображає новий запит.


Чи існує якесь співвідношення між двома методами?

Вони діляться кодом, щоб створити новий навантажувач, але вони роблять різні речі, коли завантажувач вже існує.

Чи дзвонить restartLoaderзавжди initLoader?

Ні, це ніколи не відбувається.

Чи можу я зателефонувати restartLoaderбез дзвінка initLoader?

Так.

Чи безпечно дзвонити initLoaderдвічі, щоб оновити дані?

Безпечно дзвонити initLoaderдвічі, але дані не будуть оновлені.

Коли я повинен використовувати одне з двох і чому ?


Це повинно бути (сподіваюся) зрозумілим після моїх пояснень вище.

Зміни конфігурації

LoaderManager зберігає свій стан через зміни конфігурації (включаючи зміни орієнтації), так що ви можете подумати, що нам нічого не залишається. Подумати ще раз...

Перш за все, LoaderManager не зберігає зворотні дзвінки, тому, якщо ви нічого не зробите, ви не отримуватимете дзвінки на ваші способи зворотного дзвінка, як onLoadFinished()тощо, і це, ймовірно, зламає вашу програму.

Тому ми МАЄМО закликати принаймні initLoaderдля відновлення методів зворотного виклику ( restartLoaderце, звичайно, теж можливо). У документації зазначено:

Якщо в точці виклику абонент перебуває у запущеному стані, а запитуваний завантажувач вже існує та генерував свої дані, тоді зворотний виклик onLoadFinished(Loader, D)буде викликаний негайно (всередині цієї функції) [...].

Це означає, що якщо ми зателефонуємо initLoaderпісля зміни орієнтації, ми отримаємо onLoadFinishedдзвінок одразу, оскільки дані вже завантажені (припускаючи, що це було до зміни). Хоча це звучить прямо вперед, це може бути складним (чи не всі ми любимо Android ...).

Ми маємо розрізняти два випадки:

  1. Конфігурація керує самою зміною: це стосується фрагментів, які використовують setRetainInstance (true), або для діяльності з android:configChangesтегом відповідно до маніфесту. Ці компоненти не отримуватимуть дзвінок onCreate після, наприклад, обертання екрана, тому майте на увазі, щоб викликати initLoader/restartLoaderінший метод зворотного виклику (наприклад, у onActivityCreated(Bundle)). Щоб мати можливість ініціалізувати навантажувач (и), ідентифікатори завантажувача потрібно зберігати (наприклад, у списку). Оскільки компонент зберігається через зміни конфігурації, ми можемо просто перевести цикл на існуючі ідентифікатори завантажувача та викликати initLoader(loaderid, ...).
  2. Не обробляє самих змін конфігурації: у цьому випадку завантажувачі можуть бути ініціалізовані в onCreate, але нам потрібно зберегти ідентифікатори завантажувача вручну або ми не зможемо здійснити необхідні дзвінки initLoader / restartLoader. Якщо ідентифікатори зберігаються в ArrayList, ми зробимо
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)в onSaveInstanceState і відновимо ідентифікатори в onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)перед тим, як здійснити виклик initLoader.

: +1: Остання точка. Якщо ви користуєтесь initLoader(і всі зворотні дзвінки завершені, навантажувач не працює) після обертання ви не отримаєте onLoadFinishedзворотний дзвінок, але якщо ви будете використовувати його, restartLoaderви будете?
Блонделл

Неправильно. Метод initLoader викликає метод onLoadFinished () перед його поверненням (якщо завантажувач запущений і має дані). Я додав параграф про зміни конфігурації, щоб пояснити це детальніше.
Емануель Моеклін

6
ну звичайно, поєднання вашої відповіді та @ alexlockwood дає повну картину. Я думаю, відповідь для інших є, використовувати initLoader , якщо ваш запит є статичним і restartLoader , якщо ви хочете змінити запит
BLUNDELL

1
Це чудово викликає: "використовуйте initLoader, якщо Ваш Запит статичний, і перезавантажте програму, якщо ви хочете змінити запит"
Емануель Моеклін

1
@Mhd. Тахаві, ви не змінюєте зворотних дзвінків, ви встановлюєте їх лише туди, куди вони повинні йти. Після обертання екрана їх потрібно буде знову встановити, оскільки Android не буде тримати їх, щоб запобігти витоку пам'яті. Ви можете налаштувати їх на все, що завгодно, якщо вони роблять правильно.
Емануель Моеклін

46

Виклик, initLoaderколи завантажувач уже створений (зазвичай це відбувається після зміни конфігурації, наприклад), повідомляє LoaderManager негайно доставити останні дані навантажувача onLoadFinished. Якщо навантажувач ще не створений (наприклад, коли initLoaderактивізація / фрагмент, наприклад, вперше запускається), дзвінок повідомляє LoaderManager зателефонувати, onCreateLoaderщоб створити новий навантажувач.

Виклик restartLoaderзнищує вже наявний навантажувач (як і будь-які наявні з ним пов’язані дані) та повідомляє LoaderManager зателефонувати, onCreateLoaderщоб створити новий навантажувач та ініціювати нове завантаження.


Документація щодо цього теж досить чітка:

  • initLoaderзабезпечує завантаження ініціалізації навантажувача. Якщо завантажувач ще не існує, він створюється і (якщо активність / фрагмент зараз запущений) запускає завантажувач. Інакше останній створений завантажувач буде повторно використаний.

  • restartLoaderзапускає новий або перезапускає наявний навантажувач у цьому менеджері, реєструє зворотні виклики до нього та (якщо активність / фрагмент зараз запущений) починає завантажувати його. Якщо раніше був запущений навантажувач із тим самим ідентифікатором, він автоматично знищується, коли новий завантажувач завершить свою роботу. Відкликання дзвінків буде доставлено до знищення старого навантажувача.


@TomanMoney Я пояснив, що це означає у своїй відповіді. Яка частина вас бентежить?
Алекс Локвуд

Ви просто перепрогравали док. Але документ не вказує, де слід застосовувати кожен метод, і чому це погано зіпсувати його. На мій досвід, просто дзвінок restartLoader і ніколи не викликає initLoader працює добре. Тож це все ще заплутано.
Том anMoney

3
@TomanMoney Зазвичай ви використовуєте initLoader()в onCreate()/ onActivityCreated()коли вперше починається активність / фрагмент. Таким чином, коли користувач вперше відкриє активність, завантажувач буде створений вперше ... але при будь-яких наступних змінах конфігурації, коли всю діяльність / фрагмент потрібно знищити, наступний виклик initLoader()просто поверне старий Loaderзамість створення нового. Зазвичай ви використовуєте, restartLoader()коли вам потрібно змінити Loaderзапит (тобто ви хочете отримати відфільтровані / відсортовані дані тощо).
Алекс Локвуд

4
Я все ще плутаюсь щодо рішення API мати обидва способи, оскільки вони мають однаковий підпис. Чому API не міг бути єдиним методом startLoader (), який кожен раз робить "правильну справу"? Я думаю, що це та частина, яка бентежить багато людей.
Том anMoney

1
@TomanMoney Тут міститься документація: developer.android.com/guide/components/loaders.html . "Вони автоматично підключаються до курсору останнього завантажувача, коли їх відтворюють після зміни конфігурації. Таким чином, їм не потрібно повторно запитувати свої дані."
ІгорГанапольський

16

Нещодавно у мене виникла проблема з декількома менеджерами завантажувачів та зміною орієнтації екрана, і хотілося б сказати, що після багатьох проб і помилок наступна схема працює для мене як у видах діяльності, так і в фрагментах:

onCreate: call initLoader(s)
          set a one-shot flag
onResume: call restartLoader (or later, as applicable) if the one-shot is not set.
          unset the one-shot in either case.

(Іншими словами, встановити певний прапор так , щоб initLoader це завжди виконується один раз і що restartLoader виконується на 2 - й і подальших проходах через onResume )

Крім того, не забудьте призначити різні ідентифікатори для кожного з ваших завантажувачів у межах діяльності (що може бути проблемою з фрагментами в межах цієї діяльності, якщо ви не стежите за своєю нумерацією)


Я намагався використовувати тільки initLoader ...., здається, не працює ефективно.

Пробував initLoader на OnCreate з арг нуль (документи говорять , що це нормально) і restartLoader (з дійсною арг) в onResume .... документи є неправильними і initLoader кидає виняток NullPointer.

Тільки випробуваний restartLoader ... працює деякий час, але виконує переорієнтацію на 5 або 6 екрані.

Спробував initLoader в onResume ; знову працює деякий час і потім дме. (зокрема "Викликається doRetain, коли не запущено:" ... помилка)

Спробував таке: (уривок із класу обкладинки, який передає id конструктора в конструктор)

/**
 * start or restart the loader (why bother with 2 separate functions ?) (now I know why)
 * 
 * @param manager
 * @param args
 * @deprecated use {@link #restart(LoaderManager, Bundle)} in onResume (as appropriate) and {@link #initialise(LoaderManager, Bundle)} in onCreate 
 */
@Deprecated 
public void start(LoaderManager manager, Bundle args) {
    if (manager.getLoader(this.id) == null) {
        manager.initLoader(this.id, args, this);
    } else {
        manager.restartLoader(this.id, args, this);
    }
}

(яку я знайшов десь у Stack-Overflow)

Знову ж таки, це працювало деякий час, але все ж кидало випадкові збої.


З того, що я можу зрозуміти, у час налагодження, я думаю , що є що - то робити зі збереженням / відновлення стану екземпляра, вимагає , щоб initLoader (/ с) виконується в OnCreate частини життєвого циклу , якщо вони хочуть вижити спин циклу . (Я можу помилятися.)

у випадку менеджерів, які не можна запустити, поки результати не повернуться від іншого менеджера або завдання (тобто не можна ініціалізувати в onCreate ), я використовую лише initLoader . (Я не можу помилитися в цьому, але, здається, це працює. Ці вторинні навантажувачі не є частиною безпосереднього стану екземпляра, тому використання initLoader може бути правильним у цьому випадку)

життєвий цикл


Дивлячись на діаграми та документи, я б подумав, що initLoader повинен зайти в onCreate & restartLoader в onRestart for Activity, але це залишає фрагменти, використовуючи інший шаблон, і я не встиг дослідити, чи справді це стабільно. Чи може хтось ще прокоментувати, чи є у них успіх у цій схемі діяльності?


/ @ Саймон на 100% правильний, і це має бути прийнятою відповіддю. Я не дуже повірив його відповіді і витратив кілька годин на пошук різних способів зробити цю роботу. Як тільки я перемістив дзвінок initLoader на OnCreate речі почали працювати. Тоді вам потрібен прапор однієї зйомки для обліку часу викликуStart, але немає
onCreate

2
"Випробуваний restartLoader ... працює деякий час, але виконує переорієнтацію на 5 або 6 екрані." Це робить? Я просто спробував це і сто разів обертав екран, і не підривався. Який виняток ви отримуєте?
Том anMoney

-1 Я ціную дослідницькі зусилля, що стоять за цією відповіддю, але більшість результатів є невірними.
Емануель Моеклін

1
@IgorGanapolsky майже все. Якщо ви прочитаєте та зрозумієте мою відповідь, то зрозумієте, що роблять initLoader та restartLoader і коли їх використовувати, а також зрозумієте, чому майже всі висновки Саймона неправильні. Немає зв'язку між життєвим циклом фрагмента / діяльності та рішенням про використання initLoader / restartLoader (із застереженням, який я пояснюю під змінами конфігурації). Саймон робить висновок, що життєвий цикл є підказкою для розуміння двох методів, але це не так.
Емануель Моеклін

@IgorGanapolsky Я не намагаюся рекламувати власну відповідь. Я просто намагаюся допомогти іншим розробникам і не дозволяти їм використовувати результати Саймона для власних додатків. Як тільки ви зрозумієте, для чого призначені ці два методи, вся справа стає досить очевидною і прямою в реалізації.
Емануель Моеклін

0

initLoaderбуде використовувати ті самі параметри, якщо завантажувач вже існує. Він повертається негайно, якщо старі дані вже завантажені, навіть якщо ви викликаєте їх з новими параметрами. Завантажувач в ідеалі повинен автоматично повідомляти про активність нових даних. Якщо екран обертається, initLoaderвін буде викликаний знову, і старі дані будуть негайно відображатися.

restartLoader- це коли ви хочете змусити перезавантажити і змінити параметри. Якби ви робили екран для входу за допомогою завантажувачів, ви б дзвонили лише restartLoaderкожного разу, коли натискали кнопку. (Кнопку можна натискати кілька разів через неправильні облікові дані тощо). Ви б коли-небудь дзвонили, initLoaderколи відновлювали збережений стан екземпляра активності у випадку, якщо екран повертався під час входу в систему.


-1

Якщо завантажувач вже існує, restartLoader зупинить / скасує / знищить старий, тоді як initLoader просто ініціалізує його із заданим зворотним викликом. Я не можу дізнатися, що роблять старі зворотні виклики в цих випадках, але я думаю, що вони просто відмовляться.

Я просканував http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java, але не можу дізнатися, що саме Відмінність полягає лише в тому, що методи роблять різні речі. Тому я б сказав, використовуйте initLoader перший раз і перезавантажте наступні рази, хоча я не можу з впевненістю сказати, що кожен з них буде робити саме.


І що буде initLoaderробити в цьому випадку?
theomega

-1

При першому запуску завантажувач Init використовує метод loadInBackground (), при другому запуску його буде опущено. Тож, на мою думку, краще рішення:

Loader<?> loa; 
try {
    loa = getLoaderManager().getLoader(0);
} catch (Exception e) {
    loa = null;
}
if (loa == null) {
    getLoaderManager().initLoader(0, null, this);
} else {
    loa.forceLoad();
}

////////////////////////////////////////////////// /////////////////////////

protected SimpleCursorAdapter mAdapter;

private abstract class SimpleCursorAdapterLoader 
    extends AsyncTaskLoader <Cursor> {

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

    @Override
    protected void onStartLoading() {
        if (takeContentChanged() || mAdapter.isEmpty()) {
            forceLoad();
        }
    }

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

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

Я витратив багато часу на пошук цього рішення - restartLoader (...) не працював належним чином у моєму випадку. Єдина forceLoad () дозволяє закінчити попередній потік завантаження без зворотного дзвінка (тому у вас будуть усі транзакції db належним чином завершені) та запускається знову нова тема. Так, це вимагає певного часу, але стабільніше. Лише останній запущений потік прийме зворотний дзвінок Таким чином, якщо ви хочете зробити тести на переривання транзакцій db - ваше запрошення, спробуйте перезапуститиLoader (...), інакше forceLoad (). Єдина зручність restartLoader (...) - це надання нових початкових даних, я маю на увазі параметри. І, будь ласка, не забудьте знищити завантажувач у методі onDetach () відповідного фрагмента в цьому випадку. Також майте на увазі, що в деяких випадках, коли ви займаєтесь активністю, і, скажімо, 2 фрагменти з "Завантажувачем", кожен із яких включено - ви досягнете лише 2 менеджерів навантажувача, тому "Діяльність" поділяє свій LoaderManager з фрагментами, які першими відображаються на екрані під час завантаження. Спробуйте LoaderManager.enableDebugLogging (вірно); щоб побачити деталі у кожному конкретному випадку.


2
-1 для завершення дзвінка getLoader(0)в a try { ... } catch (Exception e) { ... }.
Алекс Локвуд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.