diff --git a/.gitignore b/.gitignore
index 3a42aea..5ed2c6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,6 @@
*.war
*.nar
*.ear
-*.zip
*.tar.gz
*.rar
@@ -159,7 +158,6 @@ dist
*.war
*.nar
*.ear
-*.zip
*.tar.gz
*.rar
diff --git a/LICENSE.md b/LICENSE.md
index a69ce8b..b25210d 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,5 +1,5 @@
# Licensz
-Az itt található oktatási segédanyagok a BMEVIAUAC00 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.
+Az itt található oktatási segédanyagok a BMEVIAUAD02 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.
Az anyagok a tárgy keretében oktatott kontextusban értelmezhetőek. Az anyagokért egyéb felhasználás esetén a szerző(k) felelősséget nem vállalnak.
diff --git a/README.md b/README.md
index 5d8f46e..638407a 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Mobil- és Webes szoftverek - Laborok
-![Build docs](https://github.com/bmeviauac01/laborok/workflows/Build%20docs/badge.svg?branch=master)
+![Build docs](https://github.com/VIAUAD02/laborok/workflows/Build%20docs/badge.svg?branch=master)
-[BMEVIAUAC00 - Mobil- és Webes Szoftverek](https://www.aut.bme.hu/Course/mobilesweb) tárgy laborfeladatai.
+[BMEVIAUAD02 - Mobil- és Webes Szoftverek](https://www.aut.bme.hu/Course/mobilesweb) tárgy laborfeladatai.
-A jegyzetek MkDocs segítségével készülnek és GitHub Pages-en kerülnek publikálásra:
+A jegyzetek MkDocs segítségével készülnek és GitHub Pages-en kerülnek publikálásra:
Az MKDocs használatához [a hovatalos dokumentáció](https://squidfunk.github.io/mkdocs-material/creating-your-site/) segítségedre lehet.
diff --git a/docs/hf/index.md b/docs/hf/index.md
deleted file mode 100644
index 14d2774..0000000
--- a/docs/hf/index.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Házi feladat információk
-
-A házi feladat célja, hogy az előadáson és laborokon bemutatott technológiák segítségével egy komplex alkalmazást készítsen a hallgató, önálló funkcionalitással.
-
-## Követelmények
-
-- Legalább 4 technológia használata (pl. UI, egyedi nézetek, fragmentek, perzisztens adattárolás, animációk, stílusok/témák, RecyclerView, hálózati kommunikáció, Service, BroadcastReceiver, Content Provider, Intent, stb.)
-- Kotlin nyelven kell készülnie
-- View keretrendszerben kell készülnie (Jetpack Compose csak laborvezetővel egyeztetve)
-- Önálló alkalmazás legalább 3-4 képernyővel/nézettel
-- Bármilyen külső könyvtár használható a fejlesztéshez, hogy még látványosabb alkalmazások készüljenek:
- - [https://github.com/wasabeef/awesome-android-ui](https://github.com/wasabeef/awesome-android-ui)
- - [https://github.com/wasabeef/awesome-android-libraries](https://github.com/wasabeef/awesome-android-libraries)
- - [https://github.com/nisrulz/android-tips-tricks](https://github.com/nisrulz/android-tips-tricks)
-
-!!!tip "Néhány példa alkalmazás"
- - Kiadás/bevétel nyomkövető figyelmeztető funkcióval és grafikonokkal
- - Turisztikai látványosságokat gyűjtő alkalmazás
- - Raktár kezelő alkalmazás
- - Számla kezelő megoldás
- - Recept kezelő alkalmazás
- - Napló készítő alkalmazás fényképekkel
- - Sport tracker alkalmazás
- - Készülék esemény naplózó alkalmazás
- - Apróhirdetés alkalmazás
- - Találkozó szervező alkalmazás
- - Sportfogadó megoldás
- - Szaki kereső alkalmazás
- - Játék alkalmazás, pl. aknakereső, shooter, stb.
- - Valamilyen REST API-t használó alkalmazás, például valuta váltás, tőzsdei infók, stb:
- - [https://github.com/toddmotto/public-apis](https://github.com/toddmotto/public-apis)
- - [https://github.com/Kikobeats/awesome-api](https://github.com/Kikobeats/awesome-api)
- - [https://github.com/abhishekbanthia/Public-APIs](https://github.com/abhishekbanthia/Public-APIs)
- - A házi feladat használhat felhő megoldást is, pl. Firebase, Amazon, stb.
-
-## Beadás módja
-
-!!!danger "neptun.txt"
- Az első és legfontosabb, hogy az eddigiekhez hasonlóan töltsd ki a neptun.txt fájlt, hogy a rendszer azonosítani tudjon.
-
-### Specifikáció
-
-A specifikáció beadás határideje a **7. hét vége (2023. október 22. 23:59)**.
-A specifikáció elkészítése közben a "spec" branchen dolgozz. Erre az ágra akárhány kommitot tehetsz.
-Sablont a README.md fájl tartalmaz, azt kell kiegészíteni, és feltölteni a repóba a megadott határidőig.
-A beadás akkor teljes, ha a "spec" branch-en megtalálható a README.md fájlban a specifikáció. A beadást egy pull request jelzi, amely pull request-et a laborvezetődhöz kell rendelned.
-A specifikáció elkészítése előfeltétele a házi feladat elfogadásának.
-
-### Házi feladat
-
-A házi feladat beadás határideje a **11. hét vége (2023. november 19. 23:59)**.
-A házi feladat elkészítése közben a "hf" branchen dolgozz. Erre az ágra akárhány kommitot tehetsz.
-A projektet mindenképpen ebbe a repository-ba hozd létre, a fejlesztést végig itt végezd.
-A beadás akkor teljes, ha a "hf" branch-en megtalálható a projekted teljes forráskódja. A beadást egy pull request jelzi, amely pull request-et a laborvezetődhöz kell rendelned.
-A házi feladathoz mindenképpen tartozik **házi feladat védés** is. Ennek ideje a beadást követően (a 12-13. héten) van. Módjáról és idejéről egyeztess a laborvezetőddel.
-
-### Házi feladat pótlás - fizetésköteles!
-
-A pótbeadás határideje a **13. hét vége (2023. december 3. 23:59)**.
-A házi feladat pótlása közben a "pothf" branchen dolgozz. Erre az ágra akárhány kommitot tehetsz.
-A beadás akkor teljes, ha a "pothf" branch-en megtalálható a projekted teljes forráskódja. A beadást egy pull request jelzi, amely pull request-et a laborvezetődhöz kell rendelned.
-A házi feladat pótlásához mindenképpen tartozik **pót házi feladat védés** is. Ennek ideje a beadást követően (a 14. héten) van. Módjáról és idejéről egyeztess a laborvezetőddel.
-
-!!!danger "FONTOS: Elővizsga"
- Az elővizsgára jelentkezés feltétele a házi feladat normál határidőre való leadása. Aki mégis jelentkezik elővizsgára úgy, hogy pót házit adott le, annak a vizsgáját nem javítjuk, automatikusan elégtelen kerül beírásra.
-
-## Dokumentáció
-
-A házi feladatot a specifikáción túl dokumentálni nem szükséges.
-
-## iMSc pontok
-
-A házi feladat minimumkövetelményeinek túlteljesítéséért, extra funkciókért, igényes felületért, kiemelkedő kódminőségért a laborvezetővel egyeztetve maximum 10 iMSc pont szerezhető.
-iMSc pont szerzéséhez az elkészült alkalmazásról rövid dokumentációt kell készíteni a README.md fájlba. (A specifikáció után.) Ebben röviden ismertetni kell az elkészült alkalmazás funkcionalitását és az érdekesebb megoldásokat.
-
-!!!tip "Androidalapú szoftverfejlesztés + Mobil- és webes szoftverek közös házi feladat"
- Ha valaki mind a két tárgyat hallgatja a félévben, van lehetőség közös házi feladat írására, DE:
- - Ezt mindenképpen egyeztetni kell mindkét laborvezetővel.
- - Ugyanaz a házi csak úgy adható le mindkét tárgyon, ha a nehezebb követelményeket (vagyis az Androidalapú szoftverfejlesztését) felülteljesíti. Tehát az Androidalapú szoftverfejlesztés követelményei szerint nem 5, hanem 6-7 technológiát kell használni. Ennek mennyiségéről és a feladat komplexitásáról a laborvezetők döntenek.
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 8ab128e..898dfcd 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
# Tárgy ismertető
-A tárgykövetelményeket lásd a [hivatalos tantárgyi adatlapon](https://portal.vik.bme.hu/kepzes/targyak/VIAUAC00/).
+A tárgykövetelményeket lásd a [hivatalos tantárgyi adatlapon](https://portal.vik.bme.hu/kepzes/targyak/VIAUAD02/).
A laborok sorrendjét és a beadások határidejét Moodle-ben találod.
@@ -10,7 +10,7 @@ A laborok sorrendjét és a beadások határidejét Moodle-ben találod.
A javítás menetéről és formájáról bővebben a ["Hozzájárulás az anyaghoz"](tudnivalok/github/contributing.md) dokumentumban olvashatsz bővebben.
!!! quote "Felhasználási feltételek"
- Az itt található oktatási segédanyagok a BMEVIAUAC00 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.
+ Az itt található oktatási segédanyagok a BMEVIAUAD02 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.
Az anyagok a tárgy keretében oktatott kontextusban értelmezhetőek. Az anyagokért egyéb felhasználás esetén a szerző(k) felelősséget nem vállalnak.
diff --git a/docs/laborok/01-android-hello-world/assets/avd_create.png b/docs/laborok/01-android-hello-world/assets/avd_create.png
index b4b7bf2..9b9425c 100644
Binary files a/docs/laborok/01-android-hello-world/assets/avd_create.png and b/docs/laborok/01-android-hello-world/assets/avd_create.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/avd_extras.png b/docs/laborok/01-android-hello-world/assets/avd_extras.png
index 058cb21..35c9db0 100644
Binary files a/docs/laborok/01-android-hello-world/assets/avd_extras.png and b/docs/laborok/01-android-hello-world/assets/avd_extras.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/avd_manager.png b/docs/laborok/01-android-hello-world/assets/avd_manager.png
index ef6f49b..8448bf5 100644
Binary files a/docs/laborok/01-android-hello-world/assets/avd_manager.png and b/docs/laborok/01-android-hello-world/assets/avd_manager.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/ide_android.png b/docs/laborok/01-android-hello-world/assets/ide_android.png
index e2fa4cc..c760270 100644
Binary files a/docs/laborok/01-android-hello-world/assets/ide_android.png and b/docs/laborok/01-android-hello-world/assets/ide_android.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/nice_studio.png b/docs/laborok/01-android-hello-world/assets/nice_studio.png
index 05baa41..5193c6d 100644
Binary files a/docs/laborok/01-android-hello-world/assets/nice_studio.png and b/docs/laborok/01-android-hello-world/assets/nice_studio.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/sdk_manager.png b/docs/laborok/01-android-hello-world/assets/sdk_manager.png
index 00e60a5..393d2d0 100644
Binary files a/docs/laborok/01-android-hello-world/assets/sdk_manager.png and b/docs/laborok/01-android-hello-world/assets/sdk_manager.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/studio_new.png b/docs/laborok/01-android-hello-world/assets/studio_new.png
index a0460df..a5ce3a2 100644
Binary files a/docs/laborok/01-android-hello-world/assets/studio_new.png and b/docs/laborok/01-android-hello-world/assets/studio_new.png differ
diff --git a/docs/laborok/01-android-hello-world/assets/studio_old.png b/docs/laborok/01-android-hello-world/assets/studio_old.png
index a06577d..76b6ea9 100644
Binary files a/docs/laborok/01-android-hello-world/assets/studio_old.png and b/docs/laborok/01-android-hello-world/assets/studio_old.png differ
diff --git a/docs/laborok/01-android-hello-world/index.md b/docs/laborok/01-android-hello-world/index.md
index 41026e9..eb5d20c 100644
--- a/docs/laborok/01-android-hello-world/index.md
+++ b/docs/laborok/01-android-hello-world/index.md
@@ -58,18 +58,15 @@ A feladatok megoldása során a dokumentációt markdown formátumban készítsd
### Fordítás menete Android platformon
-A projekt létrehozása után a forráskód az `src` könyvtárban, míg a felhasználói felület leírására szolgáló XML állományok a `res` könyvtárban találhatók. Az erőforrás állományokat egy `R.java` állomány köti össze a forráskóddal, így könnyedén elérhetjük Java/Kotlin oldalról az XML-ben definiált felületi elemeket. Az Android projekt fordításának eredménye egy APK állomány, melyet közvetlenül telepíthetünk mobil eszközre.
+A projekt létrehozása után a forráskód az `src` könyvtárban található. A felhasználói felületet Jetpack Compose használatával, közvetlenül Kotlin kódban definiáljuk, így nincs szükség XML állományokra. Az Android projekt fordításának eredménye egy APK állomány, melyet közvetlenül telepíthetünk mobil eszközre. Jetpack Compose segítségével könnyedén elérhetjük és kezelhetjük a felületi elemeket Kotlin kódból, megkönnyítve ezzel a modern, deklaratív felületkialakítást.
![](assets/lab-1-compile.png)
*Fordítás menete Android platformon*
-1. A fejlesztő elkészíti a Kotlin forráskódot, valamint az XML alapú felhasználói felület leírást a szükséges erőforrás állományokkal.
+1. A fejlesztő elkészíti a Kotlin forráskódot, amelyben a felhasználói felületet Jetpack Compose segítségével definiálja. Nincs szükség külön XML alapú felhasználói felület leírásra.
-2. A fejlesztőkörnyezet az erőforrás állományokból folyamatosan naprakészen tartja az `R.java` erőforrás fájlt a fejlesztéshez és a fordításhoz.
-
- !!! danger "FONTOS"
- **Az `R.java` állomány generált, kézzel SOHA ne módosítsuk!** (Az Android Studio egyébként nem is hagyja.)
+2. A fejlesztőkörnyezet folyamatosan naprakészen tartja a Compose elemekhez kapcsolódó erőforrásokat és a szükséges build állományokat a fejlesztéshez és a fordításhoz.
3. A fejlesztő a Manifest állományban beállítja az alkalmazás hozzáférési jogosultságait (pl. Internet elérés, szenzorok használata, stb.), illetve ha futás idejű jogosultságok szükségesek, ezt kezeli.
@@ -113,13 +110,12 @@ SDK szerkezet:
Az SDK kezelésére az SDK managert használjuk, ezzel lehet letölteni és frissen tartani az eszközeinket. Indítása az Android Studion keresztül lehetséges.
-Az SDK Manager ikonja a fenti toolbaron (vagy Tools -> SDK Manager):
-
-![](assets/sdk_manager_icon.png)
+Az SDK Manager ikonja a fenti toolbaron a beállításoknál található (vagy Tools -> SDK Manager):
-vagy
+|Régi UI||Új UI|
+|-------||-----|
+|![](assets/sdk_manager_icon.png)||![](assets/sdk_manager_icon_2.png)|
-![](assets/sdk_manager_icon_2.png)
SDK manager felülete:
@@ -132,22 +128,21 @@ Indítsuk el az AVD managert, és vizsgáljuk meg a laborvezetővel, hogy rendel
### AVD
-Az AVD az Android Virtual Device rövidítése. Ahogy arról már előadáson is szó esett, nem csak valódi eszközön futtathatjuk a kódunkat, hanem emulátoron is. (Mi is a különbség szimulátor és emulátor között?) Az AVD indítása a fejlesztői környezeten keresztül lehetséges (illetve parancssorból is, de ennek a használatára csak speciális esetekben van szükség).
+Az AVD az Android Virtual Device rövidítése. Ahogy arról már előadáson is szó esett, nem csak valódi eszközön futtathatjuk a kódunkat, hanem emulátoron is. (Mi is a különbség szimulátor és emulátor között?) Az AVD indítása a fejlesztői környezeten keresztül lehetséges (*Tools->Device Manager*), illetve parancssorból is, de ennek a használatára csak speciális esetekben van szükség.
Az AVD Manager ikonja:
-![](assets/avd_icon.png)
+|Régi UI||Új UI|
+|-------||-----|
+|![](assets/avd_icon.png)||![](assets/avd_icon_2.png)|
-vagy
-
-![](assets/avd_icon_2.png)
![](assets/avd_manager.png)
-A fenti képen jobb oldalon, a kinyíló panelben, a létező virtuális eszközök listáját találjuk, bal oldalon pedig az ún. eszköz definíciókét. Itt néhány előre elkészített sablon áll rendelkezésre. Magunk is készíthetünk ilyet, ha tipikusan egy adott eszközre szeretnénk fejleszteni (pl. Galaxy S4). Készítsünk új emulátort! Értelemszerűen csak olyan API szintű eszközt készíthetünk, amilyenek rendelkezésre állnak az SDK manageren keresztül.
+A fenti képen jobb oldalon, a kinyíló panelben, a létező virtuális eszközök listáját találjuk, bal oldalon pedig az ún. eszköz definíciókét. (Ezt az *Add a new device* fül, majd a *Create Virtual Device* opcióval tudjuk megnyitni a jobb oldalon lévő `+` ikonnal) Itt néhány előre elkészített sablon áll rendelkezésre. Magunk is készíthetünk ilyet, ha tipikusan egy adott eszközre szeretnénk fejleszteni (pl. Galaxy S24). Készítsünk új emulátort! Értelemszerűen csak olyan API szintű eszközt készíthetünk, amilyenek rendelkezésre állnak az SDK manageren keresztül.
1. A jobb oldali panelon kattintsunk a fent található *Create Virtual Device...* gombra!
-2. Válasszunk az előre definiált készülék sablonokból (pl. *Pixel 7 Pro*), majd nyomjuk meg a *Next* gombot.
+2. Válasszunk az előre definiált készülék sablonokból (pl. *Pixel 8 Pro*), majd nyomjuk meg a *Next* gombot.
3. Döntsük el, hogy milyen Android verziójú emulátort kívánunk használni. CPU/ABI alapvetően x86_64 legyen, mivel ezekhez kapunk [hardveres gyorsítást](https://developer.android.com/studio/run/emulator-acceleration) is. Itt válasszunk a rendelkezésre állók közül egyet, majd *Next*.
4. Az eszköz részletes konfigurációja.
@@ -161,7 +156,7 @@ A fenti képen jobb oldalon, a kinyíló panelben, a létező virtuális eszköz
- *Emulated*, egy egyszerű szoftveres megoldás, **most legalább az egyik kamera legyen ilyen**.
- *VirtualScene*, egy kifinomultabb szoftveres megoldás, amelyben egy 3D világban mozgathatjuk a kamerát.
- Hálózat: Állíthatjuk a sebességét és a késleltetését is kommunikációs technológiák szerint.
- - *Boot Option*: Nemrég jelent meg az Android emulátor állapotáról való pillanatkép elmentésének lehetősége. Ez azt takarja, hogy a virtuális operációs rendszer csak felfüggesztésre kerül az emulátor bezáráskor (például a megnyitott alkalmazás is megmarad, a teljes állapotával), és *Quick boot* esetben a teljes OS indítása helyett másodperceken belül elindul az emulált rendszer. *Cold Boot* esetben minden alkalommal leállítja és újra indítja a virtális eszköz teljes operációs rendszerét.
+ - *Boot Option*: (Nemrég jelent meg az) Az Android emulátor állapotáról való pillanatkép elmentésének lehetősége. Ez azt takarja, hogy a virtuális operációs rendszer csak felfüggesztésre kerül az emulátor bezáráskor (például a megnyitott alkalmazás is megmarad, a teljes állapotával), és *Quick boot* esetben a teljes OS indítása helyett másodperceken belül elindul az emulált rendszer. *Cold Boot* esetben minden alkalommal leállítja és újra indítja a virtális eszköz teljes operációs rendszerét.
- Memória és tárhely:
- RAM: Ha kevés a rendszermemóriánk, nem érdemes 768 MB-nál többet adni, mert könnyen futhatunk problémákba. Ha az emulátor lefagy, vagy az egész OS megáll működés közben, akkor állítsuk alacsonyabbra ezt az értéket. 8 GB vagy több rendszermemória mellett nyugodtan állíthatjuk az emulátor memóriáját 1024, 1536, vagy 2048 MB-ra.
- VM heap: az alkalmazások virtuális gépének szól, maradhat az alapérték. Tudni kell, hogy készülékek esetében gyártónként változik.
@@ -180,35 +175,39 @@ Az elindított emulátoron próbáljuk ki az *API Demos* és *Dev Tools* alkalma
!!! note "Megjegyzés"
A gyári emulátoron kívül több alternatíva is létezik, mint pl. a [Genymotion](https://www.genymotion.com/fun-zone/) vagy a [BigNox](https://www.bignox.com/), viszont a Google féle emulátor a legelterjedtebb, így amennyiben ezzel nem jelentkeznek problémáink, maradjunk ennél.
-Tesztelés céljából nagyon jól használható az emulátor, amely az alábbi képen látható plusz funkciókat is adja. Lehetőség van többek között egyedi hely beállítására, bejövő hívás szimulálására, stb. A panelt a futó emulátor jobb oldalán található vezérlő gombok közül a *...* gombbal lehet megnyitni:
+Tesztelés céljából nagyon jól használható az emulátor, amely az alábbi képen látható plusz funkciókat is adja. Lehetőség van többek között egyedi hely beállítására, bejövő hívás szimulálására, virtuálisan szenzorok manipulálására, stb. A panelt a futó emulátor jobb oldalán található vezérlő gombok közül a *...* gombbal lehet megnyitni:
![](assets/avd_extras.png)
## Fejlesztői környezet
- Android fejlesztésre a labor során a JetBrains IntelliJ alapjain nyugvó Android Studio-t fogjuk használni. A Studio-val ismerkedők számára hasznos funkció a *Tip of the day*, érdemes egyből kipróbálni, megnézni az adott funkciót. Induláskor alapértelmezetten a legutóbbi projekt nyílik meg, ha nincs ilyen, vagy ha minden projektünket bezártuk, akkor a nyitó képernyő. (A legutóbbi projekt újranyitását a *Settings -> Appeareance & Behavior -> System Settings -> Reopen last project on startup* opcióval ki is kapcsolhatjuk.)
+ Android fejlesztésre a labor során a JetBrains IntelliJ alapjain nyugvó Android Studio-t fogjuk használni. A Studio-val ismerkedők számára hasznos funkció a *Tip of the day*, érdemes egyből kipróbálni, megnézni az adott funkciót. Induláskor alapértelmezetten a legutóbbi projekt nyílik meg, ha nincs ilyen, vagy ha minden projektünket bezártuk, akkor a nyitó képernyő. (A legutóbbi projekt újranyitását a *Settings -> Appeareance & Behavior -> System Settings -> Reopen projects on startup* opcióval ki is kapcsolhatjuk.)
![](assets/studio_old.png)
-Az Android Studio Giraffe-ban megújult a környezet felhasználói felülete. Amint látható, jóval letisztultabb dizájnt választottak, sokkal kevesebb a figyelmet elvonó extra a képernyőn, sokkal inkább a kódon van a hangsúly. Ezek között a nézetek között egyszerűen válthatunk a Beállításokban, a New UI menüpontban.
+Pár frissítéssel ezelőtt, az Android Studio Giraffe-ban megújult a környezet felhasználói felülete. Amint látható, jóval letisztultabb dizájnt választottak, sokkal kevesebb a figyelmet elvonó extra a képernyőn, sokkal inkább a kódon van a hangsúly. Ezek között a nézetek között egyszerűen válthatunk a Beállításokban, a New UI menüpontban. (*Settings -> Appeareance & Behavior -> New UI*)
![](assets/studio_new.png)
## Hello World
-A laborvezető segítségével készítsünk egy egyszerű Hello World alkalmazást, a varázsló nézeten az *Include Kotlin support* legyen bepipálva!
-1. Hozzunk létre egy új projektet, válasszuk az *Empty Views Activity* lehetőséget.
-1. A projekt neve legyen `HelloWorld`, a kezdő package `hu.bme.aut.mobweb.helloworld`, a mentési hely pedig a kicheckoutolt repository-n belül a `HelloWorld` mappa.
-1. Nyelvnek válasszuk a *Kotlin*-t.
-1. A minimum API szint legyen API24: Android 7.0.
-1. A `Build configuration language` Kotlin DSL legyen.
+A laborvezető segítségével készítsünk egy egyszerű Hello World alkalmazást.
+
+1. Hozzunk létre egy új projektet, válasszuk az *Empty Activity* lehetőséget.
+2. A projekt neve legyen `HelloWorld`, a kezdő package `hu.bme.aut.mobweb.helloworld`, a mentési hely pedig a kicheckoutolt repository-n belül a `HelloWorld` mappa.
+3. Nyelvnek válasszuk a *Kotlin*-t.
+4. A minimum API szint legyen API24: Android 7.0.
+5. A `Build configuration language` Kotlin DSL legyen.
!!!warning "FILE PATH"
A projekt a repository-ban lévő HelloWorld könyvtárba kerüljön!
+!!!danger "FILE PATH"
+ A projektnek az elérési Path-je csak az angol ábécé kis betűit tartalmazza, ugyanis az Android Studio az ékezetekre és a speciális karakterekre érzékeny!
+
### Android Studio
Ez a rész azoknak szól, akik korábban már használták az Eclipse nevű IDE-t, és szeretnék megismerni a különbségeket az Android Studio-hoz képest.
@@ -232,11 +231,10 @@ Ez a rész azoknak szól, akik korábban már használták az Eclipse nevű IDE-
* Szigorú lint. A Studio megengedi a warningot. Ezért szigorúbb a lint, több mindenre figyelmeztet (olyan apróságra is, hogy egy View egyik oldalán van padding, a másikon nincs)
* Layout szerkesztés. A grafikus layout építés lehetséges.
* CTRL-t lenyomva navigálhatunk a kódban, pl. osztályra, metódushívásra kattintva. Ezt a navigációt (és az egyszerű másik osztályba kattintást is) rögzíti, és a historyban előre-hátra gombokkal lehet lépkedni. Ha van az egerünkön/billentyűzetünkön ilyen gomb, és netes böngészés közben aktívan használjuk, ezt a funkciót nagyon hasznosnak fogjuk találni.
+ * Ha több fájl is meg van nyitva egyszerre, könnyen navigálhatunk az ALT + BAL /JOBB nyilak segítségével az fájlok között.
![](assets/nice_studio.png)
-*Szín ikonja a sor elején; kiemelve jobb oldalon, hogy melyik nézeten vagyunk; szabadszavas kiegészítés; a "Hello world" igazából `@string/very_very_very_long_hello_world`.*
-
### Billentyűkombinációk
@@ -255,6 +253,10 @@ Ez a rész azoknak szól, akik korábban már használták az Eclipse nevű IDE-
* CTRL + SHIFT + N : Keresés fájlokban
* CTRL + ALT + SHIFT + N : Keresés szimbólumokban (például függvények, property-k)
* CTRL + SHIFT + A : Keresés a beállításokban, kiadható parancsokban.
+* ALT + ENTER hiányzó elemek importálása/létrehozása.
+
+!!!tip "Keresés"
+ Hogy ha bármikor szükségünk van valamire, de esetleg nem találnánk a menüpontok között, akkor a dupla Shift lenyomásával (Shift +Shift ) kereshetünk az Android Studioban (illetve más JetBrains IDE-kben). Próbáljuk is ki és keressünk rá a "Device Manager" opcióra.
### Eszközök, szerkesztők
@@ -268,15 +270,15 @@ A *View* menü *Tool Windows* menüpontjában lehetőség van különböző abla
* Event Log
* Gradle
-Lehetőség van felosztani a szerkesztőablakot, ehhez kattinsunk egy megnyitott fájl tabfülére jobb gombbal, *Split Vertically/Horizontally*!
+Lehetőség van felosztani a szerkesztőablakot, ehhez kattinsunk egy megnyitott fájl tabfülére jobb gombbal, *Split Right/Down* vagy csak kattintsunk rá hosszan és kezdjük el húzni a kódfelületre!
### Hasznos beállítások
A laborvezető segítségével állítsák be a következő hasznos funkciókat:
-* kis- nagybetű érzékenység kikapcsolása a kódkiegészítőben (settingsben keresés: *sensitive*)
+* kis- nagybetű érzékenység kikapcsolása a kódkiegészítőben (settingsben keresés: *Match case*)
* "laptop mód" ki- és bekapcsolása (*File -> Power Save Mode*)
-* sorszámozás bekapcsolása (kód melletti részen bal oldalt: jobb egérgomb, *Show Line Numbers*)
+* sorszámozás bekapcsolása (kód melletti részen bal oldalt: jobb egérgomb, *Appearance -> Show Line Numbers*)
### Generálható elemek
@@ -299,13 +301,13 @@ Például részletes információt kaphatunk a hálózati forgalomról:
## Database Inspector
-A készüléken debuggolt alkalmazásunknak az [adatbázisát](https://developer.android.com/studio/inspect/database) is meg tudjuk tekinteni.
+A készüléken debuggolt alkalmazásunknak az [adatbázisát](https://developer.android.com/studio/inspect/database) is meg tudjuk tekinteni. (*View -> Tool Windows -> App Inspection*)
![](assets/di.png)
## Device File Explorer
-A készüléken lévő fájlrendszert is [böngészhetjük](https://developer.android.com/studio/debug/device-file-explorer).
+A készüléken lévő fájlrendszert is [böngészhetjük](https://developer.android.com/studio/debug/device-file-explorer). (*View -> Tool Windows -> Device Explorer*)
![](assets/dfe.png)
diff --git a/docs/laborok/02-android-ui/assets/asset_studio.png b/docs/laborok/02-android-ui/assets/asset_studio.png
index a10215c..976fac7 100644
Binary files a/docs/laborok/02-android-ui/assets/asset_studio.png and b/docs/laborok/02-android-ui/assets/asset_studio.png differ
diff --git a/docs/laborok/02-android-ui/assets/details.png b/docs/laborok/02-android-ui/assets/details.png
index e486f72..a97cc53 100644
Binary files a/docs/laborok/02-android-ui/assets/details.png and b/docs/laborok/02-android-ui/assets/details.png differ
diff --git a/docs/laborok/02-android-ui/assets/list.png b/docs/laborok/02-android-ui/assets/list.png
index ac6e35c..93a0c92 100644
Binary files a/docs/laborok/02-android-ui/assets/list.png and b/docs/laborok/02-android-ui/assets/list.png differ
diff --git a/docs/laborok/02-android-ui/assets/login.png b/docs/laborok/02-android-ui/assets/login.png
index c758d58..693d3c3 100644
Binary files a/docs/laborok/02-android-ui/assets/login.png and b/docs/laborok/02-android-ui/assets/login.png differ
diff --git a/docs/laborok/02-android-ui/assets/login_insets.png b/docs/laborok/02-android-ui/assets/login_insets.png
new file mode 100644
index 0000000..d605652
Binary files /dev/null and b/docs/laborok/02-android-ui/assets/login_insets.png differ
diff --git a/docs/laborok/02-android-ui/assets/pass.png b/docs/laborok/02-android-ui/assets/pass.png
index 63d99ab..22d5bac 100644
Binary files a/docs/laborok/02-android-ui/assets/pass.png and b/docs/laborok/02-android-ui/assets/pass.png differ
diff --git a/docs/laborok/02-android-ui/assets/splash.png b/docs/laborok/02-android-ui/assets/splash.png
index e90b314..915a796 100644
Binary files a/docs/laborok/02-android-ui/assets/splash.png and b/docs/laborok/02-android-ui/assets/splash.png differ
diff --git a/docs/laborok/02-android-ui/downloads/res.zip b/docs/laborok/02-android-ui/downloads/res.zip
index fe4498e..486c9f5 100644
Binary files a/docs/laborok/02-android-ui/downloads/res.zip and b/docs/laborok/02-android-ui/downloads/res.zip differ
diff --git a/docs/laborok/02-android-ui/index.md b/docs/laborok/02-android-ui/index.md
index 006dd5f..8c87c92 100644
--- a/docs/laborok/02-android-ui/index.md
+++ b/docs/laborok/02-android-ui/index.md
@@ -42,11 +42,11 @@ A feladatok megoldása során ne felejtsd el követni a [feladat beadás folyama
Első lépésként indítsuk el az Android Studio-t, majd:
-1. Hozzunk létre egy új projektet, válasszuk az *Empty Views Activity* lehetőséget.
-1. A projekt neve legyen `PublicTransport`, a kezdő package `hu.bme.aut.android.publictransport`, a mentési hely pedig a kicheckoutolt repository-n belül a PublicTransport mappa.
-1. Nyelvnek válasszuk a *Kotlin*-t.
-1. A minimum API szint legyen API24: Android 7.0.
-1. A *Build configuration language* Kotlin DSL legyen.
+1. Hozzunk létre egy új projektet, válasszuk az *Empty Activity* lehetőséget.
+2. A projekt neve legyen `PublicTransport`, a kezdő package `hu.bme.aut.android.publictransport`, a mentési hely pedig a kicheckoutolt repository-n belül a PublicTransport mappa.
+3. Nyelvnek válasszuk a *Kotlin*-t.
+4. A minimum API szint legyen API24: Android 7.0.
+5. A *Build configuration language* Kotlin DSL legyen.
!!!danger "FILE PATH"
A projekt a repository-ban lévő PublicTransport könyvtárba kerüljön, és beadásnál legyen is felpusholva! A kód nélkül nem tudunk maximális pontot adni a laborra!
@@ -54,21 +54,59 @@ Első lépésként indítsuk el az Android Studio-t, majd:
!!!info ""
A projekt létrehozásakor, a fordító keretrendszernek rengeteg függőséget kell letöltenie. Amíg ez nem történt meg, addig a projektben nehézkes navigálni, hiányzik a kódkiegészítés, stb... Éppen ezért ezt tanácsos kivárni, azonban ez akár 5 percet is igénybe vehet az első alkalommal! Az ablak alján látható információs sávot kell figyelni.
-Láthatjuk, hogy létrejött egy projekt, amiben van egy Activity, `MainActivity` néven, valamint egy hozzá tartozó layout fájl `activity_main.xml` néven. Nevezzük ezeket át `LoginActivity`-re, illetve `activity_login.xml`-re. Ezt a jobb gomb > Refactor > Rename menüpontban lehet megtenni (vagy Shift+F6). Az átnevezésnél található egy Scope nevű beállítás. Ezt állítsuk úgy be, hogy csak a jelenlegi projekten belül nevezze át a dolgokat (Project Files).
+Láthatjuk, hogy létrejött egy projekt, abban egy Activity, `MainActivity` néven. Ez be is lett írva automatikusan a *Manifest* fájlba mint Activity komponens.
+
+Következő lépésként vagyük fel a szükséges függőségeket a projektbe! Ehhez nyissuk meg a
+
+- Modul szintű `build.gradle.kts` fájlt (*app -> build.gradle.kts*)
+- Illetve a `libs.version.toml` fájlt (*gradle -> libs.versions.toml*)
+
+Először másoljuk be a következő függőségeket a `libs.version.toml` verzió katalógus fájlba:
+
+```toml
+[versions]
+...
+coreSplashscreen = "1.0.1"
+navigationCompose = "2.7.7"
+
+[libraries]
+...
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+```
+
+Itt a `[versions]` tag-en belül adhatunk egy változó nevet, majd egy verzió értéket, amit majd a következő lépésben átadunk a `version.ref`-nek. Ez mondja meg, hogy melyik verziót használja az adott modulból. A `[libraries]` tag-en belül definiálunk szintén egy változót `androidx-navigation-compose` néven, amit majd később használunk fel a `build.gradle.kts` fájlban. Ennek megadjuk, hogy melyik modul-t szeretnénk beletenni a projektbe, valamint egy verzió számot, amit korábban már definiáltunk.
+
+Hogy ha ezzel megvagyunk, nyissuk meg a `build.gradle.kts` fájlt, és adjuk hozzá az imént felvett modulokat a `dependencies` tag-en belülre:
+
+```kts
+dependencies {
+ ...
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.navigation.compose)
+}
+```
+
+Itt az `implementation` függvény segítségével tudunk új függőséget felvenni a projektbe, és ezen belül meg kell adnunk a modul nevét, amit már korábban definiáltunk a `libs.version.toml`-ban. Ezt a következő képpen tehetjük meg:
+
+- megadjuk a fájl nevét, jelen esetben `libs`
+- majd ezután megadjuk annak a változónak a nevét amihez hozzárendeltük korábban a modulunkat.
+
+Hogy ha ezzel is megvagyunk kattintsunk a `Sync Now` gombra a jobb fölső sarokban, és várjuk meg míg letölti a szükséges függőségeket.
+
+!!!danger "Sync Now"
+ Figyelem ha ezt a lépést kihagyjuk, akkor az Android Studio nem fogja megtalálni a szükséges elemeket, és ez később gondot okozhat!
-!!!note ""
- Érdemes megfigyelni, hogy az átnevezés "okos". A layout fájl átnevezése esetén a LoginActivity-ben nem kell kézzel átírnunk a layout fájl azonosítóját, mert ezt a rendszer megteszi. Ugyanez igaz a manifest fájlra is.
## Splash képernyő (0.5 pont)
-Az első Activity-nk a nevéhez híven a felhasználó bejelentkezéséért lesz felelős, azonban még mielőtt ez megjelenik a felhasználó számára, egy splash képernyővel fogjuk üdvözölni. Ez egy elegáns megoldás arra, hogy az alkalmazás betöltéséig ne egy egyszínű képernyő legyen a felhasználó előtt, hanem egy tetszőleges saját design.
+Miután a felhasználó elindította az alkalmazást, egy "üdvözlő/splash" képernyővel szeretnénk köszönteni. Ez egy elegáns megoldás arra, hogy az alkalmazás betöltéséig ne egy egyszínű képernyő legyen a felhasználó előtt, hanem jelen esetben egy alkalmazás logo, egy tetszőleges háttér színnel.
-
+
-
-???info "Android 12 (API 31) alatt"
+???info "Splash scheen Android 12 (API 31) alatt"
(A szükséges fájl [innen](./downloads/res.zip) elérhető)
@@ -141,27 +179,19 @@ Az első Activity-nk a nevéhez híven a felhasználó bejelentkezéséért lesz
}
```
-API 31 felett bevezetésre került egy [Splash Screen API](https://developer.android.com/develop/ui/views/launch/splash-screen), most ezt fogjuk használni. Ehhez adjuk hozzá a szükséges függőséget az `app` modulhoz tartozó `build.gradle.kts` fájlunkban a *dependencies* szekcióhoz:
-
-```gradle
-dependencies {
- ...
- implementation("androidx.core:core-splashscreen:1.0.0")
-}
-```
-A függőség hozzáadása után nyomjuk meg a `Sync Now` gombot a felül megjelenő kék sávon. A Környezet ez után letölti amegfelelő könyvtárakat, és újraszinkronizálja a gradle fájlokat. Innentől kezdve már használható a frissen hozzáadott könyvtár.
+API 31 felett bevezetésre került egy [Splash Screen API](https://developer.android.com/develop/ui/views/launch/splash-screen), most ezt fogjuk használni. Ehhez már korábban felvettük a szükséges függőséget a `build.gradle.kts` fájlba.
-Azonban először készítsünk egy ikont, amit majd fel fogunk használni a splash képernyőnk közepén. Ehhez az Android Studio beépített `Asset Studio` eszközét fogjuk használni. Bal oldalon a *Project* fül alatt nyissuk meg a `Resource Manager`-t, majd nyomjunk a + gombra, ott pedig az `Image Asset` lehetőségre.
+Készítsünk el egy tetszőleges ikont, amit majd fel fogunk használni a splash képernyőnk közepén. Ehhez az Android Studio beépített `Asset Studio` eszközét fogjuk használni. Bal oldalon a *Project* fül alatt nyissuk meg a `Resource Manager`-t, majd nyomjunk a + gombra, ott pedig az `Image Asset` lehetőségre.
-1. Itt *Launcher Icon*-t szeretnénk majd generálni, tehát válasszuk azt.
-1. A neve legyen *ic_transport*
-1. Az egyszerűség kedvéért most *Clip Art*-ból fogjuk elkészíteni az ikonunkat, így válasszuk azt, majd az alatta lévő gombnál válasszunk egy szimpatikusat (pl. a bus keressel).
-1. Ez után válasszunk egy szimpatikus színt.
-1. Ha akarunk, állíthatunk a méretezésen is.
-1. A `Background Layer` fülön beállíthatjuk a háttér színét is.
-1. A beállításoknál állítsuk át, hogy az ikonok *PNG* formábumban készüljenek el.
-1. Ez után nyomjunk a *Next* majd a *Finish* gombra.
+1. Itt *Launcher Icon-t* szeretnénk majd generálni, tehát válasszuk azt.
+2. A neve legyen *ic_transport*
+3. Az egyszerűség kedvéért most *Clip Art*-ból fogjuk elkészíteni az ikonunkat, így válasszuk azt, majd az alatta lévő gombnál válasszunk egy szimpatikusat (pl. a *bus* keresési szóval).
+4. Ez után válasszunk egy szimpatikus színt.
+5. Ha akarunk, állíthatunk a méretezésen is.
+6. A `Background Layer` fülön beállíthatjuk a háttér színét is.
+7. A beállításoknál állítsuk át, hogy az ikon *PNG* formában készüljön el.
+8. Ezután nyomjunk a *Next* majd a *Finish* gombra.
@@ -169,730 +199,1238 @@ Azonban először készítsünk egy ikont, amit majd fel fogunk használni a spl
Láthatjuk, hogy több féle ikon készült, több féle méretben. Ezekből a rendszer a konfiguráció függvényében fog választani.
-A splash képernyő elkészítéséhez egy új stílust kell definiálnunk a `themes.xml` fájlban.
+A splash képernyő elkészítéséhez egy új stílust kell definiálnunk a `themes.xml` fájlban. Vegyük fel az alábbi kódrészletet a meglévő stílus alá. (A tárgy keretein belül nagyon kevés XML kóddal fogunk foglalkozni.)
```xml
-
-
-
-
-
-
-
-
-
+
```
Az új stílusunk a `Theme.PublicTransport.Starting` nevet viseli, és a `Theme.SplashScreen` témából származik. Ezen kívül beállítottuk benne, hogy
-- `windowSplashScreenBackground`: a splash képernyő háttere a *primary color* legyen (természetesen más is választható),
+- `windowSplashScreenBackground`: a splash képernyő háttere (természetesen más is választható),
- `windowSplashScreenAnimatedIcon`: a középen megjelenő ikon a saját ikonunk legyen, annak is csak az előtere,
-- `android:windowSplashScreenIconBackgroundColor`: az ikonunk mögött milyen háttér legyen,
+- `android:windowSplashScreenIconBackgroundColor`: az ikonunk mögött milyen háttér legyen (ez is személyre szabható más színnel),
- `postSplashScreenTheme`: a splash screen után milyen stílusra kell visszaváltania az alkalmazásnak.
+
!!!note
A Splash Screen API ennél jóval többet tud, akár animálhatjuk is a megjelenített képet, azonban ez sajnos túlmutat a labor keretein.
-Most már, hogy bekonfiguráltuk a *splash* képernyőnket, már csak be kell állítanunk a használatát. Ehhez először az imént létrehozott stílust kell alkalmaznunk `LoginActivity`-re a `manifest.xml`-ben.
-
+Most már, hogy bekonfiguráltuk a *splash* képernyőnket, már csak be kell állítanunk a használatát. Ehhez először az imént létrehozott stílust kell alkalmaznunk `MainActivity`-re a `manifest.xml`-ben.
+
+
```xml
-
...
-
- ...
-
+
```
-Ha már itt járunk, állítsuk be az alkalmazásunk ikonját is:
+Ezután állítsuk be az alkalmazásunk ikonját is:
```xml
- ...
+ ...
```
-
-Majd meg kell hívnunk az `installSplashScreen` függvényt az `onCreate`-ben, hogy valójában elkészüljön a *splash screen*:
+Majd meg kell hívnunk az `installSplashScreen` függvényt az `onCreate`-ben, hogy az alkalmazás indításánál, valóban elkészüljön a *Splash Screen*.
```kotlin
-class LoginActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_login)
- }
+ enableEdgeToEdge()
+
+ setContent {
+ PublicTransportTheme {
+ Greeting(name = "Android")
+ }
+ }
+ }
}
```
-Próbáljuk ki az alkalmazásunkat!
+!!!note "Splash Screen-NavGraph"
+ A Splash Screent a NavGraph segítségével is meg lehet oldani, erről a labor végén egy ismertető [feladat](#ismerteto-feladat-navgrap-splash) fog segítséget mutatni. (Ez nem szükséges a labor megszerzéséhez, a feladat nélkül is el lehet érni a maximális pontot, azonban az érdekesség kedvéért érdemes végig csinálni.)
+Próbáljuk ki az alkalmazásunkat!
+
!!!example "BEADANDÓ (0.5 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **splash képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f1.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **splash képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**! A képet a megoldásban a repository-ba f1.png néven töltsd föl!
+
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
## Login képernyő (0.5 pont)
-Most már elkészíthetjük a login képernyőt. A felhasználótól egy email címet, illetve egy számokból álló jelszót fogunk bekérni, és egyelőre csak azt fogjuk ellenőrizni, hogy beírt-e valamit a mezőkbe.
+Most már elkészíthetjük a login képernyőt. A felhasználótól egy e-mail címet, illetve egy számokból álló jelszót fogunk bekérni, és egyelőre csak azt fogjuk ellenőrizni, hogy beírt-e valamit a mezőbe.
-Az `activity_login.xml` fájlba kerüljön az alábbi kód. Alapértelmezetten egy grafikus szerkesztő nyílik meg, ezt át kell állítani a szöveges szerkesztőre. Ezt az Android Studio verziójától függően a jobb felső, vagy a jobb alsó sarokban lehet megtenni:
+Ehhez először hozzunk létre egy új *Packaget* a projekt mappába `navigation` néven, majd ebbe hozzunk létre két *Kotlin Filet* (a *Package*-ünkön jobb klikk -> New -> Kotlin Class/File) `NavGraph` illetve `Screen` néven. Ez utóbbira csak azért lesz szükség, hogy a későbbiekben szebben tudjuk megoldani a navigációt a képernyők között. Ezt az [Extra feladat - Screen File](#ismerteto-feladat-screen-file) résznél fogjuk részletezve leírni az érdeklődők kedvéért.
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-- A használt elrendezés teljesen lineáris, csak egymás alá helyezünk el benne különböző View-kat egy `LinearLayout`-ban.
-- Az `EditText`-eknek és a `Button`-nek adtunk ID-kat, hogy később kódból elérjük őket.
-
-Az alkalmazást újra futtatva megjelenik a layout, azonban most még bármilyen szöveget be tudnunk írni a két beviteli mezőbe. Az `EditText` osztály lehetőséget ad számos speciális input kezelésére, XML kódban az [`inputType`](https://developer.android.com/reference/android/widget/TextView.html#attr_android:inputType) attribútum megadásával. Jelen esetben az email címet kezelő `EditText`-hez a `textEmailAddress` értéket, a másikhoz pedig a `numberPassword` értéket használhatjuk.
+Nyissuk meg a `NavGraph` fájlt, és írjuk bele a következő kódot, majd nézzük át és értelmezzük a laborvezető segítségével a kódot.
-```xml
-
+```kotlin
+@Composable
+fun NavGraph(
+ navController: NavHostController = rememberNavController(),
+){
+ NavHost(
+ navController = navController,
+ startDestination = "login"
+ ){
+ composable("login"){
+ LoginScreen(
+ onSuccess = {
+ /*TODO*/
+ }
+ )
+ }
+ }
+}
+```
-
+Miután ezzel megvagyunk, hozzunk létre egy új *Packaget* `screen` néven a projekt mappában, majd ezen belül hozzunk létre egy új *Kotlin Filet* `LoginScreen` néven. Ezen a képernyőn fognak elhelyezkedni a szükséges feliratok, gombok, és beviteli mezők. Ehhez használjuk fel az alábbi kódot:
+
+```kotlin
+@Composable
+fun LoginScreen(
+ onSuccess: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ //TODO Logo
+
+ //TODO Header Text
+
+
+ //TODO Email Field
+
+
+ //TODO Password Field
+
+
+ //TODO Login Button
+
+ }
+}
+
+
+private fun isEmailValid(email: String) = email.isEmpty()
+
+private fun isPasswordValid(password: String) = password.isEmpty()
+
+@Preview
+@Composable
+fun PreviewLoginScreen() {
+ LoginScreen(onSuccess = {})
+}
```
-Ha most kipróbáljuk az alkalmazást, már látjuk a beállítások hatását:
+Hogy ha megvan a `LoginScreen` váza, akkor kezdjük el belepakolni az egyes elemeket. (Image, Text, TextField, Button)
-- A legtöbb billentyűzettel az első mezőhöz most már megjelenik a `@` szimbólum, a másodiknál pedig csak számokat írhatunk be.
-- Mivel a második mezőt jelszó típusúnak állítottuk be, a karakterek a megszokott módon elrejtésre kerülnek a beírásuk után.
+Kezdjük az `Image` *Composable*-lel. Az egyszerűség kedvéért az alkalmazás ikonját fogjuk betenni a bejelentkező képernyő tetejére dizájn elemként.
-Még egy dolgunk van ezen a képernyőn, az input ellenőrzése. Ezt a `LoginActivity.kt` fájlban tehetjük meg. A layout-unkat alkotó `View`-kat az `onCreate` függvényben lévő `setContentView` hívás után tudjuk először elérni.
+```kotlin
+//Logo
+Image(
+ painter = painterResource(id = R.mipmap.ic_transport_round),
+ contentDescription = "Logo",
+ modifier = Modifier.size(160.dp)
+)
+```
-!!!note ""
- Ezt csinálhatnánk a klasszikus módon, azaz példányosítunk egy gombot, a `findViewById` metódussal referenciát szerzünk a felületen lévő vezérlőre, és a példányon beállítjuk az eseménykezelőt:
-
- ```kotlin
- val btnLogin = findViewById(R.id.btnLogin)
- btnLogin.setOnClickListener {
- ...
- }
- ```
+Mivel az `Image` *Composable* csak vektoros erőforrást fogad el, elsőre hibát kapunk. Ezt most a legegyszerűbben úgy oldhatjuk meg, ha az *ic_transport* és az *ic_transport_round* erőforrásoknak kiröröljük az *xml*-es verzióit, és csak a *png*-ket hagyjuk meg. Innen már az alkalmazás buildelése után megjelenik a felületünk előnézete is.
- Azonban a `findViewById` hívásnak számos problémája [van](https://developer.android.com/topic/libraries/view-binding#findviewbyid). Ezekről bővebben az előadáson lesz szó (pl.: *Null safety*, *type safety*). Ezért ehelyett "nézetkötést", azaz `ViewBinding`-ot fogunk használni.
+Folytassuk a `Text` *Composable*-lel. Ez egy üzenetként fog szolgálni a form tetején `"Please enter your credentials!"` felirattal.
-A [`ViewBinding`](https://developer.android.com/topic/libraries/view-binding) a kódírást könnyíti meg számunkra. Amennyiben ezt használjuk, az automatikusan generálódó *binding* osztályokon keresztül közvetlen referencián keresztül tudunk elérni minden *ID*-val rendelkező erőforrást az `XML` fájljainkban.
+```kotlin
+//Header Text
+Text(
+ modifier = Modifier.padding(16.dp),
+ text = "Please enter your credentials!"
+)
+```
+
+Következőnek hozzuk létre a két `TextField`-et, amit egy `OutlinedTextField` *Composable* segítségével fogunk megvalósítani. Itt szükség lesz egyéb változókra is.
-Először is be kell kapcsolnunk a modulunkra a `ViewBinding`-ot. Az `app` modulhoz tartozó `build.gradle.kts` fájlban az `android` tagen belülre illesszük be az engedélyezést (Ezek után kattintsunk jobb felül a `Sync Now` gombra.):
+**Email Field**
```kotlin
-android {
- ...
- buildFeatures {
- viewBinding = true
+//Email Field
+var email by remember { mutableStateOf("") }
+var emailError by remember { mutableStateOf(false) }
+
+OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ label = { Text("Email") },
+ value = email,
+ onValueChange =
+ {
+ email = it
+ emailError = isEmailValid(email)
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
+ isError = emailError,
+ trailingIcon = {
+ if (emailError) {
+ Icon(Icons.Filled.Warning, contentDescription = "Error", tint = Color.Red)
+ }
+ },
+ supportingText = {
+ if (emailError) {
+ Text("Please enter your e-mail address!", color = Color.Red)
+ }
}
-}
+)
```
-Ezzel után már a teljes modulunkban automatikusan elérhetővé vált a `ViewBinding`.
+**Password Field**
-!!! info "ViewBinding"
- Ebben az esetben a modul minden egyes XML layout fájljához generálódik egy úgynevezett binding osztály. Minden binding osztály tartalmaz referenciát az adott XML layout erőforrás gyökér elemére és az összes ID-val rendelkező view-ra. A generált osztály neve úgy áll elő, hogy az XML layout nevét Pascal formátumba alakítja a rendszer és a végére illeszti, hogy `Binding`. Azaz például a `result_profile.xml` erőforrásfájlból az alábbi binding osztály generálódik: `ResultProfileBinding`.
+```kotlin
+//Password Field
+var password by remember { mutableStateOf("") }
+var passwordError by remember { mutableStateOf(false) }
+
+OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ label = { Text("Password") },
+ value = password,
+ onValueChange =
+ {
+ password = it
+ passwordError = isPasswordValid(it)
+ },
+ visualTransformation = PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
+ isError = passwordError,
+ trailingIcon = {
+ if (passwordError) {
+ Icon(Icons.Filled.Warning, contentDescription = "Error", tint = Color.Red)
+ }
+ },
+ supportingText = {
+ if (passwordError) {
+ Text("Please enter your password!", color = Color.Red)
+ }
+ }
+)
+```
- ```xml
-
-
-
-
-
- ```
-
- A generált osztálynak két mezője van. A `name` id-val rendelkező `TextView` és a `button` id-jú `Button`. A layout-ban szereplő ImageView-nak nincs id-ja, ezért nem szerepel a binding osztályban.
-
- Minden generált osztály tartalmaz egy `getRoot()` metódust, amely direkt referenciaként szolgál a layout gyökerére. A példában a `getRoot()` metódus a `LinearLayout`-tal tér vissza.
+Az `OutlinedTextField` fent használt használt paraméterei:
-A `ViewBinding` használatához tehát az `Activity`-nkben csak példányosítanunk kell a `binding` objektumot, amin keresztül majd elérhetjük az erőforrásainkat.
-A `binding` példány működéséhez három dolgot kell tennünk:
+1. **label**: Ennek a segítségével tudjuk megadni azt a feliratot ami szerepelni fog az üres TextFieldben. Hogy ha írtunk már bele, akkor az `OutlinedTextField`-nek köszönhetően a *Label* szöveg, felcsúszik a bal fölső sarokba.
+2. **value**: Ennek a praméternek adjuk át, a beírt értéket.
+3. **onValueChange**: Ez egy lambda, aminek a segítségével adunk újra és újra értéket annak a változónak amit átadtunk a **value** paraméternek. Minden egyes változásnál frissül ez a paraméter a `remember`-nek köszönhetően.
+4. **visualTransformation**: Ennek a segítéségvel tudjuk változtatni, hogy *Password* vagy sima Input field legyen.
+5. **keyboardOptions**: Ezzel a paraméterrel be tudjuk állítani, és korlátozni a felhasználót, hogy milyen adatot tudjon beleírni a beviteli mezőbe.
+6. **isError**: Ennek szintén egy változót adunk át, amely minden egyes alkalommal frissül, hogy ha üres a beviteli mező. Ez azért lesz hasznos, ugyanis a feladatban azt szeretnénk elérni, hogy egy üzenetet írjon ki a TextField, hogy ha üresen szeretnénk bejelentkezni.
+7. **trailingIcon**: Itt be tudjuk állítani azt az Icon-t amit látni szeretnénk a TextField jobb oldalán.
+8. **supportingText**: Ez a paraméter felel azért, hogy a TextField alatt meg tudjunk jeleníteni szöveget.
-1. A generált `binding` osztály *statikus* `inflate` függvényével példányosítjuk a `binding` osztályunkat az `Activity`-hez.
-2. Referenciát szerzünk a gyökérelemre. Ezt kétféleképpen is megtehetjük. Vagy meghívjuk a `getRoot()` metódust, vagy a Kotlin property syntax-ot használva.
-3. Ezt a gyökérelemet odaadjuk a `setContentView()` függvénynek, hogy ez legyen az aktív *view* a képernyőn:
+Végül csináljuk meg az utolsó elemet is, ez pedig a gomb lesz, amely a bejelentkezésért fog felelni.
```kotlin
-private lateinit var binding: ActivityLoginBinding
-
-override fun onCreate(savedInstanceState: Bundle?) {
- installSplashScreen()
- super.onCreate(savedInstanceState)
- binding = ActivityLoginBinding.inflate(layoutInflater)
- setContentView(binding.root)
+//Login Button
+Button(
+ onClick = {
+ if (isEmailValid(email)) {
+ emailError = true
+ } else if (isPasswordValid(password)) {
+ passwordError = true
+ } else {
+ onSuccess()
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+) {
+ Text("Login")
}
```
-!!!info "lateinit"
- A [`lateinit`](https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties-and-variables) kulcsszóval megjelölt property-ket a fordító megengedi inicializálatlanul hagyni az osztály konstruktorának lefutása utánig, anélkül, hogy nullable-ként kéne azokat megjelölnünk (ami később kényelmetlenné tenné a használatukat, mert mindig ellenőriznünk kéne, hogy `null`-e az értékük). Ez praktikus olyan esetekben, amikor egy osztály inicializálása nem a konstruktorában történik (például ahogy az `Activity`-k esetében az `onCreate`-ben), mert később az esetleges `null` eset lekezelése nélkül használhatjuk majd a property-t. A `lateinit` használatával átvállaljuk a felelősséget a fordítótól, hogy a property-t az első használata előtt inicializálni fogjuk - ellenkező esetben kivételt kapunk.
+!!!danger "string erőforrások használata"
+ Érdemes a Stringeket kiszervezni a `./values/strings.xml` fájlba, így [lokalizálhatjuk](https://developer.android.com/guide/topics/resources/localization) az alkalmazásunkat `erőforrásminősítők` segítségével. Ezt az ALT + ENTER billentyűkombináció segítségével tehetjük meg, hogy ha a string-re kattintunk, vagy akár kézileg is felvehetjük a `strings.xml`-ben
+ ```xml
+ Email
+ ```
+
+!!!warning "kód értelmezése"
+ A laborvezető segítségével beszéljük át, és értelmezzük a kódot!
+
+Hogy ha ezzel a lépéssel is megvagyunk, akkor a `NavGraph` fájlban az errornak el kell tűnnie a szükséges importok után.
+
+Már csak egyetlen lépés van, hogy ezt a képernyőt az emulátoron láthassuk az indítás után. Nyissuk meg a `MainActivity` fájlt, és módosítsuk a következő szerint:
-Ezek után már be is állíthatjuk a gombunk eseménykezelőit az `onCreate` függvényben:
```kotlin
-binding.btnLogin.setOnClickListener {
- if(binding.etEmailAddress.text.toString().isEmpty()) {
- binding.etEmailAddress.requestFocus()
- binding.etEmailAddress.error = "Please enter your email address"
- }
- else if(binding.etPassword.text.toString().isEmpty()) {
- binding.etPassword.requestFocus()
- binding.etPassword.error = "Please enter your password"
- }
- else {
- // TODO login
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ PublicTransportTheme {
+ Box(
+ modifier = Modifier.safeDrawingPadding()
+ ) {
+ NavGraph()
+ }
+ }
+ }
}
}
```
-Amennyiben valamelyik `EditText` üres volt, a `requestFocus` függvény meghívásával aktívvá tesszük, majd az [`error`](https://developer.android.com/reference/android/widget/TextView.html#setError(java.lang.CharSequence)) property beállításával kiírunk rá egy hibaüzenetet. Ez egy kényelmes, beépített megoldás input hibák jelzésére. Így nem kell például egy külön `TextView`-t használnunk erre a célra, és abba beleírni a fellépő hibát. Ezt már akár ki is próbálhatjuk, bár helyes adatok megadása esetén még nem történik semmi.
+!!!note "EdgeToEdge"
+ Android 15-től (API 35) az alkalmazásunk képes a rendszer UI (StatusBar, NavigationBar, soft keyboard, stb.) alá is rajzolni. Ezzel valósították meg azt, hogy a készülék teljes képernyőjét használni tudjuk a szélétől a széléig. Ez hasznos helet számtalan esetben, amikor "teljes képernyős" alkalmazást szeretnénk írni, nem korlátoz minket az elfedő rendszer UI. A funkció természetesen alacsonyabb API szinteken is elérhető, erre való a fent is látható `enableEdgeToEdge` függvényhívás.
+
+ Ez viszont amennyire hasznos, annyi problémát is tud okozni, ha e miatt valami vezérlőnk becsúszik mondjuk a szoftveres billentyűzet alá, amit így nem tudunk elérni. Ennek kiküszöbölésére találták ki az [inseteket](https://developer.android.com/develop/ui/compose/layouts/insets). Ennek számos beállítása van, amellyel nem kell nekünk kézzel megtippelni, hogy például a *status bar* hány dp magas, különösen, hogy ezek az értékek futásidőben változhatnak (lásd szoftveres billentyűzet). A számos beállítás közül mi most a fent látható `safeDrawindPadding`-et használjuk, ami mint neve is mutatja, pont akkora *paddinget* állít mindenhova, hogy semmit se takarjon ki a rendszer UI. (Természetesen ez nem csak az `Activity`-ben, hanem minden `Screenen` és `Composable`-ön kölün is használható.)
-!!!info "setOnClickListener"
- A [`setOnClickListener`](https://developer.android.com/reference/android/view/View.html#setOnClickListener(android.view.View.OnClickListener)) függvény valójában olyan objektumot vár paraméterként, ami megvalósítja a [`View.OnClickListener`](https://developer.android.com/reference/android/view/View.OnClickListener) interfészt. Ezt Java-ban anonim objektumokkal szokás megoldani, amit [meg lehet tenni](https://kotlinlang.org/docs/reference/object-declarations.html#object-expressions) Kotlin nyelven is.Ehelyett azonban érdemesebb kihasználni, hogy a Kotlin rendelkezik igazi függvény típusokkal, így megadható egy olyan [lambda kifejezés](https://kotlinlang.org/docs/reference/lambdas.html#lambda-expressions-and-anonymous-functions), amelynek a fejléce megegyezik az elvárt interfész egyetlen függvényének fejlécével. Ez alapján pedig a [SAM conversion](https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions) nevű nyelvi funkció a háttérben a lambda alapján létrehozza a megfelelő `View.OnClickListener` példányt.
+ A funkció egyik jó demonstrációja, hogy a LoginScreen vezérlői, amik a teljes oldal közepére vannak helyezve, a szoftveres billentyűzet megjelenésekor nem takaródnak le, hanem a szabadon maradó hely közepére csúsznak.
+
+
+
+
+
!!!example "BEADANDÓ (0.5 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **login képernyő egy input hibával** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod az e-mail mezőbe begépelve**. A képet a megoldásban a repository-ba f2.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **login képernyő egy input hibával** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod az e-mail mezőbe begépelve**! A képet a megoldásban a repository-ba f2.png néven töltsd föl!
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
## Lehetőségek listája (1 pont)
-A következő képernyőn a felhasználó a különböző járműtípusok közül válaszhat. Egyelőre három szolgáltatás működik a fiktív vállalatunkban: biciklik, buszok illetve vonatok.
+A következő képernyőn a felhasználó a különböző járműtípusok közül választhat. Egyelőre csak három szolgáltatás működik a fiktív vállalatunkban: bicikli, buszok illetve vonatok.
-Először töltsük le [az alkalmazáshoz képeit tartalmazó tömörített fájlt](./downloads/res.zip), ami tartalmazza az összes képet, amire szükségünk lesz. A tartalmát másoljuk be az `app/src/main/res` mappába (ehhez segít, ha Android Studio-ban bal fent a szokásos Android nézetről a Project nézetre váltunk, esetleg a mappán jobb klikk > Show in Explorer).
+Először töltsük le [az alkalmazás képi erőforrásait tartalmazó tömörített fájlt](./downloads/res.zip), ami tartalmazza az összes képet, amire szükségünk lesz. A tartalmát másoljuk be a projektünkön belül az `app/src/main/res` mappába (ehhez segít, ha Android Studio-ban bal fent a szokásos Android nézetről a Project nézetre váltunk, esetleg a mappán jobb klikk > Show in Explorer).
-Hozzunk ehhez létre egy új Activity-t (a package-ünkön jobb klikk > New > Activity > Empty Views Activity), nevezzük el `ListActivity`-nek. Most, hogy ez már létezik, menjünk vissza a `LoginActivity` kódjában lévő TODO-hoz, és indítsuk ott el ezt az új Activity-t:
+Hozzunk ehhez létre egy új *Kotlin Filet* a `screen` *Packageban* és nevezzük el `ListScreen` néven, majd írjuk bele a következőt:
+
+```kotlin
+@Composable
+fun ListScreen(
+ onTransportClick: (s: String) -> Unit
+) {
+ //TODO
+}
+```
+Menjünk vissza a `NavGraph` file-ba és egészítsük ki a következővel
```kotlin
-binding.btnLogin.setOnClickListener {
- ...
- else {
- startActivity(Intent(this, ListActivity::class.java))
+@Composable
+fun NavGraph(
+ navController: NavHostController = rememberNavController(),
+) {
+
+ NavHost(
+ navController = navController,
+ startDestination = "login"
+ ) {
+ composable("login") {
+ LoginScreen(
+ onSuccess = {
+ navController.navigate("list")
+ }
+ )
+ }
+ composable("list") {
+ ListScreen(
+ onTransportClick = {
+ /*TODO*/
+ /*navController.navigate("pass/$it")*/
+ }
+ )
+ }
}
}
```
-Folytassuk a layout elkészítésével a munkát, az `activity_list.xml` tartalmát cseréljük ki az alábbira:
+Ezután készítsük el a `ListScreen`-t:
-```xml
-
-
+```kotlin
+@Composable
+fun ListScreen(
+ onTransportClick: (s: String) -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .clickable {
+ Log.d("ListScreen", "Bike clicked")
+ onTransportClick("Bike")
+ },
+ ) {
+
+ Image(
+ painter = painterResource(id = R.drawable.bikes),
+ contentDescription = "Bike Button",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds
+ )
+ Text(
+ text = "Bike",
+ fontSize = 36.sp,
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .clickable {
+ Log.d("ListScreen", "Bus clicked")
+ onTransportClick("Bus")
+ },
+ ) {
+
+ Image(
+ painter = painterResource(id = R.drawable.bus),
+ contentDescription = "Bus Button",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds
+ )
+ Text(
+ text = "Bus",
+ fontSize = 36.sp,
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .clickable {
+ Log.d("ListScreen", "Train clicked")
+ onTransportClick("Train")
+ },
+ ) {
+
+ Image(
+ painter = painterResource(id = R.drawable.trains),
+ contentDescription = "Train Button",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds
+ )
+ Text(
+ text = "Train",
+ fontSize = 36.sp,
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+ }
+ }
+}
-
+@Preview
+@Composable
+fun PreviewListScreen() {
+ ListScreen(onTransportClick = {})
+}
+```
-
-
+???info "kompakt megoldás"
+ Vagy az érdeklődők kedvéért az alábbi kódot adtuk. Ezzel a kóddal ugyanazt érhetjük el mint az előzővel, csak kevesebbet kell írni, illetve kicsit összetettebb.
-
+ ```kotlin
+ @Composable
+ fun ListScreen(
+ onTransportClick: (s: String) -> Unit
+ ) {
+ Column (
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ val type = mapOf(
+ "Bike" to R.mipmap.bikes,
+ "Bus" to R.mipmap.bus,
+ "Train" to R.mipmap.trains
+ )
+
+ for (i in type) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .clickable {
+ Log.d("ListScreen", "${i.key} clicked")
+ onTransportClick(i.key)
+ },
+ ) {
+
+ Image(
+ painter = painterResource(id = i.value),
+ contentDescription = "$i Button",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds
+ )
+ Text(
+ text = i.key,
+ fontSize = 36.sp,
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+ }
+ }
+ }
+ }
+ ```
-
+Az itt használt `Box`-ról tudjuk, hogy a benne elhelyezett Composable-k egymásra pakolódnak, így könnyen el tudjuk érni azt, hogy egy képen felirat legyen. A `Box`-nak a `modifier` segítségével tudunk kattintás eventet adni neki (`Modifier.clickable{..}`), így könnyen elérhetjük a további navigáció. Azonban ez a funkció még nem működik, mert hiányzik a `NavGraph`-ból az elérési út, illetve az onClick paraméter. Ezt a következő feladatban fogjuk orvosolni.
-
+Az `Image` *Composable* függvénynek egy `painter`, egy `contentDescription` és egy `contentScale` paramétere van. Ezeket át is tudjuk adni sorban a `painterResource`, `String` és a `ContentScale` segítségével. A `painterResource`-nak megadjuk a kép elérési útját, a `painterDescription`-nek, egy leírást, illetve a `contentScale`-nek egy `FillBounds`-ot. Ennek a segítségével el tudjuk érni, hogy a `Box` teljes területén kép legyen.
-
-```
+!!!warning "kód értelmezése"
+ A laborvezető segítségével beszéljük át, és értelmezzük a kódot!
-Ismét egy függőleges LinearLayout-ot használunk, most azonban súlyokat adunk meg benne. A gyökérelemben megadjuk, hogy a súlyok összege (`weightSum`) `3` lesz, és mindhárom gyerekének `1`-es súlyt (`layout_weight`), és `0dp` magasságot adunk. Ezzel azt érjük el, hogy három egyenlő részre osztjuk a képernyőt, amit a három `FrameLayout` fog elfoglalni.
+Próbáljuk ki az alkalmazásunkat!
-A `FrameLayout` egy nagyon egyszerű és gyors elrendezés, amely lényegében csak egymás tetejére teszi a gyerekeiként szereplő View-kat. Ezeken belül egy-egy képet, illetve azokon egy-egy feliratot fogunk elhelyezni. A három sávból az elsőt így készíthetjük el:
+A bejelentkezés után az elkészített lista nézetet kell látnunk. Habár a lista elemein való kattintás még nem navigál minket tovább, érdemes a `LogCat` segítségével leellenőrizni a logolást, ugyanis, ha mindent jól csináltunk, akkor látnunk kell az adott járműre való kattintást.
-```xml
-
-
+!!!example "BEADANDÓ (1 pont)"
+ Készíts egy **képernyőképet**, amelyen látszik a **jármúvek listája** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**! A képet a megoldásban a repository-ba f3.png néven töltsd föl!
-
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
-
-```
+## Részletes nézet (1 pont)
-Az itt használt `ImageButton` pont az, aminek hangzik: egy olyan gomb, amelyen egy képet helyezhetünk el. Azt, hogy ez melyik legyen, az `src` attribútummal adtuk meg. Az utána szereplő `TextView` fehér színnel és nagy méretű betűkkel a kép fölé fog kerülni, ebbe írjuk bele a jármű nevét.
+Miután a felhasználó kiválasztotta a kívánt közlekedési eszközt, néhány további opciót fogunk még felajánlani számára. Ezen a képernyőn tudja beállítani a bérleten szereplő dátumokat, illetve a rá vonatkozó kedvezményt, amennyiben van ilyen.
-A `@string/bike` hibát jelez. Mint látható, itt sem egy konkrét szöveget, hanem egy hivatkozást használunk. Ez azért hasznos, mert így egy helyre tudjuk szervezni a szöveges erőforrásainkat (`strings.xml`), így egyszerűen [lokalizálhatjuk](https://developer.android.com/guide/topics/resources/localization) az alkalmazásunkat `erőforrásminősítők` segítségével.
+
+
+
-Adjunk tehát értéket a `@strings/bike` elemnek. Ezt megtehetjük kézzel is a `strings.xml`-ben, de `Alt+Enter`rel a helyi menüben is:
+Hozzuk létre az új képernyőt `DetailsScreen` néven a `screen` *Packageban*, és készítsük el a felépítését, az alábbi szerint:
-```xml
-Bike
-```
+```kotlin
+@Composable
+fun DetailsScreen(
+ onSuccess: (s: String) -> Unit,
+ transportType: String
+) {
+ val context = LocalContext.current
+
+ val calendar = Calendar.getInstance()
+ val year = calendar.get(Calendar.YEAR)
+ val month = calendar.get(Calendar.MONTH)
+ val day = calendar.get(Calendar.DAY_OF_MONTH)
+
+ var startDate by remember {
+ mutableStateOf(
+ String.format(
+ Locale.US,
+ "%d. %02d. %02d",
+ year,
+ month + 1,
+ day
+ )
+ )
+ }
+ var endDate by remember {
+ mutableStateOf(
+ String.format(
+ Locale.US,
+ "%d. %02d. %02d",
+ year,
+ month + 1,
+ day
+ )
+ )
+ }
+ val currentDate = "$year. ${month + 1}. $day"
-Töltsük ki ehhez hasonló módon a másik két `FrameLayout`-ot is, ID-ként használjuk a `@+id/btnBus` és `@+id/btnTrain` értékeket, képnek pedig használhatjuk a korábban már bemásolt `@drawable/bus` és `@drawable/trains` erőforrásokat. Ne felejtsük el a `TextView`-k szövegét is értelemszerűen átírni.
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ //Pass category
+
-Próbáljuk ki az alkalmazásunkat, bejelentkezés után a most elkészített lista nézethez kell jutnunk.
+ //Start date
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **jármúvek listája** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl.
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+ //End date
+
+ //Price category
+
-## Részletes nézet (1 pont)
+ //Price
+
-Miután a felhasználó kiválasztotta a kívánt közlekedési eszközt, néhány további opciót fogunk még felajánlani számára. Ezen a képernyőn fogja kiválasztani a bérleten szereplő dátumokat, illetve a rá vonatkozó kedvezményt, amennyiben van ilyen.
+ //Buy button
+
+ }
+}
-
-
-
+@Preview
+@Composable
+fun PreviewDetailsScreen() {
+ DetailsScreen(onSuccess = {}, transportType = "Senior Bus Pass")
+}
+```
-Hozzuk létre ezt az új Activity-t `DetailsActivity` néven, a layout-ját kezdjük az alábbi kóddal:
+**Pass category**
+```kotlin
+Text(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = 16.dp),
+ text = "${transportType} pass",
+ fontSize = 24.sp
+)
+```
-```xml
-
-
+Ez a `Text` Composable egy fejléc lesz, ami azt fogja mutatni, hogy jelenleg milyen jegyet próbálunk megvásárolni. Ennek a `transportType` paramétert adjuk át szövegként, majd a `Modifier.align()` segítségével középre igazítjuk az oszlopban.
-
+**Start date**
+```kotlin
+Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = "Start date",
+ fontSize = 16.sp
+)
+TextButton(
+ modifier = Modifier.padding(top = 16.dp),
+ onClick = {
+ DatePickerDialog(
+ context,
+ { _, selectedYear, selectedMonth, selectedDay ->
+ startDate = String.format(
+ Locale.US,
+ "%d. %02d. %02d",
+ selectedYear,
+ selectedMonth + 1,
+ selectedDay
+ )
+ },
+ year, month, day
+ ).show()
+ }) {
+ Text(
+ text = if (startDate.isEmpty()) currentDate else startDate,
+ fontSize = 24.sp
+ )
+}
+```
+Egy `Text` és egy `TextButton` segítéségvel egy dátumválasztó mezőt valósítunk meg. A `Text` csak fejlécként nyújt információt, a `TextButton`-nak pedig egy onClick eventet adunk át, aminek a segítségével egy DatePicker dialógust valósítunk meg. Ennek átadjuk a szükséges paramétereket:
-
+1. context
+2. Lambda paraméter, ami azt írja le, hogy a dátum választás során mi történjen. Jelen esetben nekünk arra van szükség, hogy a startDate változónkat felülírjuk.
+3. Year - jelenlegi év
+4. Month - jelenlegi hónap
+5. Day - jelenlegi nap
-
-```
+Ez utóbbi három a DatePicker dialógus jelenlegi nap helyzetét fogja befolyásolni.
-Az eddigiekhez képest itt újdonság, hogy a használt `LinearLayout`-ot egy `ScrollView`-ba tesszük, mivel sok nézetet fogunk egymás alatt elhelyezni, és alapértelmezetten egy `LinearLayout` nem görgethető, így ezek bizonyos eszközökön már a képernyőn kívül lennének.
-Kezdjük el összerakni a szükséges layout-ot a `LinearLayout` belsejében. Az oldal tetejére elhelyezünk egy címet, amely a kiválasztott jegy típusát fogja megjeleníteni.
+**End date**
+```kotlin
+Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = "End date",
+ fontSize = 16.sp
+ )
+
+ TextButton(
+ modifier = Modifier.padding(top = 16.dp),
+ onClick = {
+ DatePickerDialog(
+ context,
+ { _, selectedYear, selectedMonth, selectedDay ->
+ endDate = String.format(
+ Locale.US,
+ "%d. %02d. %02d",
+ selectedYear,
+ selectedMonth + 1,
+ selectedDay
+ )
+ },
+ year, month, day
+ ).show()
+ }) {
+ Text(
+ text = if (endDate.isEmpty()) currentDate else endDate,
+ fontSize = 24.sp
+ )
+ }
+```
-```xml
-
+A *Start Date*-hez hasonlóan működik.
+
+**Price category**
+```kotlin
+val categories = listOf("Full price", "Senior", "Public servant")
+var selectedCategory by remember { mutableStateOf("Full price") }
+Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = "Price category",
+ fontSize = 16.sp
+)
+Column(
+ modifier = Modifier.padding(top = 16.dp)
+) {
+ categories.forEach { category ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = (category == selectedCategory),
+ onClick = { selectedCategory = category },
+ role = Role.RadioButton
+ )
+ .padding(vertical = 4.dp)
+ ) {
+ RadioButton(
+ selected = (category == selectedCategory),
+ onClick = { selectedCategory = category }
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(category)
+ }
+ }
+}
```
-!!!note ""
- Az itt használt `tools` névtérrel megadott `text` attribútum hatása csak az előnézetben fog megjelenni, az alkalmazásban ezt majd a Kotlin kódból állítjuk be, az előző képernyőn megnyomott gomb függvényében.
+Az árkategória résznek szintén adunk egy fejlécet a `Text` *Composable* segítségével, majd ezen belül elhelyezünk egy radio gomb szekciót, ami 3 kategóriából áll.
-Az első beállítás ezen a képernyőn a bérlet érvényességének időtartama lesz.
-Ezt az érvényesség első és utolsó napjának megadásával tesszük, amelyhez a `DatePicker` osztályt használjuk fel. Ez alapértelmezetten egy teljes havi naptár nézetet jelenít meg, azonban a `calendarViewShown="false"` és a `datePickerMode="spinner"` beállításokkal egy kompaktabb, "pörgethető" választót kapunk.
+**Price**
-```xml
-
+```kotlin
+Text(
+ fontSize = 24.sp,
+ text = "Price: 42000",
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = 16.dp),
+)
+```
-
+Az ár rész jelenleg csak statikus árat ír ki, ezt az iMSc feladat során lehet változtatni.
-
+**Buy button**
-
+```kotlin
+Button(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ onClick = {
+ onSuccess("${startDate};$endDate;${"$selectedCategory $transportType"}")
+ }) {
+ Text("Buy")
+}
```
-Ezeknek a `DatePicker`-eknek is adtunk ID-kat, hiszen később szükségünk lesz a Kotlin kódunkban a rajtuk beállított értékekre.
+A gombnak szintén átadunk egy onClick event eseményt, mégpedig a lambda paramétert amit paraméterként kaptunk. Ennek a módosítása is az iMSc feladat során történhet meg.
-Még egy beállítás van hátra, az árkategória kiválasztása - nyugdíjasoknak és közalkalmazottaknak különböző kedvezményeket adunk a jegyek árából.
-Mivel ezek közül az opciók közül egyszerre csak egynek akarjuk megengedni a kiválasztását, ezért `RadioButton`-öket fogunk használni, amelyeket Androidon egy `RadioGroup`-pal kell összefognunk, hogy jelezzük, melyikek tartoznak össze.
-```xml
-
+!!!warning "Értelmezés"
+ Az alábbi kódban nagyon sok formázás van, így jelentősen megnehezítheti az értelmezését, ezt a laborvezető segítségével nézzük át, és értelmezzük.
-
+Ezután bővítsük ki a `NavGrap`-unkat a következő szerint, majd beszéjük át a laborvezetővel a kód működését.
-
+```kotlin
+@Composable
+fun NavGraph(
+ navController: NavHostController = rememberNavController(),
+) {
+
+ NavHost(
+ navController = navController,
+ startDestination = "login"
+ ) {
+ composable("login") {
+ LoginScreen(
+ onSuccess = {
+ navController.navigate("list")
+ }
+ )
+ }
+ composable("list") {
+ ListScreen(
+ onTransportClick = {
+ navController.navigate("details/$it")
+ }
+ )
+ }
+ composable(
+ route = "details/{type}",
+ arguments = listOf(navArgument("type") { type = NavType.StringType })
+ ) { backStackEntry ->
+ DetailsScreen(transportType = backStackEntry.arguments?.getString("type") ?: "",
+ onSuccess = {
+ /*TODO*/
+ }
+ )
+ }
+ }
+}
+```
-
-
+!!!example "BEADANDÓ (1 pont)"
+ Készíts egy **képernyőképet**, amelyen látszik a **részletes nézet** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**! A képet a megoldásban a repository-ba f4.png néven töltsd föl!
-
-```
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
-!!!warning "FONTOS"
- Fontos, hogy adjunk ID-t a teljes csoportnak, és a benne lévő minden opciónak is, mivel később ezek alapján tudjuk majd megnézni, hogy melyik van kiválasztva.
-Végül az oldal alján kiírjuk a kiválasztott bérlet árát, illetve ide kerül a megvásárláshoz használható gomb is. Az árnak egyelőre csak egy fix értéket írunk ki.
-```xml
-
+## A bérlet (1 pont)
-
-```
+Az alkalmazás utolsó képernyője már kifejezetten egyszerű lesz, ez maga a bérletet fogja reprezentálni. Itt a bérlet típusát és érvényességi idejét fogjuk megjeleníteni, illetve egy QR kódot, amivel ellenőrizni lehet a bérletet.
-Ne felejtsük el, a stringeket itt is kiszervezni!
+
+
+
-Meg kell oldanunk még azt, hogy az előző képernyőn tett választás eredménye elérhető legyen a `DetailsActivity`-ben. Ezt úgy tehetjük meg, hogy az Activity indításához használt `Intent`-be teszünk egy azonosítót, amiből kiderül, hogy melyik típust választotta a felhasználó.
-Ehhez a `DetailsActivity`-ben vegyünk fel egy konstanst, ami ennek a paraméternek a kulcsaként fog szolgálni:
+Hozzuk létre a szükséges *Kotlin Filet* szintén a `screen` packageba, `PassScreen` néven, majd írjuk bele az alábbiakat.
```kotlin
-class DetailsActivity : AppCompatActivity() {
- companion object {
- const val KEY_TRANSPORT_TYPE = "KEY_TRANSPORT_TYPE"
+@Composable
+fun PassScreen(
+ passDetails: String
+) {
+
+ val parts = passDetails.split(";")
+
+ val startDate = parts[0]
+ val endDate = parts[1]
+ val category = parts[2]
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top
+ ) {
+ Text(
+ text = "$category Pass",
+ fontSize = 24.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+ Text(
+ text = "$startDate - $endDate",
+ fontSize = 16.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ }
+ Image(
+ painter = painterResource(
+ id = R.drawable.qrcode
+ ),
+ contentDescription = "Ticket",
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.Center),
+ contentScale = ContentScale.FillWidth
+ )
}
- ...
+}
+
+@Composable
+@Preview
+fun PreviewPassScreen() {
+ PassScreen(passDetails = "2024. 09. 01.;2024. 12. 08.;Senior Train")
}
```
-Ezután menjünk a `ListActivity` kódjához, és vegyünk fel konstansokat a különböző támogatott járműveknek:
+Mivel a `TicketScreen`-nek szüksége van a jegy típúsára, valamint az érvényességi idejére, ezt egy paraméterként kapja meg, majd ezt egy függvényen belül feldolgozzuk, és az alábbiak szerint használjuk fel.
+
+- `yyyy. mm. dd.;yyyy. mm. dd.;category` a felépítése a kapott Stringnek
+- Ezt feldaraboljuk a `;` mentén, majd a dátumot string interpoláció segítségével átadjuk a `Text` Composable értékének, a price-t pedig egy másik `Text` Composable-nak
+
+!!!info ""
+ Látható, hogy a Java-val ellentétben a Kotlin támogatja a [string interpolációt](https://kotlinlang.org/docs/reference/basic-types.html#string-templates).
+
+Végül itt is kössük be a `NavGraph`-ba az új képernyőnket az előzőhöz hasonlóan, valamint adjuk meg a lambda eseményt az előző composable-nek:
+
```kotlin
-class ListActivity : AppCompatActivity() {
- companion object {
- const val TYPE_BIKE = 1
- const val TYPE_BUS = 2
- const val TYPE_TRAIN = 3
+@Composable
+fun NavGraph(
+ navController: NavHostController = rememberNavController(),
+){
+
+ NavHost(
+ navController = navController,
+ startDestination = "login"
+ ){
+ ...
+ composable(
+ route = "details/{type}",
+ arguments = listOf(navArgument("type") { type = NavType.StringType })
+ ) { backStackEntry ->
+ DetailsScreen(transportType = backStackEntry.arguments?.getString("type") ?: "",
+ onSuccess = {
+ navController.navigate("pass/$it")
+ }
+ )
+ }
+ composable(
+ route = "pass/{passDetails}",
+ arguments = listOf(navArgument("passDetails") { type = NavType.StringType })
+ ) { backStackEntry ->
+ PassScreen(passDetails = backStackEntry.arguments?.getString("passDetails") ?: "")
+ }
}
- ...
}
```
-!!!info "static"
- A Kotlin egy nagy eltérése az eddig ismert, megszokott OOP nyelvektől, hogy nincs benne `static` kulcsszó, és így nincsenek statikus változók vagy függvények sem. Ehelyett minden osztályhoz lehet definiálni egy [`companion object`-et](https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects), ami egy olyan singleton-t definiál, ami az osztály összes példányán keresztül elérhető. Röviden, minden `companion object`-en belül definiált konstans, változó, függvény úgy viselkedik, mintha statikus lenne.
-Most már létrehozhatjuk a gombok listener-jeit, amelyek elindítják a `DetailsActivity`-t, extrának beletéve a kiválasztott típust. Az első gomb listenerjének beállítását így tehetjük meg a `ViewBinding`beállítása után:
+!!!example "BEADANDÓ (1 pont)"
+ Készíts egy **képernyőképet**, amelyen látszik a **bérlet képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl.
-```kotlin
-private lateinit var binding: ActivityListBinding
+ A képernyőkép szükséges feltétele a pontszám megszerzésének.
-...
+## Önálló feladat - Hajó bérlet (1 pont)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
+Vállalatunk terjeszkedésével elindult a hajójáratokat ajánló szolgáltatásunk is. Adjuk hozzá ezt az új bérlet típust az alkalmazásunkhoz!
- binding = ActivityListBinding.inflate(layoutInflater)
- setContentView(binding.root)
+???success "Segítség"
+ A szükséges változtatások nagy része a `ListScreen`-ben lesz. Az eddigi 3 lehetőség mellé fel kell venni egy új `Box`-ot, és az előzőekhez hasonlóan át kell alakítani az új opciót.
- binding.btnBike.setOnClickListener {
- val intent = Intent(this, DetailsActivity::class.java)
- intent.putExtra(DetailsActivity.KEY_TRANSPORT_TYPE, TYPE_BIKE)
- startActivity(intent)
- }
-}
-```
+!!!example "BEADANDÓ (1 pont)"
+ Készíts **két képernyőképet**, amelyen látszik a **jármű választó képernyő** illetve a **hajó bérlet képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), és az **ezekhez tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**! A képeket a megoldásban a repository-ba f6.png és f7.png néven töltsd föl!
-A másik két gomb listener-je ugyanerre a mintára működik, csupán az átadott típus konstanst kell megváltoztatni bennük. Hozzuk létre ezeket is! (Ezt a viselkedést érdemes lehet később kiszervezni egy külön osztályba, ami implementálja az `OnClickListener` interfészt, de ezt most nem tesszük meg.)
+ A képernyőképek szükséges feltételei a pontszám megszerzésének!
-Még hátra van az, hogy a `DetailsActivity`-ben kiolvassuk ezt az átadott paramétert, és megjelenítsük a felhasználónak. Ezt az `onCreate` függvényében tehetjük meg, az Activity indításához használt `Intent` elkérésével (`intent` property), majd az előbbi kulcs használatával:
-```kotlin
-val transportType = this.intent.getIntExtra(KEY_TRANSPORT_TYPE, -1)
-```
+## Extra feladatok
-Ezt az átadott számot még le kell képeznünk egy stringre, ehhez vegyünk fel egy egyszerű segédfüggvényt:
+!!!warning "Ismertető"
+ Ezek a feladatok nem szükségesek a labor maximális pontszámának megszerzéséhez, csupán csak ismertető jelleggel vannak a labor anyagában azok számára akik jobban beleásnák magukat a témába.
-```kotlin
-private fun getTypeString(transportType: Int): String {
- return when (transportType) {
- ListActivity.TYPE_BUS -> "Bus pass"
- ListActivity.TYPE_TRAIN -> "Train pass"
- ListActivity.TYPE_BIKE -> "Bike pass"
- else -> "Unknown pass type"
- }
-}
+
+### Extra feladat - SplashScreen animáció
+
+A SplashScreen API-nak köszönhetően, már láttuk, hogy könnyedén létre tudunk hozni egy kezdő képernyőt amit az alkalmazás megnyitása után közvetlen látunk. Ezen az a megjelenő Icont könnyen tudjuk animálni is, ehhez mindössze pár `.xml` fájlt kell létrehozunk az Android Studio segítségével, amellyekben megvalósítjuk ezeket a műveleteket.
+
+Szükségünk van a következőkre:
+
+* Logo - Ezt fogjuk megjeleníteni a kezdőképernyőn. (Ezt már korábban létrehoztuk, csak módosítani fogjuk)
+* Animator - Ebben fogjuk leírni az animációt amit szeretnénk használni az adott Logo-n.
+* Animated Vector Drawable - Ennek a segítségével lesz összekötve az Animator, és a Logo.
+* Themes - Ezt is csak módosítani fogjuk
+* Animation - Ebben meg tudunk adni Interpolációkat még az animációk mellé
+
+**Logo módosítása**
+
+Módosítsuk a már meglévő Logo-t az alábbiak szerint. (`ic_transport_foreground.xml`)
+
+```xml
+
+
+
+
+
```
-!!!info "when"
- Egy másik nagy eltérése a Kotlin-nak a megszokott nyelvektől, hogy nincs benne `switch`. Helyette a Kotlin egy [`when`](https://kotlinlang.org/docs/reference/control-flow.html#when-expression) nevű szerkezetet használ, ami egyrészről egy kifejezés (látható, hogy az értéke vissza van adva), másrészről pedig sokkal sokoldalúbb feltételeket kínál, mint a hagyományos *case*.
+A már meglévő path-et belecsomagoltuk egy group tag-be, amire azért van szükség, hogy tudjuk animálni az icont. Ennek a groupnak adunk egy nevet, amit az animálásnál fogunk felhasználni, hogy melyik csoportot szeretnénk, illetve beállítjuk a pivotX, és pivotY pontokat. Ezt jelen esetben középre tesszük, ugyanis a Logo-t középről szeretnénk animálni.
-Végül pedig az `onCreate` függvénybe visszatérve meg kell keresnünk a megfelelő `TextView`-t, és beállítani a szövegének a függvény által visszaadott értéket (készítsük el a `ViewBindingot` is):
+**Animator létrehozása**
-```kotlin
-binding.tvTicketType.text = getTypeString(transportType)
+Ahhoz hogy a Logo-t animálni tudjuk, létre kell hozunk egy Animator típusú fájlt. Kattintsunk a `res` mappára jobb klikkel, majd *New->Android Resource file*, névnek adjuk meg a `logo_animator`-t, type-nak az `Animator` típust, és Root elementnek pedig `objectAnimator`-t, majd kattintsunk az OK gombra. Ez létrehozta a szükséges fájlt, már csak meg kell írni az animációkat. Első sorban állítsuk be az animáció időtartamát, ezt az `android:duration` segítségével tehetjük meg az `objectAnimator` tagen belül.
+
+* Kezdetben állítsuk egy másodpercre (1000).
+* Ezután adjunk a Logo-nak egy Scale animációt, ennek a segítségével el tudjuk érni azt, hogy a semmiből megjelenjen, és az animáció időtartamával lineárisan megnövekedjen. Ehhez szükségünk van egy `propertyValuesHolder` tag-re az `objectAnimator`-on belül.
+
+```xml
+
+
+
+
+
+
+
```
-Próbáljuk ki az alkalmazást! A `DetailsActivity`-ben meg kell jelennie a hozzáadott beállításoknak, illetve a tetején a megfelelő jegy típusnak.
+Ebben a rövid animációs kódban csak megnöveljük a méretét a Logo-nak 0-ról 0.5-re. A properyName-n belül tudjuk megadni az animációt, ez lehet scaleX, scaleY, roation, stb... valamint a valuesFrom/To-ban tudjuk megadni a kezdő-cél méretet.
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **részletes nézet** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f4.png néven töltsd föl.
+Ahhoz, hogy ezt az animációt összekössük a Logo-val, létre kell hoznunk egy Animated Vector Drawable-t.
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+**Animated Vector Drawable**
+Hozzuk létre az Animated Vector Drawable file-t az Android Studio segítségével. Kattintsunk jobb klikkel a drawable mappánkra, majd *New->Drawable Resource File*. Itt névnek adjuk meg a `animated_logo`-t, valamint root element-nek `animated-vector`-t, majd kattintsunk az OK gombra. Ez létrehozta a szükséges file-t. Egészítsük ki az alábbiak szerint:
-## A bérlet (1 pont)
+```xml
+
-Az alkalmazás utolsó képernyője már kifejezetten egyszerű lesz, ez magát a bérletet fogja reprezentálni. Itt a bérlet típusát és érvényességi idejét fogjuk megjeleníteni, illetve egy QR kódot, amivel ellenőrizni lehet a bérletet.
+
-
-
-
+
+```
-Hozzuk létre a szükséges Activity-t, `PassActivity` néven. Ennek az Activity-nek szüksége lesz a jegy típusára és a kiválasztott dátumokra - a QR kód az egyszerűség kedvéért egy fix kép lesz.
+* Az `android:drawable` segítségével megadjuk azt a fájlt amit szeretnénk animálni.
+* Az `android:animation` segítségével pedig, hogy melyik animációt szeretnénk használni.
+* Valamint az `android:name` segítségével azt a csoportot amelyiket szeretnénk animálni az adott Logo-n belül.
-Az adatok átadásához először vegyünk fel két kulcsot a `PassActivity`-ben:
+A korábbiakban már elkészítettük a szükséges témát a splashscreenhez, viszont az még csak a sima Logo-ra történt meg. Ahhoz hogy az aninált Logo legyen használva módosítsuk az alábbiak szerint.
-```kotlin
-class PassActivity : AppCompatActivity() {
- companion object {
- const val KEY_DATE_STRING = "KEY_DATE_STRING"
- const val KEY_TYPE_STRING = "KEY_TYPE_STRING"
- }
- ...
-}
+**Themes módosítása**
+
+```xml
+
```
+Itt csak az AnimatedIcon-t lecseréltük az `animated_logo`-ra, a sima helyett.
+
+**Animation - Interpolációk**
-Ezeket az adatokat a `DetailsActivity`-ben kell összekészítenünk és beleraknunk az `Intent`-be. Ehhez adjunk hozzá a vásárlás gombhoz egy listener-t az `onCreate`-ben:
+Az instalSplashScreen-nek van egy lambda paramétere: `apply{}`. Ezen belül meg tudunk adni különböző működéseket is. Például `setKeepOnScreenCondition` ennek a segítségével a SplashScreent addig tudjuk a képernyőn tartani amíg nem teljesül valamilyen feltétel. Általánan ezen a blokkon belül érdemes végezni az adatbázis kiolvasásokat, vagy olyan dolgokat amik időigényesek és csak az alkalmazás indítása során egyszer kell végrehajtani. Hogy ha ezek végrehajtódtak teljesül egy feltétel, és eltűnik a SplashScreen. `setOnExitAnimationListener` - Ezen belül meg tudunk adni olyan animációt ami akkor hajtódik végre, hogy ha a `setKeepOnScreenCondition` nem tartja előtérben a SplashScreen-t, és éppen váltana képernyőt az alkalmazás. Ilyenkor végrehajthatunk egy kilépő animációt is. Például az alábbit:
```kotlin
-binding.btnPurchase.setOnClickListener {
- val typeString = getTypeString(transportType)
- val dateString = "${getDateFrom(binding.dpStartDate)} - ${getDateFrom(binding.dpEndDate)}"
-
- val intent = Intent(this, PassActivity::class.java)
- intent.putExtra(PassActivity.KEY_TYPE_STRING, typeString)
- intent.putExtra(PassActivity.KEY_DATE_STRING, dateString)
- startActivity(intent)
+installSplashScreen().apply {
+ setOnExitAnimationListener{ splashScreenView ->
+ val zoomX = ObjectAnimator.ofFloat(
+ splashScreenView.iconView,
+ "scaleX",
+ 0.5f,
+ 0f
+ )
+ zoomX.interpolator = OvershootInterpolator()
+ zoomX.duration = 500
+ zoomX.doOnEnd { splashScreenView.remove() }
+ val zoomY = ObjectAnimator.ofFloat(
+ splashScreenView.iconView,
+ "scaleY",
+ 0.5f,
+ 0f
+ )
+ zoomY.interpolator = OvershootInterpolator()
+ zoomY.duration = 500
+ zoomY.doOnEnd { splashScreenView.remove()}
+ zoomX.start()
+ zoomY.start()
+ }
}
```
-!!!info ""
- Látható, hogy a Java-val ellentétben a Kotlin támogatja a [string interpolációt](https://kotlinlang.org/docs/reference/basic-types.html#string-templates).
+Illesszük ezt be a `MainActivity` `onCreate()` függvényébe a megfelelő helyre, majd próbáljuk ki az alkalmazást!
+
+### Extra feladat - NavGrap-Splash
-Ebben összegyűjtjük a szükséges adatokat, és a megfelelő kulcsokkal elhelyezzük őket a `PassActivity` indításához használt `Intent`-ben.
+Korábban ezt a képernyőt a [Splash Screen API](https://developer.android.com/develop/ui/views/launch/splash-screen) segítségével oldottuk meg, azonban többfajta lehetőség is van, ezek közül most a NavGrap segítségével fogunk egyet megnézni.
-A `getDateFrom` egy segédfüggvény lesz, ami egy `DatePicker`-t kap paraméterként, és formázott stringként visszaadja az éppen kiválasztott dátumot, ennek implementációja a következő:
+Ez a képernyő lényegében egy ugyanolyan képernyő mint a többi. Itt első sorban hozzunk létre egy új *Kotlin Filet* a `screen` packagen belül, majd nevezzük el `SplashScreen`-nek, és írjuk bele az alábbi kódot:
```kotlin
-private fun getDateFrom(picker: DatePicker): String {
- return String.format(
- Locale.getDefault(), "%04d.%02d.%02d.",
- picker.year, picker.month + 1, picker.dayOfMonth
- )
+@Composable
+fun SplashScreen(
+ onSuccess: () -> Unit
+) {
+ LaunchedEffect(key1 = true) {
+ delay(1000)
+ onSuccess()
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Blue),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ modifier = Modifier
+ .size(128.dp),
+ painter = painterResource(id = R.drawable.ic_transport_foreground),
+ contentDescription = "Public Transport",
+ )
+ }
}
```
-!!!note "Megjegyzés"
- Itt a hónaphoz azért adtunk hozzá egyet, mert akárcsak a [`Calendar`](https://developer.android.com/reference/java/util/Calendar.html#MONTH) osztály esetében, a `DatePicker` osztálynál is 0 indexelésűek a hónapok.)
+A LaunchedEffect-ről bővebben előadáson lesz szó. Itt szükség volt rá, ugyanis a benne lévő delay függvényt nem lehet csak önmagában meghívni: egy *suspend* függvényen vagy egy *coroutinon* belül lehet használni. A delay függvény felel azért, hogy mennyi ideig legyen a képernyőn a SplashScreen. Jelen esetben ez 1 másodperc (1000 milisec), majd ezután meghívódik az onSucces lambda, ami átnavigál minket a LoginScreen-re.
+Módosítsuk a `NavGraph`-unkat a következő szerint:
-Most már elkészíthetjük a `PassActivity`-t. Kezdjük a layout-jával (`activity_pass.xml`), aminek már majdnem minden elemét használtuk, az egyetlen újdonság itt az `ImageView` használata.
+```kotlin
+NavHost(
+ navController = navController,
+ startDestination = "splash"
+){
+ composable("splash"){
+ SplashScreen(
+ onSuccess = {
+ navController.navigate("login"){
+ popUpTo("splash"){ inclusive = true }
+ launchSingleTop = true
+ }
+ }
+ )
+ }
+ ...
+}
+```
-```xml
-
-
-
-
-
-
-
-
-
-
-```
-
-Az Activity Kotlin kódjában pedig csak a két `TextView` szövegét kell az `Intent`-ben megkapott értékekre állítanunk az `onCreate` függvényben(illetve beállítani a `ViewBindingot`):
+A `SplashScreen` képernyő testreszabásával a labor keretein belül nem fogunk foglalkozni, ez teljesen egyénre szabható.
-```kotlin
-binding.tvTicketType.text = intent.getStringExtra(KEY_TYPE_STRING)
-binding.tvDates.text = intent.getStringExtra(KEY_DATE_STRING)
+Az újonnan hozzáadott `composable` elem a `NavGraph`-ban a következő képpen épül fel:
-```
+- Szintén kapott egy elérési *routet*
+- Valamint megkapta a kívánt képernyőt a függvény törzsében
+- Ennek van egy *onSuccess* lambda paramétere, amibe beletesszük a következő képernyőre való navigálást
+- Ezen belül a `popUpTo` segítségével kiszedjük a SplashScreen-t, hogy visszanavigálás esetén, ne dobja be ezt
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **bérlet képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl.
+Majd ezután a `Manifest` fájl személyre szabható, hogy milyen témát jelenítsen meg.
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+### Extra feladat - különálló Screen File
-## Önálló feladat - Hajó bérlet (1 pont)
+Nagy projektekben, ahol több képernyő található, egy idő után kényelmetlen megoldás lehet a *screenek* közötti *stringekkel* történő navigáció. Ezért általános megoldás, hogy a képernyőket, és a hozzájuk kapcsolódó navigációs utakat egy különálló `Screen` osztályba gyűjtjük, majd a navigációs gráfban csak a belőlük képzett objektumokat használjuk. A korábban létrehozott `Screen` fájl az alábbi kódot fogja tartalmazni:
-Vállalatunk terjeszkedésével elindult a hajójáratokat ajánló szolgáltatásunk is. Adjuk hozzá ezt az új bérlet típust az alkalmazásunkhoz!
+```kotlin
+sealed class Screen(val route: String){
+ object Login: Screen("login")
+ object List: Screen("list")
+ object Details: Screen("details/{type}"){
+ fun passType(type: String) = "details/$type"
+ }
+ object Pass: Screen("pass/{passDetails}"){
+ fun passPassDetails(passDetails: String) = "pass/$passDetails"
+ }
+}
+```
-???success "Megoldás"
- A szükséges változtatások nagy része a `ListActivity`-ben lesz. Először frissítsük az Activity layout-ját: itt egy új `FrameLayout`-ot kell hozzáadnunk, amiben a gomb ID-ja legyen `@+id/btnBoat`. A szükséges képet már tartalmazza a projekt, ezt `@drawable/boat` néven találjuk meg.
+!!!info "sealed class"
+ A Kotlin sealed class-ai olyan osztályok, amelyekből korlátozott az öröklés, és fordítási időben minden leszármazott osztálya ismert. Ezeket az osztályokat az enumokhoz hasonló módon tudjuk alkalmazni. Jelen esetben a `Details` valójában nem a `Screen` közvetlen leszármazottja, hanem anonim leszármazott osztálya, mivel a felhasználónév paraméterként történő kezelését is tartalmazza.
- Ne felejtsük el a gyökérelemünkként szolgáló `LinearLayout`-ban átállítani a `weightSum` attribútumot `3`-ról `4`-re, hiszen most már ennyi a benne található View-k súlyainak összege. (Kipróbálhatjuk, hogy mi történik, ha például `1`-re, vagy `2.5`-re állítjuk ezt a számot, a hatásának már az előnézetben is látszania kell.)
-
- Menjünk az Activity Kotlin fájljába, és következő lépésként vegyünk fel egy új konstanst a hajó típus jelölésére.
-
- ```kotlin
- const val TYPE_BOAT = 4
- ```
-
- Az előző három típussal azonos módon keressük a hajót kiválasztó gombra (`btnBoat`) is állítsunk be rá egy listener-t, amely elindítja a `DetailsActivity`-t, a `TYPE_BOAT` konstanst átadva az `Intent`-ben paraméterként.
-
- Még egy dolgunk maradt, a `DetailsActivity` kódjában értelmeznünk kell ezt a paramétert. Ehhez a `getTypeString` függvényen belül vegyünk fel egy új ágat a `when`-ben:
-
- ```kotlin
- ListActivity.TYPE_BOAT -> "Boat pass"
- ```
+Ez után tehát a `NavGraph` `route` paraméterének nem egy nyers Stringet adunk át, hanem az imént létrehozott *objecteket* a következő képpen:
-!!!example "BEADANDÓ (1 pont)"
- Készíts **két képernyőképet**, amelyen látszik a **jármű választó képernyő** illetve a **hajó bérlet képernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), és az **ezekhez tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képeket a megoldásban a repository-ba f6.png és f7.png néven töltsd föl.
+```kotlin
+@Composable
+fun NavGraph(
+ navController: NavHostController = rememberNavController(),
+) {
+
+ NavHost(
+ navController = navController,
+ startDestination = Screen.Login.route
+ ) {
+ composable(Screen.Login.route) {
+ LoginScreen(
+ onSuccess = {
+ navController.navigate(Screen.List.route)
+ }
+ )
+ }
+ composable(Screen.List.route) {
+ ListScreen(
+ onTransportClick = {
+ navController.navigate(Screen.Details.passType(it))
+ }
+ )
+ }
+ composable(
+ route = Screen.Details.route,
+ arguments = listOf(navArgument("type") { type = NavType.StringType })
+ ) { backStackEntry ->
+ DetailsScreen(transportType = backStackEntry.arguments?.getString("type") ?: "",
+ onSuccess = {
+ navController.navigate(Screen.Pass.passPassDetails(it))
+ }
+ )
+ }
+ composable(
+ route = Screen.Pass.route,
+ arguments = listOf(navArgument("passDetails") { type = NavType.StringType })
+ ) { backStackEntry ->
+ PassScreen(passDetails = backStackEntry.arguments?.getString("passDetails") ?: "")
+ }
+ }
+}
+```
+
+Jól látható, hogy a *Sealed Class* segítségével könnyebben módosítható az egyes elérési utak címe. Mind a két megoldás működőképes, viszont ez utóbbi kicsit elegánsabb nagyobb projekteknél.
- A képernyőképek szükséges feltételei a pontszám megszerzésének.
## iMSc feladat
@@ -917,25 +1455,24 @@ Ebből még az alábbi kedvezményeket adjuk:
| Nyugdíjas | 90% |
| Közalkalmazott | 50% |
-!!!tip "Tipp"
- A számolásokhoz és az eseménykezeléshez a [`Calendar`][calendar] osztályt, a `DatePicker` osztály [`init`][picker-init-link] függvényét, illetve a `RadioGroup` osztály [`setOnCheckedChangeListener`][radio-checked-changed] osztályát érdemes használni.
+???tip "Tipp"
+ A számolásokhoz és az eseménykezeléshez a [`Calendar`](https://developer.android.com/reference/java/util/Calendar.html) osztályt, a valamint a *Calendar.set* függvényt érdemes használni.
- [calendar]: https://developer.android.com/reference/java/util/Calendar.html
-
- [picker-init-link]: https://developer.android.com/reference/android/widget/DatePicker.html#init(int%2C%20int%2C%20int%2C%20android.widget.DatePicker.OnDateChangedListener)
-
- [radio-checked-changed]: https://developer.android.com/reference/android/widget/RadioGroup.html#setOnCheckedChangeListener(android.widget.RadioGroup.OnCheckedChangeListener)
+ Érdemes két függvényt írni, a számoláshoz:
+
+ - Az egyik függvény egy különbség számító, ami két dátum között eltelt napokat számol
+ - A másik függvény pedig ami a napok, és a kategória alapján kiszámolja az árat
### Különböző bérlet napi árak (1 IMSc pont)
!!!example "BEADANDÓ (1 IMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik egy **több napos bérlet részletes nézete az árral** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a bérletárakkal kapcsolatos kóddal**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f8.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik egy **több napos bérlet részletes nézete az árral** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a bérletárakkal kapcsolatos kóddal**, valamint a **neptun kódod a kódban valahol kommentként**! A képet a megoldásban a repository-ba f8.png néven töltsd föl!
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
### Százalékos kedvezmények ( 1 IMSc pont)
!!!example "BEADANDÓ (1 IMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik egy **több napos kedvezményes bérlet részletes nézete az árral** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a bérletkedvezményekkel kapcsolatos kóddal**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f9.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik egy **több napos kedvezményes bérlet részletes nézete az árral** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a bérletkedvezményekkel kapcsolatos kóddal**, valamint a **neptun kódod a kódban valahol kommentként**! A képet a megoldásban a repository-ba f9.png néven töltsd föl!
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
\ No newline at end of file
+ A képernyőkép szükséges feltétele a pontszám megszerzésének!
\ No newline at end of file
diff --git a/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png b/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png
deleted file mode 100644
index 799d602..0000000
Binary files a/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen.png b/docs/laborok/03-android-ui-adv/assets/MainScreen.png
new file mode 100644
index 0000000..46e187d
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen.png differ
diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png
new file mode 100644
index 0000000..4c93697
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png differ
diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png
new file mode 100644
index 0000000..0baca21
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png differ
diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png
new file mode 100644
index 0000000..772e642
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png differ
diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png
new file mode 100644
index 0000000..cebddf0
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png differ
diff --git a/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png b/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png
deleted file mode 100644
index d55e049..0000000
Binary files a/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/assets/menu.png b/docs/laborok/03-android-ui-adv/assets/menu.png
deleted file mode 100644
index ce47fc2..0000000
Binary files a/docs/laborok/03-android-ui-adv/assets/menu.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/assets/sample_screen.png b/docs/laborok/03-android-ui-adv/assets/sample_screen.png
deleted file mode 100644
index ad8f123..0000000
Binary files a/docs/laborok/03-android-ui-adv/assets/sample_screen.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/downloads/expense.png b/docs/laborok/03-android-ui-adv/downloads/expense.png
deleted file mode 100644
index 42e7d6a..0000000
Binary files a/docs/laborok/03-android-ui-adv/downloads/expense.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/downloads/income.png b/docs/laborok/03-android-ui-adv/downloads/income.png
deleted file mode 100644
index f95301d..0000000
Binary files a/docs/laborok/03-android-ui-adv/downloads/income.png and /dev/null differ
diff --git a/docs/laborok/03-android-ui-adv/downloads/res.zip b/docs/laborok/03-android-ui-adv/downloads/res.zip
new file mode 100644
index 0000000..34490e2
Binary files /dev/null and b/docs/laborok/03-android-ui-adv/downloads/res.zip differ
diff --git a/docs/laborok/03-android-ui-adv/index.md b/docs/laborok/03-android-ui-adv/index.md
index 5eaa81e..b25af6b 100644
--- a/docs/laborok/03-android-ui-adv/index.md
+++ b/docs/laborok/03-android-ui-adv/index.md
@@ -2,29 +2,28 @@
## Bevezető
-A labor célja egy egyszerű felhasználói felület tervezése, kivitelezése.
+A labor célja egy egyszerű felhasználói felület tervezése és kivitelezése Android platformon, Jetpack Compose felhasználásával.
-A feladat egy kiadás / bevétel naplózására alkalmas alkalmazás elkészítése AndroidWallet néven. Az alkalmazás alap funkcionalitása, hogy a felhasználó fel tudja venni egy listába a kiadásait, bevételeit, illetve törölni tudja az egész lista tartalmát.
+A feladat egy kiadás és bevétel naplózására alkalmas alkalmazás elkészítése AndroidWallet néven. Az alkalmazás alap funkcionalitása, hogy a felhasználó fel tudja venni egy listába a kiadásait és a bevételeit, törölni tudja őket, illetve törölni tudja az egész lista tartalmát.
A kész alkalmazás mintaképe:
-
+
-
+
Az alkalmazás felépítése és működése a következő:
-- Kezdőképernyő a listával illetve egy beviteli résszel, amelyen a felhasználó beír egy megnevezést és egy összeget, megadja a pénzforgalom irányát, és el tudja menteni a listába a tranzakcióját. Amennyiben itt bármelyik mező üres, a mentést meg kell akadályoznunk.
+- Kezdőképernyő egy listával (LazyColumn) illetve egy beviteli résszel rendelkezik. Itt a felhasználó beír egy megnevezést és egy összeget, megadja a pénzforgalom irányát, majd ezután el tudja menteni a listába a tranzakcióját. Amennyiben itt bármelyik mező üres, a mentést meg kell akadályoznunk.
- Egy listaelem felépítése:
- Ikon a pénzforgalom irányától függően.
- A megadott megnevezés és alatta az összeg.
- - A Toolbaron egy menüpont a lista teljes törlésére.
- - A lista görgethető kell legyen
+ - Egy gomb a tétel törlésére.
### Felhasznált technológiák:
-- Activity
-- LinearLayout, TextView, ImageView, EditText, Button, ToggleButton
-- LayoutInflater
+- **Scaffold**, TopBar, BottomBar, FloatingActionButton, Column, Row, Image, Text, Spacer, OutlinedTextField, IconButton, IconToggleButton, **LazyColumn**
+- data class
+
## Előkészületek
@@ -47,7 +46,7 @@ A feladatok megoldása során ne felejtsd el követni a [feladat beadás folyama
Hozzunk létre egy AndroidWallet nevű projektet Android Studioban:
-1. Hozzunk létre egy új projektet, válasszuk az *Empty Views Activity* lehetőséget.
+1. Hozzunk létre egy új projektet, válasszuk az *Empty Activity* lehetőséget.
1. A projekt neve legyen `AndroidWallet`, a kezdő package `hu.bme.aut.android.androidwallet`, a mentési hely pedig a kicheckoutolt repository-n belül az AndroidWallet mappa.
1. Nyelvnek válasszuk a *Kotlin*-t.
1. A minimum API szint legyen API24: Android 7.0.
@@ -56,377 +55,562 @@ Hozzunk létre egy AndroidWallet nevű projektet Android Studioban:
!!!danger "FILE PATH"
A projekt a repository-ban lévő AndroidWallet könyvtárba kerüljön, és beadásnál legyen is felpusholva! A kód nélkül nem tudunk maximális pontot adni a laborra!
-## Menü elkészítése
+!!!danger "FILE PATH"
+ A repository elérési helye ne tartalmazzon ékezeteket, illetve speciális karaktereket, mert az AndroidStudio ezekre érzékeny, így nem fog a kód lefordulni. Érdemes a C:\\ meghajtó gyökerében dolgozni.
+
-Azt szeretnénk, ha az *ActionBaron* megjelenne egy menü, ahonnan a törlés opció érhető el. Azonban ha megfigyeljük, a legenerált témának nincs *ActionBarja.* Ahhoz, hogy ezt visszahozzuk, nyissuk meg a `res/values/themes.xml-t`, ahol az alkalmazásunk témája van definiálva. Itt láthatjuk, hogy a `Base.Theme.AndroidWallet` témánk a `Theme.Material3.DayNight.NoActionBar`-ból származik. Töröljük ki innen a `.NoActionBar`-t. Így tehát az új `themes.xml` kódja:
+## Főképernyő elkészítése
+
+Első lépésként, hogy ezzel a fejlesztés során a későbbiekben ne legyen gond, illesszük be a szöveges erőforrásainkat a `strings.xml` fájlba:
```xml
-
-
-
-
-
+
+ AndroidWallet
+ Item
+ Price
+ List is empty. Start adding salary.
```
-Az *ActionBar* felhelyezése után készítsük el a menüt. Bal oldalon a `res` könyvtáron nyomjunk jobb klikket és a menüből hozzunk létre egy új `Android Resource File` elemet. Itt a varázslóban mindent ki is tudunk választani:
-
-![](assets/menu.png)
+Második lépésként, hogy a felhasználói felületületét akadálytalanul el tudjuk készíteni, készítsük el az adat struktúrát, ami a tárolandó adatokat fogja tartalmazni. Szükégünk vagy egy listára, amely az adatokat tartalmazza. Ebben a listában az alábbi `SalaryData` *data class* objektumokat fogunk tárolni. Hozzunk létre egy `data` *package*-et a fő *package*-ünkön belül, majd abba tegyük bele a `SalaryData` osztályt:
-A `menu_main` tartalma legyen az alábbi:
+```kotlin
+package hu.bme.aut.android.androidwallet.data
-```xml
-
-
-
-
+data class SalaryData(
+ val isIncome: Boolean,
+ val item: String,
+ val price: String
+)
```
-Ne felejtsük el kiszervezni a string erőforrást! Ezt egyszerűen megtehetjük a *Delete All* szövegen állva az `Alt`+`Enter` konbinációt megnyomva, az `Extract string resource` opcióval. Adjuk meg a nevet: `delete_all`
+Az osztály három változója az alábbiakat reprezentálja:
+
+- `isIncome - Boolean változó amely a kiadás/bevétel állapotért felel.`
+- `item - kiadás/bevétel neve`
+- `price - kiadás/bevétel értéke`
-!!!note ""
- Láthatjuk, hogy Android platformon a menüt is egyszerű XML erőforrásból tudjuk felvenni. A fenti esetben egyetlen elemet tettünk a menübe, amelyet majd az `action_delete_all` id-val tudunk hivatkozni.
-Ahhoz, hogy az imént létrehozott menü felkerüljön a felületre a `MainActivity`-ből fel kell "fújjuk" azt, és le kell kezelnünk a kattintásokat. Ezt az `onCreateOptionsMenu` és az `onOptionsItemSelected` függvényekkel tudjuk megtenni:
+Miután megvagyunk a modell osztállyal, áttérhetünk a felhasználói felületre. Készítsük el a főképernyő vázát, amit majd a labor során feltöltünk tartalommal. Ehhez hozzunk létre egy `screen` *package*-et a `ui` *package*-en belül, majd ebbe egy `MainScreen` nevű új Kotlin classt. Írjuk meg a főképernyőnek a felépítését az alábbi kód alapján:
```kotlin
-override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_main, menu)
- return super.onCreateOptionsMenu(menu)
+package hu.bme.aut.android.androidwallet.ui.screen
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import hu.bme.aut.android.androidwallet.data.SalaryData
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainScreen() {
+ val context = LocalContext.current
+ val salaryItems = remember { mutableStateListOf() }
+
+ var isIncome by remember { mutableStateOf(false) }
+ var item by remember { mutableStateOf("") }
+ var price by remember { mutableStateOf("") }
+
+
+
+ Scaffold(
+ modifier = Modifier.safeDrawingPadding(),
+ topBar = {
+
+ ///TODO (topbar)
+ TopAppBar(title = { Text(text = "TopAppbar") })
+
+ },
+ floatingActionButton = {
+ ///TODO (floatingactionbutton)
+
+ },
+ content =
+ { innerPadding ->
+
+ ///TODO (lista)
+ Text(
+ text = "content",
+ modifier = Modifier.padding(innerPadding)
+ )
+
+
+ },
+ bottomBar = {
+ BottomAppBar {
+ ///TODO (TextFields)
+ Text(text = "BottomAppBar")
+
+ }
+
+ }
+ )
+}
+
+@Composable
+@Preview
+fun PreviewMainScreen() {
+ MainScreen()
}
+```
+
+A `MainScreen` tartalmaz egy [`Scaffold`](https://developer.android.com/develop/ui/compose/components/scaffold)-ot, ami segít minket a felület struktúrájának kialakítában. A `Scaffold` lényege, hogy megad egy vázat (állványt), aminek az előre definiált helyeire ([lyukaiba](https://developer.android.com/develop/ui/compose/layouts/basics#slot-based-layouts)) tudunk saját *Composable*-öket illeszteni.
-override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.action_delete_all -> {
- // TODO: itt fogjuk kezelni a kattintást
- true
+Ahhoz, hogy az alkalmazásunk máris futtatható legyen, és ki tudjuk próbálni minden lépés után, cseréljük le a `MainActivity` tartalmát:
+
+```kotlin
+package hu.bme.aut.android.androidwallet
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import hu.bme.aut.android.androidwallet.ui.screen.MainScreen
+import hu.bme.aut.android.androidwallet.ui.theme.AndroidWalletTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AndroidWalletTheme {
+ MainScreen()
+ }
}
- else -> super.onOptionsItemSelected(item)
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewMainActivity() {
+ AndroidWalletTheme {
+ MainScreen()
}
}
```
-## Beviteli rész megvalósítása (1 pont)
+Jelenleg így néz ki az alkalmazásunk:
-Az alkalmazás működéséhez szükség lesz két `EditText`-re, amelyekben a felhasználó a megnevezést és az összeget adhatja meg. Szükséges továbbá egy kapcsoló működésű gomb, például egy `ToggleButton`, amellyel a pénzforgalom iránya állítható, illetve kell egy mentés gomb, amelyet egy egyszerű `Button` fog megvalósítani.
+
+
+
-Egy XML fájlt megnyitva két lehetőségünk van: vagy a beépített grafikus szerkesztőn drag and drop módszerrel összerakjuk a felületet, vagy kézzel XML-ben írjuk meg a komponenseket és a tulajdonságaikat. Előbbi a tanulási fázisban nagyon hasznos, mert könnyen tudunk puskázni, viszont később sok fejfájást okozhat, ezért az XML leírás plusz előnézettel fogjuk megvalósítani a feladatot.
-Mivel a feladatunk lineárisan összerakható elemekből épül fel, ezért érdemes egy ilyen magvalósításban gondolkodnunk. Nyissuk meg a `res/layout/activity_main.xml` fájlt. (Akinek nem jelenik meg egyből a preview nézet, jobb oldalon találja a gombot.) Módosítsuk az előre legenerált `ConstraintLayoutot` `LinearLayoutra`, és adjuk hozzá az `android:orientation="vertical"` attribútumot.
+### A menüsáv elkészítése ( 1 pont)
-Szükségünk lesz másik három LinearLayout-ra:
+Azt szeretnénk, hogy a képernyő tetején legyen egy `ActionBar` az alkalmazás nevével és egy törlési opcióval, vagy akár egy legördülő menüvel. Mint feljebb láttuk, ehhez a megvalósításhoz, nagyon jól alkalmazható a `Scaffold` *Composable*, ugyanis ennek van egy *topBar* attribútuma, aminek könnyen adhatunk egy ilyen `ActionBar`-t.
-- A név és összeg mezőnek, horizontális elrendezéssel
-- A kiadás/bevétel kapcsolónak és mentés gombnak, szintén horizontális elrendezéssel és jobbra zárással
-- A tényleges listának, amelyet mivel a lista elemek vertikálisan követik egymást, vertikálisra állítunk.
-
-Így az `activity_main.xml`-ben a `LinearLayout`-ok elrendezése az alábbi lesz:
+Hozzunk létre egy új *package*-et a már meglévő `ui` csomagban `view` néven, majd ezen belül egy új *Kotlin* classt `TopBar` néven. Töltsük föl a fájlt az alábbi kóddal:
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-```
+```kotlin
+package hu.bme.aut.android.androidwallet.ui.view
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TopBar(title: String, icon: ImageVector, onIconClick: () -> Unit) {
+ TopAppBar(
+ title = { Text(text = title) },
+ actions = {
+ IconButton(onClick = onIconClick) {
+ Icon(imageVector = icon, contentDescription = "Delete", tint = Color.Red)
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.inversePrimary)
+ )
+}
-Az első (nem gyökér) `LinearLayout`-ba vegyük fel a két `EditText`-et, adjunk nekik *id*-t, hogy a Kotlin kódból is egyszerűen elérjük őket. A két `EditText` egymáshoz képesti elhelyezkedését súlyozással fogjuk beállítani. Mindkettő legyen `singleLine`, így nem fog szétcsúszni a UI, illetve érdemes a megnevezés `EditText`-nek egy `actionNext` `imeOptions`-t adni, így a felhasználó billentyűzete a következő `EditText`-re fog ugrani az Enter/Ok billentyűre:
+@Preview
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun PreviewTopBar() {
+ TopBar(title = "AndroidWallet", icon = Icons.Default.Clear) {
-```xml
-
-
-
-
-
-
+ }
+}
```
-A középső, gombokat tartalmazó `LinearLayout`-ban a gombokat jobbra szeretnénk igazítani, ezért a `LinearLayout`*gravity*-jét *end* értékre állítjuk. Így a két gombot az operációs rendszer szerint beállított szövegirányultság szerinti végére zárja a UI. A `LinearLayout`-ba felvesszük a `ToggleButton`-t, a sima `Button`-t és *id*-t adunk nekik. A mentés gombon állítsuk be a megjelenített feliratot, ez legyen "SAVE". Ne felejtsük el ezt is kiszervezni erőforrásba!
+A `TopAppBar`-nak a cím és a szín mellett megadtunk egy akciót is: egy `IconButton`-t amivel a lista törlését fogjuk majd megvalósítani.
-```xml
-
-
-
-
-
-
-```
-Mivel erre is van lehetőség bármi kódolás nélkül, érdemes már most beállítani a `ToggleButton` két állapotának feliratát a `textOn` illetve `textOff` attribútomokkal, amelyhez az "INCOME" illetve "EXPENSE" string erőforrásokat kell felvennünk.
+Miután elkészültünk a `TopBar`-unkkal, illesszük is ezt be a `MainScreen` beli `Scaffold` megfelelő helyére. Ezt a következő képpen tesszük meg: adunk neki egy tetszőleges *title*-t (általában az alkalmazás nevét), ez most *Android Wallet* lesz, majd egy icon-t. Használjuk az Android Studio beépített iconjait. Ezután meg kell adnunk egy Lambdát, aminek a segítségével leírjuk, hogy mi történjen, hogyha a felhasználó rákattint az iconra. Jelen esetben ki kell ürítenünk a listánkat. Mivel a listánk állapotként van tárolva `val salaryItems = remember { mutableStateListOf() }`, ezért ha változás történik, akkor az összes Composable újrafordul ami függ tőle:
-```xml
-
+```kotlin
+Scaffold(
+ modifier = Modifier.safeDrawingPadding(),
+ topBar = {
+ TopBar(
+ title = stringResource(id = R.string.app_name),
+ icon = Icons.Default.Clear,
+ onIconClick = {
+ salaryItems.clear()
+ }
+ )
+ },
+ floatingActionButton = {
+ ...
```
+Ha ezzel megvagyunk, akkor már a következőt kell látnunk az alkalmazásunkban:
+
+
+
+
+
+
+Ezzel a `TopBar` kész is. Folytassuk most a beviteli résszel, amit az oldal alján található `BottomAppBar`-ban fogunk megvalósítani.
+
!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **MainActivity felülete a beviteli mezőkkel és gombokkal** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a hozzá tartozó kóddal**, valamint a **neptun kódoddal a termék neveként**. A képet a megoldásban a repository-ba f1.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **TopBar** *Composable* Kotlin kódja, a **neptun kódod kommentként**, illetve a **futó alkalmazás** (emulátoron vagy a készüléket türközve)! A képet a megoldásban a repository-ba f1.png néven töltsd föl!
A képernyőkép szükséges feltétele a pontszám megszerzésének.
-## A listaelem XML-jének összeállítása (1 pont)
-Új elem felvételekor azt várjuk, hogy a *Save* gomb hatására az adott tételből egy új sor jelenjen meg a listában. Ezek a sorok komplexek és egymáshoz nagyon hasonlóak, így érdemes az elrendezésüket külön elkészíteni, és hozzáadáskor csak felhasználni ezt a megfelelő paraméterekkel.
+### Beviteli mezők megvalósítása (1 pont)
-Egy-egy ilyen elem felhasználásakor példányosítanunk kell a felületi elemet, amit a rendszer *inflater* szolgáltatásával tudunk megtenni. Az *inflate*-elés során az operációs rendszer egy olyan szolgáltatását kérjük el, amelyet egyéb elemeknél (pl. Toolbar menu) automatikusan elvégez. Mi ezzel most egy előre meghatározott komponenst, a listánk egy elemét szeretnénk "felfújni" a megfelelő pillanatban.
+A beviteli részt ezúttal nem egy külön *Composable*-ben, hanem a `MainScreen`-en belüli `Scaffold`-ban, helyben fogjuk megvalósítani.
-!!! danger "Figyelem"
- Fontos megjegyezni hogy a későbbiekben a profi lista kezeléshez majd a [`RecyclerView`](https://developer.android.com/develop/ui/views/layout/recyclerview?gclid=EAIaIQobChMIuPGHwNnu-QIVHY1oCR1V0gRbEAAYASAAEgJSTfD_BwE&gclsrc=aw.ds) komponenst fogjuk használni. `LineraLayout`-ot lista jellegű viselkedésre használni nem ajánlott, most csak az *inflate*-elés gyakorlásához használjuk.
+Ide kerül egy `IconButton`, ami a tranzakció irányát mutatja, majd két `OutlinedTextField` a névnek és az árnak.
-Rakjuk össze először a felületi erőforrást. A listaelemünk felépítése, az előzőekhez hasonlóan, kivitelezhető teljesen lineáris elrendezéssel, így ismét a `LinearLayout`-ot használjuk. Adjunk hozzá a projektünkhöz a `salary_row.xml`-t. (res/layout mappán jobb klikk, New -> Layout Resource File)
+Első lépésként töltsük le a tranzakció irányához használt [erőforrásokat](./downloads/res.zip)!
-Egy horizontális `LinearLayout`-tal kezdünk, mivel az *icon* és a feliratok egymás mellett helyezkednek el. Mivel ez csak egy listaelem lesz, ezért `wrap_content`-re kell vennünk a szélességét magasságát. Adjunk neki *id*-t is. Ebbe a `LinearLayout`-ba bal oldalra kerül az `ImageView`. A méretét állítsuk be 40x40-re, illetve adjunk neki *id*-t is. Az `ImageView` mellett egy függőleges `LinearLayout` következik, amiben egymás alatt helyezkedik el a tétel neve és összege. A `LinearLayout` szélessége legyen `match_parent`, magassága `wrap_content`, a `TextView`-knak pedig mindenképpen adjunk *id*-t.
+Letöltés után másoljuk be a mappa tartalmát a projektünk erőforrásait tartalmazó res mappába. (...\AndroidWallet\app\src\main\res)
-Mivel igényes kinézetet szeretnénk, a megfelelő *marginokat* illetve *paddingeket* adjuk hozzá a különböző elemeinkhez: a gyökérre 4dp *padding,* a beágyazottra `marginStart` attribútum *16dp* értékkel, illetve `layout_gravity` paramétert `center_vertical`-ra állítjuk, így biztosítva a gyerekelemek középre rendezését.
+Illesszük be az alábbi kódot a `Scaffold` megfelelő pontjára:
-A `salary_row.xml` végleges kódja:
+```kotlin
+bottomBar = {
+ BottomAppBar {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ IconToggleButton(
+ modifier = Modifier.size(64.dp),
+ checked = isIncome,
+ onCheckedChange = { isIncome = !isIncome },
+ ) {
+ Image(
+ modifier = Modifier.size(64.dp),
+ painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense),
+ contentDescription = "expense/income button"
+ )
+ }
+ OutlinedTextField(
+ label = { Text(stringResource(R.string.item)) },
+ modifier = Modifier
+ .padding(start = 8.dp, end = 8.dp)
+ .weight(2f),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
+ value = item,
+ onValueChange = {
+ item = it
+ }
+ )
+ OutlinedTextField(
+ label = { Text(stringResource(R.string.price)) },
+ modifier = Modifier
+ .padding(start = 8.dp, end = 8.dp)
+ .weight(1f),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ value = price,
+ onValueChange = {
+ price = it
+ }
+ )
+ }
+ }
+}
+```
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
+Itt szintén elvégezzük a szükséges beállításokat. A legtöbb azért felel, hogy barátibb UI-t láthassunk, viszont a legfontosabb a `value`, valamint az `onValueChange` értéke, ugyanis, ha ezek nincsenek beállítva, akkor nem fog megjelenni a begépelt szöveg. A `value`-nak átadjuk az *item* értéket, valamint az `onValueChange` esetén beállítjuk, hogy minden egyes karakter leütésnél új értéket kapjon a változó. Ezzel érhetjük el azt, hogy futási időben láthassuk a *TextField* értékét.
+
+!!!note "Állapottól függő megjelenítés"
+ Az `Image` *painter* attribútumánál jól megfigyelhető a *Jetpack Compose* egyik nagy előnye. A megjelenített képet egy állapottól tesszük függővé, amit ott helyben meg tudunk oldani:
+ ```kotlin
+ ...
+ painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense)
+ ...
+ ```
+
+
+### Új elem felvétele
+
+Új elem felvételét a `FloatingActionButton` gomb megnyomásának hatására fogunk felvenni. Szerencsére a `Scaffold` ennek is biztosít helyet. Illesszük tehát be az alábbi kódot:
+
+```kotlin
+floatingActionButton = {
+ LargeFloatingActionButton(
+ shape = CircleShape,
+ onClick = {
+ if (item.isNotEmpty() && price.isNotEmpty()) {
+ salaryItems += SalaryData(isIncome, item, price)
+ } else {
+
+ Toast.makeText(
+ context,
+ "",
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ }
+ })
+ {
+ Image(
+ imageVector = Icons.Default.Add,
+ contentDescription = "save button"
+ )
+ }
+},
```
-!!!note "Megjegyzés"
- A „tools” névtérnek csak a preview-ra van hatása, tervezés közben beírhatunk oda bármit a lefordított alkalmazásban sehol nem fog látszani.
+Az itteni gomb esetén az *onClick* eseményt először egy feltétellel ellenőrizzük. Ha üresen maradt a név vagy az ár, akkor egy `Toast` üzenettel figyelmeztetjük a felhasználót. Ha megfelelő a kitöltés, akkor a *salaryItems* listához hozzáadunk egy új elemet a bevitt adatnak megfelelően. Ez az elem a már korábban definiált `data class` egy példánya.
+
+Jelenleg így néz ki a felületünk:
+
+
+
+
+
!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik **egy sor layout-ja** (*preview-ként*), **a hozzá tartozó kóddal**, valamint a **neptun kódoddal a termék neveként**. A képet a megoldásban a repository-ba f2.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **MainScreen** kódja, **a futó alkalmazás** (emulátoron vagy a készüléket türközve), valamint a **neptun kódod a beviteli mezőbe beleírva elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f2.png néven töltsd föl!
A képernyőkép szükséges feltétele a pontszám megszerzésének.
-## A listaelem példányosítása (1 pont)
+## Lista elkészítése (1 pont)
-Mostanra minden összetevőnk készen áll, már csak a mögöttes logikát kell megvalósítanunk. A kódban szükségünk lesz a mezők elérésére, illetve a kapcsolónk állapotának vizsgálatára a kattintás pillanatában. Ezeket a részeket a *Save* gombunk kattintás eseménykezelőjében
-fogjuk megvalósítani. Továbbá az említett *inflate*-elendő komponensünk példányosítását is itt kell végrehajtanunk a kapott adatok alapján. `Toast` üzenetben jelezzük, ha valamelyik mező nincs kitöltve!
+###Listaelem létrehozása
-Először készítsük el az eseménykezelő vázát. Figyeljük meg, hogy kódot adunk át paraméterként, ezért nem kerek zárójeleket, hanem kapcsos zárójelpárt használunk. Szintén fontos, hogy ha Kotlinban készítünk Android alkalmazást, akkor a `layout`-ban definiált komponenseket az *id*-jükkel el tudjuk érni. Ehhez először meg kell csinálnunk a `viewBinding`-ot az `Activity`-n. Nem szabad elfelejteni, hogy a modul szintű `build.gradle.kts` fájlban fel kell vennünk a `viewBinding` `buildFeature`-t.
+A vezérlőink megvannak, azonban ahhoz, hogy a listát elkészítsük, létre kell hoznunk egy listaelemet, amit majd a `LazyColumn`-ben fogunk látni.
-```
-buildFeatures {
- viewBinding = true
-}
-```
-
-Ezt követően az `Activity`:
+- Egy listaelem felépítése:
+ - Ikon a pénzforgalom irányától függően.
+ - A megadott megnevezés és alatta az összeg.
+ - Egy gomb a tétel törlésére.
-```kotlin
-private lateinit var binding: ActivityMainBinding
+Hozzunk létre egy a `ui/view` *package*-be egy új *Kotlin* fájlt `SalaryCard` néven.
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
+Ennek a következő képpen kell kinéznie:
- binding.saveButton.setOnClickListener {
- // TODO: ide jön az eseménykezelő kód
+```kotlin
+package hu.bme.aut.android.androidwallet.ui.view
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import hu.bme.aut.android.androidwallet.R
+
+@Composable
+fun SalaryCard(isIncome: Boolean, item: String, price: String) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ Image(
+ modifier = Modifier.size(64.dp),
+ painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense),
+ contentDescription = "Income/Expense"
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ text = item,
+ maxLines = 1
+ )
+ Spacer(modifier = Modifier.size(12.dp))
+ Text(
+ text = price,
+ color = Color.Gray,
+ maxLines = 1
+ )
+ }
+ Spacer(modifier = Modifier.size(8.dp))
+ IconButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = {
+ ///TODO
+ }
+ ) {
+ Image(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "delete button",
+ )
+ }
}
}
-```
-Az eseménykezelőben először a kitöltöttség ellenőrzését végezzük el, ehhez egy hibaüzenetet is meg kell adnunk. Ezt a jó gyakorlatnak megfelelően a `strings.xml` fájlba szervezzük is ki. A hibaüzenet legyen mondjuk "Missing data!":
+@Composable
+@Preview(showBackground = true)
+fun PreviewIncomeSalaryCard() {
+ SalaryCard(isIncome = true, item = "item", price = "500 Ft")
+}
-```kotlin
-if (binding.salaryName.text.toString().isEmpty() || binding.salaryAmount.text.toString().isEmpty()) {
- Toast.makeText(this, R.string.warn_message, Toast.LENGTH_LONG).show()
- return@setOnClickListener
+@Composable
+@Preview(showBackground = true)
+fun PreviewExpenseSalaryCard() {
+ SalaryCard(isIncome = false, item = "item", price = "500 Ft")
}
```
-Ha minden adat helyes, akkor már fel is vehetünk egy új sort. Egy sor kezeléséhez szükségünk van egy `SalaryRowBinding` példányra:
+A *SalaryCard* Composable függvény 3 paramétert tartalmaz:
-```kotlin
-private lateinit var rowBinding: SalaryRowBinding
-```
+- `isIncome - Boolean változó amely a kiadás/bevétel állapotért felel.`
+- `item - kiadás/bevétel neve`
+- `price - kiadás/bevétel értéke`
-Ezután egy row itemet példányosítunk, (*inflate*-elünk) a korábban elkészített XML-ből az `OnCreate` metódus eseménykezelőjében:
+A függvényen belül megtalálható egy `Row`, egy `Spacer`, egy `Column`, még egy `Spacer`, valamint egy `ImageButton`. A `Row` felel azért, hogy az elemeket horizontálisan egymás mellé lehessen rakni, a `Column` pedig, hogy az elemeket egymás alá. (A kiadás/bevétel neve, illetve értéke.) A képet egy `Image` *Composable*-lel helyezzük el. Itt a `modifier` segítségével sok fajta beállításra van lehetőség, most csak a size-zal foglalkozunk, hogy átláthatóbb legyen a kód. A `painter` segítségével adhatjuk meg a képet, amit szeretnénk megjeleníteni, ezt egy if-else elágazással oldjuk meg, mégpedig a paraméterként kapott *isIncome* segítségével. Miután megvagyunk az `Image`-dzsel, a `Row`-n belül a `Column`-be elhelyezünk kettő `Text`-et a maradék kettő paraméterrel. A sor végi törlő gomb funkcionalitásának megoldása önálló feladat lesz.
-```kotlin
-rowBinding = SalaryRowBinding.inflate(layoutInflater)
-```
-A példányosítás után már elérjük az adott példány különböző részeit, tehát az ikont, a nevet, és az összeget. Állítsuk is be ezeket a megadott adatok alapján.
-Az ikont a `ToggleButton` állapota alapján kell kitöltenünk. Az ikonokhoz az [income.png](downloads/income.png) és az [expense.png](downloads/expense.png) képeket fogjuk használni.
+### Listaelem példányosítása LazyColumn-ben
-!!!tip "Android Asset Studio"
- A letöltött képeket használhatjuk egyből a res/drawable mappába helyezve, azonban ha igényes alkalmazást akarunk készíteni, akkor célszerű több méretben is elérhetővé tenni ezeket. A különböző méretek legenerálásához használjuk az [Asset Studio](https://romannurik.github.io/AndroidAssetStudio/)-t (azon belül a Generic icon generator-t), forrásként válasszuk ki a képeinket, állítsuk be a Color paramétert Alpha értékét 0-ra, majd a letöltött zip fájlokat csomagoljuk ki a res mappába.
-
+Végül a `MainScreen`-ünkön a `Scaffold` *content* részébe valósítsuk meg a listát:
```kotlin
-rowBinding.salaryDirectionIcon.setImageResource(if (binding.expenseOrIncome.isChecked) R.drawable.expense else R.drawable.income)
-rowBinding.rowSalaryName.text = binding.salaryName.text.toString()
-rowBinding.rowSalaryAmount.text = binding.salaryAmount.text.toString()
-```
-
-Most, hogy megvagyunk a példányosítással és az adatok feltöltésével, hozzá kell adnunk az elemet a listához (`LinearLayout`). Ehhez az `activity_main.xml` alsó `LinearLayout`-jának egy *id*-t is kell adnunk, hogy hivatkozni tudjunk rá:
+content =
+{ innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ if (salaryItems.size == 0) {
+ Text(
+ text = stringResource(R.string.label_empty_list),
+ modifier = Modifier.fillMaxSize(),
+ textAlign = TextAlign.Center,
+ )
+ } else
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ items(salaryItems.size) {
+ SalaryCard(
+ isIncome = salaryItems[it].isIncome,
+ item = salaryItems[it].item,
+ price = "${salaryItems[it].price} Ft",
+ )
+ if (salaryItems.size - 1 > it) {
+ HorizontalDivider(modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
-```xml
-
-```
-
-```kotlin
-binding.listOfRows.addView(rowBinding.root)
+ }
+},
```
-És ezen a ponton akár futtathatjuk is az alkalmazásunkat. Próbáljuk is ki!
+Itt is megfigyelhetjük, hogy a lista elemszámától (állapotától) függően vagy egy szöveget, vagy a `LazyColumn` listát helyezzük el a felületre.
-Ezen a ponton már majdnem készen is vagyunk: hozzá tudunk adni elemeket a listánkhoz. Azonban két helyen még hiányos az alkalmazásunk. Nem lehet törölni a teljes listát, illetve ha elég sok elemet veszünk fel észrevesszük, hogy nem férnek ki, viszont görgetni nem tudunk. Az előbbi probléma megoldását már előkészítettük, erre fog szolgálni a „Delete All”-ra átalakított menüpontunk, amely megjelenni már helyesen jelenik de még nem csinál semmit. Az eseménykezelő vázát már elkészítettük az `onOptionsItemSelected()` metódusban, most ezt kell kiegészítenünk az alábbira:
+A `LazyColumn`-ön belül az `items(..)` függvénynek egy méretet kell átadni, ami azt jelöli, hogy mekkora a lista. Ez után a blokk törzsében el kell helyezni azt a *Composable* elemet amit látni szeretnénk a listában. Ez a `SalaryCard` *Composable* lesz, amit már korábban implementáltunk. Ennek paraméterként megadjuk az *salaryItems* aktuális (`it`) elemeit. Ezen fog végig iterálni a `LazyColumn`, és minden elemet ki fog rajzolni.
-```kotlin
-override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.action_delete_all -> {
- binding.listOfRows.removeAllViews()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-}
-```
+Ezzel a lépéssel elérkeztünk a kész alkalmazáshoz, és indítás során a következőt kell látnunk:
-Próbáljuk ki a törlés funkciót!
+
+
+
-Utóbbi problémánkra pedig nagyon egyszerű a megoldás, a listánkat tartalmazó `LinearLayoutot` egy `ScrollView`-ba kell foglalnunk és már működik is.
-
-```xml
-
-
-
-
-```
!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik hogy **a lista scrollozható** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **lista törlésének kódjával**, valamint a **neptun kódoddal valamelyik termék neveként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **LazyColumn** kódja, a futó alkalmazás (emulátoron vagy a készüléket türközve), valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f3.png néven töltsd föl!
A képernyőkép szükséges feltétele a pontszám megszerzésének.
## Önálló feladatok
-### Snack bar (1 pont)
+### Összegző mező (1 pont)
-A Toast üzeneteknél már van egy sokkal szebb megoldás, ami a Material Designt követi, a [Snackbar](https://material.io/develop/android/components/snackbar/). Cseréljük le a Toast figyelmeztetést Snackbarra!
+Vegyünk fel egy összegző mezőt valahova a felületre, amely minden bevitt érték után frissül. Figyeljünk arra, hogyha nincs még egyetlen bejegyzés sem, akkor ne jelenjen meg semmi, valamint a felhasználó nem mínusz karakter alapján állítja a kiadás/bevétel állapotot, hanem a kapcsoló alapján kell eldöntenünk, hogy pozitív vagy negatív érték.
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik **a Snackbar használata** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a kódja**, valamint a **neptun kódoddal a termék neveként**. A képet a megoldásban a repository-ba f4.png néven töltsd föl.
+!!!tip "Tipp"
+ Érdemes használni a `Modifier.alpha()` paramétert.
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+!!!warning "Figyelem"
+ Figyeljünk az összegző mező helyes működésére! Ha töröljük a listából a bejegyzéseket, akkor a számláló is nullázódjon és tűnjön el! (Nem elég csak akkor eltüntetni, hogyha a `sum` 0 értéket vesz fel.) (-0.5 pont)
-### Összegző mező (1 pont)
+!!!example "BEADANDÓ (1 pont)"
+ Készíts egy **képernyőképet**, amelyen látszik az **összegző mező használata** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a kódja**, valamint a **neptun kódod valamelyik termék neveként**! A képet a megoldásban a repository-ba f4.png néven töltsd föl!
-Vegyünk fel egy összegző mezőt a gombok mellé, amely minden bevitt érték után frissül. Figyeljünk rá, hogy ha még nincs egy bejegyzés sem, akkor ne jelenjen meg semmi, illetve hogy a felhasználó nem fog mínusz karaktert beírni tehát a kapcsoló alapján kell eldöntenünk, hogy ez pozitív vagy negatív érték. Az egyszerűség kedvéért megengedjük, hogy az összeg mező `inputType`-ját `numberDecimal`-ra állítsuk, így a felhasználó nem tud betűket beírni.
+ A képernyőkép szükséges feltétele a pontszám megszerzésének.
-!!!warning "Figyelem"
- Figyeljünk az összegző mező helyes működésére! Ha töröljük a listából a bejegyzéseket, akkor a számláló is nullázódjon és tűnjön el! (-0.5 pont)
+### Egyes elemek törlése (1 pont)
+
+Az egyes listaelemeken már jelen van a törlés gomb, azonban még nem működik. Valósítsuk meg az egyedi törlést!
!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik **az összegző mező használata** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a kódja**, valamint a **neptun kódod valamelyik termék neveként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl.
+ Készíts egy **képernyőképet**, amelyen látszik a **törlés** kódja, az emulátor a működő alkalmazásról, valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f5.png néven töltsd föl!
A képernyőkép szükséges feltétele a pontszám megszerzésének.
### Bonus
-Vizsgáljuk meg mi történik, ha az `EditText`-et (`TextInputEditTextet`) `TextInputLayout`-tal használjuk. [https://developer.android.com/reference/android/support/design/widget/TextInputLayout.html](https://developer.android.com/reference/android/support/design/widget/TextInputLayout.html)
+Módosítsuk a *TopBar* menü gombját, úgy hogy legördülő menü lista legyen belőle, ahol 3 opció található meg.
+
+- Delete Expenses
+- Delete Incomes
+- Delete All
+
+Ehhez módosítsuk a TopBar Composable függvényünket.
+
+???success "Segítség"
+ - Plusz két lambda operátor
+ - DropdownMenu
+ - DropdownMenuItem
+ - .filter használata a listán/sum-on
\ No newline at end of file
diff --git a/docs/laborok/04-android-ui-fragments/index.md b/docs/laborok/04-android-ui-fragments/index.md
index 796763a..bcc082a 100644
--- a/docs/laborok/04-android-ui-fragments/index.md
+++ b/docs/laborok/04-android-ui-fragments/index.md
@@ -1,4 +1,3 @@
-
# Labor 04 - Felhasználói felület készítése - HorizontalPager, Chartok
## Bevezető
@@ -906,4 +905,4 @@ A Payment menüpontra kattintva jelenjen meg egy `PaymentScreen` rajta egy Horiz
Az érdeklődők kedvéért ezen a laboron egy extra feladat is van, viszont ez csak saját tapasztalat szerzésért. **Ezért a feladatért nem jár pont!**
-* Az önálló feladathoz hasonlóan most a állítsad be, hogy a DatePicker dialógus ablakon csak a mai illetve a mai + maximális szabadnapok között lehessen választani.
\ No newline at end of file
+* Az önálló feladathoz hasonlóan most a állítsad be, hogy a DatePicker dialógus ablakon csak a mai illetve a mai + maximális szabadnapok között lehessen választani.
diff --git a/docs/laborok/05-android-sqlite/index.md b/docs/laborok/05-android-sqlite/index.md
index 653fbdb..f189eb6 100644
--- a/docs/laborok/05-android-sqlite/index.md
+++ b/docs/laborok/05-android-sqlite/index.md
@@ -1,715 +1,3 @@
# Labor 05 - Rajzoló alkalmazás készítése
-## Bevezető
-
-A labor során egy egyszerű rajzoló alkalmazás elkészítése a feladat. Az alkalmazással egy vászonra lehet vonalakat vagy pontokat rajzolni. Ezen kívül szükséges a rajzolt ábrát perzisztensen elmenteni, hogy az alkalmazás újraindítása után is visszatöltődjön.
-
-
-
-
-
-
-
-!!!info "Android Room"
- A labor során meg fogunk ismerkedni az SQLite könyvtárral, mellyel egy lokális SQL adatbázisban tudunk adatokat perszisztensen tárolni. A modern Android alapú fejlesztéseknél már általában a Room-ot használják, mely az SQLite-ra építve biztosít egy könnyen használható ORM réteget az Android életciklusokkal kombinálva. Fontosnak tartottuk viszont, hogy könnyen érthető legyen az anyag, ezért most csak az SQLite-os megoldást fogjuk vizsgálni.
-
-!!! warning "IMSc"
- A laborfeladatok sikeres befejezése után az IMSc feladat-ot megoldva 2 IMSc pont szerezhető.
-
-
-## Előkészületek
-
-A feladatok megoldása során ne felejtsd el követni a [feladat beadás folyamatát](../../tudnivalok/github/GitHub.md).
-
-### Git repository létrehozása és letöltése
-
-1. Moodle-ben keresd meg a laborhoz tartozó meghívó URL-jét és annak segítségével hozd létre a saját repository-dat.
-
-2. Várd meg, míg elkészül a repository, majd checkout-old ki.
-
- !!! tip ""
- Egyetemi laborokban, ha a checkout során nem kér a rendszer felhasználónevet és jelszót, és nem sikerül a checkout, akkor valószínűleg a gépen korábban megjegyzett felhasználónévvel próbálkozott a rendszer. Először töröld ki a mentett belépési adatokat (lásd [itt](../../tudnivalok/github/GitHub-credentials.md)), és próbáld újra.
-
-3. Hozz létre egy új ágat `megoldas` néven, és ezen az ágon dolgozz.
-
-4. A `neptun.txt` fájlba írd bele a Neptun kódodat. A fájlban semmi más ne szerepeljen, csak egyetlen sorban a Neptun kód 6 karaktere.
-
-
-## A projekt előkészítése
-
-### A projekt létrehozása
-
-Hozzunk létre egy `Simple Drawer` nevű projektet Android Studioban:
-
-1. Hozzunk létre egy új projektet, válasszuk az *No Activity* lehetőséget.
-1. A projekt neve legyen `Simple Drawer`, a kezdő package `hu.bme.aut.android.simpledrawer`, a mentési hely pedig a kicheckoutolt repository-n belül az SimpleDrawer mappa.
-1. Nyelvnek válasszuk a *Kotlin*-t.
-1. A minimum API szint legyen API24: Android 7.0.
-1. A *Build configuration language* Kotlin DSL legyen.
-
-!!!danger "FILE PATH"
- A projekt a repository-ban lévő SimpleDrawer könyvtárba kerüljön, és beadásnál legyen is felpusholva! A kód nélkül nem tudunk maximális pontot adni a laborra!
-
-Adjunk a projekthez egy új *Empty Views Activity* -t. *Activity name*-nek adjuk meg, hogy `DrawingActivity`, és hagyjuk bepipálva azt, hogy generáljon *layout* fájlt, valamint pipáljuk be a _Launcher Activity_ opciót. Ha ezekkel megvagyunk, akkor rányomhatunk a **Finish**-re.
-
-Miután létrejött a projekt, töröljük ki a teszt package-eket, mert most nem lesz rá szükségünk.
-
-
-### A resource-ok hozzáadása
-
-Először töltsük le [az alkalmazás képeit tartalmazó tömörített fájlt](./downloads/res.zip), ami tartalmazza az összes képet, amire szükségünk lesz. A tartalmát másoljuk be az `app/src/main/res` mappába (ehhez segít, ha _Android Studio_-ban bal fent a szokásos _Android_ nézetről a _Project_ nézetre váltunk erre az időre).
-
-Az alábbi, alkalmazáshoz szükséges _string resource_-okat másoljuk be a `res/values/strings.xml` fájlba:
-
-```xml
-
- Simple Drawer
-
- Stílus
- Vonal
- Pont
-
- Biztosan ki akarsz lépni?
- OK
- Mégse
-
-```
-
-### Álló layout kikényszerítése
-
-Az alkalmazásunkban az egyszerűség kedvéért most csak az álló módot támogatjuk. Ehhez az `AndroidManifest.xml`-ben a `DrawingActivity` nyitótagjához kell hozzáadni egy sort a következő módon:
-
-```xml
-
-```
-
-
-## A kezdő layout létrehozása (1 pont)
-
-Első lépésként hozzunk létre egy új _package_-et az `hu.bme.aut.android.simpledrawer`-en belül, aminek adjuk a `view` nevet. Ebben hozzunk létre egy új osztályt, amit nevezzünk el `DrawingView`-nak, és származzon le a `View` osztályból (`android.view.View`).
-
-Hozzuk létre a szükséges konstruktort ezen belül:
-
-```kotlin
-class DrawingView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
-
-}
-```
-
-Miután létrehoztuk a `DrawingView`-t, nyissuk meg a `res/layout/activity_drawing.xml`-t, és hozzunk létre gyökérelemként egy `ConstraintLayout`-ot, azon belül alulra egy `Toolbar`-t rakjunk, fölé pedig a frissen létrehozott `DrawingView`-nkból helyezzünk el egy példányt fekete háttérrel. Végezetül a layoutnak így kell kinéznie:
-
-```xml
-
-
-
-
-
-
-
-```
-
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik az **elkészült DrawingActivity** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f1.png néven töltsd föl.
-
-## Stílusválasztó (1 pont)
-
-Miután létrehoztuk a rajzolás tulajdonságainak állításáért felelős `Toolbar`-t, hozzuk létre a menüt, amivel be lehet állítani, hogy pontot vagy vonalat rajzoljunk. Ehhez hozzunk létre egy új _Android resource directory_-t `menu` néven a `res` mappában, és _Resource type_-nak is válasszuk azt, hogy `menu`. Ezen belül hozzunk létre egy új _Menu resource file_-t `menu_toolbar.xml` néven. Ebben hozzunk létre az alábbi hierarchiát:
-
-```xml
-
-
- -
-
-
-
-
-
-
-
-
-```
-
-Ezután kössük be a menüt, hogy megjelenjen a `Toolbar`-on.
-Ahhoz, hogy elérjük a létrehozott erőforrásokat kódból, view binding-ra lesz szükségünk. A modul szintű gradle file-ba fegyük fel a következő elemet. ***Ne felejtsünk*** el a `Sync` now gombra kattintani a módosítást követően.
-
-```groovy
-android {
- ...
- buildFeatures {
- viewBinding = true
- }
-}
-```
-Ezután hozzunk létre egy binding adattagot a `DrawingActivity`-n belül `toolbarBinding` néven és inicializáljuk az `onCreate` függvényben.
-
-```kotlin
-private lateinit var binding: ActivityDrawingBinding
-
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityDrawingBinding.inflate(layoutInflater)
- setContentView(binding.root)
-}
-```
-
-Már csak annyi van hátra, hogy a `DrawingActivity`-ben felüldefiniáljuk az _Activity_ `onCreateOptionsMenu()` és `onOptionsItemSelected()` függvényét az alábbi módon:
-
-```kotlin
-override fun onCreateOptionsMenu(menu: Menu): Boolean {
- val toolbarMenu: Menu = binding.toolbar.menu
- menuInflater.inflate(R.menu.menu_toolbar, toolbarMenu)
- for (i in 0 until toolbarMenu.size()) {
- val menuItem: MenuItem = toolbarMenu.getItem(i)
- menuItem.setOnMenuItemClickListener { item -> onOptionsItemSelected(item) }
- if (menuItem.hasSubMenu()) {
- val subMenu: SubMenu = menuItem.subMenu!!
- for (j in 0 until subMenu.size()) {
- subMenu.getItem(j)
- .setOnMenuItemClickListener { item -> onOptionsItemSelected(item) }
- }
- }
- }
- return super.onCreateOptionsMenu(menu)
-}
-```
-```kotlin
-override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.menu_style_line -> {
- item.isChecked = true
- true
- }
- R.id.menu_style_point -> {
- item.isChecked = true
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-}
-```
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **elkészült menü kinyitva** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f2.png néven töltsd föl.
-
-## A `DrawingView` osztály implementálása (1 pont)
-
-### A modellek létrehozása
-
-A rajzprogramunk, ahogy az már az előző feladatban is kiderült, kétféle rajzolási stílust fog támogatni. Nevezetesen a pont- és vonalrajzolást. Ahhoz, hogy a rajzolt alakzatokat el tudjuk tárolni szükségünk lesz két új típusra, modellre, amihez hozzunk létre egy új _package_-et a `hu.bme.aut.android.simpledrawer`-en belül `model` néven.
-
-Ezen belül először hozzunk létre egy `Point` osztályt, ami értelemszerűen a pontokat fogja reprezentálni. Kétparaméteres konstruktort fogunk létrehozni, amihez alapértékeket rendelünk.
-
-```kotlin
-data class Point(
- var x: Float = 0F,
- var y: Float = 0F
-)
-```
-
-Miután ezzel megvagyunk, hozzunk létre egy `Line` osztályt. Mivel egy vonalat a két végpontjának megadásával ki tudunk
-rajzoltatni, így elegendő két `Point`-ot tartalmaznia az osztálynak.
-
-```kotlin
-data class Line(
- var start: Point,
- var end: Point
-)
-```
-
-### A rajzolási stílus beállítása
-
-Most, hogy megvannak a modelljeink el lehet kezdeni magának a rajzolás funkciójának fejlesztését. Ehhez a `DrawingView` osztályt fogjuk ténylegesen is elkészíteni. Először vegyünk fel az osztályon belül egy `companion object`-et, amiben a rajzolási stílus konstansait fogjuk meghatározni. Ehhez kapcsolódóan vegyünk fel egy új `field`-et az osztályunkba, amiben eltároljuk, hogy jelenleg milyen stílus van kiválasztva.
-
-```kotlin
-companion object {
- const val DRAWING_STYLE_LINE = 1
- const val DRAWING_STYLE_POINT = 2
-}
-
-var currentDrawingStyle = DRAWING_STYLE_LINE
-```
-
-Ha ezek megvannak, akkor egészítsük ki a `DrawingActivity`-ben a menükezelést, úgy, hogy a megfelelő függvények hívódjanak meg. Az `onOptionsItemSelected()` függvény megfelelő `case` ágában meg kell hívnunk a `canvas`-ra a `setDrawingStyle()` függvényt a megfelelő paraméterrel.
-
-```kotlin
-override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.menu_style_line -> {
- binding.canvas.currentDrawingStyle = DrawingView.DRAWING_STYLE_LINE
- item.isChecked = true
- true
- }
- R.id.menu_style_point -> {
- binding.canvas.currentDrawingStyle = DrawingView.DRAWING_STYLE_POINT
- item.isChecked = true
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-}
-```
-### Inicializálások
-
-A rajzolási funkció megvalósításához fel kell vennünk néhány további `field`-et a `DrawingView` osztályban, amiket a konstruktorban inicializálnunk kell. A paint objektumhoz hozzáadjuk a `lateinit` kulcsszót, hogy elég legyen az `init` blokkban inicializálnunk. A `Point` osztály import-ja során használjuk a korábban definiált osztályunkat.
-
-```kotlin
-private lateinit var paint: Paint
-
-private var startPoint: Point? = null
-
-private var endPoint: Point? = null
-
-var lines: MutableList = mutableListOf()
-var points: MutableList = mutableListOf()
-
-init {
- initPaint()
-}
-
-private fun initPaint() {
- paint = Paint()
- paint.color = Color.GREEN
- paint.style = Paint.Style.STROKE
- paint.strokeWidth = 5F
-}
-```
-
-### Gesztusok kezelése
-
-Ahhoz, hogy vonalat vagy pontot tudjunk rajzolni a `View`-nkra, kezelnünk kell a felhasználótól kapott gesztusokat, mint például amikor hozzáér a kijelzőhöz, elhúzza rajta vagy felemeli róla az ujját. Szerencsére ezeket a gesztusokat nem szükséges manuálisan felismernünk és lekezelnünk, a `View` ősosztály `onTouchEvent()` függvényének felüldefiniálásával egyszerűen megolható a feladat.
-
-```kotlin
-@SuppressLint("ClickableViewAccessibility")
-override fun onTouchEvent(event: MotionEvent): Boolean {
- endPoint = Point(event.x, event.y)
- when (event.action) {
- MotionEvent.ACTION_DOWN -> startPoint = Point(event.x, event.y)
- MotionEvent.ACTION_MOVE -> {
- }
- MotionEvent.ACTION_UP -> {
- when (currentDrawingStyle) {
- DRAWING_STYLE_POINT -> addPointToTheList(endPoint!!)
- DRAWING_STYLE_LINE -> addLineToTheList(startPoint!!, endPoint!!)
- }
- startPoint = null
- endPoint = null
- }
- else -> return false
- }
- invalidate()
- return true
-}
-
-private fun addPointToTheList(startPoint: Point) {
- points.add(startPoint)
-}
-
-private fun addLineToTheList(startPoint: Point, endPoint: Point) {
- lines.add(Line(startPoint, endPoint))
-}
-```
-
-Ahogy a fenti kódrészletből is látszik minden gesztusnál elmentjük az adott `TouchEvent` pontját, mint a rajzolás végpontját, illetve ha `MotionEvent.ACTION_DOWN` történt, tehát a felhasználó hozzáért a `View`-hoz, elmentjük ezt kezdőpontként is. Amíg a felhasználó mozgatja az ujját a `View`-n (`MotionEvent.ACTION_MOVE`), addig nem csinálunk semmit, de amint felemeli (`MotionEvent.ACTION_UP`), elmentjük az adott elemet a korábban már definiált listákba. Ezen kívül minden egyes alkalommal meghívjuk az `invalidate()` függvényt, ami kikényszeríti a `View` újrarajzolását.
-
-### A rajzolás
-
-A rajzolás megvalósításához a `View` ősosztály `onDraw()` metódusát kell felüldefiniálnunk. Egyrészt ki kell rajzolnunk a már meglévő objektumokat (amiket a `MotionEvent.ACTION_UP` eseménynél beleraktunk a listába), valamint ki kell rajzolnunk az aktuális kezdőpont (a `MotionEvent.ACTION_DOWN` eseménytől) és a felhasználó ujja közötti vonalat.
-
-```kotlin
-override fun onDraw(canvas: Canvas) {
- super.onDraw(canvas)
- for (point in points) {
- drawPoint(canvas, point)
- }
- for (line in lines) {
- drawLine(canvas, line.start, line.end)
- }
- when (currentDrawingStyle) {
- DRAWING_STYLE_POINT -> drawPoint(canvas, endPoint)
- DRAWING_STYLE_LINE -> drawLine(canvas, startPoint, endPoint)
- }
-}
-
-private fun drawPoint(canvas: Canvas, point: Point?) {
- if (point == null) {
- return
- }
- canvas.drawPoint(point.x, point.y, paint)
-}
-
-private fun drawLine(canvas: Canvas, startPoint: Point?, endPoint: Point?) {
- if (startPoint == null || endPoint == null) {
- return
- }
- canvas.drawLine(
- startPoint.x,
- startPoint.y,
- endPoint.x,
- endPoint.y,
- paint
- )
-}
-```
-
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik az **elkészült kirajzolás** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl.
-
-## Perzisztencia megvalósítása _SQLite_ adatbázis segítségével (1 pont)
-
-Ahhoz, hogy az általunk rajzolt objektumok megmaradjanak az alkalmazásból való kilépés után is, az adatainkat valahogy olyan formába kell rendeznünk, hogy azt könnyedén el tudjuk tárolni egy _SQLite_ adatbázisban.
-
-Hozzunk létre egy új _package_-et az `hu.bme.aut.android.simpledrawer`-en belül, aminek adjuk az `sqlite` nevet.
-
-### Táblák definiálása
-
-Az adatbáziskezelés során sok konstans jellegű változóval kell dolgoznunk, mint például a táblákban lévő oszlopok nevei, táblák neve, adatbázis fájl neve, séma létrehozó és törlő szkiptek, stb. Ezeket érdemes egy közös helyen tárolni, így szerkesztéskor vagy új entitás bevezetésekor nem kell a forrásfájlok között ugrálni, valamint egyszerűbb a teljes adatbázist létrehozó és törlő szkripteket generálni. Hozzunk létre egy új _singleton_ osztályt az `object` kulcsszóval az `sqlite` _package_-en belül `DbConstants` néven.
-
-Ezen belül először is konstansként felvesszük az adatbázis nevét és verzióját is. Ha az adatbázisunk sémáján szeretnénk változtatni, akkor ez utóbbit kell inkrementálnunk, így elkerülhetjük az inkompatibilitás miatti nem kívánatos hibákat.
-
-```kotlin
-object DbConstants {
-
- const val DATABASE_NAME = "simpledrawer.db"
- const val DATABASE_VERSION = 1
-}
-```
-
-Ezek után a `DbConstants` nevű osztályba hozzuk létre a `Point` osztályhoz a konstansokat. Az osztályokon belül létrehozunk egy `enum`-ot is, hogy könnyebben tudjuk kezelni a tábla oszlopait, majd konstansokban eltároljuk a tábla létrehozását szolgáló _SQL utasítást_ valamint a tábla nevét is. Végezetül elkészítjük azokat a függvényeket, amelyeket a tábla létrehozásakor, illetve upgrade-elésekor kell meghívni:
-
-```kotlin
-object DbConstants {
-
- const val DATABASE_NAME = "simpledrawer.db"
- const val DATABASE_VERSION = 1
-
- object Points {
- const val DATABASE_TABLE = "points"
-
- enum class Columns {
- ID, COORD_X, COORD_Y
- }
-
- private val DATABASE_CREATE = """create table if not exists $DATABASE_TABLE (
- ${Columns.ID.name} integer primary key autoincrement,
- ${Columns.COORD_X.name} real not null,
- ${Columns.COORD_Y} real not null
- );"""
-
- private const val DATABASE_DROP = "drop table if exists $DATABASE_TABLE;"
-
- fun onCreate(database: SQLiteDatabase) {
- database.execSQL(DATABASE_CREATE)
- }
-
- fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
- Log.w(
- Points::class.java.name,
- "Upgrading from version $oldVersion to $newVersion"
- )
- database.execSQL(DATABASE_DROP)
- onCreate(database)
- }
- }
-}
-```
-
-Figyeljük meg, hogy a `DbConstants` osztályon belül létrehoztunk egy belső `Points` nevű osztályt, amiben a `Points` entitásokat tároló táblához tartozó konstans értékeket tároljuk. Amennyiben az alkalmazásunk több entitást is adatbázisban tárol, akkor érdemes az egyes osztályokhoz tartozó konstansokat külön-külön belső osztályokban tárolni. Így sokkal átláthatóbb és karbantarthatóbb lesz a kód, mint ha ömlesztve felvennénk a DbConstants-ba az összes tábla összes konstansát. Ezek a belső osztályok praktikusan ugyanolyan névvel léteznek, mint az entitás osztályok. Vegyük tehát fel hasonló módon a `Lines` nevű osztályt is:
-
-```kotlin
-object Lines {
- const val DATABASE_TABLE = "lines"
-
- enum class Columns {
- ID, START_X, START_Y, END_X, END_Y
- }
-
- private val DATABASE_CREATE ="""create table if not exists $DATABASE_TABLE (
- ${Columns.ID.name} integer primary key autoincrement,
- ${Columns.START_X} real not null,
- ${Columns.START_Y} real not null,
- ${Columns.END_X} real not null,
- ${Columns.END_Y} real not null
-
- );"""
-
- private const val DATABASE_DROP = "drop table if exists $DATABASE_TABLE;"
-
- fun onCreate(database: SQLiteDatabase) {
- database.execSQL(DATABASE_CREATE)
- }
-
- fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
- Log.w(
- Lines::class.java.name,
- "Upgrading from version $oldVersion to $newVersion"
- )
- database.execSQL(DATABASE_DROP)
- onCreate(database)
- }
-}
-```
-
-Érdemes megfigyelni továbbá azt is, hogy az osztályokat nem a class kulcsszóval deklaráltuk. Helyette az `object`-et használjuk, amivel a Kotlin nyelv azt biztosítja számunkra, hogy a `DbConstants` és a benne lévő `Points` és `Lines` osztály is singletonként viselkednek, azaz az alkalmazás futtatásakor létrejön belőlük egy példány, további példányokat pedig nem lehet létrehozni belőlük.
-
-
-### A segédosztályok létrehozása
-
-Az adatbázis létrehozásához szükség van egy olyan segédosztályra, ami létrehozza magát az adatbázist, és azon belül inicializálja a táblákat is. Esetünkben ez lesz a `DbHelper` osztály, ami az `SQLiteOpenHelper` osztályból származik. Vegyük fel ezt is az `sqlite` _package_-be.
-
-
-```kotlin
-class DbHelper(context: Context) :
- SQLiteOpenHelper(context, DbConstants.DATABASE_NAME, null, DbConstants.DATABASE_VERSION) {
-
- override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
- DbConstants.Lines.onCreate(sqLiteDatabase)
- DbConstants.Points.onCreate(sqLiteDatabase)
- }
-
- override fun onUpgrade(
- sqLiteDatabase: SQLiteDatabase,
- oldVersion: Int,
- newVersion: Int
- ) {
- DbConstants.Lines.onUpgrade(sqLiteDatabase, oldVersion, newVersion)
- DbConstants.Points.onUpgrade(sqLiteDatabase, oldVersion, newVersion)
- }
-}
-```
-
-Ezen kívül szükségünk van még egy olyan segédosztályra is, ami ezt az egészet összefogja, és amivel egyszerűen tudjuk kezelni az adatbázisunkat. Ez lesz a `PersistentDataHelper` továbbra is az `sqlite` _package_-ben. Ebben olyan függényeket fogunk megvalósítani, mint pl. az `open()` és a `close()`, amikkel az adatbáziskapcsolatot nyithatjuk meg, illetve zárhatjuk le. Ezen kívül ebben az osztályban valósítjuk meg azokat a függvényeket is, amik az adatok adatbázisba való kiírásáért, illetve az onnan való kiolvasásáért felelősek. Figyeljünk rá, hogy a saját Point osztályunkat válasszuk az _import_ során.
-
-```kotlin
-class PersistentDataHelper(context: Context) {
- private var database: SQLiteDatabase? = null
- private val dbHelper: DbHelper = DbHelper(context)
-
- private val pointColumns = arrayOf(
- DbConstants.Points.Columns.ID.name,
- DbConstants.Points.Columns.COORD_X.name,
- DbConstants.Points.Columns.COORD_Y.name
- )
-
- private val lineColumns = arrayOf(
- DbConstants.Lines.Columns.ID.name,
- DbConstants.Lines.Columns.START_X.name,
- DbConstants.Lines.Columns.START_Y.name,
- DbConstants.Lines.Columns.END_X.name,
- DbConstants.Lines.Columns.END_Y.name
-
- )
-
- @Throws(SQLiteException::class)
- fun open() {
- database = dbHelper.writableDatabase
- }
-
- fun close() {
- dbHelper.close()
- }
-
- fun persistPoints(points: List) {
- clearPoints()
- for (point in points) {
- val values = ContentValues()
- values.put(DbConstants.Points.Columns.COORD_X.name, point.x)
- values.put(DbConstants.Points.Columns.COORD_Y.name, point.y)
- database!!.insert(DbConstants.Points.DATABASE_TABLE, null, values)
- }
- }
-
- fun restorePoints(): MutableList {
- val points: MutableList = ArrayList()
- val cursor: Cursor =
- database!!.query(DbConstants.Points.DATABASE_TABLE, pointColumns, null, null, null, null, null)
- cursor.moveToFirst()
- while (!cursor.isAfterLast) {
- val point: Point = cursorToPoint(cursor)
- points.add(point)
- cursor.moveToNext()
- }
- cursor.close()
- return points
- }
-
- fun clearPoints() {
- database!!.delete(DbConstants.Points.DATABASE_TABLE, null, null)
- }
-
- private fun cursorToPoint(cursor: Cursor): Point {
- val point = Point()
- point.x =cursor.getFloat(DbConstants.Points.Columns.COORD_X.ordinal)
- point.y =cursor.getFloat(DbConstants.Points.Columns.COORD_Y.ordinal)
- return point
- }
-
- fun persistLines(lines: List) {
- clearLines()
- for (line in lines) {
- val values = ContentValues()
- values.put(DbConstants.Lines.Columns.START_X.name, line.start.x)
- values.put(DbConstants.Lines.Columns.START_Y.name, line.start.y)
- values.put(DbConstants.Lines.Columns.END_X.name, line.end.x)
- values.put(DbConstants.Lines.Columns.END_Y.name, line.end.y)
- database!!.insert(DbConstants.Lines.DATABASE_TABLE, null, values)
- }
- }
-
- fun restoreLines(): MutableList {
- val lines: MutableList = ArrayList()
- val cursor: Cursor =
- database!!.query(DbConstants.Lines.DATABASE_TABLE, lineColumns, null, null, null, null, null)
- cursor.moveToFirst()
- while (!cursor.isAfterLast) {
- val line: Line = cursorToLine(cursor)
- lines.add(line)
- cursor.moveToNext()
- }
- cursor.close()
- return lines
- }
-
- fun clearLines() {
- database!!.delete(DbConstants.Lines.DATABASE_TABLE, null, null)
- }
-
- private fun cursorToLine(cursor: Cursor): Line {
- val startPoint = Point(
- cursor.getFloat(DbConstants.Lines.Columns.START_X.ordinal),
- cursor.getFloat(DbConstants.Lines.Columns.START_Y.ordinal)
- )
- val endPoint = Point(
- cursor.getFloat(DbConstants.Lines.Columns.END_X.ordinal),
- cursor.getFloat(DbConstants.Lines.Columns.END_Y.ordinal)
- )
- return Line(startPoint, endPoint)
- }
-
-}
-```
-
-### A `DrawingView` kiegészítése
-
-Ahhoz, hogy a rajzolt objektumainkat el tudjuk menteni az adatbázisba, fel kell készíteni a `DrawingView` osztályunkat arra, hogy átadja, illetve meg lehessen adni neki kívülről is őket. Ehhez a következő függvényeket kell felvennünk:
-
-```kotlin
-fun restoreObjects(points: MutableList?, lines: MutableList?) {
- points?.also { this.points = it }
- lines?.also { this.lines = it }
- invalidate()
-}
-```
-
-### A `DrawingActivity` kiegészítése
-
-A perzisztencia megvalósításához már csak egy feladatunk maradt hátra, mégpedig az, hogy bekössük a frissen létrehozott osztályainkat a `DrawingActivity`-nkbe. Ehhez először is példányosítanunk kell a `PersistentDataHelper` osztályunkat. Mivel az adatbázishozzáférés drága erőforrás, ezért ne felejtsük el az `Activity` `onResume()` függvényében megnyitni, az `onPause()` függvényében pedig lezárni a vele való kapcsolatot:
-
-```kotlin
-private lateinit var dataHelper: PersistentDataHelper
-
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityDrawingBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- dataHelper = PersistentDataHelper(this)
- dataHelper.open()
- restorePersistedObjects()
-}
-
-override fun onResume() {
- super.onResume()
- dataHelper.open()
-}
-
-override fun onPause() {
- dataHelper.close()
- super.onPause()
-}
-
-private fun restorePersistedObjects() {
- binding.canvas.restoreObjects(dataHelper.restorePoints(), dataHelper.restoreLines())
-}
-```
-
-Végezetül szeretnénk, hogy amikor a felhasználó ki szeretne lépni az alkalmazásból, akkor egy dialógusablak jelenjen meg, hogy biztos kilép-e, és ha igen, csak abban az esetben mentsük el a rajzolt objektumokat, és lépjünk ki az alkalmazásból. Ehhez felül kell definiálnunk az `Activity` `onBackPressed()` függvényét. Az _AlertDialog_-nál válasszuk az _androidx.appcompat.app_-ba tartozó verziót.
-
-```kotlin
-override fun onBackPressed() {
- AlertDialog.Builder(this)
- .setMessage(R.string.are_you_sure_want_to_exit)
- .setPositiveButton(R.string.ok) { _, _ -> onExit() }
- .setNegativeButton(R.string.cancel, null)
- .show()
-}
-
-private fun onExit() {
- dataHelper.persistPoints(binding.canvas.points)
- dataHelper.persistLines(binding.canvas.lines)
- dataHelper.close()
- finish()
-}
-```
-
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **kilépő dialógus** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **a perzisztens mentéshez tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f4.png néven töltsd föl.
-
-## Önálló feladat: A vászon törlése (1 pont)
-
-Vegyünk fel a vezérlők közé egy olyan gombot, amelynek segíségével a törölhetjük a vásznat, valósítsuk is meg a funkciót!
-
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **törlés gomb** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), a **törlést elvégző kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl.
-
-## Kiegészítő iMSc feladat (2 iMSc pont)
-
-Vegyünk fel az alkalmazásba egy olyan vezérlőt, amivel változtatni lehet a rajzolás színét a 3 fő szín között (_RGB_).
-
-**Figyelem:** az adatbázisban is el kell menteni az adott objektum színét!
-
-
-!!!example "BEADANDÓ (1 iMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **rajzoló oldal a különböző színekkel** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f6.png néven töltsd föl.
-
-
-!!!example "BEADANDÓ (1 iMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **különböző színek mentését végző kódrészletet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f7.png néven töltsd föl.
+## TODO 2024
\ No newline at end of file
diff --git a/docs/laborok/06-android-room/index.md b/docs/laborok/06-android-room/index.md
index a879348..17fc06a 100644
--- a/docs/laborok/06-android-room/index.md
+++ b/docs/laborok/06-android-room/index.md
@@ -1,770 +1,3 @@
# Labor 06 - Bevásárló alkalmazás készítése
-## Bevezető
-A labor során egy bevásárló lista alkalmazás elkészítése a feladat. Az alkalmazásban fel lehet venni megvásárolni kívánt termékeket, valamint megvásároltnak lehet jelölni és törölni lehet meglévőket.
-
-Az alkalmazás a termékek listáját [`RecyclerView`](https://developer.android.com/guide/topics/ui/layout/recyclerview)-ban jeleníti meg, a lista elemeket és azok állapotát a [`Room`](https://developer.android.com/topic/libraries/architecture/room) nevű ORM library segítségével tárolja perzisztensen. Új elem felvételére egy [`FloatingActionButton`](https://developer.android.com/guide/topics/ui/floating-action-button) megnyomásával van lehetőség.
-
-!!!info "ORM"
- ORM = [Object-relational mapping](https://en.wikipedia.org/wiki/Object-relational_mapping)
-
-Felhasznált technológiák:
-
-- [`Activity`](https://developer.android.com/guide/components/activities/intro-activities)
-- [`Fragment`](https://developer.android.com/guide/components/fragments)
-- [`RecyclerView`](https://developer.android.com/guide/topics/ui/layout/recyclerview)
-- [`FloatingActionButton`](https://developer.android.com/guide/topics/ui/floating-action-button)
-- [`Room`](https://developer.android.com/topic/libraries/architecture/room)
-
-
-## Az alkalmazás specifikációja
-Az alkalmazás egy `Activity`-ből áll, ami bevásárlólista elemeket jelenít meg. Új elemet a jobb alsó sarokban található `FloatingActionButton` segítségével vehetünk fel. Erre kattintva egy dialógus jelenik meg, amin megadhatjuk a vásárolni kívánt áru nevét, leírását, kategóriáját és becsült árát.
-A dialóguson az *OK* gombra kattintva a dialógus eltűnik, a benne megadott adatokkal létrejön egy lista elem a listában. Az egyes lista elemeken `CheckBox` segítségével jelezhetjük, hogy már megvásároltuk őket. A kuka ikonra kattintva törölhetjük az adott elemet.
-A menüben található „Remove all” opcióval az összes lista elemet törölhetjük.
-
-
-
-
-
-
-## Laborfeladatok
-A labor során az alábbi feladatokat a laborvezető segítségével, illetve a jelölt feladatokat önállóan kell megvalósítani.
-
-1. Perzisztens adattárolás megvalósítása: 1 pont
-2. Lista megjelenítése `RecyclerView`-val: 2 pont
-3. Dialógus megvalósítása új elem hozzáadásához: 1 pont
-4. **Önálló feladat** (törlés megvalósítása): 1 pont
-
-
-!!! warning "IMSc"
- A laborfeladatok sikeres befejezése után az IMSc feladatokat megoldva 2 IMSc pont szerezhető:
- Megerősítő dialógus: 1 pont
- Elemek szerkesztése: 1 pont
-
-
-## Előkészületek
-
-A feladatok megoldása során ne felejtsd el követni a [feladat beadás folyamatát](../../tudnivalok/github/GitHub.md).
-
-
-### Git repository létrehozása és letöltése
-
-1. Moodle-ben keresd meg a laborhoz tartozó meghívó URL-jét és annak segítségével hozd létre a saját repository-dat.
-
-2. Várd meg, míg elkészül a repository, majd checkout-old ki.
-
- !!! tip ""
- Egyetemi laborokban, ha a checkout során nem kér a rendszer felhasználónevet és jelszót, és nem sikerül a checkout, akkor valószínűleg a gépen korábban megjegyzett felhasználónévvel próbálkozott a rendszer. Először töröld ki a mentett belépési adatokat (lásd [itt](../../tudnivalok/github/GitHub-credentials.md)), és próbáld újra.
-
-3. Hozz létre egy új ágat `megoldas` néven, és ezen az ágon dolgozz.
-
-4. A `neptun.txt` fájlba írd bele a Neptun kódodat. A fájlban semmi más ne szerepeljen, csak egyetlen sorban a Neptun kód 6 karaktere.
-
-
-### Projekt létrehozása
-
-Első lépésként indítsuk el az Android Studio-t, majd:
-1. Hozzunk létre egy új projektet, válasszuk az *Empty Views Activity* lehetőséget.
-2. A projekt neve legyen `ShoppingList`, a kezdő package pedig `hu.bme.aut.android.shoppinglist`
-3. Nyelvnek válasszuk a *Kotlin*-t.
-4. A minimum API szint legyen **API24: Android 7.0**.
-
-!!!danger "FILE PATH"
- A projekt a repository-ban lévő ShoppingList könyvtárba kerüljön, és beadásnál legyen is felpusholva! A kód nélkül nem tudunk maximális pontot adni a laborra!
-
-Amint elkészült a projektünk, kapcsoljuk is be a `ViewBinding`-ot. Az `app` modulhoz tartozó `build.gradle.kts` fájlban az `android` tagen belülre illesszük be az engedélyezést (Ezek után kattintsunk jobb felül a `Sync Now` gombra.):
-```gradle
-android {
- ...
- buildFeatures {
- viewBinding = true
- }
-}
-```
-
-A kezdő Activity neve maradhat MainActivity, valamint töltsük le és tömörítsük ki [az alkalmazáshoz szükséges erőforrásokat](https://github.com/VIAUAC00/laborok/raw/master/docs/laborok/06-android-room/downloads/res.zip), majd másoljuk be őket a projekt *app/src/main/res* mappájába (Studio-ban a *res* mappán állva *Ctrl+V*)!
-
-
-
-### Perzisztens adattárolás megvalósítása (1 pont)
-Az adatok perzisztens tárolásához a `Room` könyvtárat fogjuk használni.
-
-#### Room hozzáadása a projekthez
-
-Kezdjük azzal, hogy az app modulhoz tartozó `build.gradle.kts` fájlban a pluginokhoz hozzáírunk egy sort (bekapcsoljuk a Kotlin Annotation Processort - KAPT):
-```gradle
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- kotlin("kapt")
-}
-
-//...
-```
-
-Ezt követően, szintén ebben a `build.gradle.kts` fájlban a `dependencies` blokkhoz adjuk hozzá a `Room` libraryt:
-```gradle
-dependencies {
- //...
- val room_version = "2.3.0"
- implementation("androidx.room:room-runtime:$room_version")
- implementation("androidx.room:room-ktx:$room_version")
- kapt("androidx.room:room-compiler:$room_version")
-}
-```
-Ezután kattintsunk a jobb felső sarokban megjelenő **Sync now** gombra.
-
-!!!info "Room"
- A `Room` egy kényelmes adatbazáskezelést lehetővé tevő API-t nyújt a platform szintű SQLite implementáció fölé. Megspórolható vele a korábban látott sok újra és újra megírandó kód, például a táblák adatait és létrehozó scriptjét tartalmazó *Table osztályok, a DBHelper és a PersistentDataHelper*. Ezeket és más segédosztályokat a `Room` *annotation* alapú kódgenerálással hozza létre a *build* folyamat részeként.
-
- A `Room` alapvető komponenseinek, architektúrájának és használatának leírása megtalálható a megfelelő [developer.android.com](https://developer.android.com/training/data-storage/room/) oldalon.
-
-#### Egy modell osztály létrehozása
-A `hu.bme.aut.android.shoppinglist` package-ben hozzunk létre egy új package-et `data` néven. A `data` package-ben hozzunk létre egy új Kotlin osztályt, aminek a neve legyen `ShoppingItem`:
-```kotlin
-@Entity(tableName = "shoppingitem")
-data class ShoppingItem(
- @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) var id: Long? = null,
- @ColumnInfo(name = "name") var name: String,
- @ColumnInfo(name = "description") var description: String,
- @ColumnInfo(name = "category") var category: Category,
- @ColumnInfo(name = "estimated_price") var estimatedPrice: Int,
- @ColumnInfo(name = "is_bought") var isBought: Boolean
-) {
- enum class Category {
- FOOD, ELECTRONIC, BOOK;
- companion object {
- @JvmStatic
- @TypeConverter
- fun getByOrdinal(ordinal: Int): Category? {
- var ret: Category? = null
- for (cat in values()) {
- if (cat.ordinal == ordinal) {
- ret = cat
- break
- }
- }
- return ret
- }
-
- @JvmStatic
- @TypeConverter
- fun toInt(category: Category): Int {
- return category.ordinal
- }
- }
- }
-}
-```
-Látható, hogy az osztályon, az osztály változóin, valamint az osztályon belül lévő *enum* osztály függvényein *annotációkat* helyeztünk el. Az `@Entity` jelzi a `Room` kódgenerátorának, hogy ennek az osztálynak a példányai adatbázis rekordoknak fognak megfelelni egy táblában és hogy az egyes változói felelnek majd meg a tábla oszlopainak. A `@ColumnInfo` *annotációval* megadjuk, hogy mi legyen a tagváltozónak megfelelő oszlop neve. `@PrimaryKey`-jel jelöljük a tábla egyszerű kulcs attribútumát.
-
-Az osztályban létrehoztunk egy `enum`-ot is, amivel egy kategóriát akarunk kódolni. Az enum-nak van két statikus metódusa, `@TypeConverter` annotációval ellátva. Ezekkel oldható meg, hogy az adatbázis akár összetett adatszerkezeteket is tárolni tudjon. Ezek a függvények felelősek azért, hogy egy felhasználói típust lefordítsanak egy, az adatbázis által támogatott típusra, illetve fordítva.
-Megfigyelhető továbbá, hogy ezen függvények el vannak látva a `@JvmStatic` annotációval is. Erre azért van szükség, mert alapvetően, amikor a companion object-ek Jvm bájtkódra fordulnak, akkor egy külön statikus osztály jön számukra létre. Ezzel az annotációval lehet megadni, hogy ne jöjjön létre külön statikus osztály, ehelyett a bennfoglaló osztály (jelen esetben Category) statikus függvényei legyenek. Erre a speciális viselkedésre pedig a Room működése miatt van szükség, ugyanis tudnia kell, hol keresse egy-egy típusra a konvertereket.
-
-!!!info "data class"
- Kotlinban van lehetőség úgynevezett data class létrehozására. Ezt talán legkönnyebben a Java-s POJO (Plain-Old-Java-Object) osztályoknak lehet megfeleltetni. A céljuk, hogy publikus property-kben összefüggő adatokat tároljanak, semmi több! Ezen kívül automatikusan létrejönnek bizonyos segédfüggvények is, például egy megfelelő equals, toString és copy implementáció.
-
-#### Egy DAO osztály létrehozása
-
-!!!info "DAO"
- DAO = [Data Access Object](https://en.wikipedia.org/wiki/Data_access_object)
-
-A `data` package-ben hozzunk létre egy új Kotlin interfészt, aminek a neve legyen `ShoppingItemDao`:
-
-```kotlin
-@Dao
-interface ShoppingItemDao {
- @Query("SELECT * FROM shoppingitem")
- fun getAll(): List
-
- @Insert
- fun insert(shoppingItems: ShoppingItem): Long
-
- @Update
- fun update(shoppingItem: ShoppingItem)
-
- @Delete
- fun deleteItem(shoppingItem: ShoppingItem)
-}
-```
-
-Egy `@Dao` *annotációval* ellátott interfész a `Room` kódgenerátora számára azt jelzi, hogy generálni kell az interfészhez egy olyan implementációt, ami az interfész függvényeket az azokon lévő annotációk (`@Query`, `@Insert`, `@Update`, `@Delete`) alapján valósítja meg.
-
-Figyeljük meg, hogy az Android Studio a `@Query` *annotáció* paramétereként átadott SQLite scriptre is nyújt kódkiegészítést, hiba jelzést!
-
-#### Az adatbázis osztály létrehozása
-
-A `data` package-ben hozzunk létre egy új Kotlin osztályt, aminek a neve legyen `ShoppingListDatabase`:
-
-```kotlin
-@Database(entities = [ShoppingItem::class], version = 1)
-@TypeConverters(value = [ShoppingItem.Category::class])
-abstract class ShoppingListDatabase : RoomDatabase() {
- abstract fun shoppingItemDao(): ShoppingItemDao
-
- companion object {
- fun getDatabase(applicationContext: Context): ShoppingListDatabase {
- return Room.databaseBuilder(
- applicationContext,
- ShoppingListDatabase::class.java,
- "shopping-list"
- ).build();
- }
- }
-}
-```
-
-A `@Database` *annotációval* lehet jelezni a kódgenerátornak, hogy egy osztály egy adatbázist fog reprezentálni. Az ilyen osztálynak *absztraktnak* kell lennie, valamint a `RoomDatabase`-ből kell származnia. Az *annotáció* `entities` paraméterének egy listát kell átadni, ami az adatbázis tábláknak megfelelő `@Entity`-vel jelzett osztályokat tartalmazza. A `version` paraméter értéke a lokális adatbázis verzió. A `@TypeConverters` *annotációval* lehet megadni a `Room`-nak olyan osztályokat, amik `@TypeConverter`-rel ellátott függvényeket tartalmaznak, ezzel támogatva a típuskonverziót adatbázis és objektum modell között. A `ShoppingListDatabase` osztály felelős a megfelelő DAO osztályok elérhetőségéért is.
-
-Ezen kívül van még egy statikus *getDatabase* függvény, ami azt írja le, hogyan kell létrehozni az adatbázist (melyik osztályból, milyen néven). Ez a függvény az alkalmazás kontextusát várja paraméterül.
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **database osztály kódja**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f1.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### Lista megjelenítése `RecyclerView`-val (2 pont)
-
-#### A lista adapter létrehozása
-Következő lépésként a lista adaptert fogjuk létrehozni, ami a modell elemeket fogja majd szolgáltatni a `RecyclerView`-nak.
-
-A `hu.bme.aut.android.shoppinglist` package-ben hozzunk létre egy új package-et `adapter` néven!
-
-Az `adapter` package-ben hozzunk létre egy új Kotlin osztályt `ShoppingAdapter` néven:
-
-```kotlin
-class ShoppingAdapter(private val listener: ShoppingItemClickListener) :
- RecyclerView.Adapter() {
-
- private val items = mutableListOf()
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ShoppingViewHolder(
- ItemShoppingListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- )
-
- override fun onBindViewHolder(holder: ShoppingViewHolder, position: Int) {
- // TODO implementation
- }
-
- override fun getItemCount(): Int = items.size
-
- interface ShoppingItemClickListener {
- fun onItemChanged(item: ShoppingItem)
- }
-
- inner class ShoppingViewHolder(val binding: ItemShoppingListBinding) : RecyclerView.ViewHolder(binding.root)
-}
-```
-
- A listát `RecyclerView` segítségével szeretnénk megjeleníteni, ezért az adapter a `RecyclerView.Adapter` osztályból származik. Az adapter a modell elemeket egy listában tárolja. A rendszer a `RecyclerView`-val való hatékony lista megjelenítéshez a [*ViewHolder* tervezési mintát](https://developer.android.com/training/improving-layouts/smooth-scrolling#java) valósítja meg, ezért szükség van egy `ViewHolder` osztály megadására is. `ViewHolder`-eken keresztül érhetjük majd el a lista elemekhez tartozó `View`-kat. Mivel a `ViewHolder` osztály példányai az Adapterhez lesznek csatolva (azért, hogy elérjék a belső változóit), `inner class` osztályként kell definiálni.
-
-A `RecyclerView.Adapter` három absztrakt függvényt definiál, amelyeket kötelező megvalósítani. Az `onCreateViewHolder()`-ben hozzuk létre az adott lista elemet megjelenítő `View`-t és a hozzá tartozó `ViewHolder`-t. Az `onBindViewHolder()`-ben kötjük hozzá a modell elemhez a nézetet, a `getItemCount()` pedig a listában található (általános esetre fogalmazva a megjelenítendő) elemek számát kell, hogy visszaadja.
-
-A `ShoppingAdapter`-ben definiáltunk egy `ShoppingItemClickListener` nevű interfészt is, aminek a segítségével jelezhetjük az alkalmazás többi része felé, hogy esemény történt egy lista elemen.
-
-Az ItemShoppingListBinding-ra hibát jelez a fordító, hiszen még nem hoztuk létre a hozzá tartozó layout erőforrást. Ezt tegyük is meg: Hozzuk létre `item_shopping_list.xml` néven és cseréljük le a fájl tartalmát az alábbira:
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-Hozzuk létre a `@string/bought` erőforrást! Kattintsunk rá az erőforrás hivatkozásra, majd *Alt + Enter* lenyomása után válasszuk a *„Create string value resource ’bought’”* lehetőséget! A felugró ablakban az erőforrás értékének adjuk a `Bought` értéket!
-
-Térjünk vissza az `ShoppingAdapter`-hez, és írjuk meg `onBindViewHolder`-ben az adatok megjelenítésének logikáját. Érdemes megfigyelni a `getImageResource` függvényt, ami az enum-hoz társítja a megfelelő képi erőforrást.
-
-```kotlin
-override fun onBindViewHolder(holder: ShoppingViewHolder, position: Int) {
- val shoppingItem = items[position]
-
- holder.binding.ivIcon.setImageResource(getImageResource(shoppingItem.category))
- holder.binding.cbIsBought.isChecked = shoppingItem.isBought
- holder.binding.tvName.text = shoppingItem.name
- holder.binding.tvDescription.text = shoppingItem.description
- holder.binding.tvCategory.text = shoppingItem.category.name
- holder.binding.tvPrice.text = "${shoppingItem.estimatedPrice} Ft"
-
- holder.binding.cbIsBought.setOnCheckedChangeListener { buttonView, isChecked ->
- shoppingItem.isBought = isChecked
- listener.onItemChanged(shoppingItem)
- }
-
-}
-
-@DrawableRes()
-private fun getImageResource(category: ShoppingItem.Category): Int {
- return when (category) {
- ShoppingItem.Category.FOOD -> R.drawable.groceries
- ShoppingItem.Category.ELECTRONIC -> R.drawable.lightning
- ShoppingItem.Category.BOOK -> R.drawable.open_book
- }
-}
-```
-Látható, hogy a felületet a *holder* nevű *ViewHolder* objektum *binding* attribútumán keresztül érjük el, innen tudjuk használni a *resource id*-kat.
-
-Biztosítsuk egy elem hozzáadásának, valamint a teljes lista frissítésének lehetőségét az alábbi függvényekkel:
-
-```kotlin
-fun addItem(item: ShoppingItem) {
- items.add(item)
- notifyItemInserted(items.size - 1)
-}
-
-fun update(shoppingItems: List) {
- items.clear()
- items.addAll(shoppingItems)
- notifyDataSetChanged()
-}
-```
-!!!info "RecyclerView notify"
- A RecyclerView megírásánál figyeltek arra, hogy hatékony legyen, ezért az adathalmaz változásakor csak azokat a nézeteket frissíti, amit feltétlen szükséges. Azonban szintén hatékonyság miatt, nem az adapter fogja kiszámolni a változást, hanem ezt a programozónak kell kézzel jeleznie. Erre szolgál a `notify***` függvénycsalád, aminek két tagja fent látható. Az alsó hatására a teljes adathalmaz lecserélődik, és újrarajzolódik minden. Az első hatására viszont a már létező elemek nem módosulnak, csak egy újonnan beszúrt elem lesz kirajzolva.
-
-#### A `RecyclerView` és az adatok megjelenítése
-
-Kezdjük azzal, hogy kiegészítjük a themes.xml fájl tartalmát az alábbira:
-
-```xml
-
-
-
-
-
-
-```
-
-Szeretnénk, hogy a bevásárlólista alkalmazás egyetlen `Activity`-jét teljesen elfoglalja. Ennek az eléréséhez cseréljük le az `activity_main.xml` tartalmát az alábbiakra:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-Megfigyelhető, hogy a témában kikapcsoltuk az ActionBar megjelenését, helyette az xml fájlban szerepel egy [Toolbar](https://developer.android.com/reference/android/widget/Toolbar) típusú elem, egy AppBarLayout-ba csomagolva. Mostanában tanácsos nem a beépített ActionBar-t használni, hanem helyette egy Toolbar-t lehelyezni, mert ez több, hasznos funkciót is támogat, például integrálódni tud egy NavigationDrawer-rel, vagy az újabb navigációs komponenssel (amit ebből a tárgyból nem veszünk).
-
-A `tools:listitem` paraméter segítségével az Android Studio layout megjelenítő felületén megjelenik a paraméterben átadott listaelem.
-
-Adjuk hozzá az alábbi változókat a `MainActivity`-hez és cseréljük le a projekt létrehozásakor generált `onCreate()` függvényt:
-
-```kotlin
-private lateinit var binding: ActivityMainBinding
-
-private lateinit var database: ShoppingListDatabase
-private lateinit var adapter: ShoppingAdapter
-
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
- setSupportActionBar(binding.toolbar)
-
- database = ShoppingListDatabase.getDatabase(applicationContext)
-
- binding.fab.setOnClickListener {
- //TODO
- }
-}
-```
-
-A `MainActivity`-hez adjuk hozzá a `RecyclerView`-t inicializáló kódrészletet:
-```kotlin
-private fun initRecyclerView() {
- adapter = ShoppingAdapter(this)
- binding.rvMain.layoutManager = LinearLayoutManager(this)
- binding.rvMain.adapter = adapter
- loadItemsInBackground()
-}
-
-private fun loadItemsInBackground() {
- thread {
- val items = database.shoppingItemDao().getAll()
- runOnUiThread {
- adapter.update(items)
- }
- }
-}
-```
-Mivel az adatbázis kérés nem történhet az alkalmazás főszálán, a Kotlin által biztosított `thread()` segédfüggvénnyel létrehozunk egy új szálat, a kiolvasott listát pedig az Activity által biztosított `runOnUiThread` függvény segítségével a főszálon adjuk át az adapternek.
-Ez nem tökéletes megoldás, mivel ha elhagynánk az activity-t a kiolvasás során, a thread életben maradna, ami akár memóriaszivárgást is okozhat.
-Egy jobb megoldást biztosít a [Kotlin Coroutine](https://kotlinlang.org/docs/coroutines-guide.html) támogatása, ennek bemutatására azonban sajnos a labor keretei között nincsen idő.
-
-A `ShoppingAdapter` létrehozásakor a `MainActivity`-t adjuk át az adapter konstruktor paramétereként, de a `MainActivity` még nem implementálja a szükséges interfészt. Pótoljuk a hiányosságot:
-
-```kotlin
-class MainActivity : AppCompatActivity(), ShoppingAdapter.ShoppingItemClickListener {
-
-//...
-
- override fun onItemChanged(item: ShoppingItem) {
- thread {
- database.shoppingItemDao().update(item)
- Log.d("MainActivity", "ShoppingItem update was successful")
- }
- }
-}
-```
-
-Hívjuk meg az `initRecyclerView()` függvényt az `onCreate()` függvény utolsó lépéseként:
-
-```kotlin
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
- setSupportActionBar(binding.toolbar)
-
- database = ShoppingListDatabase.getDatabase(applicationContext)
-
- binding.fab.setOnClickListener {
- //TODO
- }
-
- initRecyclerView()
-}
-```
-Ezen a ponton az alkalmazásunk már meg tudja jeleníteni az adatbázisban tárolt vásárolni valókat, azonban sajnos még egy elemünk sincs, mivel lehetőségünk sem volt felvenni őket. A következő lépés az új elem létrehozását biztosító funkció implementálása.
-
-!!!example "BEADANDÓ (2 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **lista** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f2.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### Dialógus megvalósítása új elem hozzáadásához (1 pont)
-A dialógus megjelenítéséhez `DialogFragment`-et fogunk használni.
-
-Hozzuk létre a dialógushoz tartozó *layoutot* `dialog_new_shopping_item.xml`, majd másoljuk be a dialógushoz tartozó layoutot:
-
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-Vegyük fel a hiányzó szöveges erőforrásokat a `strings.xml`-ben:
-```xml
-
- ...
- Name
- Description
- Category
- Estimated Price
- Already purchased
- New Shopping Item
- OK
- Cancel
-
-
- - Food
- - Electronic
- - Book
-
- ...
-
-```
-Látható, hogy felveszünk egy `string-array`-t is, ezeket a szövegeket a Spinnerben fogjuk megjeleníteni.
-
-A `hu.bme.aut.android.shoppinglist` package-ben hozzunk létre egy új package-et `fragments` néven. A `fragments` package-ben hozzunk létre egy új Kotlin osztályt, aminek a neve legyen `NewShoppingItemDialogFragment`:
-
-```kotlin
-class NewShoppingItemDialogFragment : DialogFragment() {
- interface NewShoppingItemDialogListener {
- fun onShoppingItemCreated(newItem: ShoppingItem)
- }
-
- private lateinit var listener: NewShoppingItemDialogListener
-
- private lateinit var binding: DialogNewShoppingItemBinding
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- listener = context as? NewShoppingItemDialogListener
- ?: throw RuntimeException("Activity must implement the NewShoppingItemDialogListener interface!")
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- binding = DialogNewShoppingItemBinding.inflate(LayoutInflater.from(context))
- binding.spCategory.adapter = ArrayAdapter(
- requireContext(),
- android.R.layout.simple_spinner_dropdown_item,
- resources.getStringArray(R.array.category_items)
- )
-
- return AlertDialog.Builder(requireContext())
- .setTitle(R.string.new_shopping_item)
- .setView(binding.root)
- .setPositiveButton(R.string.button_ok) { dialogInterface, i ->
- // TODO implement item creation
- }
- .setNegativeButton(R.string.button_cancel, null)
- .create()
- }
-
- companion object {
- const val TAG = "NewShoppingItemDialogFragment"
- }
-}
-```
-
-A `DialogFragment`-et az `androidx.fragment.app` csomagból, az `AlertDialog`-ot pedig az `androidx.appcompat.app` csomagból importáljuk! Ha az auto-import beimportálja az android.R package-t, azt töröljük ki.
-
-Az osztályban definiáltunk egy `NewShoppingItemDialogListener` nevű *callback interface*-t, amelyen keresztül a dialógust megjelenítő `Activity` értesülhet az új elem létrehozásáról.
-
-A megjelenő dialógust az `onCreateDialog()` függvényben állítjuk össze. Ehhez az `AlertDialog.Builder` osztályt használjuk fel. Az új elemet az *OK* gomb `ClickListener`-jében fogjuk létrehozni, amennyiben a bevitt adatok érvényesek. Jelen esetben az érvényesség a név mező kitöltöttségét jelenti.
-
-Implementáljuk a dialógus pozitív gombjának eseménykezelőjét a `NewShoppingItemDialogFragment` osztály `onCreateDialog` függvényén belül:
-
-```kotlin
-.setPositiveButton(R.string.button_ok) { dialogInterface, i ->
- if (isValid()) {
- listener.onShoppingItemCreated(getShoppingItem())
- }
-}
-```
-
-Implementáljuk a hiányzó függvényeket:
-
-```kotlin
-private fun isValid() = binding.etName.text.isNotEmpty()
-
-private fun getShoppingItem() = ShoppingItem(
- name = binding.etName.text.toString(),
- description = binding.etDescription.text.toString(),
- estimatedPrice = binding.etEstimatedPrice.text.toString().toIntOrNull() ?: 0,
- category = ShoppingItem.Category.getByOrdinal(binding.spCategory.selectedItemPosition)
- ?: ShoppingItem.Category.BOOK,
- isBought = binding.cbAlreadyPurchased.isChecked
-)
-```
-
-
-A fenti kódrészletben két dolgot érdemes megfigyelni. Egyrészt, a konstruktor paramétereit (és Kotlinban általánosan bármely függvény paramétereit) név szerint is át lehet adni, így nem szükséges megjegyezni a paraméterek sorrendjét, ha esetleg sok paraméterünk lenne. Amennyiben a függvényparamétereknek még alapértelmezett értéket is adunk, úgy még kényelbesebbé válhat ez a funkció, hiszen csak az "érdekes" paraméterek kapnak értéket. Ez a módszer esetleg a Python nyelvből lehet ismerős.
-
-!!!info "Elvis operátor"
- Egy másik érdekesség a `?:`, avagy az [Elvis operátor](https://kotlinlang.org/docs/null-safety.html#elvis-operator). Ez azt csinálja, hogy amennyiben a bal oldali kifejezés nem null-ra értékelődik ki, akkor értékül a bal oldali kifejezést adja, ha pedig null-ra értékelődik ki, akkor a jobb oldali kifejezést. Így egyszerű null értéktől függő értékadást tömören le lehet írni.
-
-A `MainActivity` `onCreate()` függvényében frissítsük a `FloatingActionButton` `OnClickListener`-jét, hogy az a fentebb megvalósított dialógust dobja fel:
-```kotlin
-binding.fab.setOnClickListener{
- NewShoppingItemDialogFragment().show(
- supportFragmentManager,
- NewShoppingItemDialogFragment.TAG
- )
-}
-```
-Frissítsük a `MainActivity`-t, hogy implementálja a dialógusban definiált interface-t:
-```kotlin
-class MainActivity : AppCompatActivity(), ShoppingAdapter.ShoppingItemClickListener,
- NewShoppingItemDialogFragment.NewShoppingItemDialogListener {
-
-//...
-
- override fun onShoppingItemCreated(newItem: ShoppingItem) {
- thread {
- val insertId = database.shoppingItemDao().insert(newItem)
- newItem.id = insertId
- runOnUiThread {
- adapter.addItem(newItem)
- }
- }
- }
-```
-
-
-Figyeljük meg, hogy ebben az esetben is `thread`-be csomagolva futtatunk adatbázis műveletet. A `Room` tiltja a UI szálon történő adatbázis műveletek futtatását. Emellett a *user experience (UX)* is romlik, ha az esetlegesen lassú műveletek megakasztják a UI szálat.
-
-Az adatbázisba való beillesztés után szükséges az eredeti objektumunk id-jét az adatbázistól kapott id-re beállítani, különben egyéb műveletek nem végezhetők rajta.
-
-Próbáljuk ki az alkalmazást!
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **dialógus ablak** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### Önálló feladat: törlés megvalósítása (1 pont)
-Elem törlése egyesével, az elemeken található szemetes ikonra kattintás hatására.
-???success "Megoldás"
- - Gomb eseménykezelőjének megvalósítása
- - Interfész kibővítése
- - Interfész függvény megvalósítása
- - Törlés az adatbázisból
- - Törlés az adapterből
- - `RecyclerView` frissítése
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik az **üres lista** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **a törléshez tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f4.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### IMSc feladatok
-#### Megerősítő dialógus (1 pont)
-Implementáljunk egy *Remove all* feliratú menüpontot és a hozzá tartozó funkciót!
-
-Az alkalmazás jelenítsen meg egy megerősítő dialógust, amikor a felhasználó a *Remove all* menüpontra kattint. A dialógus tartalmazzon egy rövid szöveges figyelmeztetést, hogy minden elem törlődni fog, egy pozitív és negatív gombot (*OK* és *Cancel*). A pozitív gomb lenyomásakor törlődjenek csak az elemek.
-
-!!!example "BEADANDÓ (1 iMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik az **megerősítő dialógus** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f6.png néven töltsd föl.
-
-
-#### Elemek szerkesztése (1 pont)
-Teremtsük meg a lista elemek szerkesztésének lehetőségét. A lista elemre helyezzünk egy szerkesztés gombot, melynek hatására nyíljon meg a már korábban implementált felviteli dialógus, a beviteli mezők pedig legyenek előre kitöltve a mentett értékekkel. Az *OK* gomb hatására a meglévő lista elem módosuljon az adatbázisban és a nézeten is.
-
-!!!example "BEADANDÓ (1 iMSc pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **szerkesztési dialógus** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), egy **ahhoz tartozó kódrészlet**, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f7.png néven töltsd föl.
+## TODO 2024
\ No newline at end of file
diff --git a/docs/laborok/07-android-network/index.md b/docs/laborok/07-android-network/index.md
index 6df08cc..9e51393 100644
--- a/docs/laborok/07-android-network/index.md
+++ b/docs/laborok/07-android-network/index.md
@@ -1,1028 +1,3 @@
# Labor 07 - Weather Info alkalmazás készítése
-## Bevezető
-
-A labor során egy időjárás információkat megjelenítő alkalmazás elkészítése a feladat. A korábban látott UI elemek használata mellett láthatunk majd példát hálózati kommunkáció hatékony megvalósítására is a [`Retrofit`](https://square.github.io/retrofit/) library felhasználásával.
-
-Az alkalmazás városok listáját jeleníti meg egy [`RecyclerView`](https://developer.android.com/guide/topics/ui/layout/recyclerview)-ban, egy kiválasztott város részletes időjárás adatait pedig az [OpenWeatherMap](https://openweathermap.org/) REST API-jának segítségével kérdezi le. A részletező nézeten egy [`ViewPager`](https://developer.android.com/training/animation/screen-slide)-ben két [`Fragment`](https://developer.android.com/guide/components/fragments)-en lehet megtekinteni a részleteket. Új város hozzáadására egy [`FloatingActionButton`](https://developer.android.com/guide/topics/ui/floating-action-button) megnyomásával van lehetőség.
-
-!!!info "REST"
- REST = [Representational State Transfer](https://en.wikipedia.org/wiki/Representational_state_transfer)
-
-
-
-
-
-
-
-Felhasznált technológiák:
-
-- [`Activity`](https://developer.android.com/guide/components/activities/intro-activities)
-- [`Fragment`](https://developer.android.com/guide/components/fragments)
-- [`RecyclerView`](https://developer.android.com/guide/topics/ui/layout/recyclerview)
-- [`ViewPager`](https://developer.android.com/training/animation/screen-slide)
-- [`Retrofit`](https://square.github.io/retrofit/)
-- [`Gson`](https://github.com/google/gson)
-- [`Glide`](https://github.com/bumptech/glide)
-
-## Az alkalmazás specifikációja
-
-Az alkalmazás két `Activity`-ből áll.
-
-Az alkalmazás indulásakor megjelenő `Activity` a felhasználó által felvett városok listáját jeleníti meg. Minden lista elemhez tartozik egy *Remove* gomb, aminek a megnyomására az adott város törlődik a listából. Új várost a nézet jobb alsó sarkában található `FloatingActionButton` megnyomásával lehet felvenni.
-
-Egy városra való kattintás hatására megnyílik egy új `Activity` két `Fragment`-tel, amik között `ViewPager`-rel lehet váltani. Az első `Fragment` a kiválasztott város időjárásának leírását és az ahhoz tartozó ikont jeleníti meg. A második `Fragment`-en a városban mért átlagos, minimum és maximum hőmérséklet, a légnyomás és a páratartalom értéke látható.
-
-## Laborfeladatok
-
-A labor során az alábbi feladatokat a laborvezető segítségével, illetve a jelölt feladatokat önállóan kell megvalósítani.
-
-1. Város lista megvalósítása: 1 pont
-2. Részletező nézet létrehozása és bekötése a navigációba: 1 pont
-3. Hálózati kommunikáció megvalósítása: 1 pont
-4. A hálózati réteg bekötése a részletező nézetbe: 1 pont
-5. Önálló feladat: város listából törlés megvalósítása: 1 pont
-
-A labor során egy komplex időjárás alkalmazás készül el. A labor szűkös időkerete miatt szükség lesz nagyobb kódblokkok másolására, azonban minden esetben figyeljünk a laborvezető magyarázatára, hogy a kódrészek érthetőek legyenek. A cél a bemutatott kódok megértése és a felhasznált libraryk használatának elsajátítása.
-
-*Elnézést kérünk az eddigieknél nagyobb kód blokkokért, de egy ilyen, bemutató jellegű feladat kisebb méretben nem oldható meg, illetve a labor elveszítené a lényegét, ha csak egy „hello world” hálózati kommunikációs lekérést valósítanánk meg. Köszönjük a megértést.*
-
-## Előkészületek
-
-A feladatok megoldása során ne felejtsd el követni a [feladat beadás folyamatát](../../tudnivalok/github/GitHub.md).
-
-
-### Git repository létrehozása és letöltése
-
-1. Moodle-ben keresd meg a laborhoz tartozó meghívó URL-jét és annak segítségével hozd létre a saját repository-dat.
-
-2. Várd meg, míg elkészül a repository, majd checkout-old ki.
-
- !!! tip ""
- Egyetemi laborokban, ha a checkout során nem kér a rendszer felhasználónevet és jelszót, és nem sikerül a checkout, akkor valószínűleg a gépen korábban megjegyzett felhasználónévvel próbálkozott a rendszer. Először töröld ki a mentett belépési adatokat (lásd [itt](../../tudnivalok/github/GitHub-credentials.md)), és próbáld újra.
-
-3. Hozz létre egy új ágat `megoldas` néven, és ezen az ágon dolgozz.
-
-4. A `neptun.txt` fájlba írd bele a Neptun kódodat. A fájlban semmi más ne szerepeljen, csak egyetlen sorban a Neptun kód 6 karaktere.
-
-
-### Projekt létrehozása
-
-Első lépésként indítsuk el az Android Studio-t, majd:
-1. Hozzunk létre egy új projektet, válasszuk az *Empty Views Activity* lehetőséget.
-2. A projekt neve legyen `WeatherInfo`, a kezdő package pedig `hu.bme.aut.android.weatherinfo`
-3. A többi beállítást hagyjuk érintetlenül.
-
-!!!danger "FILE PATH"
- A projekt a repository-ban lévő WeatherInfo könyvtárba kerüljön, és beadásnál legyen is felpusholva! A kód nélkül nem tudunk maximális pontot adni a laborra!
-
-Nevezzük át a generált Activityt `CityActivity`, a hozzá tartozó layout fájlt pedig ` activity_city` névre.
-
-Kapcsoljuk be a `ViewBinding`-ot. Ehhez az `app` modulhoz tartozó `build.gradle` fájlban az `android` blokkon belülre illesszük be az engedélyező kódrészletet (majd kattintsunk a jobb felül megjelenő `Sync Now` gombra).
-
-```kotlin
-buildFeatures {
- viewBinding = true
-}
-```
-
-Töltsük le és tömörítsük ki [az alkalmazáshoz szükséges erőforrásokat](./assets/drawables.zip) , majd másoljuk be őket a projekt *app/src/main/res* mappájába (Studio-ban a *res* mappa kijelölése után *Ctrl+V*)!
-
-Az *app* modulhoz tartozó `build.gradle` fájlban a `dependencies` blokkhoz adjuk hozzá a `Retrofit` és `Glide` libraryket:
-
-```kotlin
-dependencies{
- //...
- val retrofitVersion = "2.9.0"
- implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
- implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
-
- val glideVersion = "4.16.0"
- implementation("com.github.bumptech.glide:glide:$glideVersion")
- annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion")
-}
-```
-
-Ezután kattintsunk a jobb felső sarokban megjelenő **Sync now** gombra.
-
-!!!info "Retrofit"
- A `Retrofit` a fejlesztő által leírt egyszerű, megfelelően annotált interfészek alapján kódgenerálással állít elő HTTP hivásokat lebonyolító implementációt. Kezeli az URL-ben inline módon adott paramétereket, az URL queryket, stb. Támogatja a legnépszerűbb szerializáló/deszerializáló megoldásokat is (pl.: [`Gson`](https://github.com/google/gson), [`Moshi`](https://github.com/square/moshi), [`Simple XML`](simple.sourceforge.net), stb.), amikkel Java/Kotlin objektumok, és JSON vagy XML formátumú adatok közötti kétirányú átalakítás valósítható meg. A laboron ezek közül a Gsont fogjuk használni a JSON-ban érkező időjárás adatok konvertálására.
-
-!!!info "Glide"
- A `Glide` egy hatékony képbetöltést és -cache-elést megvalósító library Androidra. Egyszerű interfésze és hatékonysága miatt használjuk.
-
-Az alkalmazásban szükségünk lesz internet elérésre. Vegyük fel az `AndroidManifest.xml` állományban az *Internet permission*-t az `application` tagen *kívülre*:
-
-```xml
-
-```
-
-!!!info "Engedélyek"
- Androidon API 23-tól (6.0, Marshmallow) az engedélyek két csoportba lettek osztva. A *normal* csoportba tartozó engedélyeket elég felvenni az `AndroidManifest.xml` fájlba az előbb látott módon és az alkalmazás automatikusan megkapja őket. A *dangerous* csoportba tartozó engedélyek esetén ez már nem elég, futás időben explicit módon el kell kérni őket a felhasználótól, aki akármikor meg is tagadhatja az alkalmazástól a kért engedélyt. Az engedélyek kezeléséről bővebben a [developer.android.com](https://developer.android.com/guide/topics/permissions/overview) oldalon lehet tájékozódni.
-
-Vegyük fel az alábbi szöveges erőforrásokat a `res/values/strings.xml`-be:
-
-```xml
-
- WeatherInfo
-
- Settings
-
- Cities
- Remove
-
- New city
- City
- OK
- Cancel
-
- DetailsActivity
- Weather
- Temperature
- Min temperature
- Max temperature
- Pressure
- Humidity
- Main
- Details
-
-
-```
-
-#### OpenWeatherMap API kulcs
-
-Regisztráljunk saját felhasználót az [OpenWeatherMap](https://openweathermap.org/) oldalon, és hozzunk létre egy API kulcsot, aminek a segítségével használhatjuk majd a szolgáltatást az alkalmazásunkban!
-
-1. Kattintsunk a *Sign in* majd a *Create an account* gombra.
-2. Töltsük ki a regisztrációs formot
-3. A *Company* mező értéke legyen "BME", a *Purpose* értéke legyen "Education/Science"
-4. Sikeres regisztráció után az *API keys* tabon található az alapértelmezettként létrehozott API kulcs.
-
-A kapott API kulcsra később szükségünk lesz az időjárás adatokat lekérő API hívásnál.
-
-### 1. Városlista megvalósítása (1 pont)
-
-Valósítsuk meg az egy `RecyclerView`-ból álló, városok listáját megjelenítő `CityActivity`-t!
-
-A város nevére kattintva jelenik majd meg egy részletező nézet (*DetailsAcitivity*), ahol az időjárás információk letöltése fog történni. Új város felvételére egy *FloatingActionButton* fog szolgálni.
-
-Cseréljük le az `activity_city.xml` tartalmát egy `RecyclerView`-ra és egy `FloatingActionButton`-re:
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-Az egyes funkciókhoz tartozó osztályokat külön package-ekbe fogjuk szervezni. Előfordulhat, hogy a másolások miatt az Android Studio nem ismeri fel egyből a package szerkezetet, így ha ilyen problémánk lenne, az osztály néven állva Alt+Enter után állítassuk be a megfelelő package nevet.
-
-A `hu.bme.aut.android.weatherinfo` package-ben hozzunk létre egy `feature` nevű package-et. A `feature` package-ben hozzunk létre egy `city` nevű package-et. *Drag and drop* módszerrel helyezzük át a `CityActivity`-t a `city` *package*-be, a felugró dialógusban pedig kattintsunk a *Refactor* gombra.
-
-A `CityActivity` kódját cseréljük le a következőre:
-
-```kotlin
-class CityActivity : AppCompatActivity(), CityAdapter.OnCitySelectedListener,
- AddCityDialogFragment.AddCityDialogListener {
-
- private lateinit var binding: ActivityCityBinding
- private lateinit var adapter: CityAdapter
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityCityBinding.inflate(layoutInflater)
- setContentView(binding.root)
- initFab()
- initRecyclerView()
- }
-
- private fun initFab() {
- binding.fab.setOnClickListener {
- // TODO: Show new city dialog
- }
- }
-
- private fun initRecyclerView() {
- binding.mainRecyclerView.layoutManager = LinearLayoutManager(this)
- adapter = CityAdapter(this)
- adapter.addCity("Budapest")
- adapter.addCity("Debrecen")
- adapter.addCity("Sopron")
- adapter.addCity("Szeged")
- binding.mainRecyclerView.adapter = adapter
- }
-
- override fun onCitySelected(city: String?) {
- // Todo: Start DetailsActivity with the selected city
- }
-
- override fun onCityAdded(city: String?) {
- adapter.addCity(city!!)
- }
-}
-```
-
-A `city` package-ben hozzuk létre a `CityAdapter` osztályt:
-
-```kotlin
-class CityAdapter(private val listener: OnCitySelectedListener) : RecyclerView.Adapter() {
- private val cities: MutableList = ArrayList()
-
- interface OnCitySelectedListener {
- fun onCitySelected(city: String?)
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CityViewHolder {
- val view = LayoutInflater.from(parent.context).inflate(R.layout.item_city, parent, false)
- return CityViewHolder(view)
- }
-
- override fun onBindViewHolder(holder: CityViewHolder, position: Int) {
- val item = cities[position]
- holder.bind(item)
- }
-
- override fun getItemCount(): Int = cities.size
-
- fun addCity(newCity: String) {
- cities.add(newCity)
- notifyItemInserted(cities.size - 1)
- }
-
- fun removeCity(position: Int) {
- cities.removeAt(position)
- notifyItemRemoved(position)
- if (position < cities.size) {
- notifyItemRangeChanged(position, cities.size - position)
- }
- }
-
- inner class CityViewHolder(private val itemView: View) : RecyclerView.ViewHolder(itemView) {
- var binding = ItemCityBinding.bind(itemView)
- var item: String? = null
-
- init {
- binding.root.setOnClickListener { listener.onCitySelected(item) }
- }
-
- fun bind(newCity: String?) {
- item = newCity
- binding.CityItemNameTextView.text = item
- }
- }
-}
-```
-
-Hozzuk létre a `res/layout` mappában az `item_city.xml` layoutot:
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-Igény szerint vizsgáljuk meg a laborvezetővel a `CityAdapter` osztályban az alábbiakat:
-- Hogyan történik a lista tartalmi elemeinek kezelése?
-- Hogyan épül fel egy lista elem?
-- Hogyan történik a lista elemen a kiválasztás események kezelése? Hogyan értesítjük a `CityActivity`-t egy elem kiválasztásáról?
-- Hogyan kerültek megvalósításra az `addCity(...)` és `removeCity(…)` metódusok?
-
-A `CityActivity`-vel kapcsolatos következő lépés az új város nevét bekérő dialógus (`DialogFragment`) megvalósítása és bekötése.
-
-Hozzunk létre egy `dialog_new_city.xml` nevű layout fájlt a `res/layout` mappában a következő tartalommal:
-
-```xml
-
-
-
-
-
-
-```
-
-A `city` package-ben hozzuk létre az `AddCityDialogFragment` osztályt:
-
-```kotlin
-class AddCityDialogFragment : AppCompatDialogFragment() {
-
- private lateinit var binding: DialogNewCityBinding
- private lateinit var listener: AddCityDialogListener
-
- interface AddCityDialogListener {
- fun onCityAdded(city: String?)
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- binding = DialogNewCityBinding.inflate(LayoutInflater.from(context))
-
- listener = context as? AddCityDialogListener
- ?: throw RuntimeException("Activity must implement the AddCityDialogListener interface!")
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return AlertDialog.Builder(requireContext())
- .setTitle(R.string.new_city)
- .setView(binding.root)
- .setPositiveButton(R.string.ok) { _, _ ->
- listener.onCityAdded(
- binding.NewCityDialogEditText.text.toString()
- )
- }
- .setNegativeButton(R.string.cancel, null)
- .create()
- }
-}
-```
-
-Igény szerint vizsgáljuk meg a laborvezetővel az `AddCityDialogFragment` implementációjában az alábbiakat:
-- Hogyan ellenőrizzük azt, hogy az `Activity`, amihez a `DialogFragment` felcsatolódott implementálja-e az `AddCityDialogListener` interfészt?
-- Hogyan kerül beállításra az egyedi layout a `DialogFragment`-ben?
-- Hogyan térünk vissza a beírt városnévvel?
-
-!!!note ""
- Szorgalmi feladat otthonra: az alkalmazás ne engedje a város létrehozását, ha a városnév mező üres!
- Tipp: [http://stackoverflow.com/questions/13746412/prevent-dialogfragment-from-dismissing-when-button-is-clicked](http://stackoverflow.com/questions/13746412/prevent-dialogfragment-from-dismissing-when-button-is-clicked)
-
-Végül egészítsük ki a `CityActivity` `initFab(…)` függvényét úgy, hogy a gombra kattintva jelenjen meg az új dialógus:
-
-```kotlin
-private fun initFab() {
- binding.fab.setOnClickListener {
- AddCityDialogFragment().show(supportFragmentManager, AddCityDialogFragment::class.java.simpleName)
- }
-}
-```
-
-Indítsuk el az alkalmazást, amely már képes városnevek bekérésére és megjelenítésére.
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik a **városnevek listája egy újonnan hozzáadott várossal**, az **AddCityDialogFragment** kódja, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f1.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### 2. Részletező nézet létrehozása és bekötése a navigációba (1 pont)
-
-A következő lépésben a `hu.bme.aut.android.weatherinfo.feature` package-en belül hozzunk létre egy `details` nevű packaget. (Ehhez a legegyszerűbb megoldás, ha a Project nézeten az Options-re (3 pötty) kattintva kiszedjük a pipát a Tree Appearance -> Compact Middle Packages elemnél.)
-
-A `details` package-ben hozzunk létre egy *Empty Views Activity* típusú `Activity`-t `DetailsActivity` néven.
-
-A hozzá tartozó `activity_details.xml` layout kódja:
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-Hozzunk létre a hiányzó *dimen* erőforrásokat (*Alt+Enter* -> *Create dimen value...*), értékük legyen *16dp*!
-
-A felület gyakorlatilag egy `ViewPager`-t tartalmaz, melyben két `Fragment`-et fogunk megjeleníteni. A `TabLayout` biztosítja a *Tab* jellegű fejlécet.
-
-A `DetailsActivity.kt` kódja legyen a következő:
-
-```kotlin
-class DetailsActivity : AppCompatActivity() {
-
- private lateinit var binding: ActivityDetailsBinding
- private var city: String? = null
-
- companion object {
- private const val TAG = "DetailsActivity"
- const val EXTRA_CITY_NAME = "extra.city_name"
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityDetailsBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- city = intent.getStringExtra(EXTRA_CITY_NAME)
-
- supportActionBar?.title = getString(R.string.weather, city)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
-
- override fun onResume() {
- super.onResume()
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == android.R.id.home) {
- finish()
- return true
- }
- return super.onOptionsItemSelected(item)
- }
-}
-```
-
-Cseréljük le a `strings.xml`-ben a *weather* szöveges erőforrást:
-
-```xml
-Weather in %s
-```
-
-A string erőforrásba írt *%s* jelölő használatával lehetővé válik egy *String argumentum* beillesztése a stringbe, ahogy a fenti kódrészletben láthatjuk.
-
-!!!note ""
- Figyeljük meg, hogy a `DetailsActivity` hogyan állítja be az `ActionBar` címét a paraméterül kapott város nevével, illetve és azt, hogy az `ActionBar` bal felső sarkában a *vissza gomb* kezelése hogyan került megvalósításra.
-
-Valósítsuk meg a `CityActivity` `onCitySelected(…)` függvényében azt, hogy egy városnév kiválasztásakor a `DetailsActivity` megfelelően felparaméterezve induljon el:
-
-```kotlin
- override fun onCitySelected(city: String?) {
- val showDetailsIntent = Intent()
- showDetailsIntent.setClass(this@CityActivity, DetailsActivity::class.java)
- showDetailsIntent.putExtra(DetailsActivity.EXTRA_CITY_NAME, city)
- startActivity(showDetailsIntent)
- }
-```
-
-Próbáljuk ki az alkalmazást, kattintsunk egy város nevére!
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik az **üres részletes nézet a megfelelő fejléccel**, a **DetailsActivity** kódja, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f2.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### 3. Hálózati kommunikáció megvalósítása (1 pont)
-
-#### Modell osztályok létrehozása
-
-A modell osztályok számára a `hu.bme.aut.android.weatherinfo` package-ben hozzunk létre új package-et `model` néven.
-
-A `model` package-ben hozzunk létre egy új osztályt `WeatherData` néven:
-
-```kotlin
-data class WeatherData (
- var coord: Coord,
- var weather: List? = null,
- var base: String,
- var main: MainWeatherData? = null,
- var visibility: Int,
- var wind: Wind? = null,
- var clouds: Cloud,
- var dt: Int,
- var sys: Sys,
- var timezone: Int,
- var id: Int,
- var name: String,
- var cod: Int
-)
-```
-
- Az időjárás szolgáltatástól kapott *JSON* válasz alapján egy ilyen `WeatherData` példány fog létrejönni a `Retrofit` és a `Gson` együttműködésének köszönhetően.
-
-A `model` package-ben hozzuk létre a `Weather` osztályt:
-
-```kotlin
-data class Weather (
- val id: Long = 0,
- val main: String? = null,
- val description: String? = null,
- val icon: String? = null
-)
-```
-
-Szintén a `model` package-ben hozzuk létre a `MainWeatherData` osztályt:
-
-```kotlin
-data class MainWeatherData (
- val temp: Float = 0f,
- val pressure: Float = 0f,
- val humidity: Float = 0f,
- val temp_min: Float = 0f,
- val temp_max: Float = 0f
-)
-```
-
-Szintén a `model` package-ben hozzuk létre a `Coord` osztályt:
-
-```kotlin
-data class Coord (
- var lon: Float = 0f,
- var lat: Float = 0f
-)
-```
-
-Szintén a `model` package-ben hozzuk létre a `Cloud` osztályt:
-
-```kotlin
-data class Cloud (
- var all: Int = 0
-)
-```
-
-Szintén a `model` package-ben hozzuk létre a `Sys` osztályt:
-
-```kotlin
-data class Sys (
- var type: Int = 0,
- var id: Int = 0,
- var country: String? = null,
- var sunrise: Int = 0,
- var sunset: Int = 0
-)
-```
-
-Végül hozzuk létre a `Wind` osztályt is:
-
-```kotlin
-class Wind (
- val speed: Float = 0f,
- val deg: Float = 0f
-)
-```
-
-A `details` *package*-ben hozzuk létre a `WeatherDataHolder` interfészt:
-
-```kotlin
-interface WeatherDataHolder {
- fun getWeatherData(): WeatherData?
-}
-```
-
- A `WeatherDataHolder` -en keresztül fogják lekérni a `Fragment`-ek az `Activity`-től az időjárás adatokat.
-
-Vegyünk fel egy `WeatherData` típusú tagváltozót a `DetailsActiviy`-be:
-
-```kotlin
-private var weatherData: WeatherData? = null
-```
-
-Módosítsuk úgy a `DetailsActivity` -t, hogy implementálja a `WeatherDataHolder` interfészt:
-
-```kotlin
-class DetailsActivity : AppCompatActivity(), WeatherDataHolder {
-```
-
-Implementáljuk a szükséges függvényt:
-
-```kotlin
- override fun getWeatherData(): WeatherData? {
- return weatherData
- }
-```
-
-A használt `weatherData` változónak fogunk később értéket adni, amikor visszaérkezett az értéke a hálózati hívás eredményeként. A `ViewPager` két lapján levő `Fragment`-ek a `WeatherDataHolder` interfészen keresztül fogják lekérni az `Activity`-től a `weatherData` objekutmot a megjelenítéshez.
-
-#### A hálózati réteg megvalósítása
-
-A `hu.bme.aut.android.weatherinfo` package-ben hozzuk létre egy `network` nevű package-et, amely a hálózati kommunikációhoz kapcsolódó osztályokat fogja tartalmazni.
-
-A `network` package-en belül hozzuk létre egy `WeatherApi` nevű interfészt.
-
-```kotlin
-interface WeatherApi {
- @GET("/data/2.5/weather")
- fun getWeather(
- @Query("q") cityName: String?,
- @Query("units") units: String?,
- @Query("appid") appId: String?
- ): Call?
-}
-```
-
-Látható, hogy *annotációk* alkalmazásával tuduk jelezni, hogy az adott függvényhívás milyen hálózati hívásnak fog megfelelni. A `@GET` annotáció *HTTP GET* kérést jelent, a paraméterként adott string pedig azt jelzi, hogy hogy a szerver alap *URL*-éhez képest melyik végpontra szeretnénk küldeni a kérést.
-
-!!!note ""
- Hasonló módon tudjuk leírni a többi HTTP kérés típust is: @POST, @UPDATE, @PATCH, @DELETE
-
-A függvény paremétereit a `@Query` annotációval láttuk el. Ez azt jelenti, hogy a `Retrofit` az adott paraméter értékét a kéréshez fűzi *query paraméterként* az annotációban megadott kulccsal.
-
-!!!note ""
- További említésre méltó annotációk a teljesség igénye nélkül: @HEAD, @Multipart, @Field
-
-A hálózati hívást jelölő interfész függvény visszatérési értéke egy`Call` típusú objektum lesz. (A retrofites Callt importáljuk a megjelenő lehetőségek közül.) Ez egy olyan hálózati hívást ír le, aminek a válasza `WeatherData` típusú objektummá alakítható.
-
-Hozzunk létre a `network` package-ben egy `NetworkManager` osztályt:
-
-```kotlin
-object NetworkManager {
- private val retrofit: Retrofit
- private val weatherApi: WeatherApi
-
- private const val SERVICE_URL = "https://api.openweathermap.org"
- private const val APP_ID = "ide_jon_a_token"
-
- init {
- retrofit = Retrofit.Builder()
- .baseUrl(SERVICE_URL)
- .client(OkHttpClient.Builder().build())
- .addConverterFactory(GsonConverterFactory.create())
- .build()
- weatherApi = retrofit.create(WeatherApi::class.java)
- }
-
- fun getWeather(city: String?): Call? {
- return weatherApi.getWeather(city, "metric", APP_ID)
- }
-}
-```
-
-Ez az osztály lesz felelős a hálózati kérések lebonyolításáért. Egyetlen példányra lesz szükségünk belőle, így [singleton](https://en.wikipedia.org/wiki/Singleton_pattern)ként implementáltuk. Konstansokban tároljuk a szerver alap címét, valamint a szolgáltatás használatához szükséges API kulcsot.
-
-A `Retrofit.Builder()` hívással kérhetünk egy pareméterezhető `Builder` példányt. Ebben adhatjuk meg a hálózati hívásaink tulajdonságait. Jelen példában beállítjuk az elérni kívánt szolgáltatás címét, a HTTP kliens implementációt ([OkHttp](http://square.github.io/okhttp/)), valamint a JSON és objektum reprezentációk közötti konvertert (Gson).
-
-A `WeatherApi` interfészből a `Builder`-rel létrehozott `Retrofit` példány segítségével tudjuk elkérni a fordítási időben generált, paraméterezett implementációt.
-
- A `retrofit.create(WeatherApi.class)` hívás eredményeként kapott objektum megvalósítja a `WeatherApi` interfészt. Ha ezen az objektumon meghívjuk a `getWeather(...)` függvényt, akkor megtörténik az általunk az interfészben definiált hálózati hívás.
-
-Az `APP_ID` paramétert elfedjük az időjárást lekérdező osztályok elől, ezért a `NetworkManager` is tartalmaz egy `getWeather(...)` függvényt, ami a `WeatherApi` implementációba hív tovább.
-
-**Cseréljük le** az `APP_ID` értékét az [OpenWeatherMap](https://openweathermap.org/) oldalon kapott saját API kulcsunkra!
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszanak a **Project nézetben a létrehozott modell osztályok**, az editorban a **WeatherApi** osztály kódja, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### 4. A hálózati réteg bekötése a részletező nézetbe (1 pont)
-
-A modell elemek és a hálózati réteg megvalósítása után a részletező nézetet fogjuk a specifikációnak megfelelően implementálni, majd bekötjük a hálózati réteget is.
-
-#### A részletező nézetek továbbfejlesztése
-
-A `ViewPager` megfelelő működéséhez létre kell hoznunk egy `FragmentStateAdapter`-ből származó osztályt a `details` package-ben, ami az eddig látott adapterekhez hasonlóan azt határozza meg, hogy milyen elemek jelenjenek meg a hozzájuk tartozó nézeten (jelen esetben az elemek `Fragment`-ek lesznek):
-
-```kotlin
-class DetailsPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
-
- companion object {
- private const val NUM_PAGES: Int = 2
- }
-
- override fun createFragment(position: Int): Fragment {
- return when (position) {
- 0 -> DetailsMainFragment()
- 1 -> DetailsMoreFragment()
- else -> DetailsMainFragment()
- }
- }
-
- override fun getItemCount(): Int = NUM_PAGES
-}
-```
-
-Implementáljuk a hiányzó `Fragment`-eket a hozzájuk tartozó nézetekkel együtt:
-
-`res/layout/fragment_details_main.xml`:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-```
-
-A `details` package-ben a `DetailsMainFragment`:
-
-```kotlin
-class DetailsMainFragment : Fragment() {
-
- private lateinit var binding: FragmentDetailsMainBinding
- private var weatherDataHolder: WeatherDataHolder? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- weatherDataHolder = if (activity is WeatherDataHolder) {
- activity as WeatherDataHolder?
- } else {
- throw RuntimeException(
- "Activity must implement WeatherDataHolder interface!"
- )
- }
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
- binding = FragmentDetailsMainBinding.inflate(LayoutInflater.from(context))
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- if (weatherDataHolder?.getWeatherData() != null) {
- displayWeatherData()
- }
- }
-
- private fun displayWeatherData() {
- val weather = weatherDataHolder?.getWeatherData()?.weather?.first()
- binding.tvMain.text = weather?.main
- binding.tvDescription.text = weather?.description
-
- Glide.with(this)
- .load("https://openweathermap.org/img/w/${weather?.icon}.png")
- .transition(DrawableTransitionOptions().crossFade())
- .into(binding.ivIcon)
- }
-}
-```
-
-Figyeljük meg, hogy hogy használjuk a kódban a `Glide` libraryt!
-
-!!!note ""
- Az *OpenWeatherMap* API-tól a képek lekérhetők a visszakapott adatok alapján, pl: [https://openweathermap.org/img/w/10d.png](http://openweathermap.org/img/w/10d.png)
-
-`res/layout/fragment_details_more.xml`:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-A `details` package-ben a `DetailsMoreFragment`:
-
-```kotlin
-class DetailsMoreFragment : Fragment() {
-
- private lateinit var binding: FragmentDetailsMoreBinding
- private var weatherDataHolder: WeatherDataHolder? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- weatherDataHolder = if (activity is WeatherDataHolder) {
- activity as WeatherDataHolder?
- } else {
- throw RuntimeException("Activity must implement WeatherDataHolder interface!")
- }
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- binding = FragmentDetailsMoreBinding.inflate(LayoutInflater.from(context))
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- if (weatherDataHolder?.getWeatherData() != null) {
- showWeatherData()
- }
- }
-
- private fun showWeatherData() {
- val weatherData = weatherDataHolder!!.getWeatherData()
- binding.tvTemperature.text = weatherData?.main?.temp.toString()
- binding.tvMinTemp.text = weatherData?.main?.temp_min.toString()
- binding.tvMaxTemp.text = weatherData?.main?.temp_max.toString()
- binding.tvPressure.text = weatherData?.main?.pressure.toString()
- binding.tvHumidity.text = weatherData?.main?.humidity.toString()
- }
-}
-```
-
-Figyeljük meg, hogyan ellenőrzi a `DetailsMainFragment` és a `DetailsMoreFragment` azt, hogy az `Activity` implementálja-e a `WeatherDataHolder` interfészt. Fontos, hogy ezt a két `Fragment` majd csak azután kerül a `DetailsActivity`-re a `ViewPager`-en keresztül, amikor az adatokat lekérő hálózati kérés már adott vissza eredményt.
-
-Ideiglenesen a `DetailsActivity` `onResume()` függvénye legyen az alábbi:
-
-
-```kotlin
-override fun onResume() {
- super.onResume()
- val detailsPagerAdapter = DetailsPagerAdapter(this)
- binding.mainViewPager.adapter = detailsPagerAdapter
-
- TabLayoutMediator(binding.tabLayout, binding.mainViewPager) { tab, position ->
- tab.text = when(position) {
- 0 -> getString(R.string.main)
- 1 -> getString(R.string.details)
- else -> ""
- }
- }.attach()
-}
-```
-
-Próbáljuk ki az alkalmazást, kattintsunk egy városra! jelenleg még nem jelennek meg valós adatok, mivel még nem kötöttük be a az adatok lekéréséért felelős hívást.
-
-#### Hálózati hívás bekötése
-
-Az időjárás adatok lekérdezésének bekötéséhez implementáljunk egy `loadWeatherData()` nevű függvényt a `DetailsActivity`-ben:
-
-
-```kotlin
-private fun loadWeatherData() {
- NetworkManager.getWeather(city)?.enqueue(object : Callback {
- override fun onResponse(
- call: Call,
- response: Response
- ) {
- Log.d(TAG, "onResponse: " + response.code())
- if (response.isSuccessful) {
- displayWeatherData(response.body())
- } else {
- Toast.makeText(this@DetailsActivity, "Error: " + response.message(), Toast.LENGTH_LONG).show()
- }
- }
-
- override fun onFailure(
- call: Call,
- throwable: Throwable
- ) {
- throwable.printStackTrace()
- Toast.makeText(this@DetailsActivity, "Network request error occured, check LOG", Toast.LENGTH_LONG).show()
- }
- })
-}
-```
-
-Implementáljuk a hiányzó `displayWeatherData(...)` függvényt, ami sikeres API hívás esetén megjeleníti az eredményt:
-
-```kotlin
-private fun displayWeatherData(receivedWeatherData: WeatherData?) {
- weatherData = receivedWeatherData
- val detailsPagerAdapter = DetailsPagerAdapter(this)
- binding.mainViewPager.adapter = detailsPagerAdapter
-}
-```
-
-A `DetailsActivity` `onResume()` függvényében hívjuk meg a `loadWeatherData()` függvényt:
-
-```kotlin
-override fun onResume() {
- super.onResume()
- ...
- loadWeatherData()
-}
-```
-
-Futtassuk az alkalmazást és figyeljük meg a működését! Próbáljuk ki azt is, hogy mi történik akkor, ha megszakítjuk a futtató eszköz internet kapcsolatát és megpróbáljuk megnyitni a részletező nézetet!
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszanak az emulátoron a **betöltött adatok**, a **DetailsActivity** kódja, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f4.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
-
-### 5. Önálló feladat: város listából törlés megvalósítása (1 pont)
-
-Valósítsuk meg a városok törlését a *Remove* gomb megnyomásának hatására.
-
-!!!example "BEADANDÓ (1 pont)"
- Készíts egy **képernyőképet**, amelyen látszik az emulátoron a **városok listája CSAK Budapesttel**, a **törlés releváns** kódrészlete, valamint a **neptun kódod a kódban valahol kommentként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl.
-
- A képernyőkép szükséges feltétele a pontszám megszerzésének.
+## TODO 2024
\ No newline at end of file
diff --git a/docs/laborok/12-js-advanced/index.md b/docs/laborok/12-js-advanced/index.md
index db375e8..36ca5b0 100644
--- a/docs/laborok/12-js-advanced/index.md
+++ b/docs/laborok/12-js-advanced/index.md
@@ -90,7 +90,7 @@ A feladatok megoldása során ne felejtsd el követni a feladat beadás folyamat
!!! warning "NPM cache"
Fontos! A laborgépeken nem vagy nem mindig érhető el megfelelően az NPM lokális cache példánya, ezért használjuk helyette itt az `npm install --cache .cache` parancsot, ami az aktuális mappában egy `.cache` nevű mappát használ a központi gyorsítótár helyett. Lokális gépen is használhatjuk ezt a parancsot, de ott elegendő (kell, hogy legyen) az `npm install` is.
- **Ez a `.cache` mappa NE KERÜLJÖN commuttolásra!**
+ **Ez a `.cache` mappa NE KERÜLJÖN commitolásra!**
7. Próbáljuk ki az alkalmazást böngészőben!
@@ -124,9 +124,9 @@ A webpack konfigurációja a `webpack.config.js` fájlban található, amelyben
## Feladat 2 - Modern JavaScript funkciók
-A megoldás során használjuk az objektumorientált megközelítést és a modern JS funkciókat! Igyekezzünk komponens-orientáltan gondolkodni: **egy objektum komponens, ha megjelenik a felületen a reprezentációja**, képes kommunikálni más objektumokkal és komponensekkel, ezen felül lehet állapota (mezői, tulajdonságai, amiket karban tart).
+A megoldás során használjuk az objektumorientált megközelítést és a modern JS funkciókat! Igyekezzünk komponens-orientáltan gondolkodni: **egy objektum komponens, ha megjelenik a felületen a reprezentációja**, képes kommunikálni más objektumokkal és komponensekkel, ezen felül lehet állapota (mezői, tulajdonságai, amiket karbantart).
-Az alkalmazásunknak szüksége lesz egy "gépre", aki majd kigondolja a számot. Az egyszerűség kedvéért most ez egy 1 és 100 közötti szám lesz, az érték nem konfigurálható. Szimuláljuk, hogy a "számítás" komplex, úgyhogy kis késleltetést viszünk majd abba, amíg a választ visszakapjuk a tippünkre.
+Az alkalmazásunknak szüksége lesz egy "gépre", ami majd kigondolja a számot. Az egyszerűség kedvéért most ez egy 1 és 100 közötti szám lesz, az érték nem konfigurálható. Szimuláljuk, hogy a "számítás" komplex, úgyhogy kis késleltetést viszünk majd abba, amíg a választ visszakapjuk a tippünkre.
Az objektumaink, melyek a felületen is megjelennek, rendelkezni fognak egy `render()` függvénnyel, és lesz egy (az alkalmazás szempontjából) globális `render()` függvényünk is, ami minden komponenst kirajzol azok `render()` függvényének meghívásával.
@@ -336,7 +336,7 @@ export class Guess {
element.disabled = !this.enabled;
}
- if (!this.enabled) {
+ if (this.enabled) {
document.getElementById('guess-input').focus();
}
}
diff --git a/docs/tudnivalok/github/contributing.md b/docs/tudnivalok/github/contributing.md
index d1f9b82..9629864 100644
--- a/docs/tudnivalok/github/contributing.md
+++ b/docs/tudnivalok/github/contributing.md
@@ -57,7 +57,7 @@ Amennyiben a hozzájárulásod meg tudod valósítani indíts pull requestet
3. Ellenőrizd, hogy ne kerüljön bele a commitba olyan file, amit az editor generált (pl.: `.idea` mappa)
illetve olyan file aminek nem kéne kikerülnie, pl.: Github Private Access Token
- 4. Ha kész vagy a laborok beadásához hasonlóan indíts egy pull requestet a `VIAUAC00/laborok` `master` branchére.
+ 4. Ha kész vagy a laborok beadásához hasonlóan indíts egy pull requestet a `VIAUAD02/laborok` `master` branchére.
5. Lásd el a megfelelő címkékkel
1. A labor típusa (`android` az androidos laboroknál és `web` a webes laboroknál)
diff --git a/docs/tudnivalok/laborvezetoknek/index.md b/docs/tudnivalok/laborvezetoknek/index.md
index cba33f7..7202fa5 100644
--- a/docs/tudnivalok/laborvezetoknek/index.md
+++ b/docs/tudnivalok/laborvezetoknek/index.md
@@ -65,7 +65,7 @@ A **labor termekhez** kulcsra és/vagy kártyára van szükség. Ezeket a titká
A laborok megoldását adott határidőig kell beadni GitHub-on. Ennek pontos menete a hallgató szemszögéből [itt](../github/GitHub.md) elolvasható.
-Ahhoz, hogy hozzáférj a GitHub-on a beadott megoldásokhoz (és ahhoz, hogy a hallgatók ezt hozzád tudják rendelni), kell egy GitHub account. A GitHub nevedet írd meg a tárgyfelelősnek, és felvesz GitHub-on a organization-be.
+Ahhoz, hogy hozzáférj a GitHub-on a beadott megoldásokhoz (és ahhoz, hogy a hallgatók ezt hozzád tudják rendelni), kell egy GitHub account. A GitHub nevedet írd meg a tárgyfelelősnek, és felvesz GitHub-on a organization-be.
### Mikor kell értékelni a labort?
@@ -73,7 +73,7 @@ A laborokat a határidő lejárta után kell értékelni. A határidő előtt a
### Hol kell értékelni a labort?
-A határidő lejárta után a feladatod a **hozzád rendelt** pull request-ek értékelése. A hallgató azzal adja be a labort, hogy a pull request-et a laborvezetőjéhez rendeli. Ezeket a GitHub keresőjével a legegyszerűbb megtalálni: .
+A határidő lejárta után a feladatod a **hozzád rendelt** pull request-ek értékelése. A hallgató azzal adja be a labort, hogy a pull request-et a laborvezetőjéhez rendeli. Ezeket a GitHub keresőjével a legegyszerűbb megtalálni: .
Alternatívaként a GitHub értesítő felületét is lehet használni a címen, itt minden hozzád rendelt, vagy review-ra váró PR megjelenik.
@@ -85,7 +85,7 @@ A PR-eket egyesével kell megnyitni, és meg kell nézni a PR komment felületé
Automatikus értékelés esetén (ami nem minden labornál van) a forráskódot nem szükséges betűről betűre megnézni - a részletes ellenőrzést elvégzi az automata. A laborvezető feladata a képernyőképek ellenőrzése, valamint annak eldöntése, hogy a forráskód konzisztens-e a kapott eredménnyel, és nincs-e benne olyan kódrészlet, amely ugyan működik, de kifejezetten rosszul oldja meg a problémát. Amely labornál nincs automata értékelés, ott több munka hárul a laborvezetőre, alaposabban meg kell nézni a megoldást.
-A feladatok minta megoldása itt érhető el: . Ezek csak lehetséges megoldások, a hallgató megoldása nem kell ezzel egyezzen.
+A feladatok minta megoldása itt érhető el: . Ezek csak lehetséges megoldások, a hallgató megoldása nem kell ezzel egyezzen.
Az értékelés végeztével:
diff --git a/docs/tudnivalok/mobillaborvezetoknek/index.md b/docs/tudnivalok/mobillaborvezetoknek/index.md
index 09f7a18..5f473fa 100644
--- a/docs/tudnivalok/mobillaborvezetoknek/index.md
+++ b/docs/tudnivalok/mobillaborvezetoknek/index.md
@@ -44,7 +44,7 @@ Az első héten nincs beugró.
### Github Classroom
A 2022 őszi félévtől a laboranyagok a Github Classromon érhetőek el.
-Az ezzel kapcsolatos információk [itt](https://viauac00.github.io/laborok/) olvashatók.
+Az ezzel kapcsolatos információk [itt](https://VIAUAD02.github.io/laborok/) olvashatók.
Ha hibát, elgépelést találsz benne, arra kérünk, hogy javítsd: minden anyag jobb felső sarkában van egy kis ceruza ikon, javítsd a hibát, és küldj PR-t.
### Labormunka
@@ -62,7 +62,7 @@ Az első laboron markdown-t, a többi laboron néhány screenshotot és a forrá
A laborok megoldását adott határidőig kell beadni GitHub-on. Ennek pontos menete a hallgató szemszögéből [itt](../github/GitHub.md) elolvasható.
-Ahhoz, hogy hozzáférj a GitHub-on a beadott megoldásokhoz (és ahhoz, hogy a hallgatók ezt hozzád tudják rendelni), kell egy GitHub account. A GitHub nevedet írd meg a tárgyfelelősnek, és felvesz GitHub-on a organization-be.
+Ahhoz, hogy hozzáférj a GitHub-on a beadott megoldásokhoz (és ahhoz, hogy a hallgatók ezt hozzád tudják rendelni), kell egy GitHub account. A GitHub nevedet írd meg a tárgyfelelősnek, és felvesz GitHub-on a organization-be.
### Mikor kell értékelni a labort?
@@ -70,7 +70,7 @@ A laborokat a határidő lejárta után kell értékelni. A határidő előtt a
### Hol kell értékelni a labort?
-A határidő lejárta után a feladatod a **hozzád rendelt** pull request-ek értékelése. A hallgató azzal adja be a labort, hogy a pull request-et a laborvezetőjéhez rendeli. Ezeket a GitHub keresőjével a legegyszerűbb megtalálni: .
+A határidő lejárta után a feladatod a **hozzád rendelt** pull request-ek értékelése. A hallgató azzal adja be a labort, hogy a pull request-et a laborvezetőjéhez rendeli. Ezeket a GitHub keresőjével a legegyszerűbb megtalálni: .
Alternatívaként a GitHub értesítő felületét is lehet használni a címen, itt minden hozzád rendelt, vagy review-ra váró PR megjelenik.
diff --git a/mkdocs.yml b/mkdocs.yml
index 3d42899..59f0904 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,8 +1,8 @@
-site_name: BMEVIAUAC00 - Mobil- és webes szoftverek laborfeladatok
+site_name: BMEVIAUAD02 - Mobil- és webes szoftverek laborfeladatok
site_author: Tóth Tibor, Gincsai Gábor, Ekler Péter, Gazdi László, Pomázi Krisztián, Kövesdán Gábor
copyright: Copyright © BME VIK AUT
-repo_name: viauac00/laborok
-repo_url: https://github.com/viauac00/laborok
+repo_name: viauad02/laborok
+repo_url: https://github.com/viauad02/laborok
theme:
name: material
@@ -74,7 +74,7 @@ plugins:
timezone: Europe/Budapest
locale: hu
- git-committers:
- repository: VIAUAC00/laborok
+ repository: VIAUAD02/laborok
branch: master
nav:
@@ -92,8 +92,6 @@ nav:
- laborok/05-android-sqlite/index.md
- laborok/06-android-room/index.md
- laborok/07-android-network/index.md
- - "Házi feladat":
- - hf/index.md
- "Webes Laborok":
- laborok/08-http/index.md
- laborok/09-css/index.md