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ů:

  1. Dialog může být volán z více typů Activity.
  2. 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).
  3. 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:

  1. Zkontroluj, zda Activity může vytvořit callback.
  2. Pokud ano, vytvoř ho.
  3. Zkontroluj callback, zda je jeho třída správná.
  4. 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é.

Tagy: H15, Android, Java

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...