Online Android szakkör (DKRMG)
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.
Az általunk tervezett program négy Activityből áll.
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.
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.
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.
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.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:
<application
sor fölött már találsz egy user-permission sort, amit az előző leckében a
rezgő motor használatához kellett beírnod. Ez alá írd be a következő két
sort most! Figyelj, az Android Studio segít: amint elkezded írni a
parancsot, megjelenik egy kis ablak, ahol a fel-le nyilakkal és az enterrel
ki tudod választani, hogy mit szeretnél, és beírja helyetted!
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
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.
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!
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.
Í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.
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:
writer.println("blah");
parancs kiírja a megadott szöveget a fájlba, majd új sort
kezd. Tehát minden println paranccsal kiírt szöveg a saját sorában lesz.writer.close()
). Sok
izgalmas dolog történhet, ha ez a sor lemarad, de inkább ne próbáljuk ki őket.Írd be a fenti kódot az 1. feladat kódja alá. Ezt a SzavakActivity onPause függvényében kéne megtalálnod.
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).
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!
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.
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.
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());
}
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"
.
Fejezd be az onResume függvényedet!
Segítségként:
Szo
objektumot (A hozzáadás gomb onClick-jében találsz rá példát)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
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.
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!
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.
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:
-1
), ha az a
szó kerül előrébb0
. ha szerintünk a két szó ugyanaz.+1
, ha a b
szó kerül előrébbA 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:
item
létrehozása, megfelelő
megjelenő szöveggel (title) és értelmes azonosítóval (id).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
).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 .