10. lecke Szótár (3)

Online Android szakkör (DKRMG)


Hol tartunk most?

Ez a szótárfüzet alkalmazás projekt harmadik leckéje. Gyorsan összegeznénk, hogy miként fest majd a kész program, mennyit sikerült ebből megvalósítani az előző leckékben, illetve milyen munkát tartogat a mostani segédanyag.

A program részei

Az általunk tervezett program négy Activityből áll.

Emlékeztetésként: hol tartunk most?

Az előző leckékben írtunk egy névjegy (about) activityt, illetve majdnem teljesen elkészítettük a menünket és a SzavakActivityt. Utóbbi ablak egészen pontosan ott tart, hogy az általunk beírt szavakat szépen kilistázza, ráadásul újabb szavakat is hozzáadhatunk / a nem kívánatos szavakat kitörölhetjük menet közben.

Tehát mi maradt a mai napra?

Megtanuljuk, hogy miként menthetjük ki a szavainkat egy külső fájlba, illetve olvashatjuk vissza őket. Ilyen minta fájlokat is láthatsz. A módszer szépsége, hogy ezek a fájlok a számítógépeden (pl. Excelben) könnyedén szerkeszthetőek (Mentés másként -> csv)! A szógyűjteményeket pedig egymással is könnyedén megoszthatjátok majd.

Fájlkezelés

Ha a program futása folyamán új szavakat adunk a listánkhoz, vagy annak egyes elemeit töröljük, az sajnos csak addig marad meg, amíg fut az alkalmazás. Újraindítás után ismét az a kezdeti pár szó fogad csak minket, amit az onCreate-ben adtunk hozzá a listához. Mennyivel jobb lenne, ha valahogy el tudnánk menteni a szavainkat, és bármikor újra be tudnánk tölteni őket! Sőt, továbbmenve, tegyük fel, hogy már működik a kikérdező része is a programunknak. Elég nehezen használható így, hogy előtte minden egyes szót kézzel be kell írni, és csak utána tudja kikérdezni. Majd ha valamiért kilépünk (mert pl. felhívnak), akkor kezdhetjük előröl az egész macerás műveletet.

Az Android rendszer többféle módot nyújt adatok tárolására. A mi esetünkben az a módszer a legpraktikusabb, ha a külső tárhelyen (SD kártyán) egy külön fájlban fogjuk tárolni a szavakat. Így amellett, hogy a telefonon be lehet írni az új szavakat, a fájlt gépen is lehet szerkeszteni (Jegyzettömb, Excel, stb.), sőt, akár egymás között meg is lehet osztani!

Android rendszeren merevlemez hiányában fájltárolásra az SD-kártya használható. Illetve egyes telefonokban, mint pl. Nexusok, ehelyett elérhető egy beépített SD-kártya. Az SD kártya az az adattároló egység, amit a Sajátgépben is látsz, amikor a telefonodat a számítógépedhez csatlakoztatod egy USB kábelen keresztül . Ha most megnézed (akár a számítógéped segítségével, akár a készülék fájlkezelőjével), itt egy csomó alkalmazásnak megtalálod a saját mappáját. A saját mappákat azért használják, hogy ne keveredjenek össze a különböző applikációk fájljai. Mi is létre fogunk hozni egy saját mappát, és abban fogjuk tárolni a szavakat tartalmazó fájlt.

Formátum

Maga a fájl így fog kinézni:

kutya,dog
macska,cat
ébresztőóra,alarm clock

Figyeld meg, hogy minden egyes sor egy-egy szó különböző alakjait tartalmazza, és a soron belül az egyes nyelvek vesszővel vannak elválasztva (csak egy vessző! szóköz nincs köztük!). A nyelvek mindig ugyanabban a sorrendben szerepelnek (pl. magyar,angol). A szavak tartalmazhatnak szóközt, kötőjelet, sőt, igazából bármit, vessző kivételével. Érdekesség kedvéért ezt a méltán népszerű, bár kissé öreg, formátumot CSV-nek (comma separated values) hívják.

Egy kicsit hosszabb mintafájl.

Manifest

Az SD kártya tartalmához nem fér hozzá minden alkalmazás az Android rendszer alatt. Ez is egy olyan eszköz/erőforrás, amihez engedélyt kell kérnünk, mint például a rezgő motorhoz, vagy az internet használathoz (karácsonyi szakkör). Ahhoz, hogy a program hozzáférjen az SD-kártya tartalmához, onnan fájlokat olvashasson be, illetve fájlokat írhasson oda, először engedélyt kell kérnie a rendszertől. A következőket kell tennünk:

Android 6.0 rendszertől kezdve az engedélyek kezelése kicsit megváltozik a Runtime Permission rendszerrel. Ez annyit jelent, hogy nem telepítéskor kérdezzük meg, hogy mihez férhet hozzá az applikáció, hanem ideális esetben egyesével, amikor épp először használni akarjuk az adott engedélyt.

Ebben a leckében most egy egyszerű (ám nem feltétlenül elegáns) megoldást fogunk alkalmazni: mikor először futtatod az Android Studioból az appod, rögtön lépj ki belőle, majd a Beállítások > Alkalmazások > Szotar > Endedélyek menüben engedélyezd amit kér. A következő futtatásnál már nem kell megismételni.

Írás a fájlba

A könnyebbség kedvéért azzal kezdünk, hogy a már meglévő szavainkat elmentjük egy fájlba. Előbb olvasd végig az alábbi leírást, és csak utána kezdd el írni a Java kódot!

Fájl megkeresése

Először is meg kell keresnünk a fájlunkat, amibe írni szeretnénk. Az Android rendszerben (és általában a Java világban) a mappák és fájlok kezelésére a File típust használjuk. Az alábbi kód megkeresi az SD kártya "DkrmgSzakkor" mappán belül a szavak.csv fájlt.


File sdcard = Environment.getExternalStorageDirectory(); // megkeresi az SDkártyát
File folder = new File(sdcard, "DkrmgSzakkor");          // az SDkártyán a mappát
folder.mkdir();
File file = new File(folder, "szavak.csv");              // a mappában a fájlt

A program első futtatásakor előfordulhat, hogy a mappa még nem létezik. Ezért használjuk a folder.mkdir(); parancsot, ami készségesen létrehozza a DkrmgSzakkor mappát, amennyiben még nem létezik. Ha a mappát korábban már létrehoztuk, akkor ez a parancs nem csinál semmit.

1. feladat

Írd be a fenti kódot a SzavakActivity onPause függvényébe! Ha a függvény még nem létezik, akkor hozd létre a már ismert módon (az onPause szó első pár betűjének begépelésével, az Android Studio kódkiegészítését használva)! Emlékeztetésként: azért az onPause függvényt használjuk, mert szeretnénk, hogy a listában található fájlok a szavak ablakának bezárása után is megmaradjanak.

Megnyitás (és egyebek)

A következő fontos lépés a fájl megtalálása után, a fájl megnyitása. Ehhez egy PrintWriter típusú változót fogunk használni, ami megnyitja nekünk a fájlt, illetve olyan hasznos függvényeket tartalmaz, mint a println, vagy a close. A Java nyelvben biztonsági okokból a fájlkezelést kicsit megnehezítik az úgynevezett kivételek (Exceptions). A kivételek és kezelésük a Java nyelv szerves részét képezik, azonban a szakkör ezen pontján idő és hely hiányában nem mutatnánk be őket részletesen, csak amennyire feltétlen szükséges a leckéhez. Amennyit fontos azonban tudnunk: kivételekkel akkor találkozunk, ha futás közben a programmal valami váratlan probléma történik. Ilyen kivétel esetünkben az lehet, ha a fájl megnyitása sikertelen (pl. SD kártya nincs bedugva, az applikációnk nem rendelkezik írás joggal, vagy a meghajtó írásvédett). A fájlkezeléssel kapcsolatos kivételeket pedig a Javában nem lehet csak úgy figyelmen kívül hagyni. Nézzük meg az alábbi kódot:

try {
    // PrintWriter segítségével megnyitjuk a fájlt
    PrintWriter writer = new PrintWriter(file);

    // írás a fájlba
    writer.println("hahó, ezt a fájlba írom ki");
    writer.println("Ez a második sorba fog kerülni");

    // A végén le kell zárnunk a fájlt
    writer.close();

} catch (IOException e) {
    // A catch rész csak akkor fut le, ha valami baj történt a fájl használata közben
    Log.e("SZAVAK", "nem sikerült kiírni a szavakat a fájlba"+e.getMessage());
}

Tehát:

2. feladat

Írd be a fenti kódot az 1. feladat kódja alá. Ezt a SzavakActivity onPause függvényében kéne megtalálnod.

A szólista kiírása a fájlba

Ezen a ponton, ha valaki vakmerően lefuttatja az alkalmazását (és bátorítanánk mindenkit, hogy tegye ezt meg), akkor láthatja: az SD kártyán létrejön a DkrmgSzakkor mappa, azon belül pedig megszületik a szavak.csv fájl. Ha mégsem, akkor most kell még gyorsan visszafordulni, és megnézni, hogy mi a gond. Szükség esetén írhatsz e-mailt is a szokásos dkrmg.android@gmail.com címre.

A szép új fájlunk azonban szavak helyett az alábbi két sort tartalmazza:

hahó, ezt a fájlba írom ki
Ez a második sorba fog kerülni

Nem túl meglepő, hiszen a kódunkban ezt a két sort írtuk ki a println parancsokkal. Most cseréljük le a két println utasítást, és írjuk ki a szavak lista tartalmát inkább! Ehhez egy igen egyszerű for ciklust fogunk használni, amivel egyesével kiírjuk a szavak lista elemeit. Ha nem találkoztál még a for ciklussal, akkor olvasd el az alábbi kitérőt!

Kitérő - for ciklus

A program azon ismétlődő részeit, melyeket egymás után többször is végre szeretnénk hajtani, ciklusoknak nevezzük. A ciklusok egyik leggyakrabban használt típusa a for ciklus (számlálós ciklus), ahol a ciklusmagról előre tudjuk, hogy hányszor fog lefutni. Klasszikus példák, ha például ki akarjuk írni a számokat egytől tízig.

Egy Java ciklus többnyire így néz ki

for (int i = 0; i < X; i++) {
    // ciklusmag
    // Ide kerül az, amit X-szer szeretnénk lefuttatni
}

A fenti ciklus X-szer fog lefutni. Az X lecserélhető bármilyen számra, szám típusú változóra, vagy függvényre, ami számot ad eredményül. Például a szavak.size() kifejezés, ami megadja, hogy hány elem van a szólistánkban.

A ciklusmagban felhasználhatjuk az i változót. Ez a változó minden lefutásnál egyel növekszik majd (0, 1, 2, 3, …, X-1).

3. feladat

A ciklusunk legyen a fájl megnyitása után (de a lezárás előtt). Azt szeretnénk, ha a ciklus határértéke szavak.size() lenne, hiszen minden szót pontosan egyszer ki szeretnénk írni.

Írd meg az a ciklust, ami a szavakat az alábbi formában írja ki:

magyar,angol

Segítség: a szavak lista i-edik elemét így éred el: szavak.get(i)

Tehát ha a ciklusmagba beírod az alábbi parancsot, az a szavak angol nyelvű formáját fogja kiírni:

writer.println(szavak.get(i).angol);

for (int i = 0; i < szavak.size(); i++) {
    writer.println(szavak.get(i).magyar + "," + szavak.get(i).angol);
}

Teszteld a programodat, hogy a SzavakActivity lezárásakor a kívánt tartalom jelenik-e meg a fájlban!

Fájl olvasása

Megbirkóztunk a fájl írásának nehéz feladatával. A létező fájl beolvasása ezek után lényegesen könnyebb lesz, hiszen a kódunk egy része szinte megegyezik.

Fájl megkeresése

Másold be a fájl megkeresésére vonatkozó kódot egy az egyben az előző feladatból a SzavakActivity onResume függvényébe! Ezt követően az onResume függvényben dolgozunk majd, így garantálva, hogy a szavak beolvasása az szavak ablakának megjelenésekor időben megtörténjen.

Fájl megnyitása olvasásra, kivételek

A kivételekkel kapcsolatban megszerzett ismereteink itt is felhasználhatóak. A fájlok olvasásához most a BufferedReader típust használjuk, az elképzelés ugyanaz: meg kell nyitnunk a fájlunkat, le kell zárnunk időben, illetve el kell kapnunk a váratlan kivételeket (pl. nem található a fájl)

Írd be az alábbi kódot az onResume függvénybe

try {
    // fájl megnyitása
    BufferedReader reader = new BufferedReader(new FileReader(file));

    // TODO: itt az alkalom, olvassunk a fájlból.

    // lezárás
    reader.close();
} catch (IOException e) {
    Log.e("FILE",e.getMessage());
}

Szavak beolvasása a fájlból a szavak listába

Ezen a ponton a programunk fájlolvasás része nehezen nevezhető hasznosnak. A kódunk megnyitja a fájlt, majd azonnal le is zárja.

A reader változónk (BufferedReader típusú) szerencsére sok hasznos függvényt tartalmaz a fájlunk tartalmának kiolvasásához. Ezek közül a legpraktikusabb megoldást választjuk: kiolvassuk a fájl tartalmát, soronként, elejétől a végéig. Tehát végigmegyünk a fájlon, mintha egy izgalmas könyvet olvasnánk (soronként). Talán már látható: ehhez is írnunk kell egy ciklust, ami egyesével kiolvassa a fájl sorait, majd a vesszővel elválasztott szövegekből Szo objektumokat készít.

A három alap ciklus (számlálós, elöl- és hátultesztelős) részletesebb bemutatása nem célunk ebben a leckében, az alábbi kód megértéséhez remélhetőleg elegendő háttér információt szolgáltattunk. A ciklusokról hallhatsz még az iskolai programozás szakkörön (Pascal).

while (reader.ready()) {
    String sor = reader.readLine();
    // a kiolvasott sor most a sor változóban lakik.
}

String változók szétvágása (split)

Sokszor előfordul, hogy szeretnénk egy String szöveget egy bizonyos karakter mentén szétszabdalni. Erre kitűnő példa jelen feladatunk, ahol a beolvasott sort két (vagy három) részre kell választanunk egy vessző (,) mentén. Máskor előfordulhat, hogy mondatokat szeretnénk szóközök mentén szavakra bontani, vagy pontok mentén egész bekezdéseket mondatokra stb.

A fenti probléma megoldására használhatjuk a String típus split függvényét. A split függvény (miután megadtuk neki, hogy milyen karakterek mentén kívánjuk a szövegünket felszabdalni) ad nekünk egy String tömböt, amiben ott sorakoznak a szétválasztott részek.

Egy példa segítségével illusztrálva:

Ha a sor változónk tartalma "kutya,dog"

a sor.split(",") parancs egy olyan tömböt ad vissza, melynek a nulladik eleme (Java 0-tól indexel!) "kutya", az első eleme "dog".

4. feladat

Fejezd be az onResume függvényedet!

Segítségként:

String sor = reader.readLine();
String[] nyelvek = sor.split(",");
// most már ott vannak a szavak a megfelelő helyen: nyelvek[0] és nyelvek[1]

Ha mindent jól csináltál, akkor a programod mostantól megjegyzi a listában tartott szavakat az alkalmazás bezárása után is. A CSV fájlt szerkesztheted a számítógépeden is (ehhez használj egy egyszerű szövegszerkesztőt, vagy az Excel importálás/exportálás funkcióit).

Amikor végeztél, töltsd fel a projektedet a szakkör feltöltő oldalán keresztül. Ha elakadtál, vagy kérdésed támad a fentiekkel kapcsolatban, nyugodtan írd meg emailben dkrmg.android@gmail.com

Szorgalmi

1*. for each ciklus

A for ciklus segítségével viszonylag könnyen végig tudtuk járni a listánk elemeit. Elég volt tudnunk, hogy a listák első elemének az indexe 0, az elemek száma pedig kinyerhető a size függvénnyel. Ennél azonban létezik egy még egyszerűbb megoldás is, ami a listák és egyéb komplexebb adattárolók elterjedésével vált ismertté.

Magyarázkodás helyett nézzünk egy példát.

Tegyük fel, hogy van egy nevsor nevű listánk, amiben egy osztály névsorát tároljuk
ArrayList<String> nevsor;
Ha most szeretnénk a tanulók nevét kiírni:

for (String nev : nevsor) {
    Log.i("EZ_EGY_NEV", nev);
}
                

Láthatjuk, hogy ennél az új fajta for (for each) ciklusnál elég megadnunk, a lista nevét, és az listában szerepelő elemek típusát. Innentől kezdve a ciklus magától végiglépeget a lista összes elemén. Számokkal, első és utolsó elem indexszel már nem is kell foglalkoznunk.

A fenti ciklus végigmegy a névsor lista elemein (melyek String típusúak), az aktuális elemet mindig a nev változóba helyezve.

Ezek alapján írd át a fájlba kiírás kódját úgy, hogy ezt az új fajta for each ciklust használja a szavak fájlba kiírásánál!

2*. Szavak rendezése

A szótárprogramunk ugyan szépen használható, mégis némi hiányérzete támad az embernek, amikor a szavak hosszú listáját böngészi. Miért nem ABC rendben jelennek meg a szavaink? Szerencsére a Java nyelvben van egy viszonylag egyzerű módszer listák rendezésére!

Első megközelítésnek itt is mutatunk egy példát. Egészen pontosan az előző feladat példáját folytatjuk tovább.

Tehát van egy nevsor nevű lista, amiben egy osztály névsorát tároljuk
ArrayList<String> nevsor;
Ha most szeretnénk a tanulók nevét ABC rendbe rendezni, azt egy sorban megtehetjük:
Collections.sort(nevsor);

Ha a listánk beépített típusokat használ, mint például a String vagy Integer, akkor valóban egyetlen paranccsal elvégezhető a rendezés. A mi esetünk egy árnyalatnyival bonyolultabb, hiszen a szótár programunkban a szolista változó Szo típusú elemeket tartalmaz. Miért is gond ez? A Java sajnos nem tudja eldönteni, hogy két Szo típusú változó közül melyik kerüljön előrébb a rendezésben. Az angol vagy a magyar nyelv szerint rendezünk? Ezért nekünk kell megmondani a rendezés előtt, hogy miként történjen az összehasonlítás. Ehhez tekintsük meg az alábbi kódot:


Collections.sort(szavak, new Comparator<Szo>() {
    @Override
    public int compare(Szo a, Szo b) {

        // TODO: melyik elem a nagyobb?
        int eredmeny = 0;

        return eredmeny;
    }
});

A fenti parancsot írhatjuk például a fájlból történő beolvasás, vagy egy új elem hozzáadása után.

Még nem vagyunk készen. Az összehasonlító (compare) függvény befejezését rátok bízzuk!

Az eredmeny változóba kell helyeznünk az összehasonlítás eredményét. Pl. az értéket, amit a szavak magyar formájának összehasonlításakor kapunk.

A fenti vizsgálathoz használhatjuk a String típusú változók compareTo() belső függvényét.

Pl.


String egyik = "kutya";
String masik = "macska";
egyik.compareTo(masik);

Az a és b változók összehasonlításnál a Java nyelvben az eredmény egy szám:

3*. Nyelvek szerinti rendezés

A 2*. feladatban láthattad, hogyan tudod a listádat rendezni a magyar nyelvű alak szerint. Mit kéne változtatni, ha inkább az angol alak szerint szeretnénk rendezni? Vagy a harmadik alak (német/francia) szerint? Szerencsére ezt is nagyon egyszerűen megtehetjük.

Például létrehozhatunk 3 új gombot mondjuk a ListView alá, és mindegyik onClick-jében a fent megismert módon rendezzük a listát, csak az egyikben a két szó angol alakját, a másikban a németet, magyart, stb. hasonlítjuk össze. Aki szeretné, írja meg!

Ez a megoldás azonban kis képernyőn nem a legjobb, mert a gombok elég sok helyet foglalnak. Sokkal felhasználóbarátabb lenne, ha a menüből lehetne változtatni a rendezést. Ez nagyon hasonló lesz a 8. lecke első szorgalmijához, amikor is az About activityt nyitottuk meg menüből. Aki megcsinálta, érdemes lehet gyorsan átfutni, akinek még hiányzik, gyorsan pótolja!.

Vázlatosan a szükséges lépések:

  1. Új menü erőforrás (xml fájl) létrehozása az app -> res -> menu mappában
  2. Ebbe a két (vagy 3) fajta rendezésnek egy-egy item létrehozása, megfelelő megjelenő szöveggel (title) és értelmes azonosítóval (id).
  3. Az onCreateOptionsMenu függvény megírása a Szavak activityben. Tartalma nagyon hasonló a 8. lecke szorgalmijában megismerthez, de az R.menu.menu helyett a frissen létrehozott menü erőforrást kell írni (R.menu.valami).
  4. Az onOptionsItemSelected függvényben meg kell vizsgálni, hogy HA a magyar nyelvhez tartozó menuItem-et nyomta meg a felhasználó, AKKOR a magyar alak szerint kell rendezni; HA az angolhoz tartozót, AKKOR az angol szerint, stb. Az éppen lenyomott menuItem azonosítóját a 8. lecke első szorgalmijának 7. pontjában leírtak szerint éred el.

Ha ellenőrizni szeretnéd a megoldásod, vagy esetleg elakadtál, kattintás után láthatod a .