Patnáctka: DialogFragment a otáčení telefonu
Jeden z problémů, který jsem řešil v H15 verzi 1.1.1, se týkal dialogů při otáčení telefonu: ztrácel se obsah dialogu a tlačítka přestávala fungovat korektně. Podíváme se na příčiny a řešení a ukážeme si konkrétní implementaci.
Učení se Androidu přináší jednu velkou výhodu: je za tím konkrétní produkt, ikdyby se mělo jednat o něco zcela stupidního. Aplikace, kterou si může každý snadno stáhnout do mobilu nebo tabletu. Mohu vyvíjet, publikovat a blogovat o tom. (Ano, i při vývoji jednoduchých aplikací mohu překonávat neočekávané překážky.) Což je výhoda oproti Java EE. Samozřejmě, mohl bych také vyvíjet, publikovat ke stažení třeba na těchto stránkách a blogovat o tom, ale jednak budu mít užší okruh témat ke zpracování, ale hlavně bude výrazně užší okruh uživatelů. A možná by zůstalo jenom u nudného soukromého experimentování. Takhle vyvíjím pro široké publikum.
Ale dost řečí, pojďme k tématu. Nejprve probereme příčiny problémů a jejich řešení, pak se podíváme, jak to všechno dát elegantně dohromady.
Ztrácení textů v dialogu
Při otáčení telefonu docházelo ke ztrátě textů v dialogu, což bylo patrné zejména u dotazů a potvrzení. Od verze 1.1.0 jsem přestal používat standardní AlertDialog
, protože neumožňoval aplikovat vlastní styly, a misto něj jsem vytvořil dialog vlastní. U něj se chyba projevovala nejvíce.
Hlavní příčina problému spočívá v tom, že se dialogy při otáčení telefonu chovají podobně jako Activity
: nejprve jsou zrušeny a pak znovu vytvořeny. Pokud tedy předávám do dialogu data pomocí vlastních nadefinovaných metod, pak tyto metody po otočení telefonu nebudou spuštěny. Je tudíž potřeba zajistit, aby takto externě předaná data byla po dobu otáčení uchována.
Popis řešení můžeme nalézt třeba v této diskusi. Při vytváření dialogu je potřeba zavolat metodu setRetainInstance(true)
, což zajistí uchování dat při otáčení. Aby toho nebylo málo, implementace knihovny Compatibility obsahuje nepříjemný bug projevující se při rušení dialogu. Workaround radí v metodě DialogFragment.onDestroyView()
zavolat getDialog().setOnDismissListener(null)
. Jsou však zdroje, které doporučují místo toho použít getDialog().setDismissMessage(null)
, má to být spolehlivější. To jsem použil i v mém řešení.
Narušení funkce tlačítek
Po otočení telefonu tlačítka pouze zavřela dialog, ale už nespustila požadovanou akci. Příčina podobná jako v předchozím případě: dialog v rámci zrušení a znovuvytvoření neobdržel novou instanci Activity
coby callback pro tlačítka. Vlastní nadefinovaná metoda pro předání callbacku po otočení telefonu opět nebude zavolána.
Řešením je využít standardní metodu DialogFragment.onAttach()
, která dostává jako parametr aktuální Activity
, a předání callbacku implementovat tam.
Všechno dohromady
Toť vše, co se teorie týče, pojďme nyní udělat konkrétní implementaci. Začněme přehledem požadavků:
- Dialog může být volán z více typů
Activity
. - V rámci jedné
Activity
je dialog použit vícekrát, pokaždé v jiném kontextu (dotaz na vzdání se a informace o výsledku hry). - Implementace získávání callbacku pro tlačítka je vždy stejná, callback je však pokaždé jiného typu. Jak zamezit zbytečnému copypaste?
Vyjděme z myšlenky, která zazněla ve výše odkazované diskusi: vytvořit abstraktní třídu, která bude rozšiřovat DialogFragment
a kterou budou dědit dialogy z naší aplikace.
package eu.cranion.h15.dialogs;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
public abstract class AbstractDialogFragment<T> extends DialogFragment {
protected T listener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance()) {
getDialog().setDismissMessage(null);
}
super.onDestroyView();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
listener = null;
if(activity instanceof IDialogListenerFactory) {
Class<T> type = getListenerClass();
String tag = getListenerTag();
Object o = ((IDialogListenerFactory) activity).createDialogListener(this.getClass(), tag);
if(type.isAssignableFrom(o.getClass())) {
listener = type.cast(o);
}
}
}
protected abstract String getListenerTag();
protected abstract Class<T> getListenerClass();
}
Třída definuje metody onCreate()
a onDestroyView()
tak, jak bylo již diskutováno, aby se z dialogů neztrácely texty a jiná data.
Získávání callbacku je implementováno následovně: třída pro callback definuje proměnnou listener
generického typu T
. Konkrétní typ je pak určen třídou implementující konkrétní dialog. A protože při použití generics nebude fungovat operátor instanceof
a klasické přetypování, musíme definovat ještě metodu getListenerClass()
, pomocí které získáme konkrétní třídu callbacku.
Vlastní callback můžeme, ale nemusíme implementovat přímo do Activity
. V případě jednoho dialogu ve více kontextech je to dokonce nežádoucí. Proto udělejme z Activity
něco jako factory na callbacky.
package eu.cranion.h15.dialogs;
public interface IDialogListenerFactory {
public Object createDialogListener(Class<?> clazz, String tag);
}
Metoda createDialogListener()
slouží k vytvoření callbacku. Parametr clazz
určuje třídu implementující dialog, parametr tag
slouží k předání doplňujících informací, např. pro určení kontextu. Návratovou hodnotou je pak objekt, jehož metody se budou volat stisknutím příslušného tlačítka.
Vytváření callbacku implementované v metodě AbstractDialogFragment.onAttach()
pak sleduje jednoduché schéma:
- Zkontroluj, zda
Activity
může vytvořit callback. - Pokud ano, vytvoř ho.
- Zkontroluj callback, zda je jeho třída správná.
- Pokud ano, přiřaď ho do proměnné.
Při implementaci třídy dialogu stačí jenom nadefinovat požadovanou třídu callbacku, implementovat metodu getListenerClass()
, aby tuto třídu vracela, a implementovat třídu getListenerTag()
, která bude vracet doplňující informace pro vytvoření callbacku. A samozřejmě volání patřičných metod callbacku.
Tímto způsobem mám implementovány všechny dialogy s výjimkou dialogu výběru obrázků. Uchovávání kompletní instance dialogu včetně obrázků není potřebné a navíc by bylo příliš paměťově náročné.