Používateľské nastavenia appky

Používateľské nastavenia appky: Preferences

Každá zložitejšia appka poskytuje používateľské nastavenia. Prihlasovacie mená a heslá. Možnosti zobrazenia. Zapínanie a vypínanie vlastností. Na platforme Android nemusíte vyvíjať vlastné riešenia: máte k dispozícii triedy pre preferences, ktoré sa starajú o jednotné zobrazovanie a zmenu nastavení a zároveň jednoduchý centralizovaný spôsob ich zapamätávania.

Nastavenia sú v API reprezentované pomocou akejsi hashmapy. Každé jedno nastavenie má svoj kľúč (key), hodnotu (value) a popisok.

Ukážková aplikácia Settr

Vyrobme si ukážkovú appku Settr, ktorá nebude mať žiadnu funkcionalitu, ale bude mať zato množstvo nastavení!

Plán práce

  1. nadefinujeme si výzor fragmentu, ktorý bude zobrazovať nastavenia
  2. vytvoríme aktivitu obaľujúcu fragment s nastaveniami
  3. ukážeme si, ako možno pristúpiť k uloženým nastaveniam

Fragment a aktivita s používateľskými nastaveniami

Samotné ovládacie prvky s nastaveniami získame oddedením od fragmentu typu PreferenceFragment a prípravou jeho layoutu.

Layout fragmentu s nastaveniami

Fragment s nastaveniami neobsahuje tradičné widgety. Miesto nich využíva sadu preferenčných widgetov deklarovaných v špeciálnom XML súbore. Vytvorme nový resource typu XML a nazvime ho settings.xml. (Vytvorený súbor sa ocitne v adresári res/xml/settings.xml.

Nastavenia z ukážky vyzerajú nasledovne:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="https://schemas.android.com/apk/res/android">
    <EditTextPreference android:key="login" android:title="Prihlasovacie meno" />
    <CheckBoxPreference android:key="autoLogin"
        android:title="Automaticky prihlásiť"
        android:defaultValue="true"/>
</PreferenceScreen>

Definujeme dve nastavenia: textové políčko s kľúčom login reprezentované ako EditTextPreference a začiarkávadlo CheckBoxPreference. Všimnite si, že každé nastavenie má definovaný kľúč a titulok, prípadne i preddefinovanú hodnotu.

Zoznam možných preferenčných widgetov je v dokumentácii triedy Preference. Stručný prehľad udáva tiež výber zo zoznamu, viacnásobný výber položiek zo zoznamu, či alternatívny prepínač typu "zapnuté/vypnuté".

Kód fragmentu s nastaveniami

Layout je hotový! Poďme k nemu vytvoriť fragment:

import android.os.Bundle;
import android.preference.PreferenceFragment;

public class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.settings);
    }
}

Je to malina: dedíme od systémového PreferenceFragment a layout nafúkneme zo XML cez addPreferencesFromResource().

Aktivita obaľujúca fragment s nastaveniami

Fragment s nastaveniami musí byť samozrejme zabalený v niektorej aktivite. Vytvoríme teda novú aktivitu SettingsActivity, ktorej jedinou úlohou bude obsiahnuť preferenčný fragment:

public class SettingsActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getFragmentManager()
                .beginTransaction()
                .add(android.R.id.content, new SettingsFragment())
                .commit();
    }
}

Identifikátor android.R.id.content je systémový identifikátor koreňového widgetu v layoute aktivity. V strome widgetov sa tento pseudowidget nachádza na úplnom vrchole, čo znamená, že v XML layoute nie je deklarovaný.

Zobrazenie aktivity s používateľskými nastaveniami

Aktivitu s nastaveniami vyvoláme jednoducho: do lišty akcií v hlavnej aktivite jednoducho dodáme tlačidlo, ktorým zobrazíme nastavenia. Do menu_main.xml dodajme tlačidlo:

<item android:id="@+id/action_settings" android:title="@string/action_settings"
    android:orderInCategory="100" app:showAsAction="always" />

(Je možné, že také tlačidlo tam je, ibaže je schované.)

Obsluha tlačidla na lište aktivít

V hlavnej aktivite MainActivity obslúžme tlačidlo na lište akcií v onOptionsItemSelected() a naštartujme aktivitu tradičným spôsobom cez intent.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    if (id == R.id.action_settings) {
        startActivity(new Intent(this, SettingsActivity.class));
        return true;
    }

    return super.onOptionsItemSelected(item);
}

Inicializácia používateľských nastavení

Počas prvého spustenia appky by sme mali inicializovať nastavenia: teda zosynchronizovať deklarácie v XML súbore so skutočným stavom nastavení. V každom vstupnom bode appky treba zavolať PreferenceManager.setDefaultValues(), ktorá to spraví za nás. Tri parametre postupne uvádzajú kontext, definičný súbor s nastaveniami a booleovskú premennú, ktorá v nastavení na false hovorí, že ak sa už raz nastavenia inicializovali (a používateľ ich zmenil), nemajú sa inicializovať nanovo znova. (Hodnota true funguje skvele pre prípady resetu všetkých nastavení.)

public static final boolean DO_NOT_READ_AGAIN = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    PreferenceManager.setDefaultValues(this, R.xml.settings, DO_NOT_READ_AGAIN);
}

Získanie hodnoty nastavení

Ak chceme získať hodnotu používateľskeho nastavenia, využijeme na to triedu PreferenceManager. Ak pri štarte hlavnej aktivity chceme nastaviť titulok okna podľa aktuálne prihláseného používateľa, využime:

@Override
protected void onResume() {
    super.onResume();
    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
    setTitle(preferences.getString("login", "anonymous"));
}

Ako vidno, nastavenia sú ukladané v objekte SharedPreferences, ktorý sa tvári ako mapa, z ktorej možno vyberať hodnoty podľa kľúča. V našom prípade získavame hodnotu loginu, kde v prípade neexistujúceho záznamu vrátime anonymous.

Bonus: Inicializácia sumárov

Každá položka nastavenia môže mať voliteľný sumár vykreslený ako druhý, zmenšený, riadok popisujúci nastavenie. V niektorých prípadoch ho môžeme použiť na výpis aktuálnej hodnoty nastavenia.

Pred štartom aktivity však musíme ručne preklopiť hodnoty z nastavení do sumárov, čo môžeme urobiť nasledovne:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    addPreferencesFromResource(R.xml.settings);
    initializeSummaries();
}

private void initializeSummaries() {
    SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();
    for(String key : sharedPreferences.getAll().keySet()) {
        onSharedPreferenceChanged(sharedPreferences, key);
    }
}

Poslucháči zmien v nastaveniach

Neraz je potrebné sledovať zmeny v nastaveniach a dokázať na nich reagovať. Správca preferencií preference manager dokáže na seba zavesiť poslucháča typu SharedPreferences.OnSharedPreferenceChangeListener s jedinou metódou:

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    ...
}

Ak chceme sledovať zmeny vo fragmente nastavení a po zmene používateľského mena aktualizovať sumár, v metóde onResume() zaregistrujeme poslucháča, a v metóde onPause() ho odregistrujeme (nemá zmysel sledovať zmeny, keď je aktivita na pozadí.)

V ukážke zaregistrujeme poslucháča v podobe inštancie fragmentu:

@Override
public void onResume() {
    super.onResume();
    getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}


@Override
public void onPause() {
    getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    super.onPause();
}

Prirodzene, fragment musí implementovať príslušný interfejs

public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
    ...
}

Ukážka metódy, ktorá aktualizuje sumáre po zmene vyzerá nasledovne:

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    if("login".equals(key)) {
        findPreference(key).setSummary(sharedPreferences.getString(key, ""));
    }
}

Pomocou findPreference() nájdeme objekt Preference reprezentujúci preferenčný widget a nastavíme na ňom príslušnú vlastnosť, teda sumár.

Poslucháči v správcovi nastavení sú registrovaní pomocou slabých referencií. Treba dať pozor na to, aby sa objekt poslucháča náhodou neodpratal garbage collectorom a teda aby sme neprišli o informácie o zmene nastavení. V našom príklade je poslucháčom fragment, ktorý sa z pamätí odprace len ak je zničený (a v tej chvíli bol pozastavený a explicitne odregistrovaný v metóde onPause().

Kam ďalej?

S nastaveniami sa dá robiť viacero vecí. V prípade priveľkého počtu možností je možné nakonfigurovať podfragmenty nastavení pomocou hlavičiek nastavení (preference headers). Nastavenia tiež možno zoskupovať do logických panelov s hlavičkou (setting group).

Ak vám nevyhovuje štandardná sada preferenčných widgetov, môžete si vytvoriť aj vlastný widget. Dôležité je nahliadnuť do oficiálnej dokumentácie, kde existuje viacero riešení pre tieto situácie.