08
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.
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);
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();
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.
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);
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.
java.io.Serializable
ve třídách DataKlientu
a Klient
:
public class Data extends Observable implements Observer, Serializable { …
public class Zaznam extends Observable implements Serializable { …
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
).
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(); } }
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.
public class Konstanty { public static String nazevSouboru() { return "data.dat"; } }
onCreate(...)
třídy MainActivity
tak, aby metodě nactiData()
předala kontext.
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); } }); }
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. openFileOutput
resp. openFileInput
třídy Context
.
Serializable
a pro zápis a čtení použijeme třídy ObjectOutputStream
a ObjectInputStream
.
fileList()
a deleteFile()
třídy Context
.