08

Ukládání dat do souboru

Veškeré změny, které jsme v naší aplikaci provedli, se zatím nikam neukládají. Po vypnutí aplikace začínáme znovu s původními daty. Nyní se naučíme ukládat data do souborů pro pozdější použití. Pro ukládání souborů použijeme serializaci dat. Jedná se o zabudovaný mechanismus jazyka Java, který umožňuje převést objekty Javy na posloupnost byte a tu posléze uložit do souboru. Tento postup je výhodný pro svou jednoduchost. Neumožňuje ale zpracování dat jinou aplikací. Pro export a možnost použití dat v jiné aplikaci jsou výhodnější formáty XML či CSV.


Soubory budeme ukládat do interního úložiště mobilního zařízení. Postup pro ukládání na SD kartu by byl o něco složitější, v tomto směru odkážeme čtenáře na studium dalších pramenů, například kapitolu Saving Files na serveru Developer.Android.com.


Otevření souboru pro zápis

Pro otevření souboru v interním úložišti můžeme použít metodu openFileOutput třídy Context. Tuto metodu dědí třída Activity. Jako parametr předáme metodě openFileOutput název souboru (jako textový řetězec) a režim přístupu. Režim přístupu zadáme pomocí číselné konstanty definované ve třídě Context. Pro interní úložiště se běžné používá režim přístupu MODE_PRIVATE.

FileOutputStream fos;
fos = context.openFileOutput(Konstanty.nazevSouboru(), Context.MODE_PRIVATE);

Serializace datových objektů

Objekty, které chceme připravit pro ukládání do souboru prostřednictvím serializace musí implementovat rozhraní java.io.Serializable. Stejně tak musí rozhraní Serializable implementovat i všechny třídy objektů, ze kterých je serializovaný prvek složen. Naštěstí většina běžně používaných tříd Javy rozhraní implementuje.


Pokud je třída serializovatelná, stačí pro uložení objektů této třídy do souboru vytvořit ObjectOutputStream, kterému jako parametr předáme FileOutputStream, vytvořený metodou Context.openFileOutput (...). Následně použijeme metodu writeObject(…) - třídy ObjectOutputStream.


Nezapomeňte výstupní proudy po použití uzavřít metodou close()!

ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(data);
oos.close();

Načtení serializovaných dat

Pro načtení již uložených dat použijeme opačný postup. Vytvoříme FileInputStream (opět pomocí třídy Context), vytvoříme ObjectInputStream a serializované objekty převedeme na objekty Javy metodou readObject(…). Výstupem jsou původní data. Návratovým datovým typem metody readObject () je třída Object, je tedy třeba následně provést přetypování na správný datový typ. Chceme-li být opatrní, nejprve zkontrolujeme správnost dat a možnost přetypování operátorem instaceof:

FileInputStream fis;
ObjectInputStream ois;
fis = context.openFileInput(nazevSouboru);
ois = new ObjectInputStream(fis);
Object dataObject = ois.readObject();
ois.close();
if (! (dataObject instanceof PuvodniTridaDat)) {
    throw new IOException("Nesprávný obsah souboru, nepodařilo se dekódovat obsah souboru!");
}
PuvodniTridaDat data = (PuvodniTridaDat) vstup; // Přetypování na původní třídu.

Přehled souborů, mazání souboru

Pokud vytvoříme více souborů, může se hodit jejich seznam. Seznam souborů, které aplikace v interním úložišti vytvořila, získáme metodou fileList() třídy Context. Seznam získáme jako pole řetězců. Pro výpis můžeme použít následující kód:

String seznamSouboru = "Složka: " + this.getFilesDir() + "\n" +
        "Soubory aplikace: ";
for (String nazevSouboru : this.fileList()) {
    seznamSouboru += nazevSouboru + ", ";
}
System.out.println(seznamSouboru);

Pro smazání souboru, který již nepotřebujeme, můžeme použít metodu deleteFile(nazev) třídy Context:

this.deleteFile(nazevSouboru);

Příklad Můj pestrý život – ukládání dat

Doplňme předchozí příklad o ukládání dat do souboru. Většinu kódu přidáme do třídy Data. Nesmíme zapomenout implementovat rozhraní java.io Serializable u tříd Data a Záznam. Následně upravíme kód metody nactiData () ve třídě Data tak, aby při vytvoření okna načetla data ze souboru.

  1. Implementujte rozhraní java.io.Serializable ve třídách DataKlientu a Klient:

  2. public class Data extends Observable implements Observer, Serializable {
    
    

    public class Zaznam extends Observable  implements Serializable {
    
    

  3. Dále upravíme metodu nactiData() a přidáme metodu ulozData() ve třídě Data. Ty budou provádět samotné ukládání a načítání dat. Pro zařazení souboru do správného kontextu (přiřazení k naší aplikaci) potřebují obě metody jako parametr dostat aktuální kontext aplikace (instanci třídy MainActivity).

  4. public void nactiData(Context context) {
        // Zatím vytvořím prázdnou kolekci záznamů. Následně ji zkusím přepsat daty, načtenými
        //  ze souboru, pokud soubor existuje.
        this.data = new ArrayList<Zaznam>();
        // Pokusím se načíst data ze souboru:
        try {
            FileInputStream fis = context.openFileInput(Konstanty.nazevSouboru());
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object vstup = ois.readObject();
            if (vstup instanceof ArrayList) {
                this.data = (ArrayList<Zaznam>) vstup;
            }
        } catch (FileNotFoundException ex) {
            Toast.makeText(context, "Soubor zatím neexistuje!", Toast.LENGTH_SHORT).show();
        } catch (IOException ex) {
            Toast.makeText(context, "Chyba při čtení vstupního souboru: "+ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        } catch (ClassNotFoundException ex) {
            Toast.makeText(context, "Nesprávný formát vstupního souboru: "+ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        }
        // Pokud je kolekce prázdná, naplním ji vzorovými daty. To proto, že zatím aplikace neumí
        //  přidat nové záznamy sama. Později tuto část vypustíme:
        if (this.data.isEmpty()) {
            this.data.add(new Zaznam("Programování pro Android", "Tvorba aplikací pro Android.", true));
            this.data.add(new Zaznam("Hra na kytaru", "Základní akordy na kytaru.", false));
            this.data.add(new Zaznam("Terra Mystica", "Naučil jsem se novou deskovou hru.", false));
        }
        // Nastav všem záznamům pozorovatele, aby se jejich změny hlásily:
        for (Zaznam z : this.data) {
            z.addObserver(this);
        }
        // Upozorním na změnu dat pozorovatele:
        this.setChanged();
        this.notifyObservers();
    }
    
    
    public void ulozData(Context context) {
        try {
            FileOutputStream fos = context.openFileOutput(Konstanty.nazevSouboru(), Context.MODE_PRIVATE);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(this.data);
        } catch (IOException ex) {
            Toast.makeText(context, "Chyba při zápisu do souboru: "+ ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        }
    }
    

  5. Co se týče volby názvu souboru, doporučujeme podobná nastavení ukládat do jedné třídy s názvem Konstanty. Všechna nastavení tak budou po hromadě a lze je v budoucnu jednoduše ukládat do souboru či jiným způsobem zpřístupnit uživateli či dalším vývojářům.

  6. public class Konstanty {
        public static String nazevSouboru() { return "data.dat"; }
    }
    

  7. V posledním kroku upravíme metodu onCreate(...) třídy MainActivity tak, aby metodě nactiData() předala kontext.

  8. protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Vytvoření a inicializace třídy Data:
        this.zaznamy = new Data();
        this.zaznamy.nactiData(this);
        // Registrace jako pozorovatel -- chceme být informování o změnách v datech
        this.zaznamy.addObserver(this);
        // Vytvoření ListView a propojení s daty:
        this.seznam = (ListView) this.findViewById(R.id.lv_seznam);
        this.seznam.setAdapter(new ArrayAdapter<Zaznam>(this, android.R.layout.simple_list_item_1, this.zaznamy.getData()));
        this.seznam.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> zvolenyView, View polozka, int poziceVAdapteru, long cisloRadkuUvnitrView) {
                zvolenaPolozka(zvolenyView, polozka, poziceVAdapteru, cisloRadkuUvnitrView);
            }
        });
    }
    

  9. Poznamenejme, že z hlediska oddělení logiky aplikace od zobrazovací části (návrhový vzor MVC apod.) by bylo vhodnější nevypisovat chybové hlášení přímo ve třídě Data. Bylo by vhodnější pouze vygenerovat vlastní vyjímku, která by nesla informaci o nastálé akci. Samostné rozhodnutí, zda uživateli informaci o chybě zobrazit formou vyskakovacího okna či jinak, by měla učinit hlavní aktivita, nikoli datová třída Data. Implementaci tohoto řešen ponecháme jako cvičení z výjimek v Javě (exception) pro čtenáře.

  10. Zbývá doplnit možnost data do souboru uložit (zavolat metodu ulozData třídy Data). Můžeme přidat tlačítko a volat metodu z jeho kódu, nebo přidáme hlavní menu. Postup pro přidání menu popíšeme v další kapitole.

Shrnutí

  1. Vstupní a výstupní proud pro práci se soubory získáme metodami openFileOutput resp. openFileInput třídy Context.

  2. Pro jednoduché ukládání dat můžeme použít serializaci. Ukládáné třídy musí implementovat rozhraní Serializable a pro zápis a čtení použijeme třídy ObjectOutputStream a ObjectInputStream.

  3. Seznam souborů aplikace a mazání souborů použijeme třídami fileList() a deleteFile() třídy Context.