diff --git a/laborok/network/index.html b/laborok/network/index.html index 268c588..3d624aa 100644 --- a/laborok/network/index.html +++ b/laborok/network/index.html @@ -1025,23 +1025,23 @@

KönyvtárakFüggőségek

A Retrofit, Paging és Coil könyvtárak használatához a következő függőségek szükségesek (ezek már szerepelnek a projektben, ne vegyük fel őket újra): -

    [versions]
-    retrofit = "2.11.0"
-    paging= "3.3.2"
-    coil = "2.5.0"
-
-    [libraries]
-    retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
-    retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
-    paging = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
-    coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
-
-    dependencies {
-        implementation(libs.retrofit)
-        implementation(libs.retrofit.moshi)
-        implementation(libs.paging)
-        implementation(libs.coil)
-    }
+
[versions]
+retrofit = "2.11.0"
+paging= "3.3.2"
+coil = "2.5.0"
+
+[libraries]
+retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
+retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
+paging = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
+coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
+
+dependencies {
+    implementation(libs.retrofit)
+    implementation(libs.retrofit.moshi)
+    implementation(libs.paging)
+    implementation(libs.coil)
+}
 

A data.model package-be hozzuk létre az alábbi két fájlt, melyek az API használatához szükségesek:

UnsplashPhoto.kt: @@ -1297,7 +1297,7 @@

Távoli kulcsok
@Entity(tableName = "remote_keys")
 data class UnsplashPhotoRemoteKeys(
@@ -1435,10 +1435,11 @@ 

PagingUtil .connectTimeout(15, TimeUnit.SECONDS) .build() + val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() val retrofit = Retrofit.Builder() .baseUrl("https://api.unsplash.com/") .client(client) - .addConverterFactory(MoshiConverterFactory.create()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() val api = retrofit.create(UnsplashApi::class.java) @@ -1462,12 +1463,12 @@

PagingUtilÖnálló feladat 1 - Különböző képernyőméretek támogatása

Szeretnénk felkészíteni az alkalmazásunkat, hogy különböző méretű képernyők esetén máshogy, az adott készülék számára optimális módon jelenleg meg. -Ezzez elsőként a util package-ben hozzunk létre egy osztályt a kategóriáinkra:

+Ezzel elsőként a util package-ben hozzunk létre egy osztályt a kategóriáinkra:

WindowSize.kt:

enum class WindowSize { Compact, Medium, Expanded }
 

Ezt követően hozzuk létre a maradék két elrendezésünket a feature.photos_feed.screensbysize package-ben:

-

PhotosFeedScreen_Compact.kt: +

PhotosFeed_Compact.kt:

@ExperimentalMaterial3Api
 @ExperimentalMaterialApi
 @Composable
@@ -1501,8 +1502,8 @@ 

Önálló feladat LazyColumn( modifier = modifier.fillMaxSize() ) { - items(photos) { photo -> - photo?.let { model -> + items(photos.itemCount) { index -> + photos[index]?.let { model -> PhotoItem( photo = model, onClick = onPhotoItemClick @@ -1520,7 +1521,7 @@

Önálló feladat } }

-

PhotosFeedScreen_Expanded.kt: +

PhotosFeed_Expanded.kt:

@ExperimentalMaterial3Api
 @ExperimentalMaterialApi
 @Composable
@@ -1559,8 +1560,8 @@ 

Önálló feladat .padding(it) .weight(1f) ) { - items(photos) { photo -> - photo?.let { model -> + items(photos.itemCount) { index -> + photos[index]?.let { model -> PhotoItem( photo = model, onClick = onPhotoItemClick @@ -1697,8 +1698,9 @@

Önálló feladat } } } -

-

Majd az Activity-t is állítsuk be ezek használatára:

+

+Vegyük fel a material3 könyvtár mintájára a material3-window-size-class függőséget (link)[https://developer.android.com/jetpack/androidx/releases/compose-material3] +Majd az Activity-t is állítsuk be ezek használatára:

MainActivity.kt:

class MainActivity : ComponentActivity() {
     @OptIn(
@@ -1749,8 +1751,8 @@ 

Önálló feladat 2 - Dependency - - 2024-11-10 + + 2024-11-11 diff --git a/search/search_index.json b/search/search_index.json index c585030..39ec71a 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["hu"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"T\u00e1rgy ismertet\u0151","text":"

A t\u00e1rgyk\u00f6vetelm\u00e9nyeket l\u00e1sd a hivatalos tant\u00e1rgyi adatlapon.

A laborok sorrendj\u00e9t \u00e9s a bead\u00e1sok hat\u00e1ridej\u00e9t Moodle-ben tal\u00e1lod.

Jav\u00edt\u00e1s az anyagban

A t\u00e1rgy hallgat\u00f3inak az anyagban t\u00f6rt\u00e9n\u0151 jav\u00edt\u00e1s\u00e9rt, kieg\u00e9sz\u00edt\u00e9s\u00e9rt plusz pontot adunk! Ha hib\u00e1t tal\u00e1lsz, vagy kieg\u00e9sz\u00edten\u00e9d/pontos\u00edtan\u00e1d a feladatle\u00edr\u00e1sokat, nyiss egy pull request-et! A repository linkj\u00e9t a jobb fels\u0151 sarokban tal\u00e1lod.

A jav\u00edt\u00e1s menet\u00e9r\u0151l \u00e9s form\u00e1j\u00e1r\u00f3l b\u0151vebben a \"Hozz\u00e1j\u00e1rul\u00e1s az anyaghoz\" dokumentumban olvashatsz b\u0151vebben.

Felhaszn\u00e1l\u00e1si felt\u00e9telek

Az itt tal\u00e1lhat\u00f3 oktat\u00e1si seg\u00e9danyagok a BMEVIAUAV21 t\u00e1rgy hallgat\u00f3inak k\u00e9sz\u00fcltek. Az anyagok oly m\u00f3d\u00fa felhaszn\u00e1l\u00e1sa, amely a t\u00e1rgy oktat\u00e1s\u00e1hoz nem szorosan kapcsol\u00f3dik, csak a szerz\u0151(k) \u00e9s a forr\u00e1s megjel\u00f6l\u00e9s\u00e9vel t\u00f6rt\u00e9nhet.

Az anyagok a t\u00e1rgy keret\u00e9ben oktatott kontextusban \u00e9rtelmezhet\u0151ek. Az anyagok\u00e9rt egy\u00e9b felhaszn\u00e1l\u00e1s eset\u00e9n a szerz\u0151(k) felel\u0151ss\u00e9get nem v\u00e1llalnak.

"},{"location":"#altalanos-tudnivalok","title":"\u00c1ltal\u00e1nos tudnival\u00f3k","text":""},{"location":"#laborok-megoldasainak-beadasa","title":"Laborok megold\u00e1sainak bead\u00e1sa","text":"

A laborok megold\u00e1s\u00e1t egy szem\u00e9lyre sz\u00f3l\u00f3 git repository-ban kell beadni. Ennek pontos folyamat\u00e1t l\u00e1sd itt. K\u00e9r\u00fcnk, hogy alaposan olvasd v\u00e9gig a le\u00edr\u00e1st!

FONTOS

A laborok elk\u00e9sz\u00edt\u00e9se \u00e9s bead\u00e1sa sor\u00e1n az itt le\u00edrtak szerint kell elj\u00e1rnod. A nem ilyen form\u00e1ban beadott megold\u00e1sokat nem \u00e9rt\u00e9kelj\u00fck.

A bead\u00e1s sor\u00e1n a munkafolyamati hib\u00e1k\u00e9rt (pl. nem megfelel\u0151 emberhez hozz\u00e1rendel\u00e9se, hozz\u00e1rendel\u00e9s elfelejt\u00e9se) pontot vonunk le.

"},{"location":"#laborok-ertekelese","title":"Laborok \u00e9rt\u00e9kel\u00e9se","text":"

Minden labort k\u00fcl\u00f6n jeggyel \u00e9rt\u00e9kel\u00fcnk. A teljes\u00edt\u00e9s felt\u00e9tele a hat\u00e1rid\u0151ig t\u00f6rt\u00e9n\u0151 bead\u00e1s. A jegy (1-5 sk\u00e1l\u00e1n) a labor feladatokon megszerezhet\u0151 5 pont alapj\u00e1n t\u00f6rt\u00e9nik. A feladatok bead\u00e1s\u00e1hoz minden esetben a GitHub platformot haszn\u00e1ljuk.

A feladatok ki\u00e9rt\u00e9kel\u00e9se egyes laborok eset\u00e9n r\u00e9szben automatikusan t\u00f6rt\u00e9nik. A futtathat\u00f3 k\u00f3dokat val\u00f3ban le fogjuk futtatni, ez\u00e9rt minden esetben fontos a feladatle\u00edr\u00e1sok pontos k\u00f6vet\u00e9se (kiindul\u00f3 k\u00f3d v\u00e1z haszn\u00e1lata, csak a megengedett f\u00e1jlok v\u00e1ltoztat\u00e1sa, stb.)!

A ki\u00e9rt\u00e9kel\u00e9s eredm\u00e9ny\u00e9r\u0151l a GitHub-on kapsz sz\u00f6veges visszajelz\u00e9st (l\u00e1sd itt). Ha enn\u00e9l t\u00f6bb inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ged, a GitHub Actions webes fel\u00fclete seg\u00edts\u00e9g\u00fcl szolg\u00e1lhat. Err\u0151l itt tal\u00e1lsz egy r\u00f6vid ismertet\u0151t.

"},{"location":"#kepernyokepek","title":"K\u00e9perny\u0151k\u00e9pek","text":"

A laborok k\u00e9rik, hogy k\u00e9sz\u00edts k\u00e9perny\u0151k\u00e9pet a megold\u00e1s egy-egy r\u00e9sz\u00e9r\u0151l. Ez k\u00fcl\u00f6n\u00f6sen akkor fontos, ha a feladatot otthon k\u00e9sz\u00edted el, mert ezzel bizony\u00edtod, hogy a megold\u00e1sod saj\u00e1t magad k\u00e9sz\u00edtetted. A k\u00e9perny\u0151k\u00e9pek elv\u00e1rt tartalm\u00e1t a feladat minden esetben pontosan megnevezi. A k\u00e9perny\u0151k\u00e9p k\u00e9sz\u00fclhet a teljes desktopr\u00f3l is, de lehet csak a k\u00e9rt alkalmaz\u00e1sr\u00f3l k\u00e9sz\u00edteni.

A k\u00e9perny\u0151k\u00e9peket a megold\u00e1s r\u00e9szek\u00e9nt kell beadni, \u00edgy felker\u00fclnek a git repository tartalm\u00e1val egy\u00fctt. Mivel a repository priv\u00e1t, azt az oktat\u00f3kon k\u00edv\u00fcl m\u00e1s nem l\u00e1tja. Amennyiben olyan tartalom ker\u00fcl a k\u00e9perny\u0151k\u00e9pre, amit nem szeretn\u00e9l felt\u00f6lteni, kitakarhatod a k\u00e9pr\u0151l.

"},{"location":"#elvarasaink-a-munkaval-kapcsolatban","title":"Elv\u00e1r\u00e1saink a munk\u00e1val kapcsolatban","text":"

Hova kell felt\u00f6lteni a megold\u00e1st? Fentebb megtal\u00e1lod a le\u00edr\u00e1st.

Egy\u00e9ni munka? Otthoni munka? Mivel a laborokra jegyet kapsz, elv\u00e1r\u00e1s, hogy mindenki saj\u00e1t megold\u00e1st k\u00e9sz\u00edtsen el \u00e9s adjon be. Ez nem z\u00e1rja ki az egym\u00e1snak ny\u00fajtott seg\u00edts\u00e9get. Kiz\u00e1rja viszont m\u00e1s megold\u00e1s\u00e1nak lem\u00e1sol\u00e1s\u00e1t. Ez\u00e9rt k\u00e9rj\u00fck a k\u00e9perny\u0151k\u00e9peket, mert \u00edgy a munka folyamat\u00e1val bizony\u00edtod a megold\u00e1s saj\u00e1t elk\u00e9sz\u00edt\u00e9s\u00e9t.

M\u00e1s munk\u00e1j\u00e1nak lem\u00e1sol\u00e1sa: A BME etikai k\u00f3dexe \u00e9s a TVSZ szab\u00e1lyozza. Komolyan vessz\u00fck.

Egy labor csak 2 \u00f3ra, nem? Nem. A t\u00e1rgy 4 kredit, amely a f\u00e9l\u00e9v sor\u00e1n megk\u00f6zel\u00edt\u0151leg 120 munka\u00f3ra befektet\u00e9s\u00e9t ig\u00e9nyli. A labor teh\u00e1t nem csak a teremben elt\u00f6lt\u00f6tt 2 \u00f3ra, hanem az el\u0151zetes felk\u00e9sz\u00fcl\u00e9s \u00e9s a feladat befejez\u00e9se / otthoni elv\u00e9gz\u00e9se is.

Egy apr\u00f3 el\u00edr\u00e1s miatt nem m\u0171k\u00f6d\u00f6tt a k\u00f3dom, \u00e9s nem \u00e9rt\u00e9kelt\u00e9tek. A laborok sor\u00e1n m\u0171k\u00f6d\u0151 programot, k\u00f3dot, k\u00f3dr\u00e9szletet kell k\u00e9sz\u00edteni. Az\u00e9rt sz\u00e1m\u00edt\u00f3g\u00e9p laborban vagy otthon k\u00e9sz\u00edtj\u00fck a feladatot, mert \u00edgy tudod magad ellen\u0151rizni. Minimum elv\u00e1r\u00e1s, hogy a beadott k\u00f3d leforduljon, lefusson. Ha a viselked\u00e9s nem teljesen helyes, azt \u00e9rt\u00e9kelj\u00fck. De ha egy\u00e1ltal\u00e1n nem m\u0171k\u00f6dik, nem \u00e9rt\u00e9kelj\u00fck a megold\u00e1st.

Az\u00e9rt \u00edgy tesz\u00fcnk, mert m\u00e9rn\u00f6kk\u00e9nt a feladatod a probl\u00e9m\u00e1k megold\u00e1sa lesz, \u00e9s nem csak egy k\u00eds\u00e9rlet a megold\u00e1sra. Mit gondolsz, ha a munkahelyeden a f\u0151n\u00f6k\u00f6dnek \u00e1tadsz egy nem fordul\u00f3 k\u00f3dot, mit fog tenni?

Ha otthonr\u00f3l k\u00e9sz\u00edtem el a megold\u00e1st, hogyan kapok seg\u00edts\u00e9get? Ak\u00e1r otthonr\u00f3l dolgozol, ak\u00e1r egyetemi laborban, egy laborvezet\u0151h\u00f6z tartozol. \u0150 felel nem csak a kontakt\u00f3ra megtart\u00e1s\u00e1\u00e9rt, hanem az\u00e9rt is, hogy a f\u00e9l\u00e9v k\u00f6zben a feladatok bead\u00e1sa \u00e9s ellen\u0151rz\u00e9se rendben t\u00f6rt\u00e9njen.

Nem seg\u00edt a laborvezet\u0151. Mi\u00e9rt? Dehogynem seg\u00edt. Viszont ha egyb\u0151l megmondan\u00e1 a megold\u00e1st, csak azt tanuln\u00e1d meg, hogy legk\u00f6zelebb is meg kell k\u00e9rdezni. Pr\u00f3b\u00e1ld magad megoldani, mutass alternat\u00edv\u00e1kat, k\u00e9rdezz konkr\u00e9tan. Mutasd meg, hogy professzion\u00e1lis a hozz\u00e1\u00e1ll\u00e1sod.

Akkor mit k\u00e9rdezhetek meg a laborvezet\u0151t\u0151l? R\u00f6viden: https://stackoverflow.com/help/how-to-ask. Hosszabban: Ha valamivel elakadsz, \u00e9rtsd meg a probl\u00e9m\u00e1t. A probl\u00e9ma nem az, hogy \"nem m\u0171k\u00f6dik\" vagy \"nem tudom, hogyan csin\u00e1ljam\". Akkor tudsz j\u00f3l k\u00e9rdezni, ha m\u00e1r k\u00f6r\u00fclj\u00e1rtad a probl\u00e9m\u00e1t, \u00e9s azt is meg tudod mutatni, mivel pr\u00f3b\u00e1lkozt\u00e1l m\u00e1r.

Sz\u00f3val Google \u00e9s StackOverflow a megold\u00e1s? Nem. Minden tud\u00e1s, amire sz\u00fcks\u00e9ged van, m\u00e1r el\u0151fordult egyetemi tanulm\u00e1nyaid sor\u00e1n. A Google j\u00f3, a StackOverflow m\u00e9g jobb.... De! A v\u00e1laszt is meg kell \u00e9rteni. Lehet, hogy a megtal\u00e1lt v\u00e1lasz megold\u00e1s, csak \u00e9pp nem a te probl\u00e9m\u00e1dra.

Sok a hat\u00e1rid\u0151, meg az el\u0151\u00edr\u00e1s. Ez n\u00e9z\u0151pont k\u00e9rd\u00e9se. A m\u00e9rn\u00f6k nem csak programozni tud, hanem meghat\u00e1rozott keretek k\u00f6z\u00f6tt dolgozni. Mert a vil\u00e1g bonyolult, \u00e9s a bonyolults\u00e1got szab\u00e1lyokkal lehet kord\u00e1ban tartani. Ha id\u0151d engedi, \u00e9rdemes megn\u00e9zni, mit mond Robert C. Martin (Bob Martin, \"Uncle Bob\") arr\u00f3l, honnan sz\u00e1rmazik a szoftverfejleszt\u0151i szakmai: https://www.youtube.com/watch?v=ecIWPzGEbFc

"},{"location":"hf/","title":"H\u00e1zi feladat inform\u00e1ci\u00f3k","text":"

A t\u00e1rgyb\u00f3l h\u00e1zi feladat k\u00e9sz\u00edt\u00e9se nem k\u00f6telez\u0151, de aj\u00e1nlott. H\u00e1zi feladat k\u00e9sz\u00edt\u00e9s\u00e9vel a vizsg\u00e1t kiv\u00e1ltva megaj\u00e1nlott 4-es vagy 5-\u00f6s szerezhet\u0151. A h\u00e1zi feladatra maximum 40 pont kaphat\u00f3, amib\u0151l a megaj\u00e1nlott jegyhez minimum 25 pontot el kell \u00e9rni. Akinek nem siker\u00fcl ezt a pontsz\u00e1mot megszerezni, az a vizsg\u00e1ra vihet maximum 20 pontot.

"},{"location":"hf/#kovetelmenyek","title":"K\u00f6vetelm\u00e9nyek","text":"
  • Technolo\u00f3gi\u00e1k:
    • K\u00f6telez\u0151en:
      • Compose UI
      • MVVM vagy ezzel egyen\u00e9rt\u00e9k\u0171 egy\u00e9b architekt\u00fara
      • Dependency Injection
    • Legal\u00e1bb 3 komplexebb technol\u00f3gia haszn\u00e1lata, melyet minden esetben a laborvezet\u0151 d\u00f6nt el, hogy el\u00e9gs\u00e9ges-e, pl.:
      • perzisztencia,
      • h\u00e1l\u00f3zat,
      • Firebase,
      • poz\u00edci\u00f3meghat\u00e1roz\u00e1s,
      • komplex anim\u00e1ci\u00f3,
      • st\u00edlusok/t\u00e9m\u00e1k (komplex, teljes alkalmaz\u00e1sra kiterjed\u0151 kin\u00e9zet),
      • Service,
      • BroadcastReceiver,
      • Content Provider,
      • stb.
  • Az alkalmaz\u00e1s felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9hez Jetpack Compose-t kell haszn\u00e1lni.
  • Kotlin nyelven kell k\u00e9sz\u00fclnie.
  • \u00d6n\u00e1ll\u00f3 alkalmaz\u00e1s legal\u00e1bb 3-4 k\u00e9perny\u0151vel/n\u00e9zettel.
  • B\u00e1rmilyen k\u00fcls\u0151 k\u00f6nyvt\u00e1r haszn\u00e1lhat\u00f3 a fejleszt\u00e9shez, hogy m\u00e9g l\u00e1tv\u00e1nyosabb alkalmaz\u00e1sok k\u00e9sz\u00fcljenek:
    • https://github.com/wasabeef/awesome-android-ui
    • https://github.com/nisrulz/android-tips-tricks
    • https://foso.github.io/Jetpack-Compose-Playground
    • https://www.jetpackcompose.app/compose-catalog

N\u00e9h\u00e1ny p\u00e9lda alkalmaz\u00e1s

  • Kiad\u00e1s/bev\u00e9tel nyomk\u00f6vet\u0151 figyelmeztet\u0151 funkci\u00f3val \u00e9s grafikonokkal
  • Turisztikai l\u00e1tv\u00e1nyoss\u00e1gokat gy\u0171jt\u0151 alkalmaz\u00e1s
  • Rakt\u00e1r kezel\u0151 alkalmaz\u00e1s
  • Sz\u00e1mla kezel\u0151 megold\u00e1s
  • Recept kezel\u0151 alkalmaz\u00e1s
  • Napl\u00f3 k\u00e9sz\u00edt\u0151 alkalmaz\u00e1s f\u00e9nyk\u00e9pekkel
  • Sport tracker alkalmaz\u00e1s
  • K\u00e9sz\u00fcl\u00e9k esem\u00e9ny napl\u00f3z\u00f3 alkalmaz\u00e1s
  • Apr\u00f3hirdet\u00e9s alkalmaz\u00e1s
  • Tal\u00e1lkoz\u00f3 szervez\u0151 alkalmaz\u00e1s
  • Sportfogad\u00f3 megold\u00e1s
  • Szaki keres\u0151 alkalmaz\u00e1s
  • J\u00e1t\u00e9k alkalmaz\u00e1s, pl. aknakeres\u0151, shooter, stb.
  • Valamilyen REST API-t haszn\u00e1l\u00f3 alkalmaz\u00e1s, p\u00e9ld\u00e1ul valuta v\u00e1lt\u00e1s, t\u0151zsdei inf\u00f3k, stb:
    • https://github.com/toddmotto/public-apis
    • https://github.com/Kikobeats/awesome-api
    • https://github.com/abhishekbanthia/Public-APIs
  • A h\u00e1zi feladat haszn\u00e1lhat felh\u0151 megold\u00e1st is, pl. Firebase, Amazon, stb.
"},{"location":"hf/#beadas-modja","title":"Bead\u00e1s m\u00f3dja","text":"

A h\u00e1zi feladat bead\u00e1s\u00e1nak platformja a laborokhoz hasonl\u00f3an a Github Classroom. A megh\u00edv\u00f3 a Moodle oldalon tal\u00e1lhat\u00f3.

neptun.txt

Az els\u0151 \u00e9s legfontosabb, hogy az eddigiekhez hasonl\u00f3an t\u00f6ltsd ki a neptun.txt f\u00e1jlt, hogy a rendszer azonos\u00edtani tudjon.

"},{"location":"hf/#specifikacio","title":"Specifik\u00e1ci\u00f3","text":"

A specifik\u00e1ci\u00f3 bead\u00e1s hat\u00e1rideje a 9. h\u00e9t v\u00e9ge (2024. november 3. 23:59). A specifik\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se k\u00f6zben a \"spec\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. Sablont a README.md f\u00e1jl tartalmaz, azt kell kieg\u00e9sz\u00edteni, \u00e9s felt\u00f6lteni a rep\u00f3ba a megadott hat\u00e1rid\u0151ig. A bead\u00e1s akkor teljes, ha a \"spec\" branch-en megtal\u00e1lhat\u00f3 a README.md f\u00e1jlban a specifik\u00e1ci\u00f3. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A specifik\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se el\u0151felt\u00e9tele a h\u00e1zi feladat elfogad\u00e1s\u00e1nak.

"},{"location":"hf/#hazi-feladat","title":"H\u00e1zi feladat","text":"

A h\u00e1zi feladat bead\u00e1s hat\u00e1rideje a 13. h\u00e9t v\u00e9ge (2024. december 1. 23:59). A h\u00e1zi feladat elk\u00e9sz\u00edt\u00e9se k\u00f6zben a \"hf\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. A projektet mindenk\u00e9ppen ebbe a repository-ba hozd l\u00e9tre, a fejleszt\u00e9st v\u00e9gig itt v\u00e9gezd. A bead\u00e1s akkor teljes, ha a \"hf\" branch-en megtal\u00e1lhat\u00f3 a projekted teljes forr\u00e1sk\u00f3dja. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A h\u00e1zi feladathoz mindenk\u00e9ppen tartozik h\u00e1zi feladat v\u00e9d\u00e9s is. Ennek ideje a bead\u00e1st k\u00f6vet\u0151en 14. heti laboron van.

"},{"location":"hf/#hazi-feladat-potlas-fizeteskoteles","title":"H\u00e1zi feladat p\u00f3tl\u00e1s - fizet\u00e9sk\u00f6teles!","text":"

A p\u00f3tbead\u00e1s hat\u00e1rideje a p\u00f3tl\u00e1si h\u00e9ten, laborvezet\u0151vel egyeztetve. A h\u00e1zi feladat p\u00f3tl\u00e1sa k\u00f6zben a \"pothf\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. A bead\u00e1s akkor teljes, ha a \"pothf\" branch-en megtal\u00e1lhat\u00f3 a projekted teljes forr\u00e1sk\u00f3dja. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A h\u00e1zi feladat p\u00f3tl\u00e1s\u00e1hoz mindenk\u00e9ppen tartozik p\u00f3t h\u00e1zi feladat v\u00e9d\u00e9s is. Ennek m\u00f3dj\u00e1r\u00f3l \u00e9s idej\u00e9r\u0151l egyeztess a laborvezet\u0151ddel.

"},{"location":"hf/#dokumentacio","title":"Dokument\u00e1ci\u00f3","text":"

A h\u00e1zi feladatot a specifik\u00e1ci\u00f3n t\u00fal dokument\u00e1lni is kell a README.md f\u00e1jlba. (A specifik\u00e1ci\u00f3 ut\u00e1n.) Ebben r\u00f6viden ismertetni kell az elk\u00e9sz\u00fclt alkalmaz\u00e1s funkcionalit\u00e1s\u00e1t \u00e9s az \u00e9rdekesebb megold\u00e1sokat.

Androidalap\u00fa szoftverfejleszt\u00e9s + Mobil- \u00e9s webes szoftverek k\u00f6z\u00f6s h\u00e1zi feladat

Ha valaki mind a k\u00e9t t\u00e1rgyat hallgatja a f\u00e9l\u00e9vben, van lehet\u0151s\u00e9g k\u00f6z\u00f6s h\u00e1zi feladat \u00edr\u00e1s\u00e1ra, DE: - Ezt mindenk\u00e9ppen egyeztetni kell mindk\u00e9t laborvezet\u0151vel. - Ugyanaz a h\u00e1zi csak \u00fagy adhat\u00f3 le mindk\u00e9t t\u00e1rgyon, ha a nehezebb k\u00f6vetelm\u00e9nyeket (vagyis az Androidalap\u00fa szoftverfejleszt\u00e9s\u00e9t) fel\u00fclteljes\u00edti. Teh\u00e1t az Androidalap\u00fa szoftverfejleszt\u00e9s k\u00f6vetelm\u00e9nyei szerint nem 5, hanem 6-7 technol\u00f3gi\u00e1t kell haszn\u00e1lni. Ennek mennyis\u00e9g\u00e9r\u0151l \u00e9s a feladat komplexit\u00e1s\u00e1r\u00f3l a laborvezet\u0151k d\u00f6ntenek.

"},{"location":"laborok/alarm/","title":"Labor10 - Id\u0151z\u00edt\u00e9s \u00e9s \u00e9rtes\u00edt\u00e9sek (Alarm)","text":""},{"location":"laborok/alarm/#bevezetes","title":"Bevezet\u00e9s","text":"

Ebben a laborban egy id\u0151z\u00edt\u0151 alkalmaz\u00e1st k\u00e9sz\u00edt\u00fcnk, amely a be\u00e1ll\u00edtott id\u0151intervallum eltelte ut\u00e1n \u00e9rtes\u00edt\u00e9st k\u00fcld, akkor is, ha az alkalamz\u00e1s felhaszn\u00e1l\u00f3i fel\u00fclete nincs az el\u0151t\u00e9rben, mert k\u00f6zben a felhaszn\u00e1l\u00f3 m\u00e1sik alkalmaz\u00e1st ind\u00edtott.

A laborban \u00e9rintett f\u0151bb t\u00e9mak\u00f6r\u00f6k:

  • H\u00e1tt\u00e9rfeladat futtat\u00e1sa Service seg\u00edts\u00e9g\u00e9vel
  • Id\u0151z\u00edtett feladatok
  • \u00c9rtes\u00edt\u00e9sek k\u00fcld\u00e9se
"},{"location":"laborok/alarm/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/alarm/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

"},{"location":"laborok/alarm/#a-projekt-megnyitasa","title":"A projekt megnyit\u00e1sa","text":"

Nyissuk meg a template-ben lev\u0151 projektet, \u00e9s a laborvezet\u0151vel tekints\u00fck \u00e1t a tartalm\u00e1t. A projektben a UI \u00e9p\u00edt\u0151elemei \u00e9s a drawable er\u0151forr\u00e1sok m\u00e1r megtal\u00e1lhat\u00f3k. Ezekhez fogjuk elk\u00e9sz\u00edteni az id\u0151z\u00edt\u0151 \u00fczleti logik\u00e1j\u00e1t, amit \u00f6sszek\u00f6t\u00fcnk az el\u0151k\u00e9sz\u00edtett felhaszn\u00e1l\u00f3i fel\u00fclettel.

"},{"location":"laborok/alarm/#a-fuggosegek-beallitasa","title":"A f\u00fcgg\u0151s\u00e9gek be\u00e1ll\u00edt\u00e1sa","text":"

Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket a modul szint\u0171 build.gradle.kts f\u00e1jlba. Ezekre az id\u0151 megad\u00e1s\u00e1hoz lesz majd sz\u00fcks\u00e9g\u00fcnk.

implementation(\"com.maxkeppeler.sheets-compose-dialogs:core:1.0.3\")\nimplementation(\"com.maxkeppeler.sheets-compose-dialogs:clock:1.0.3\")\nimplementation(\"com.maxkeppeler.sheets-compose-dialogs:duration:1.0.3\")\n
"},{"location":"laborok/alarm/#a-domenmodellek-elkeszitese","title":"A dom\u00e9nmodellek elk\u00e9sz\u00edt\u00e9se","text":"

El\u0151sz\u00f6r n\u00e9h\u00e1ny olyan oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, amelyekkel az alkalmaz\u00e1s aktu\u00e1lis \u00e1llapota, illetve az \u00e1llapotv\u00e1ltoz\u00e1st el\u0151id\u00e9z\u0151 esem\u00e9nyek reprezent\u00e1lhat\u00f3k. Ehhez k\u00e9sz\u00edts\u00fcnk egy util package-et. Ebbe hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt:

sealed class AlarmEvent {\ndata class SetAlarmDuration(val duration: Duration): AlarmEvent()\ndata class SetAlarm(val context: Context): AlarmEvent()\ndata class PauseAlarm(val context: Context): AlarmEvent()\ndata class ResumeAlarm(val context: Context): AlarmEvent()\ndata class StopAlarm(val context: Context): AlarmEvent()\n}\n

Az oszt\u00e1ly tartalma k\u00f6nnyen meg\u00e9rthet\u0151, az egyes felhaszn\u00e1l\u00f3i interakci\u00f3kat mint lehets\u00e9ges esem\u00e9nyeket jelk\u00e9pezi.

Most hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyokat egy f\u00e1jlban:

enum class AlarmServiceState {\nIDLE, SET, PAUSE, CANCELED\n}\n\ndata class AlarmState(\nval currentAlarmDuration: Duration = Duration.ZERO,\nval alarmDuration: Duration = Duration.ZERO,\nval alarmState: AlarmServiceState = AlarmServiceState.IDLE,\n) {\ncompanion object {\nval state = MutableStateFlow(AlarmState())\n\nfun isAlarmSet(): Boolean = state.value.alarmState == AlarmServiceState.SET\n\nfun isAlarmPaused(): Boolean = state.value.alarmState == AlarmServiceState.PAUSE\n\nfun isAlarmIdle(): Boolean = state.value.alarmState == AlarmServiceState.IDLE || state.value.alarmState == AlarmServiceState.CANCELED\n\nfun isStopEnabled(): Boolean = state.value.alarmState == AlarmServiceState.SET\n\n}\n}\n

Az AlarmServiceState lehets\u00e9ges \u00e9rt\u00e9kei \u00edrj\u00e1k le, hogy az id\u0151z\u00edt\u0151 \u00e9pp v\u00e1rakozik, fut, pauz\u00e1lt vagy le\u00e1ll\u00edtott. Az AlarmState ezt az aktu\u00e1lis \u00e1llapotot t\u00e1rolja, \u00e9s mell\u00e9 m\u00e9g egys\u00e9gbe z\u00e1rja, hogy mennyi az id\u0151z\u00edt\u0151 c\u00e9lideje, \u00e9s mennyi telt el eddig.

"},{"location":"laborok/alarm/#a-felhasznaloi-felulet-es-a-domenmodell-osszekotese","title":"A felhaszn\u00e1l\u00f3i fel\u00fclet \u00e9s a dom\u00e9nmodell \u00f6sszek\u00f6t\u00e9se","text":"

A felhaszn\u00e1l\u00f3i fel\u00fclet\u00fcnk a template-ben m\u00e1r rendelkez\u00e9sre \u00e1ll, de a viewmodelleket m\u00e9g el kell k\u00e9sz\u00edten\u00fcnk, hogy az im\u00e9nt l\u00e9trehozott dom\u00e9nmodellel a UI-t \u00f6ssze tudjuk kapcsolni. A ui.alarm package-be hozzuk l\u00e9tre a ViewModel\u00fcnket:

@HiltViewModel\nclass AlarmViewModel @Inject constructor(): ViewModel() {\n\nprivate val _state = AlarmState.state\nval state = _state.asStateFlow()\n\nfun onEvent(event: AlarmEvent) {\nwhen(event) {\nis AlarmEvent.SetAlarmDuration -> {\n_state.update { it.copy(\ncurrentAlarmDuration = event.duration,\nalarmDuration = event.duration\n) }\n}\nis AlarmEvent.SetAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.PauseAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.PAUSE,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.ResumeAlarm -> {\nval context = event.context\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.StopAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\n}\n}\n}\n

L\u00e1that\u00f3, hogy a ViewModel kezeli az esem\u00e9nyeket, \u00e9s az alkalmaz\u00e1s \u00e1llapot\u00e1t ennek megfelel\u0151en friss\u00edti, azonban a h\u00e1tt\u00e9rben fut\u00f3, t\u00e9nyleges id\u0151z\u00edt\u00e9st v\u00e9gz\u0151 AlarmService m\u00e9g nincs k\u00e9sz, \u00edgy annak h\u00edv\u00e1sai ide nincsenek bek\u00f6tve.

Most ugyanebbe a package-ben hozzuk l\u00e9tre a teljes k\u00e9perny\u0151t is:

import androidx.compose.animation.ExperimentalAnimationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.maxkeppeker.sheets.core.models.base.rememberSheetState\nimport com.maxkeppeler.sheets.duration.DurationDialog\nimport com.maxkeppeler.sheets.duration.models.DurationConfig\nimport com.maxkeppeler.sheets.duration.models.DurationFormat\nimport com.maxkeppeler.sheets.duration.models.DurationSelection\nimport hu.bme.aut.android.alarm.R\nimport hu.bme.aut.android.alarm.ui.common.AlarmStateButton\nimport hu.bme.aut.android.alarm.ui.common.DurationCounter\nimport hu.bme.aut.android.alarm.ui.theme.pauseColor\nimport hu.bme.aut.android.alarm.ui.theme.startColor\nimport hu.bme.aut.android.alarm.ui.theme.stopColor\nimport hu.bme.aut.android.alarm.util.AlarmEvent\nimport hu.bme.aut.android.alarm.util.AlarmState\nimport kotlin.time.DurationUnit\nimport kotlin.time.toDuration\n\n@OptIn(ExperimentalMaterial3Api::class)\n@ExperimentalPermissionsApi\n@ExperimentalAnimationApi\n@Composable\nfun AlarmScreen(\nviewModel: AlarmViewModel = hiltViewModel()\n) {\n\nval state by viewModel.state.collectAsState()\n\nval context = LocalContext.current\n\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\nhorizontalAlignment = Alignment.CenterHorizontally,\nverticalArrangement = Arrangement.Center\n) {\n\nvar isDialogShown by remember { mutableStateOf(false) }\nDurationCounter(\nduration = state.currentAlarmDuration,\nclickEnabled = AlarmState.isAlarmIdle(),\nonClick = { isDialogShown = !isDialogShown }\n)\n\nif (isDialogShown) {\nPopup(\nalignment = Alignment.Center,\nonDismissRequest = {\nisDialogShown = !isDialogShown\n}\n) {\nBox(\nmodifier = Modifier.wrapContentSize()\n) {\nSurface(\nshape = MaterialTheme.shapes.medium,\ncolor = MaterialTheme.colorScheme.surface,\n) {\nDurationDialog(\nstate = rememberSheetState(\nvisible = true,\nonCloseRequest = { isDialogShown = !isDialogShown }\n),\nselection = DurationSelection {\nval duration = it.toDuration(DurationUnit.SECONDS)\nviewModel.onEvent(AlarmEvent.SetAlarmDuration(duration))\n},\nconfig = DurationConfig(\ntimeFormat = DurationFormat.HH_MM_SS,\ncurrentTime = 0L,\n),\n)\n}\n}\n}\n}\n\nRow(\nmodifier = Modifier\n.padding(vertical = 10.dp)\n.wrapContentSize(Alignment.Center)\n) {\nAlarmStateButton(\niconId = if (AlarmState.isAlarmSet() && !AlarmState.isAlarmPaused()) {\nR.drawable.pause_24\n} else R.drawable.play_24,\nsurfaceColor = if (AlarmState.isAlarmSet() && !AlarmState.isAlarmPaused()) {\nMaterialTheme.colorScheme.pauseColor\n} else MaterialTheme.colorScheme.startColor,\n) {\nif (!AlarmState.isAlarmSet()) {\nviewModel.onEvent(AlarmEvent.SetAlarm(context))\n} else {\nif (AlarmState.isAlarmPaused()) {\nviewModel.onEvent(AlarmEvent.ResumeAlarm(context))\n} else {\nviewModel.onEvent(AlarmEvent.PauseAlarm(context))\n}\n}\n}\nSpacer(modifier = Modifier.width(5.dp))\nAlarmStateButton(\niconId = R.drawable.stop_24,\nsurfaceColor = MaterialTheme.colorScheme.stopColor,\nenabled = AlarmState.isStopEnabled()\n) {\nviewModel.onEvent(AlarmEvent.StopAlarm(context))\n}\n}\n\n}\n}\n

M\u00e9g l\u00e9tre kell hoznunk az AlarmApplication oszt\u00e1lyt puszt\u00e1n a Hilt inicializ\u00e1l\u00e1s\u00e1hoz:

@HiltAndroidApp\nclass AlarmApplication : Application()\n

\u00c9s az oszt\u00e1lyt regisztr\u00e1ljuk is be a Manifest f\u00e1jlban:

    <application\nandroid:name=\".AlarmApplication\"\n\n...\n

B\u00e1r m\u00e9g az alkalmaz\u00e1slogik\u00e1nk nem m\u0171k\u00f6dik t\u00e9nylegesen, vegy\u00fck fel ide a sz\u00fcks\u00e9ges enged\u00e9lyeket is. Az alkalmaz\u00e1sunk egy el\u0151t\u00e9rben fut\u00f3 (foreground) Service-t fog haszn\u00e1lni ahhoz, hogy az id\u0151z\u00edt\u00e9s akkor is m\u0171k\u00f6dj\u00f6n, ha az alkalmaz\u00e1s Activity-je m\u00e1r nem l\u00e1that\u00f3. Az ilyen Service-ekhez k\u00f6telez\u0151, hogy legyen \u00e9rtes\u00edt\u00e9s az \u00e9rtes\u00edt\u00e9si s\u00e1von, hogy a felhaszn\u00e1l\u00f3 mindig l\u00e1ssa, hogy milyen alkalmaz\u00e1sok futnak a h\u00e1tt\u00e9rben. (Ennek egy tipikus p\u00e9ld\u00e1ja a zenelej\u00e1tsz\u00f3 alkalmaz\u00e1sok esete is. Ez\u00e9rt az \u00e9rtes\u00edt\u00e9sekhez sz\u00fcks\u00e9ges enged\u00e9lyt is fel kell venni. Sz\u00fcks\u00e9ges m\u00e9g a pontos riaszt\u00e1s, \u00e9s a pontos riaszt\u00e1s id\u0151z\u00edt\u00e9s\u00e9nek enged\u00e9lye:

<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_SPECIAL_USE\" />\n\n<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n<uses-permission android:name=\"android.permission.SCHEDULE_EXACT_ALARM\" />\n<uses-permission android:name=\"android.permission.USE_EXACT_ALARM\" />\n

Android 14 (API level 34) \u00f3ta az el\u0151t\u00e9rben fut\u00f3 service-ek t\u00edpus\u00e1t is sz\u00fcks\u00e9ges megadni a Manifestben az enged\u00e9lyn\u00e9l/Service komponensn\u00e9l, illetve a Service ind\u00edt\u00e1sakor. L\u00e1sd: https://developer.android.com/develop/background-work/services/fg-service-types

M\u00e9g a MainActivity elk\u00e9sz\u00edt\u00e9se maradt h\u00e1tra az alapvet\u0151 feladatok k\u00f6z\u00fcl. Ebben elk\u00e9rj\u00fck az enged\u00e9lyeket, \u00e9s megjelen\u00edtj\u00fck a l\u00e9trehozott k\u00e9perny\u0151t:

@ExperimentalPermissionsApi\n@ExperimentalAnimationApi\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\n\nsetContent {\nAlarmTheme {\nAlarmScreen()\n}\n}\n\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\nrequestPermissions(\nManifest.permission.POST_NOTIFICATIONS\n)\n}\n}\n\nprivate fun requestPermissions(vararg permissions: String) {\nval requestPermissionLauncher = registerForActivityResult(\nActivityResultContracts.RequestMultiplePermissions()\n) { result ->\nresult.entries.forEach {\nLog.d(\"MainActivity\", \"${it.key} = ${it.value}\")\n}\n}\nrequestPermissionLauncher.launch(permissions.asList().toTypedArray())\n}\n}\n

Ha a POST_NOTIFICATIONS konstansot nem tal\u00e1lja a ford\u00edt\u00f3, akkor az import android.Manifest sort vegy\u00fck fel a f\u00e1jl elej\u00e9re.

Most m\u00e1r ind\u00edthat\u00f3 az alkalmaz\u00e1s, \u00e9s megjelenik a felhaszn\u00e1l\u00f3i fel\u00fclet, de m\u00e9g nem m\u0171k\u00f6dik \u00e9rdemi m\u00f3don, hiszen a az \u00fczleti logik\u00e1t v\u00e9gz\u0151 Service nincs k\u00e9sz.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/alarm/#az-idozites-elkeszitese","title":"Az id\u0151z\u00edt\u00e9s elk\u00e9sz\u00edt\u00e9se","text":"

A Service komponens\u00fcnk a fentiek alapj\u00e1n id\u0151z\u00edt\u00e9st \u00e9s \u00e9rtes\u00edt\u00e9seket is fog haszn\u00e1lni, ez\u00e9rt el\u0151sz\u00f6r ezekhez k\u00e9sz\u00edt\u00fcnk n\u00e9mi seg\u00e9dlogik\u00e1t, hogy a Service ut\u00e1na k\u00f6nnyebben elk\u00e9sz\u00edthet\u0151 legyen.

Az id\u0151z\u00edt\u00e9st kiszervezz\u00fck egy seg\u00e9doszt\u00e1lyba, hogy a Service k\u00f3dja \u00e1tl\u00e1that\u00f3bb maradjon. El\u0151sz\u00f6r egy AlarmItem oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, ez egy id\u0151z\u00edtend\u0151 esem\u00e9nyt jel\u00f6l, tulajdonk\u00e9ppen csak az id\u0151z\u00edt\u00e9s idej\u00e9t tartalmazza. Ezt egy \u00faj, scheduler nev\u0171 package-be tegy\u00fck:

data class AlarmItem(\nval time: LocalDateTime\n)\n

Szint\u00e9n ebbe a package-be k\u00e9sz\u00edt\u00fcnk egy AlarmReceiver nev\u0171 BroadcastReceivert, ezt fogjuk beregisztr\u00e1lni majd az id\u0151z\u00edt\u00e9s letelt\u00e9re, \u00e9s az Android id\u0151z\u00edt\u0151 rendszere az id\u0151 leteltekor ennek fog \u00e9rtes\u00edt\u00e9st k\u00fcldeni. Ebben egy Intent seg\u00edts\u00e9g\u00e9vel a Service komponenst fogjuk megh\u00edvni, de mivel ez m\u00e9g nincs k\u00e9sz, \u00edgy a k\u00f3d jelenleg nem fordul le, majd a Service elk\u00e9sz\u00fcltekor v\u00e1lik az eg\u00e9sz m\u0171k\u00f6d\u0151k\u00e9pess\u00e9. Ezt az oszt\u00e1lyt is a scheduler package-be tegy\u00fck:

class AlarmReceiver: BroadcastReceiver() {\n\noverride fun onReceive(context: Context?, intent: Intent?) {\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_PLAY\ncontext?.startService(this)\n}\n}\n}\n

Mivel a BroadcastReceiver is egy f\u0151 komponenst\u00edpus Androidon, ez\u00e9rt ezt a Manifest f\u00e1jlban is sz\u00fcks\u00e9ges regisztr\u00e1lni:

<receiver android:name=\".scheduler.AlarmReceiver\" />\n

\u00c9s v\u00e9g\u00fcl egy AlarmScheduler oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk:

class AlarmScheduler @Inject constructor(\n@ApplicationContext private val context: Context,\n) {\nprivate val alarmManager = context.getSystemService(AlarmManager::class.java)\n\nfun schedule(alarmItem: AlarmItem) {\nval intent = Intent(context, AlarmReceiver::class.java).apply {\nputExtra(\"ALARM_TIME\", alarmItem.time)\n}\nalarmManager.setExactAndAllowWhileIdle(\nAlarmManager.RTC_WAKEUP,\nalarmItem.time.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000,\nPendingIntent.getBroadcast(\ncontext,\nalarmItem.hashCode(),\nintent,\nPendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n)\n)\n}\n\nfun cancel(item: AlarmItem) {\nalarmManager.cancel(\nPendingIntent.getBroadcast(\ncontext,\nitem.hashCode(),\nIntent(context, AlarmReceiver::class.java),\nPendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n)\n)\n}\n\n}\n

Ebben az oszt\u00e1lyban a rendszert\u0151l k\u00e9r\u00fcnk referenci\u00e1t az AlarmManagerre, amelynek seg\u00edts\u00e9g\u00e9vel a rendszerben lev\u0151 id\u0151z\u00edt\u0151 szolg\u00e1ltat\u00e1sok el\u00e9rhet\u0151ek. Az oszt\u00e1ly k\u00e9t met\u00f3dust defini\u00e1l, az egyikkel esem\u00e9nyt id\u0151z\u00edthet\u00fcnk, a m\u00e1sikkal pedig megl\u00e9v\u0151 id\u0151z\u00edt\u00e9st t\u00f6r\u00f6lhet\u00fcnk.

"},{"location":"laborok/alarm/#az-ertesitesek-kuldese","title":"Az \u00e9rtes\u00edt\u00e9sek k\u00fcld\u00e9se","text":"

Az \u00e9rtes\u00edt\u00e9sek k\u00fcld\u00e9s\u00e9t is kiszervezz\u00fck, hogy a Service oszt\u00e1lyunk egyszer\u0171bb legyen. El\u0151sz\u00f6r is a notification package-et hozzuk l\u00e9tre, \u00e9s ebben k\u00e9sz\u00edts\u00fck el a NotificationHelper oszt\u00e1lyt:

class NotificationHelper @Inject constructor(\nval notificationManager: NotificationManager,\nval notificationBuilder: NotificationCompat.Builder\n) {\nfun updateNotification(\nhours: Int,\nminutes:Int,\nseconds:Int,\ndurationInSeconds: Int,\n) {\nval progress = durationInSeconds - hours * 60 * 60 - minutes * 60 - seconds\nnotificationManager.notify(\nNOTIFICATION_ID,\nnotificationBuilder\n.setProgress(durationInSeconds,progress,false)\n.setContentText(\nString.format(\"%02d:%02d:%02d\",hours,minutes,seconds)\n).build()\n)\n}\n\nfun cancelNotification() {\nnotificationManager.cancel(NOTIFICATION_ID)\n}\n\n@SuppressLint(\"RestrictedApi\")\nfun setNotificationButton(vararg actions: NotificationCompat.Action) {\nnotificationBuilder.mActions.clear()\nactions.forEachIndexed { index, action ->\nnotificationBuilder.mActions.add(index, action)\n}\n}\n\nfun createNotificationChannel() {\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\nval alarmChannel = NotificationChannel(\nNOTIFICATION_CHANNEL_ID,\nNOTIFICATION_CHANNEL_NAME,\nNotificationManager.IMPORTANCE_LOW\n)\nnotificationManager.createNotificationChannel(alarmChannel)\n}\n}\n\ncompanion object {\nconst val NOTIFICATION_ID = 101\nconst val NOTIFICATION_CHANNEL_NAME = \"ALARM_NOTIFICATION\"\nconst val NOTIFICATION_CHANNEL_ID = \"ALARM_NOTIFICATION_ID\"\n}\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

Tekints\u00fck \u00e1t, hogyan \u00e9p\u00fcl fel ez az oszt\u00e1ly!

  • K\u00e9t f\u00fcgg\u0151s\u00e9ge van: a NotificationManager seg\u00edts\u00e9g\u00e9vel kezelhet\u0151k az \u00e9rtes\u00edt\u00e9sek a rendszeren, viszont az ennek \u00e1tadott \u00e9rtes\u00edt\u00e9sek ak\u00e1r el\u00e9g \u00f6sszetettek is lehetnek, ez\u00e9rt a builder tervez\u00e9si minta szerint a NotificationBuilder seg\u00edts\u00e9g\u00e9vel kell \u0151ket fel\u00e9p\u00edten\u00fcnk.
  • Az updateNotification l\u00e9trehozza vagy friss\u00edti az \u00e9rtes\u00edt\u00e9st, \u00e9s ezen megjelenik a h\u00e1tralev\u0151 id\u0151 \u00f3ra, perc, m\u00e1sodperc bont\u00e1sban, valamint egy folyamatjelz\u0151 s\u00e1v, \"progress bar\" is, amelyet a h\u00e1tralev\u0151 id\u0151 alapj\u00e1n sz\u00e1m\u00edtunk. L\u00e1that\u00f3, hogy egy numerikus azonos\u00edt\u00f3t is megadunk az \u00e9rtes\u00edt\u00e9shez, ez teszi lehet\u0151v\u00e9, hogy ut\u00e1na majd t\u00f6r\u00f6lni tudjuk.
  • A cancelNotification t\u00f6rli a m\u00e1r l\u00e9trehozott \u00e9rtes\u00edt\u00e9st.
  • A setNotificationButton seg\u00edts\u00e9g\u00e9vel gombokat adhatunk az \u00e9rtes\u00edt\u00e9shez. Hogy milyen \u00e9s h\u00e1ny gombot szeretn\u00e9nk hozz\u00e1adni, ez az id\u0151z\u00edt\u0151 \u00e1llapot\u00e1t\u00f3l f\u00fcgg, ez\u00e9rt hasznos, hogy k\u00fcl\u00f6n met\u00f3dussal adhassuk meg. P\u00e9ld\u00e1ul sz\u00fcneteltet\u00e9st v\u00e9gz\u0151 gomb hozz\u00e1ad\u00e1s\u00e1nak akkor van \u00e9rtelme, ha az id\u0151z\u00edt\u0151 \u00e9pp fut.

  • A createNotificationChannel \u00e9rtes\u00edt\u00e9si csatorn\u00e1t hoz l\u00e9tre. Az Android O \u00f3ta az \u00e9rtes\u00edt\u00e9seket csatorn\u00e1khoz kell rendelni. Ez az\u00e9rt hasznos, mert a felhaszn\u00e1l\u00f3 az alkalmaz\u00e1son bel\u00fcl csatorn\u00e1nk\u00e9nt tudja tiltani vagy enged\u00e9lyezni a csatorn\u00e1kat. Pl. a fontos \u00e9rtes\u00edt\u00e9sek k\u00fcl\u00f6n csatorn\u00e1ra szervezhet\u0151k, \u00e9s az elhanyagolhat\u00f3bb jelent\u0151s\u00e9g\u0171eket a felhaszn\u00e1l\u00f3 k\u00f6nnyed\u00e9n letilthatja. Jelen alkalmaz\u00e1sban ezt a lehet\u0151s\u00e9get nem haszn\u00e1ljuk ki, csup\u00e1n egy csatorn\u00e1nk lesz, de azt akkor is l\u00e9tre kell hozzuk. A csatorna t\u00f6bbsz\u00f6ri l\u00e9trehoz\u00e1sa nem okoz probl\u00e9m\u00e1t, ez\u00e9rt el\u00e9g minden \u00e9rtes\u00edt\u00e9s el\u0151tt megh\u00edvni, hogy biztosan l\u00e9trej\u00f6jj\u00f6n, nem sz\u00fcks\u00e9ges tesztelni, hogy l\u00e9tezik-e.

L\u00e1that\u00f3, hogy a NotificationManager \u00e9s a NotificationBuilder a NotificationHelper f\u00fcgg\u0151s\u00e9gei, amelyeket a konstruktoron kereszt\u00fcl kap meg a Dagger/Hilt seg\u00edts\u00e9g\u00e9vel. Viszont ehhez m\u00e9g konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges, hogy ezek a komponensek val\u00f3ban l\u00e9trej\u00f6jjenek. Hozzuk l\u00e9tre a notification.di package-et, majd ebben az al\u00e1bbi modult:

@ExperimentalAnimationApi\n@Module\n@InstallIn(ServiceComponent::class)\nobject NotificationModule {\n\n@Provides\n@ServiceScoped\nfun provideNotificationBuilder(\n@ApplicationContext context: Context\n) = NotificationCompat.Builder(context,NOTIFICATION_CHANNEL_ID)\n.setSmallIcon(R.drawable.ic_round_alarm_24)\n.setContentTitle(context.getString(R.string.alarm_notification_title))\n.setContentText(\"00:00\")\n.setOngoing(true)\n.addAction(\nR.drawable.ic_round_stop_24,\ncontext.getString(R.string.alarm_notification_action_cancel),\nnull\n)\n.setContentIntent(null)\n\n@Provides\n@ServiceScoped\nfun provideNotificationManager(\n@ApplicationContext context: Context\n) = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n@Provides\n@ServiceScoped\nfun provideNotificationHelper(\nnotificationBuilder: NotificationCompat.Builder,\nnotificationManager: NotificationManager\n) = NotificationHelper(notificationManager, notificationBuilder)\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/alarm/#a-service-elkeszitese","title":"A Service elk\u00e9sz\u00edt\u00e9se","text":"

Hozzunk l\u00e9tre egy service package-et, majd ebbe k\u00e9sz\u00edts\u00fck el az AlarmService oszt\u00e1lyt:

import android.app.PendingIntent\nimport android.app.Service\nimport android.content.Intent\nimport android.media.MediaPlayer\nimport android.media.RingtoneManager\nimport android.os.Build\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport dagger.hilt.android.AndroidEntryPoint\nimport hu.bme.aut.android.alarm.notification.NotificationHelper\nimport hu.bme.aut.android.alarm.notification.NotificationHelper.Companion.NOTIFICATION_ID\nimport hu.bme.aut.android.alarm.scheduler.AlarmItem\nimport hu.bme.aut.android.alarm.scheduler.AlarmScheduler\nimport hu.bme.aut.android.alarm.util.AlarmServiceState\nimport hu.bme.aut.android.alarm.util.AlarmState\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport java.time.LocalDateTime\nimport java.util.Timer\nimport javax.inject.Inject\nimport kotlin.concurrent.fixedRateTimer\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n@AndroidEntryPoint\nclass AlarmService : Service(), MediaPlayer.OnPreparedListener {\n\n@Inject\nlateinit var notificationHelper: NotificationHelper\n\n@Inject\nlateinit var alarmScheduler: AlarmScheduler\n\nprivate val _state = AlarmState.state\nprivate val state = _state.asStateFlow()\n\nprivate lateinit var timer: Timer\n\nprivate var alarmItem: AlarmItem? = null\n\nprivate var mMediaPlayer: MediaPlayer? = null\n\noverride fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\nintent?.action.let { action ->\nwhen (action) {\nACTION_SET -> {\nintent?.getStringExtra(ALARM_TIME)?.let { durationString ->\nDuration.parse(durationString).let { duration ->\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\ncurrentAlarmDuration = duration\n) }\n}\n}\n\nalarmItem = AlarmItem(\nLocalDateTime.now().plusSeconds(\nstate.value.currentAlarmDuration.inWholeSeconds\n))\nalarmItem?.let(alarmScheduler::schedule)\n\nnotificationHelper.createNotificationChannel()\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Pause\",\npauseAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)\n{\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build(),\nFOREGROUND_SERVICE_TYPE_SPECIAL_USE\n)\n}\nelse\n{\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\n}\nsetAlarm { h, m, s ->\nnotificationHelper.updateNotification(\nh, m, s,\nstate.value.alarmDuration.inWholeSeconds.toInt()\n)\n}\n}\nACTION_PAUSE -> {\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Resume\",\nresumeAlarm()\n)\n)\nnotificationHelper.notificationManager.notify(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\n_state.update { it.copy(alarmState = AlarmServiceState.PAUSE) }\n\nalarmItem?.let(alarmScheduler::cancel)\n}\nACTION_CANCEL -> {\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n_state.update { it.copy(alarmState = AlarmServiceState.CANCELED) }\nnotificationHelper.cancelNotification()\nif (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {\nstopForeground(STOP_FOREGROUND_REMOVE)\n} else stopForeground(STOP_FOREGROUND_DETACH)\nstopSelf()\nalarmItem?.let(alarmScheduler::cancel)\nmMediaPlayer?.stop()\n}\nACTION_PLAY -> {\nmMediaPlayer = MediaPlayer()\nmMediaPlayer?.apply {\nreset()\nsetDataSource(\nthis@AlarmService,\nRingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)\n)\nsetOnPreparedListener(this@AlarmService)\nprepareAsync()\n}\n}\nelse -> Unit\n}\n}\n\nwhen(intent?.getStringExtra(ALARM_STATE)) {\nAlarmServiceState.SET.name -> {\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\nalarmItem = AlarmItem(\nLocalDateTime.now().plusSeconds(\nstate.value.currentAlarmDuration.inWholeSeconds\n)\n)\n\nalarmItem?.let(alarmScheduler::schedule)\n\nnotificationHelper.createNotificationChannel()\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Pause\",\npauseAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\nsetAlarm { h, m, s ->\nnotificationHelper.updateNotification(\nh, m, s,\nstate.value.alarmDuration.inWholeSeconds.toInt()\n)\n}\n}\nAlarmServiceState.PAUSE.name -> {\n_state.update { it.copy(alarmState = AlarmServiceState.PAUSE) }\n\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Resume\",\nresumeAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nnotificationHelper.notificationManager.notify(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\nalarmItem?.let(alarmScheduler::cancel)\n}\nAlarmServiceState.CANCELED.name -> {\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.IDLE,\n) }\n\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n\nnotificationHelper.cancelNotification()\nif (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {\nstopForeground(STOP_FOREGROUND_REMOVE)\n} else stopForeground(STOP_FOREGROUND_DETACH)\nstopSelf()\nalarmItem?.let(alarmScheduler::cancel)\nmMediaPlayer?.stop()\n}\n}\nreturn super.onStartCommand(intent, flags, startId)\n}\n\nprivate fun setAlarm(onTick: (hours: Int, minutes: Int, seconds: Int) -> Unit) {\ntimer = fixedRateTimer(initialDelay = 1000L, period = 1000L) {\nif (state.value.currentAlarmDuration != Duration.ZERO) {\nval newDuration = state.value.currentAlarmDuration - 1.seconds\n_state.update { it.copy(currentAlarmDuration = newDuration) }\nstate.value.currentAlarmDuration.toComponents { hours, minutes, seconds, _ ->\nonTick(hours.toInt(),minutes,seconds)\n}\n}\n}\n}\n\nprivate fun cancelAlarm(): PendingIntent {\nval cancelIntent = Intent(this, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.CANCELED.name)\n}\nreturn PendingIntent.getService(\nthis, CANCEL_REQUEST_CODE, cancelIntent, flag\n)\n}\n\nprivate fun resumeAlarm(): PendingIntent {\nval resumeIntent = Intent(this, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.SET.name)\n}\nreturn PendingIntent.getService(\nthis, RESUME_REQUEST_CODE, resumeIntent, flag\n)\n}\n\nprivate fun pauseAlarm(): PendingIntent {\nval pauseIntent = Intent(applicationContext, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.PAUSE.name)\n}\nreturn PendingIntent.getService(\nthis, PAUSE_REQUEST_CODE, pauseIntent, flag\n)\n}\n\noverride fun onDestroy() {\nsuper.onDestroy()\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\nif (this::timer.isInitialized) timer.cancel()\nmMediaPlayer?.release()\nmMediaPlayer = null\n}\n\noverride fun onBind(p0: Intent?): IBinder? = null\n\ncompanion object {\nconst val ALARM_STATE = \"ALARM_STATE\"\n\nconst val ACTION_SET = \"ALARM_SET\"\nconst val ACTION_PAUSE = \"ALARM_PAUSE\"\nconst val ACTION_CANCEL = \"ALARM_CANCEL\"\nconst val ACTION_PLAY = \"ALARM_PLAY\"\n\nconst val ALARM_TIME = \"ALARM_TIME\"\n\nprivate const val flag = PendingIntent.FLAG_IMMUTABLE\n\nprivate const val CANCEL_REQUEST_CODE = 102\nprivate const val RESUME_REQUEST_CODE = 103\nprivate const val PAUSE_REQUEST_CODE = 104\n}\n\noverride fun onPrepared(mediaPlayer: MediaPlayer) {\nmediaPlayer.start()\n}\n}\n

Tekints\u00fck \u00e1t, hogyan is m\u0171k\u00f6dik a fenti Service! Egy Android Service k\u00e9t m\u00f3don m\u0171k\u00f6dhet. Lehet \"Started Service\", ez azt jelenti, hogy indul\u00e1s ut\u00e1n a h\u00e1tt\u00e9rben (Background Service) teszi a dolg\u00e1t, am\u00edg az alkalmaz\u00e1s valamilyen komponense l\u00e1that\u00f3, vagy ak\u00e1r akkor is m\u0171k\u00f6dik, amikor az alkalmaz\u00e1s nem l\u00e1that\u00f3 (Foreground Service), viszont ilyenkor egy \u00e9rtes\u00edt\u00e9st k\u00f6telez\u0151 megjelen\u00edtenie, hogy a felhaszn\u00e1l\u00f3 sz\u00e1m\u00e1ra ne maradhasson \u00e9szrev\u00e9tlen (biztons\u00e1gi okokb\u00f3l). Az AlarmService az alkalmaz\u00e1sunkban egy Foreground Service-k\u00e9nt m\u0171k\u00f6dik, hogy az id\u0151z\u00edt\u0151 akkor se \u00e1lljon le, ha az alkalmaz\u00e1s fel\u00fclete m\u00e1r nincs el\u0151t\u00e9rben. A Service m\u00e1sik lehets\u00e9ges m\u0171k\u00f6d\u00e9si m\u00f3dja a \"Bound Service\". Ez azt jelenti, hogy m\u00e1s komponensek kapcsol\u00f3dhatnak hozz\u00e1, \u00e9s feladatokat v\u00e9geztethetnek vele. Addig m\u0171k\u00f6dik ilyen m\u00f3don, am\u00edg legal\u00e1bb egy kapcsol\u00f3d\u00f3 komponens van. Egy Service lehetne egyszerre \"Started Service\" \u00e9s \"Bound Service\" is, de most ut\u00f3bbi funkcionalit\u00e1sra nincs sz\u00fcks\u00e9g az alkalmaz\u00e1sban, mert \"Started Service\"-k\u00e9nt k\u00e9pes az \u00e1llapotot friss\u00edteni, aminek v\u00e1ltoz\u00e1sa ut\u00e1na a felhaszn\u00e1l\u00f3i fel\u00fcleten is megjelenik.

N\u00e9zz\u00fck \u00e1t el\u0151bb az AlarmService seg\u00e9dmet\u00f3dusait:

  • setAlarm: ez elind\u00edtja az id\u0151z\u00edt\u00e9st, \u00e9s ehhez be\u00e1ll\u00edt egy m\u00e1sodpercenk\u00e9nti \u00fctemez\u0151t is, amellyel majd lehet friss\u00edteni a felhaszn\u00e1l\u00f3i fel\u00fcleten, illetve a megjelen\u00edtett \u00e9rtes\u00edt\u00e9sben kijelzett h\u00e1tralev\u0151 id\u0151t. Ez a met\u00f3dus callbackk\u00e9nt kapja meg, hogy mit kell futtatnia, amikor egy m\u00e1sodperc eltelt.
  • cancelAlarm: ez egy PendingIntentet gy\u00e1rt le, amellyel meg\u00e1ll\u00edthat\u00f3 az id\u0151z\u00edt\u0151. A PendingIntent mag\u00e1nak az AlarmService-nek k\u00fcldi a megfelel\u0151 Intentet, \u00e9s ez a PendingIntent majd az \u00e9rtes\u00edt\u00e9sben megjelen\u00edtett akci\u00f3hoz rendelhet\u0151.
  • resumeAlarm: az el\u0151z\u0151h\u00f6z hasonl\u00f3, de az id\u0151z\u00edt\u00e9s \u00fajraind\u00edt\u00e1s\u00e1hoz haszn\u00e1land\u00f3.
  • pauseAlarm: mint az el\u0151z\u0151ek, de pauz\u00e1l\u00e1sra.

Az AlarmService l\u00e9nyegi logik\u00e1ja az onStartCommand met\u00f3dusban van megval\u00f3s\u00edtva. Ez az esem\u00e9nykezel\u0151 akkor fut le, amikor a Service-t \"Started Service\"-k\u00e9nt ind\u00edtj\u00e1k. Ez egy Intent k\u00fcld\u00e9s\u00e9vel t\u00f6rt\u00e9nik, amelyben az action \u00e9s az extras seg\u00edts\u00e9g\u00e9vel utaznak a param\u00e9terek, hogy a Service-t\u0151l mit is k\u00e9r\u00fcnk. N\u00e9zz\u00fck v\u00e9gig az egyes eseteket! Alapvet\u0151en mindig az \u00e1llapot megfelel\u0151 be\u00e1ll\u00edt\u00e1sa, az id\u0151z\u00edt\u0151 logika megh\u00edv\u00e1sa, illetve az \u00e9rtes\u00edt\u00e9s megjelen\u00edt\u00e9se/friss\u00edt\u00e9se t\u00f6rt\u00e9nik a kor\u00e1bban \u00e1ttekintett k\u00f3dr\u00e9szletek megh\u00edv\u00e1s\u00e1val. K\u00fcl\u00f6n\u00f6sen fontos r\u00e9szlet, hogy az ACTION_SET akci\u00f3, azaz az id\u0151z\u00edt\u0151 elind\u00edt\u00e1sa eset\u00e9n a Service a startForeground h\u00edv\u00e1ssal foreground m\u00f3dba ker\u00fcl, hogy akkor is m\u0171k\u00f6dj\u00f6n tov\u00e1bbra is az id\u0151z\u00edt\u00e9s, ha az alkalmaz\u00e1s nem l\u00e1that\u00f3. Megfigyelend\u0151 m\u00e9g az ACTION_PLAY eset\u00e9ben a MediaPlayer API haszn\u00e1lata az \u00e9breszt\u0151 dallam lej\u00e1tsz\u00e1s\u00e1hoz.

Mivel a Service is egy f\u0151 komponenst\u00edpus Androidon, ez\u00e9rt ezt a Manifest f\u00e1jlban is sz\u00fcks\u00e9ges regisztr\u00e1lni:

<service\nandroid:name=\".service.AlarmService\"\nandroid:enabled=\"true\"\nandroid:exported=\"false\"\nandroid:foregroundServiceType=\"specialUse\" />\n

Most m\u00e1r \u00f6sszek\u00f6thet\u0151k a ViewModel \u00e9s a Service is az AlarmViewModel megfelel\u0151 kieg\u00e9sz\u00edt\u00e9s\u00e9vel:

@HiltViewModel\nclass AlarmViewModel @Inject constructor(): ViewModel() {\n\nprivate val _state = AlarmState.state\nval state = _state.asStateFlow()\n\nfun onEvent(event: AlarmEvent) {\nwhen(event) {\nis AlarmEvent.SetAlarmDuration -> {\n_state.update { it.copy(\ncurrentAlarmDuration = event.duration,\nalarmDuration = event.duration\n) }\n}\nis AlarmEvent.SetAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_SET\nstate.value.currentAlarmDuration.toString()\ncontext.startService(this)\n}\n}\nis AlarmEvent.PauseAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.PAUSE,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_PAUSE\ncontext.startService(this)\n}\n}\nis AlarmEvent.ResumeAlarm -> {\nval context = event.context\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_SET\ncontext.startService(this)\n}\n}\nis AlarmEvent.StopAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_CANCEL\ncontext.startService(this)\n}\n}\n}\n}\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/alarm/#onallo-feladat","title":"\u00d6n\u00e1ll\u00f3 feladat","text":"

Val\u00f3s\u00edtsd meg az \u00e9breszt\u0151\u00f3r\u00e1kon megszokott \"szundi\" funkci\u00f3t! Amikor az id\u0151z\u00edt\u0151 letelik, jelen\u00edts meg az \u00e9rtes\u00edt\u00e9sen egy \"Snooze\" gombot is, amivel e riaszt\u00e1s abbamarad, \u00e9s egy \u00fajabb 5 perces id\u0151z\u00edt\u00e9s indul.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/basics/","title":"Labor 01 - Alapok (HighLowGame)","text":"

Az els\u0151 labor rendhagy\u00f3 a t\u00f6bbihez k\u00e9pest. Itt kev\u00e9s k\u00f3ddal fogunk tal\u00e1lkozni, ink\u00e1bb az alapok \u00e1tn\u00e9z\u00e9s\u00e9n van a hangs\u00faly.

A labor c\u00e9lja, hogy bemutassa az Android fejleszt\u0151k\u00f6rnyezetet, az alkalmaz\u00e1sk\u00e9sz\u00edt\u00e9s, illetve a tesztel\u00e9s \u00e9s ford\u00edt\u00e1s folyamat\u00e1t, az alkalmaz\u00e1s fel\u00fcgyelet\u00e9t, valamint az emul\u00e1tor \u00e9s a fejleszt\u0151k\u00f6rnyezet funkci\u00f3it. Ismertetj\u00fck egy egyszer\u0171 barchoba alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9s\u00e9nek m\u00f3dj\u00e1t \u00e9s labor sor\u00e1n a laborvezet\u0151 r\u00e9szletesen bemutatja az eszk\u00f6z\u00f6ket.

A labor v\u00e9g\u00e9n egy jegyz\u0151k\u00f6nyvet kell beadni a jegy megszerz\u00e9s\u00e9hez.

A m\u00e9r\u00e9s az al\u00e1bbi t\u00e9m\u00e1kat \u00e9rinti:

  • Az Android platform alapfogalmainak ismerete
  • Android Studio fejleszt\u0151k\u00f6rnyezet alapok
  • Android Emul\u00e1tor tulajdons\u00e1gai
  • Android projekt l\u00e9trehoz\u00e1sa \u00e9s futtat\u00e1sa emul\u00e1toron
  • Manifest \u00e1llom\u00e1ny fel\u00e9p\u00edt\u00e9se
  • Android Profiler
"},{"location":"laborok/basics/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/basics/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

"},{"location":"laborok/basics/#markdown-fajl-megnyitasa","title":"Markdown f\u00e1jl megnyit\u00e1sa","text":"

A feladatok megold\u00e1sa sor\u00e1n a dokument\u00e1ci\u00f3t markdown form\u00e1tumban k\u00e9sz\u00edtsd. Az el\u0151bb let\u00f6lt\u00f6tt git repository-t nyisd meg egy markdown kompatibilis szerkeszt\u0151vel. Javasolt a Visual Studio Code haszn\u00e1lata:

  1. Ind\u00edtsd el a VS Code-ot.

  2. A File > Open Folder... men\u00fcvel nyisd meg a git repository k\u00f6nyvt\u00e1r\u00e1t.

  3. A bal oldali f\u00e1ban keresd meg a README.md f\u00e1jlt \u00e9s dupla kattint\u00e1ssal nyisd meg.

  4. Ezt a f\u00e1jlt szerkeszd.

  5. Ha k\u00e9pet k\u00e9sz\u00edtesz, azt is tedd a repository al\u00e1 a t\u00f6bbi f\u00e1jl mell\u00e9. \u00cdgy relat\u00edv el\u00e9r\u00e9si \u00fatvonallal (f\u00e1jln\u00e9v) fogod tudni hivatkozni.

    F\u00e1jln\u00e9v: csupa kisbet\u0171 \u00e9kezet n\u00e9lk\u00fcl

    A k\u00e9pek f\u00e1jlnev\u00e9ben ne haszn\u00e1lj \u00e9kezetes karaktereket, sz\u00f3k\u00f6z\u00f6ket, se kis- \u00e9s nagybet\u0171ket keverve. A k\u00fcl\u00f6nb\u00f6z\u0151 platformok \u00e9s a git elt\u00e9r\u0151en kezelik a f\u00e1jlneveket. A GitHub webes fel\u00fclet\u00e9n akkor fog minden rendben megjelenni, ha csak az angol \u00e1b\u00e9c\u00e9 kisbet\u0171it haszn\u00e1lod a f\u00e1jlnevekben.

  6. A k\u00e9nyelmes szerkeszt\u00e9shez nyisd meg az el\u0151n\u00e9zet funkci\u00f3t (Ctrl-K + V).

M\u00e1s szerkeszt\u0151eszk\u00f6z

Ha nem szimpatikus ez a szerkeszt\u0151, haszn\u00e1lhatod a GitHub webes fel\u00fclet\u00e9t is a dokument\u00e1ci\u00f3 szerkeszt\u00e9s\u00e9hez, itt is van el\u0151n\u00e9zet. Ekkor a f\u00e1jlok felt\u00f6lt\u00e9se kicsit k\u00f6r\u00fclm\u00e9nyesebb lesz.

"},{"location":"laborok/basics/#android-alapok","title":"Android alapok","text":""},{"location":"laborok/basics/#forditas-menete-android-platformon","title":"Ford\u00edt\u00e1s menete Android platformon","text":"

A projekt l\u00e9trehoz\u00e1sa ut\u00e1n a forr\u00e1sk\u00f3d az src k\u00f6nyvt\u00e1rban, m\u00edg a felhaszn\u00e1l\u00f3i fel\u00fclet le\u00edr\u00e1s\u00e1ra szolg\u00e1l\u00f3 XML \u00e1llom\u00e1nyok a res k\u00f6nyvt\u00e1rban tal\u00e1lhat\u00f3k. Az er\u0151forr\u00e1s \u00e1llom\u00e1nyokat egy R.java \u00e1llom\u00e1ny k\u00f6ti \u00f6ssze a forr\u00e1sk\u00f3ddal, \u00edgy k\u00f6nnyed\u00e9n el\u00e9rhetj\u00fck Java/Kotlin oldalr\u00f3l az XML-ben defini\u00e1lt fel\u00fcleti elemeket. Az Android projekt ford\u00edt\u00e1s\u00e1nak eredm\u00e9nye egy APK \u00e1llom\u00e1ny, melyet k\u00f6zvetlen\u00fcl telep\u00edthet\u00fcnk mobil eszk\u00f6zre.

Ford\u00edt\u00e1s menete Android platformon

  1. A fejleszt\u0151 elk\u00e9sz\u00edti a Kotlin forr\u00e1sk\u00f3dot, valamint az XML alap\u00fa felhaszn\u00e1l\u00f3i fel\u00fclet le\u00edr\u00e1st a sz\u00fcks\u00e9ges er\u0151forr\u00e1s \u00e1llom\u00e1nyokkal.

  2. A fejleszt\u0151k\u00f6rnyezet az er\u0151forr\u00e1s \u00e1llom\u00e1nyokb\u00f3l folyamatosan naprak\u00e9szen tartja az R.java er\u0151forr\u00e1s f\u00e1jlt a fejleszt\u00e9shez \u00e9s a ford\u00edt\u00e1shoz.

    FONTOS

    Az R.java \u00e1llom\u00e1ny gener\u00e1lt, k\u00e9zzel SOHA ne m\u00f3dos\u00edtsuk! (Az Android Studio egy\u00e9bk\u00e9nt nem is hagyja.)

  3. A fejleszt\u0151 a Manifest \u00e1llom\u00e1nyban be\u00e1ll\u00edtja az alkalmaz\u00e1s hozz\u00e1f\u00e9r\u00e9si jogosults\u00e1gait (pl. Internet el\u00e9r\u00e9s, szenzorok haszn\u00e1lata, stb.), illetve ha fut\u00e1s idej\u0171 jogosults\u00e1gok sz\u00fcks\u00e9gesek, ezt kezeli.

  4. A ford\u00edt\u00f3 a forr\u00e1sk\u00f3db\u00f3l, az er\u0151forr\u00e1sokb\u00f3l \u00e9s a k\u00fcls\u0151 k\u00f6nyvt\u00e1rakb\u00f3l el\u0151\u00e1ll\u00edtja az ART virtu\u00e1lis g\u00e9p g\u00e9pi k\u00f3dj\u00e1t.

  5. A g\u00e9pi k\u00f3db\u00f3l \u00e9s az er\u0151forr\u00e1sokb\u00f3l el\u0151\u00e1ll a nem al\u00e1\u00edrt APK \u00e1llom\u00e1ny.

  6. V\u00e9g\u00fcl a rendszer v\u00e9grehajtja az al\u00e1\u00edr\u00e1st \u00e9s el\u0151\u00e1ll a k\u00e9sz\u00fcl\u00e9kekre telep\u00edthet\u0151, al\u00e1\u00edrt APK.

Az Android Studio a Gradle build rendszert haszn\u00e1lja ezeknek a l\u00e9p\u00e9seknek az elv\u00e9g\u00e9z\u00e9s\u00e9hez.

Megjegyz\u00e9sek

  • A teljes folyamat a fejleszt\u0151i g\u00e9pen megy v\u00e9gbe, a k\u00e9sz\u00fcl\u00e9kekre m\u00e1r csak bin\u00e1ris \u00e1llom\u00e1ny jut el.

  • A k\u00fcls\u0151 k\u00f6nyvt\u00e1rak \u00e1ltal\u00e1ban JAR \u00e1llom\u00e1nyk\u00e9nt, vagy egy m\u00e1sik projekt hozz\u00e1ad\u00e1s\u00e1val illeszthet\u0151k az aktu\u00e1lis projekthez (de ezt nem kell k\u00e9zzel megtenn\u00fcnk, a f\u00fcgg\u0151s\u00e9gek kezel\u00e9s\u00e9ben is a Gradle fog seg\u00edteni).

  • Az APK \u00e1llom\u00e1ny legink\u00e1bb a Java vil\u00e1gban ismert JAR \u00e1llom\u00e1nyokhoz hasonl\u00edthat\u00f3.

  • A Manifest \u00e1llom\u00e1nyban meg kell adni a t\u00e1mogatni k\u00edv\u00e1nt Android verzi\u00f3t, mely felfele kompatibilis az \u00fajabb verzi\u00f3kkal, enn\u00e9l r\u00e9gebbi verzi\u00f3ra azonban az alkalmaz\u00e1s m\u00e1r nem telep\u00edthet\u0151.

  • Az Android folyamatosan friss\u00fcl\u0151 verzi\u00f3ival folymatosan l\u00e9p\u00e9st kell tartaniuk a fejleszt\u0151knek.

  • Az Android alkalmaz\u00e1sokat tipikusan a Google Play Store-ban szokt\u00e1k publik\u00e1lni, \u00edgy az APK form\u00e1tumban val\u00f3 terjeszt\u00e9s nem annyira elterjedt.

"},{"location":"laborok/basics/#sdk-es-konyvtarai","title":"SDK \u00e9s k\u00f6nyvt\u00e1rai","text":"

A developer.android.com/studio oldalr\u00f3l let\u00f6lthet\u0151 az IDE \u00e9s az SDK. Ennek fontosabb mapp\u00e1it, eszk\u00f6zeit tekints\u00fck \u00e1t a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel!

SDK szerkezet:

  • docs: Dokument\u00e1ci\u00f3
  • extras: K\u00fcl\u00f6nb\u00f6z\u0151 extra szoftverek helye. Maven repository, support libes anyagok, analytics SDK, Google Android USB driver (amennyiben SDK managerrel ezt is let\u00f6lt\u00f6tt\u00fck) stb.
  • platform-tools: Fastboot \u00e9s ADB bin\u00e1risok helye (legt\u00f6bbet haszn\u00e1lt eszk\u00f6z\u00f6k)
  • platforms, samples, sources, system-images: Minden API levelhez k\u00fcl\u00f6n almapp\u00e1ban a platform anyagok, forr\u00e1sok, p\u00e9ldaprojektek, OS image-ek
  • tools: Ford\u00edt\u00e1st \u00e9s tesztel\u00e9st seg\u00edt\u0151 eszk\u00f6z\u00f6k, SDK manager, 9Patch drawer, emul\u00e1tor bin\u00e1risok stb.
"},{"location":"laborok/basics/#avd-es-sdk-manager","title":"AVD \u00e9s SDK manager","text":"

Az SDK kezel\u00e9s\u00e9re az SDK managert haszn\u00e1ljuk, ezzel lehet let\u00f6lteni \u00e9s frissen tartani az eszk\u00f6zeinket. Ind\u00edt\u00e1sa az Android Studion kereszt\u00fcl lehets\u00e9ges.

Az SDK Manager ikonja a fenti toolbaron (vagy Tools -> SDK Manager):

vagy

SDK manager fel\u00fclete:

Megjegyz\u00e9s

Kor\u00e1bban l\u00e9tezett egy standalone SDK manager de ennek haszn\u00e1lata m\u00e1ra deprecated lett. Ha online forr\u00e1sokban ilyet l\u00e1tunk ne lep\u0151dj\u00fcnk meg.

Ind\u00edtsuk el az AVD managert, \u00e9s vizsg\u00e1ljuk meg a laborvezet\u0151vel, hogy rendelkez\u00e9sre \u00e1ll-e minden, ami az els\u0151 alkalmaz\u00e1sunkhoz kelleni fog.

"},{"location":"laborok/basics/#avd","title":"AVD","text":"

Az AVD az Android Virtual Device r\u00f6vid\u00edt\u00e9se. Ahogy arr\u00f3l m\u00e1r el\u0151ad\u00e1son is sz\u00f3 esett, nem csak val\u00f3di eszk\u00f6z\u00f6n futtathatjuk a k\u00f3dunkat, hanem emul\u00e1toron is. (Mi is a k\u00fcl\u00f6nbs\u00e9g szimul\u00e1tor \u00e9s emul\u00e1tor k\u00f6z\u00f6tt?) Az AVD ind\u00edt\u00e1sa a fejleszt\u0151i k\u00f6rnyezeten kereszt\u00fcl lehets\u00e9ges (illetve parancssorb\u00f3l is, de ennek a haszn\u00e1lat\u00e1ra csak speci\u00e1lis esetekben van sz\u00fcks\u00e9g).

Az AVD Manager ikonja:

vagy

A fenti k\u00e9pen jobb oldalon, a kiny\u00edl\u00f3 panelben, a l\u00e9tez\u0151 virtu\u00e1lis eszk\u00f6z\u00f6k list\u00e1j\u00e1t tal\u00e1ljuk, bal oldalon pedig az \u00fan. eszk\u00f6z defin\u00edci\u00f3k\u00e9t. Itt n\u00e9h\u00e1ny el\u0151re elk\u00e9sz\u00edtett sablon \u00e1ll rendelkez\u00e9sre. Magunk is k\u00e9sz\u00edthet\u00fcnk ilyet, ha tipikusan egy adott eszk\u00f6zre szeretn\u00e9nk fejleszteni (pl. Galaxy S4). K\u00e9sz\u00edts\u00fcnk \u00faj emul\u00e1tort! \u00c9rtelemszer\u0171en csak olyan API szint\u0171 eszk\u00f6zt k\u00e9sz\u00edthet\u00fcnk, amilyenek rendelkez\u00e9sre \u00e1llnak az SDK manageren kereszt\u00fcl.

  1. A jobb oldali panelon kattintsunk a fent tal\u00e1lhat\u00f3 Create Virtual Device... gombra!
  2. V\u00e1lasszunk az el\u0151re defini\u00e1lt k\u00e9sz\u00fcl\u00e9k sablonokb\u00f3l (pl. Pixel 7 Pro), majd nyomjuk meg a Next gombot.
  3. D\u00f6nts\u00fck el, hogy milyen Android verzi\u00f3j\u00fa emul\u00e1tort k\u00edv\u00e1nunk haszn\u00e1lni. CPU/ABI alapvet\u0151en x86_64 legyen, mivel ezekhez kapunk hardveres gyors\u00edt\u00e1st is. Itt v\u00e1lasszunk a rendelkez\u00e9sre \u00e1ll\u00f3k k\u00f6z\u00fcl egyet, majd Next.
  4. Az eszk\u00f6z r\u00e9szletes konfigur\u00e1ci\u00f3ja.

    • A virtu\u00e1lis eszk\u00f6z neve legyen p\u00e9ld\u00e1ul Labor_1.
    • V\u00e1lasszuk ki az alap\u00e9rtelmezett orient\u00e1ci\u00f3t, tetsz\u00e9s szerint kapcsoljuk ki vagy be a k\u00e9sz\u00fcl\u00e9k keret\u00e9nek megjelen\u00edt\u00e9s\u00e9t.

    A Show Advanced Settings alatt tov\u00e1bbi opci\u00f3kat tal\u00e1lunk:

    • Kamera opci\u00f3k:
      • WebcamX, hardveres kamera, ami a sz\u00e1m\u00edt\u00f3g\u00e9pre van csatlakoztatva
      • Emulated, egy egyszer\u0171 szoftveres megold\u00e1s, most legal\u00e1bb az egyik kamera legyen ilyen.
      • VirtualScene, egy kifinomultabb szoftveres megold\u00e1s, amelyben egy 3D vil\u00e1gban mozgathatjuk a kamer\u00e1t.
    • H\u00e1l\u00f3zat: \u00c1ll\u00edthatjuk a sebess\u00e9g\u00e9t \u00e9s a k\u00e9sleltet\u00e9s\u00e9t is kommunik\u00e1ci\u00f3s technol\u00f3gi\u00e1k szerint.
    • Boot Option: Nemr\u00e9g jelent meg az Android emul\u00e1tor \u00e1llapot\u00e1r\u00f3l val\u00f3 pillanatk\u00e9p elment\u00e9s\u00e9nek lehet\u0151s\u00e9ge. Ez azt takarja, hogy a virtu\u00e1lis oper\u00e1ci\u00f3s rendszer csak felf\u00fcggeszt\u00e9sre ker\u00fcl az emul\u00e1tor bez\u00e1r\u00e1skor (p\u00e9ld\u00e1ul a megnyitott alkalmaz\u00e1s is megmarad, a teljes \u00e1llapot\u00e1val), \u00e9s Quick boot esetben a teljes OS ind\u00edt\u00e1sa helyett m\u00e1sodperceken bel\u00fcl elindul az emul\u00e1lt rendszer. Cold Boot esetben minden alkalommal le\u00e1ll\u00edtja \u00e9s \u00fajra ind\u00edtja a virt\u00e1lis eszk\u00f6z teljes oper\u00e1ci\u00f3s rendszer\u00e9t.
    • Mem\u00f3ria \u00e9s t\u00e1rhely:

      • RAM: Ha kev\u00e9s a rendszermem\u00f3ri\u00e1nk, nem \u00e9rdemes 768 MB-n\u00e1l t\u00f6bbet adni, mert k\u00f6nnyen futhatunk probl\u00e9m\u00e1kba. Ha az emul\u00e1tor lefagy, vagy az eg\u00e9sz OS meg\u00e1ll m\u0171k\u00f6d\u00e9s k\u00f6zben, akkor \u00e1ll\u00edtsuk alacsonyabbra ezt az \u00e9rt\u00e9ket. 8 GB vagy t\u00f6bb rendszermem\u00f3ria mellett nyugodtan \u00e1ll\u00edthatjuk az emul\u00e1tor mem\u00f3ri\u00e1j\u00e1t 1024, 1536, vagy 2048 MB-ra.
      • VM heap: az alkalmaz\u00e1sok virtu\u00e1lis g\u00e9p\u00e9nek sz\u00f3l, maradhat az alap\u00e9rt\u00e9k. Tudni kell, hogy k\u00e9sz\u00fcl\u00e9kek eset\u00e9ben gy\u00e1rt\u00f3nk\u00e9nt v\u00e1ltozik.
      • Bels\u0151 flash mem\u00f3ria \u00e9s SD k\u00e1rtya m\u00e9rete, alapvet\u0151en j\u00f3k az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1sai.
    • Ha mindent rendben tal\u00e1l az ablak, akkor Finish!

Az Android Virtual Device Manager-ben megjelent az im\u00e9nt l\u00e9trehozott eszk\u00f6z\u00fcnk. Itt lehet\u0151s\u00e9g van a kor\u00e1bban megadott param\u00e9terek szerkeszt\u00e9s\u00e9re, a \"k\u00e9sz\u00fcl\u00e9kr\u0151l\" a felhaszn\u00e1l\u00f3i adatok t\u00f6rl\u00e9s\u00e9re (Wipe Data - Teljes vissza\u00e1ll\u00edt\u00e1s), illetve az emul\u00e1tor p\u00e9ld\u00e1ny duplik\u00e1l\u00e1s\u00e1ra vagy t\u00f6rl\u00e9s\u00e9re.

A Play gombbal ind\u00edtsuk el az \u00faj emul\u00e1tort!

Az elind\u00edtott emul\u00e1toron pr\u00f3b\u00e1ljuk ki az API Demos \u00e9s Dev Tools alkalmaz\u00e1sokat!

Megjegyz\u00e9s

A gy\u00e1ri emul\u00e1toron k\u00edv\u00fcl t\u00f6bb alternat\u00edva is l\u00e9tezik, mint pl. a Genymotion vagy a BigNox, viszont a Google f\u00e9le emul\u00e1tor a legelterjedtebb, \u00edgy amennyiben ezzel nem jelentkeznek probl\u00e9m\u00e1ink, maradjunk enn\u00e9l.

Tesztel\u00e9s c\u00e9lj\u00e1b\u00f3l nagyon j\u00f3l haszn\u00e1lhat\u00f3 az emul\u00e1tor, amely az al\u00e1bbi k\u00e9pen l\u00e1that\u00f3 plusz funkci\u00f3kat is adja. Lehet\u0151s\u00e9g van t\u00f6bbek k\u00f6z\u00f6tt egyedi hely be\u00e1ll\u00edt\u00e1s\u00e1ra, bej\u00f6v\u0151 h\u00edv\u00e1s szimul\u00e1l\u00e1s\u00e1ra, stb. A panelt a fut\u00f3 emul\u00e1tor jobb oldal\u00e1n tal\u00e1lhat\u00f3 vez\u00e9rl\u0151 gombok k\u00f6z\u00fcl a ... gombbal lehet megnyitni:

"},{"location":"laborok/basics/#fejlesztoi-kornyezet","title":"Fejleszt\u0151i k\u00f6rnyezet","text":"

Android fejleszt\u00e9sre a labor sor\u00e1n a JetBrains IntelliJ alapjain nyugv\u00f3 Android Studio-t fogjuk haszn\u00e1lni. A Studio-val ismerked\u0151k sz\u00e1m\u00e1ra hasznos funkci\u00f3 a Tip of the day, \u00e9rdemes egyb\u0151l kipr\u00f3b\u00e1lni, megn\u00e9zni az adott funkci\u00f3t. Indul\u00e1skor alap\u00e9rtelmezetten a legut\u00f3bbi projekt ny\u00edlik meg, ha nincs ilyen, vagy ha minden projekt\u00fcnket bez\u00e1rtuk, akkor a nyit\u00f3 k\u00e9perny\u0151. (A legut\u00f3bbi projekt \u00fajranyit\u00e1s\u00e1t a Settings -> Appeareance & Behavior -> System Settings -> Reopen last project on startup opci\u00f3val ki is kapcsolhatjuk.)

Az Android Studio Giraffe-ban meg\u00fajult a k\u00f6rnyezet felhaszn\u00e1l\u00f3i fel\u00fclete. Amint l\u00e1that\u00f3, j\u00f3val letisztultabb diz\u00e1jnt v\u00e1lasztottak, sokkal kevesebb a figyelmet elvon\u00f3 extra a k\u00e9perny\u0151n, sokkal ink\u00e1bb a k\u00f3don van a hangs\u00faly. Ezek k\u00f6z\u00f6tt a n\u00e9zetek k\u00f6z\u00f6tt egyszer\u0171en v\u00e1lthatunk a Be\u00e1ll\u00edt\u00e1sokban, a New UI men\u00fcpontban.

"},{"location":"laborok/basics/#high-low-game","title":"High Low Game","text":"

A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edtsenek egy \u00faj alkalmaz\u00e1st!

"},{"location":"laborok/basics/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

  1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Views Activity lehet\u0151s\u00e9get.
  2. A projekt neve legyen HighLowGame, a kezd\u0151 package pedig hu.bme.aut.android.highlowgame.
  3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
  4. A minimum API szint legyen API24: Android 7.0.
  5. A Build configuration language Kotlin DSL legyen.

FILE PATH

A projekt a repository-ban l\u00e9v\u0151 HighLowGame k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n!

A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel tekints\u00e9k \u00e1t a l\u00e9trej\u00f6tt projekt strukt\u00far\u00e1j\u00e1t!

Miut\u00e1n \u00e1ttekintett\u00fck a projektet, val\u00f3s\u00edtsuk meg a barch\u00f3ba j\u00e1t\u00e9kot! El\u0151sz\u00f6r is kapcsoljuk be a modulunkra a ViewBinding-ot a fel\u00fcleti elemek el\u00e9r\u00e9hez. Az app modulhoz tartoz\u00f3 build.gradle.kts f\u00e1jlban az android tagen bel\u00fclre illessz\u00fck be az enged\u00e9lyez\u00e9st:

android {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

Az alkalmaz\u00e1sunk fel\u00fclete (activity_main.xml) a k\u00f6vetkez\u0151 lesz: - lesz k\u00e9t beviteli mez\u0151nk: egy a tippnek, egy a n\u00e9vnek - lesz egy gombunk a tipp lead\u00e1s\u00e1hoz - lesz egy eredm\u00e9ny mez\u0151 az eredm\u00e9ny megjelen\u00edt\u00e9s\u00e9hez.

<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\nandroid:orientation=\"vertical\"\ntools:context=\".MainActivity\">\n\n<com.google.android.material.textfield.TextInputLayout\nstyle=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:hint=\"Enter number here\">\n\n<com.google.android.material.textfield.TextInputEditText\nandroid:id=\"@+id/etGuess\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\" />\n</com.google.android.material.textfield.TextInputLayout>\n\n<com.google.android.material.textfield.TextInputLayout\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\">\n\n<EditText\nandroid:id=\"@+id/etName\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:hint=\"Enter a name here\" />\n</com.google.android.material.textfield.TextInputLayout>\n\n<Button\nandroid:id=\"@+id/btnGuess\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:text=\"Guess\" />\n\n<TextView\nandroid:id=\"@+id/tvResult\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:text=\"have fun :)\"\nandroid:textSize=\"28sp\" />\n\n</LinearLayout>\n

A j\u00e1t\u00e9k k\u00f3dja pedig a k\u00f6vetkez\u0151k\u00e9ppen alakul (MainActivity.kt):

class MainActivity : AppCompatActivity() {\n\ncompanion object {\nconst val KEY_NUM = \"KEY_NUM\"\n}\n\nlateinit var binding: ActivityMainBinding\n\nvar generatedNum = 0\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\n\n\nbinding = ActivityMainBinding.inflate(layoutInflater)\nsetContentView(binding.root)\n\nif (savedInstanceState != null && savedInstanceState!!.containsKey(KEY_NUM)) {\ngeneratedNum = savedInstanceState.getInt(KEY_NUM)\n} else {\ngenerateNewNumber()\n}\n\nbinding.btnGuess.setOnClickListener {\ntry {\n\nif (binding.etGuess.text!!.isNotEmpty()) {\nval myNum = binding.etGuess.text.toString().toInt()\n\nif (myNum == generatedNum) {\nbinding.tvResult.text = \"${binding.etName.text.toString()}, You have won!\"\n\n} else if (myNum < generatedNum) {\nbinding.tvResult.text = \"The number is higher\"\n} else if (myNum > generatedNum) {\nbinding.tvResult.text = \"The number is lower\"\n}\n} else {\nbinding.etGuess.error = \"This value is not valid\"\n\n}\n\n} catch (e: Exception) {\nbinding.etGuess.error = e.message\n}\n}\n}\n\noverride fun onSaveInstanceState(outState: Bundle) {\noutState.putInt(KEY_NUM, generatedNum)\n\nsuper.onSaveInstanceState(outState)\n}\n\n\nfun generateNewNumber() {\nval rand = Random(System.currentTimeMillis())\ngeneratedNum = rand.nextInt(3) // 0..2\n}\n}\n

Az Activty els\u0151 indul\u00e1sakor sorsol egy kital\u00e1land\u00f3 sz\u00e1mot. A tippel\u00e9s gombnyom\u00e1sra t\u00f6rt\u00e9nik, aminek hat\u00e1s\u00e1ra friss\u00fcl az eredm\u00e9ny mez\u0151.

Figyelj\u00fck meg, hogy a j\u00e1t\u00e9k t\u00fal\u00e9li a forgat\u00e1sokat is! Ez annak k\u00f6sz\u00f6nhet\u0151, hogy az Activity-n bel\u00fcl elt\u00e1rolt c\u00e9lsz\u00e1mot konfigur\u00e1ci\u00f3 v\u00e1lt\u00e1skor elmentj\u00fck, majd \u00faj Activity ind\u00edt\u00e1sa eset\u00e9n bet\u00f6ltj\u00fck.

"},{"location":"laborok/basics/#android-studio","title":"Android Studio","text":"

Ez a r\u00e9sz azoknak sz\u00f3l, akik kor\u00e1bban m\u00e1r haszn\u00e1lt\u00e1k az Eclipse nev\u0171 IDE-t, \u00e9s szeretn\u00e9k megismerni a k\u00fcl\u00f6nbs\u00e9geket az Android Studio-hoz k\u00e9pest.

  • Import r\u00e9gi projektekb\u0151l: Android Studioban lehets\u00e9ges a projekt import\u00e1l\u00e1sa r\u00e9gebbi verzi\u00f3j\u00fa projektekb\u0151l \u00e9s a r\u00e9gi Eclipse projektekb\u0151l is.
  • Projektstrukt\u00fara: Az Android Studio Gradle-lel ford\u00edt, \u00e9s m\u00e1s fel\u00e9p\u00edt\u00e9st haszn\u00e1l. Projekten bel\u00fcl:

    • .idea: IDE f\u00e1jlok
    • app: forr\u00e1s
      • build: ford\u00edtott \u00e1llom\u00e1nyok
      • libs: libraryk
      • src: forr\u00e1sk\u00f3d, azon bel\u00fcl is k\u00fcl\u00f6n projekt a tesztnek, \u00e9s azon bel\u00fcl pedig res k\u00f6nyvt\u00e1r, illetve java. Ut\u00f3bbin bel\u00fcl m\u00e1r a csomagok vannak.
    • gradle: Gradle f\u00e1jlok
  • Hasznos funkci\u00f3k:

    • IntelliSense, fejlett refaktor\u00e1l\u00e1s t\u00e1mogat\u00e1s
    • Ha egy sorban sz\u00ednre, vagy k\u00e9pi er\u0151forr\u00e1sra hivatkozunk, a sor elej\u00e9re kitesz egy miniat\u0171r v\u00e1ltozatot.
    • Ha k\u00f6zvetve hivatkozott er\u0151forr\u00e1st (ak\u00e1r resources.get..., ak\u00e1r R...) adunk meg, \u00f6sszecsukja a hivatkoz\u00e1st \u00e9s a t\u00e9nyleges \u00e9rt\u00e9ket mutatja. Ha r\u00e1vissz\u00fck az egeret felfedi, ha kattintunk kibontja a hivatkoz\u00e1st.
    • N\u00e9vtelen bels\u0151 oszt\u00e1lyokkal is hasonl\u00f3t tud, jav\u00edtva a k\u00f3d olvashat\u00f3s\u00e1g\u00e1t.
    • K\u00f3dkieg\u00e9sz\u00edt\u00e9sn\u00e9l szabad a keres\u0151, a sz\u00f3t\u00f6red\u00e9ket keresi, nem pedig a sz\u00f3val kezd\u0151d\u0151 lehet\u0151s\u00e9geket (l\u00e1sd k\u00e9pen)
    • V\u00e1ltoz\u00f3n\u00e9v aj\u00e1nl\u00e1s: amikor v\u00e1ltoz\u00f3n\u00e9vre van sz\u00fcks\u00e9g\u00fcnk, nyomjunk Ctrl+Space-t. Ha adottak a k\u00f6r\u00fclm\u00e9nyek, a Studio eg\u00e9sz j\u00f3 neveket tud felaj\u00e1nlani.
    • Szigor\u00fa lint. A Studio megengedi a warningot. Ez\u00e9rt szigor\u00fabb a lint, t\u00f6bb mindenre figyelmeztet (olyan apr\u00f3s\u00e1gra is, hogy egy View egyik oldal\u00e1n van padding, a m\u00e1sikon nincs)
    • Layout szerkeszt\u00e9s. A grafikus layout \u00e9p\u00edt\u00e9s lehets\u00e9ges.
    • CTRL-t lenyomva navig\u00e1lhatunk a k\u00f3dban, pl. oszt\u00e1lyra, met\u00f3dush\u00edv\u00e1sra kattintva. Ezt a navig\u00e1ci\u00f3t (\u00e9s az egyszer\u0171 m\u00e1sik oszt\u00e1lyba kattint\u00e1st is) r\u00f6gz\u00edti, \u00e9s a historyban el\u0151re-h\u00e1tra gombokkal lehet l\u00e9pkedni. Ha van az eger\u00fcnk\u00f6n/billenty\u0171zet\u00fcnk\u00f6n ilyen gomb, \u00e9s netes b\u00f6ng\u00e9sz\u00e9s k\u00f6zben akt\u00edvan haszn\u00e1ljuk, ezt a funkci\u00f3t nagyon hasznosnak fogjuk tal\u00e1lni.

Sz\u00edn ikonja a sor elej\u00e9n; kiemelve jobb oldalon, hogy melyik n\u00e9zeten vagyunk; szabadszavas kieg\u00e9sz\u00edt\u00e9s; a \"Hello world\" igaz\u00e1b\u00f3l @string/very_very_very_long_hello_world.

"},{"location":"laborok/basics/#billentyukombinaciok","title":"Billenty\u0171kombin\u00e1ci\u00f3k","text":"
  • CTRL + ALT + L: K\u00f3dform\u00e1z\u00e1s
  • CTRL + SPACE: K\u00f3dkieg\u00e9sz\u00edt\u00e9s
  • SHIFT + F6 \u00c1tnevez\u00e9s (Mindenhol)
  • F2: A k\u00f6vetkez\u0151 error-ra ugrik. Ha nincs error, akkor warningra.
  • CTRL + Z illetve CTRL + SHIFT + Z: Visszavon\u00e1s \u00e9s M\u00e9gis
  • CTRL + P: Param\u00e9terek mutat\u00e1sa
  • ALT + INSERT: Met\u00f3dus gener\u00e1l\u00e1sa
  • CTRL + O: Met\u00f3dus fel\u00fcldefini\u00e1l\u00e1sa
  • CTRL + F9: Ford\u00edt\u00e1s
  • SHIFT + F10: Ford\u00edt\u00e1s \u00e9s futtat\u00e1s
  • SHIFT SHIFT: Keres\u00e9s mindenhol
  • CTRL + N: Keres\u00e9s oszt\u00e1lyokban
  • CTRL + SHIFT + N: Keres\u00e9s f\u00e1jlokban
  • CTRL + ALT + SHIFT + N: Keres\u00e9s szimb\u00f3lumokban (p\u00e9ld\u00e1ul f\u00fcggv\u00e9nyek, property-k)
  • CTRL + SHIFT + A: Keres\u00e9s a be\u00e1ll\u00edt\u00e1sokban, kiadhat\u00f3 parancsokban.
"},{"location":"laborok/basics/#eszkozok-szerkesztok","title":"Eszk\u00f6z\u00f6k, szerkeszt\u0151k","text":"

A View men\u00fc Tool Windows men\u00fcpontj\u00e1ban lehet\u0151s\u00e9g van k\u00fcl\u00f6nb\u00f6z\u0151 ablakok ki- \u00e9s bekapcsol\u00e1s\u00e1ra. Laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel tekints\u00e9k \u00e1t az al\u00e1bbi eszk\u00f6z\u00f6ket!

  • Project
  • Structure
  • TODO
  • Logcat
  • Terminal
  • Event Log
  • Gradle

Lehet\u0151s\u00e9g van felosztani a szerkeszt\u0151ablakot, ehhez kattinsunk egy megnyitott f\u00e1jl tabf\u00fcl\u00e9re jobb gombbal, Split Vertically/Horizontally!

"},{"location":"laborok/basics/#hasznos-beallitasok","title":"Hasznos be\u00e1ll\u00edt\u00e1sok","text":"

A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel \u00e1ll\u00edts\u00e1k be a k\u00f6vetkez\u0151 hasznos funkci\u00f3kat:

  • kis- nagybet\u0171 \u00e9rz\u00e9kenys\u00e9g kikapcsol\u00e1sa a k\u00f3dkieg\u00e9sz\u00edt\u0151ben (settingsben keres\u00e9s: sensitive)
  • \"laptop m\u00f3d\" ki- \u00e9s bekapcsol\u00e1sa (File -> Power Save Mode)
  • sorsz\u00e1moz\u00e1s bekapcsol\u00e1sa (k\u00f3d melletti r\u00e9szen bal oldalt: jobb eg\u00e9rgomb, Show Line Numbers)
"},{"location":"laborok/basics/#generalhato-elemek","title":"Gener\u00e1lhat\u00f3 elemek","text":"

A Studio sok sablont tartalmaz, r\u00f6viden tekints\u00e9k \u00e1t a lehet\u0151s\u00e9geket:

  • Projektf\u00e1ban, projektre jobb gombbal kattintva -> new -> module
  • Projektf\u00e1ban, modulon bel\u00fcl, \"java\"-ra kattintva jobb gombbal -> new
  • Forr\u00e1sk\u00f3dban ALT+INSERT billenty\u0171kombin\u00e1ci\u00f3ra
"},{"location":"laborok/basics/#android-profiler","title":"Android Profiler","text":"

A k\u00e9sz\u00fcl\u00e9k er\u0151forr\u00e1shaszn\u00e1lata monitorozhat\u00f3 ezen a fel\u00fcleten, amelyet az eml\u00edtett View -> Tool Windows-b\u00f3l \u00e9rhet\u00fcnk el.

P\u00e9ld\u00e1ul r\u00e9szletes inform\u00e1ci\u00f3t kaphatunk a h\u00e1l\u00f3zati forgalomr\u00f3l:

"},{"location":"laborok/basics/#database-inspector","title":"Database Inspector","text":"

A k\u00e9sz\u00fcl\u00e9ken debuggolt alkalmaz\u00e1sunknak az adatb\u00e1zis\u00e1t is meg tudjuk tekinteni.

"},{"location":"laborok/basics/#device-file-explorer","title":"Device File Explorer","text":"

A k\u00e9sz\u00fcl\u00e9ken l\u00e9v\u0151 f\u00e1jlrendszert is b\u00f6ng\u00e9szhetj\u00fck.

"},{"location":"laborok/basics/#feladatok-10-x-05-pont","title":"Feladatok (10 x 0.5 pont)","text":"
  1. Az \u00faj alkalmaz\u00e1st futtass\u00e1k emul\u00e1toron (akinek saj\u00e1t k\u00e9sz\u00fcl\u00e9ke van, az is pr\u00f3b\u00e1lja ki)!
  2. Helyezzenek breakpointot a k\u00f3dba, \u00e9s debug m\u00f3dban ind\u00edts\u00e1k az alkalmaz\u00e1st! (\u00c9rdemes megyfigyelni, hogy most m\u00e1sik Gradle Task fut a k\u00e9perny\u0151 alj\u00e1n.)
  3. Ind\u00edtsanak h\u00edv\u00e1st \u00e9s k\u00fcldjenek SMS-t az emul\u00e1torra! Mit tapasztalnak?
  4. Ind\u00edtsanak h\u00edv\u00e1st \u00e9s k\u00fcldjenek SMS-t az emul\u00e1torr\u00f3l! Mit tapasztalnak?
  5. Tekintse \u00e1t az Android Profiler n\u00e9zet funkci\u00f3it a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel!
  6. V\u00e1ltoztassa meg a k\u00e9sz\u00fcl\u00e9k tart\u00f3zkod\u00e1si hely\u00e9t (GPS) az emul\u00e1tor megfelel\u0151 panelj\u00e9nek seg\u00edts\u00e9g\u00e9vel!
  7. Vizsg\u00e1lja meg az elind\u00edtott HighLowGame projekt nyitott sz\u00e1lait, mem\u00f3riafoglal\u00e1s\u00e1t!
  8. Vizsg\u00e1lja meg a Logcat panel tartalm\u00e1t!
  9. Vizsg\u00e1lja meg a Code -> Inspect code eredm\u00e9ny\u00e9t!
  10. Keresse ki a l\u00e9trehozott HighLowGame projekt mapp\u00e1j\u00e1t \u00e9s a build k\u00f6nyvt\u00e1ron bel\u00fcl vizsg\u00e1lja meg az .apk \u00e1llom\u00e1ny tartalm\u00e1t! Ezen bel\u00fcl hol tal\u00e1lhat\u00f3 a leford\u00edtott k\u00f3d?

BEADAND\u00d3

A labor teljes\u00edt\u00e9s\u00e9hez a fenti feladatokat kell v\u00e9grehajtani \u00e9s az eredm\u00e9nyeket dokument\u00e1lni. Ezt minden egyes feladatn\u00e1l egy k\u00e9perny\u0151k\u00e9ppel \u00e9s r\u00f6vid, n\u00e9h\u00e1ny mondatos magyar\u00e1zattal kell megtenni. A jegyz\u0151k\u00f6nyvet a repository-ban l\u00e9v\u0151 README.md f\u00e1jlban kell elk\u00e9sz\u00edteni.

A dokument\u00e1ci\u00f3nak a k\u00e9pekkel egy\u00fctt helyesen kell megjelenni\u00fck a GitHub webes fel\u00fclet\u00e9n is! Ezt ellen\u0151rizd a bead\u00e1s sor\u00e1n: nyisd meg a repository-d webes fel\u00fclet\u00e9t, v\u00e1ltsd \u00e1t a megfelel\u0151 \u00e1gra, \u00e9s a GitHub automatikusan renderelni fogja a README.md f\u00e1jlt a k\u00e9pekkel egy\u00fctt.

"},{"location":"laborok/calculator/","title":"Labor03 - Sz\u00e1mol\u00f3g\u00e9p (Calculator)","text":""},{"location":"laborok/calculator/#a-labor-celja","title":"A labor c\u00e9lja","text":"

A legfontosabb XML-alap\u00fa UI fejleszt\u00e9si komponensek haszn\u00e1lat\u00e1nak bemutat\u00e1sa egy sz\u00e1mol\u00f3g\u00e9p alkalmaz\u00e1son kereszt\u00fcl. A labor sor\u00e1n megismerked\u00fcnk a Jetpack Navigation k\u00f6nyvt\u00e1rral, a Fragment-ekkel \u00e9s a RecyclerView elk\u00e9sz\u00edt\u00e9si l\u00e9p\u00e9seivel.

"},{"location":"laborok/calculator/#felhasznalt-technologiak","title":"Felhaszn\u00e1lt technol\u00f3gi\u00e1k:","text":"
  • Activity
  • Fragment
  • Jetpack Navigation
  • RecyclerView \u00e9s RecyclerViewAdapter
  • TableLayout, TextView, Button
  • View Binding
"},{"location":"laborok/calculator/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/calculator/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

"},{"location":"laborok/calculator/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

  1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk ki a No Activity opci\u00f3t, majd kattintsunk a Next gombra.
  2. A projekt neve legyen Calculator, a kezd\u0151 package pedig hu.bme.aut.android.calculator.
  3. Nyelvnek tov\u00e1bbra is a Kotlin-t haszn\u00e1ljuk.
  4. A minimum API szint pedig legyen 24: Android 7.0 (Nougat).
  5. A Build configuration language legyen Kotlin DSL.

FILE PATH

A projekt a repository-ban l\u00e9v\u0151 Calculator k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

Ha ezzel megvagyunk, akkor t\u00e9rj\u00fcnk r\u00e1 a Gradle f\u00e1jlokra, amik a projekt\u00fcnk buildel\u00e9si folyamat\u00e1nak konfigur\u00e1ci\u00f3j\u00e1\u00e9rt felelnek. Els\u0151re n\u00e9zz\u00fck a project szint\u0171 Gradle f\u00e1jlt:

A Jetpack Navigation k\u00f6nyvt\u00e1r haszn\u00e1lata miatt vegy\u00fck fel a t\u00f6bbi plugin mell\u00e9 a androidx.navigation.safeargs-ot:

buildscript {\nrepositories {\ngoogle()\n}\ndependencies {\nval nav_version = \"2.8.0\"\nclasspath(\"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version\")\n}\n}\n

A libs.version.toml f\u00e1jlba vegy\u00fck fel a k\u00f6vetkez\u0151ket:

[versions]\n...\nnavigation = \"2.8.0\"\n\n[libraries]\n...\nandroidx-navigation-fragment-ktx = { group = \"androidx.navigation\", name = \"navigation-fragment-ktx\", version.ref = \"navigation\" }\nandroidx-navigation-ui-ktx = { group = \"androidx.navigation\", name = \"navigation-ui-ktx\", version.ref = \"navigation\" }\n

Nyissuk meg a module szint\u0171 Gradle f\u00e1jlunkat.

Enged\u00e9lyezz\u00fck a View Binding-ot:

...\nandroid {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

Vegy\u00fck fel azokat a tov\u00e1bbi f\u00fcgg\u0151s\u00e9geket, amikre m\u00e9g sz\u00fcks\u00e9g\u00fcnk lesz a projekt sor\u00e1n. Ehhez a pluginok k\u00f6z\u00e9 m\u00e9g vegy\u00fck fel a androidx.navigation.safeargs.kotlin-t.

plugins {\n...\nid(\"androidx.navigation.safeargs.kotlin\")\n}\n\nandroid { ... }\n\ndependencies {\n...\nimplementation (libs.androidx.navigation.fragment.ktx)\nimplementation (libs.androidx.navigation.ui.ktx)\n}\n

Ha v\u00e9gezt\u00fcnk szinkroniz\u00e1ljuk a Gradle f\u00e1jlokat. (Ha esetleg nem lenne, meg hogy ezt hol lehet, akkor a SHIFT gomb dupla lenyom\u00e1s\u00e1val megny\u00edlik egy keres\u0151, amiben a Sync project with Gradle files-t megadva ez elv\u00e9gezhet\u0151.)

Sz\u00fcks\u00e9g\u00fcnk lesz m\u00e9g n\u00e9h\u00e1ny string \u00e9s drawable er\u0151forr\u00e1sra, amit most \u00e9rdemes el\u0151re felvenni. A string er\u0151forr\u00e1sok el\u00e9r\u00e9s\u00e9hez nyissuk meg a res/values/string.xml f\u00e1jlt \u00e9s vegy\u00fck fel az al\u00e1bbi er\u0151forr\u00e1sokat:

<resources>\n<string name=\"app_name\">Calculator</string>\n<string name=\"text_calculator_console\">0</string>\n<string name=\"text_operation\">%1$.2f %2$s %3$.2f = %4$.2f</string>\n<string name=\"button_text_delete\">C</string>\n<string name=\"button_text_sign\">+/-</string>\n<string name=\"button_text_modulo\">%</string>\n<string name=\"button_text_division\">/</string>\n<string name=\"button_text_number_seven\">7</string>\n<string name=\"button_text_number_eight\">8</string>\n<string name=\"button_text_number_nine\">9</string>\n<string name=\"button_text_multiplication\">*</string>\n<string name=\"button_text_number_four\">4</string>\n<string name=\"button_text_number_five\">5</string>\n<string name=\"button_text_number_six\">6</string>\n<string name=\"button_text_subtraction\">-</string>\n<string name=\"button_text_number_one\">1</string>\n<string name=\"button_text_number_two\">2</string>\n<string name=\"button_text_number_three\">3</string>\n<string name=\"button_text_addition\">+</string>\n<string name=\"button_text_number_zero\">0</string>\n<string name=\"button_text_come\">,</string>\n<string name=\"button_text_equivalence\">=</string>\n<string name=\"menu_item_title_delete\">Delete</string>\n<string name=\"menu_item_content_description_delete\">Action to delete history</string>\n<string name=\"button_text_load\">Load</string>\n<string name=\"app_bar_title_history\">History</string>\n</resources>\n

Majd nyissuk meg a ResourceManager-t (bal oldali men\u00fcs\u00e1v), v\u00e1lasszuk ki a drawable er\u0151forr\u00e1sokat, majd kattintsunk a + gombra, ahol v\u00e1lasszuk ki a Vector Asset lehet\u0151s\u00e9get.

Ekkor megny\u00edlik az Asset Studio. Itt kattintsunk a Clip art mellett l\u00e9v\u0151 gombra, \u00e9s keress\u00fck meg az arrow back nev\u0171 ikont. Az Name attrib\u00fatumot \u00e1ll\u00edtsuk \u00e1t ic_arrow_back_24-re. Kattintsunk a Next majd a Finish gombra.

V\u00e9gezet\u00fcl szeretn\u00e9nk a Material You keretrendszerre \u00e1tt\u00e9rni a UI kin\u00e9zete eset\u00e9ben. Ehhez nyissuk a res/values/ el\u00e9r\u00e9si \u00faton l\u00e9v\u0151 themes.xml f\u00e1jlt, \u00e9s \u00edrjuk \u00e1t a tartalm\u00e1t a k\u00f6vetkez\u0151re:

<resources xmlns:tools=\"http://schemas.android.com/tools\">\n<!-- Base application theme. -->\n<style name=\"Base.Theme.Calculator\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n<!-- Customize your light theme here. -->\n<!-- <item name=\"colorPrimary\">@color/my_light_primary</item> -->\n</style>\n\n<style name=\"Theme.Calculator\" parent=\"Base.Theme.Calculator\" />\n</resources>\n

Majd t\u00f6r\u00f6lj\u00fck ki a night er\u0151forr\u00e1smin\u0151s\u00edt\u0151vel ell\u00e1tott verzi\u00f3t.

"},{"location":"laborok/calculator/#jetpack-navigation","title":"Jetpack Navigation","text":"

A k\u00f6vetkez\u0151 r\u00e9szben a Jetpack Navigation k\u00f6nyvt\u00e1rral fogunk megismerkedni. Seg\u00edts\u00e9g\u00e9vel Activity-k \u00e9s Fragment-ek k\u00f6z\u00f6tti navig\u00e1ci\u00f3t lehet megval\u00f3s\u00edtani \u00fagy, hogy az egyes k\u00e9perny\u0151k k\u00f6zti \u00fatvonalakat egy gr\u00e1ffal modellezz\u00fck.

Els\u0151 l\u00e9p\u00e9sk\u00e9nt a ResourceManager seg\u00edts\u00e9g\u00e9vel, hozzunk l\u00e9tre egy navig\u00e1ci\u00f3s gr\u00e1fot. V\u00e1lasszuk ki a Navigation opci\u00f3t, majd kattintsunk a + gombra, ahol v\u00e1lasszuk a Navigation Resource File lehet\u0151s\u00e9get. Az er\u0151forr\u00e1s f\u00e1jl neve legyen nav_graph.

Ezut\u00e1n hozzunk l\u00e9tre egy \u00faj Empty Views Activity-t (jobb klikk calculator package-n \u2192 New \u2192 Activity \u2192 Empty Views Activity). Itt pip\u00e1ljuk be a Launcher Activity opci\u00f3t, ugyanis szeretn\u00e9nk, hogy az Activity a futtat\u00f3 eszk\u00f6z men\u00fcj\u00e9b\u0151l ind\u00edthat\u00f3 legyen. Majd kattintsunk a Finish gombra.

Keress\u00fck meg a MainActivity-hez tartoz\u00f3 activity_main.xml f\u00e1jlt (res/layout), \u00e9s vegy\u00fcnk fel benne egy FragmentContainerView komponenst:

<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:id=\"@+id/main\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".MainActivity\">\n\n<androidx.fragment.app.FragmentContainerView\nandroid:id=\"@+id/nav_host_fragment\"\nandroid:name=\"androidx.navigation.fragment.NavHostFragment\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\napp:defaultNavHost=\"true\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintLeft_toLeftOf=\"parent\"\napp:layout_constraintRight_toRightOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:navGraph=\"@navigation/nav_graph\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

FragmentContainerView

Ez egy egyedi layout t\u00edpus, ami a Fragment-ek megjelen\u00edt\u00e9s\u00e9re haszn\u00e1latos.

  • Az android:name attrib\u00fatum tartalmazza a NavHost implement\u00e1ci\u00f3nk oszt\u00e1lynev\u00e9t.
  • Az app:navGraph attrib\u00fatum hivatkozik arra a navig\u00e1ci\u00f3s er\u0151forr\u00e1sra, amit kor\u00e1bban gener\u00e1ltunk.
  • Az app:defaultNavhost=\"true\" attrib\u00fatum biztos\u00edtja, hogy a NavHostFragment kezelni tudja a visszafel\u00e9 navig\u00e1l\u00e1st (amit egy dedik\u00e1lt fizikai gombbal vagy interakci\u00f3val v\u00e1lthatunk ki). Csak egyetlen NavHost lehet alap\u00e9rtelmezettnek (default) be\u00e1ll\u00edtva.

K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt nyissuk meg a nav_graph.xml-t (res/navigation) Design m\u00f3dban. Kattintsunk a New Destination gombra, ott v\u00e1lasszuk ki a Create New Destination majd Fragment (Blank) opci\u00f3kat. A Fragment neve legyene CalculatorFragment. Ezut\u00e1n v\u00e9gleges\u00edts\u00fck a l\u00e9trehoz\u00e1st a Next \u00e9s Finish gombra val\u00f3 kattint\u00e1ssal.

Hozzunk l\u00e9tre ugyanezzel a m\u00f3dszerrel egy \u00fajabb Fragment-t HistoryFragment n\u00e9ven. Ha ezzel megvagyunk, vigy\u00fck a kurzorunkat a CalculatorFragment f\u00f6l\u00e9, ekkor megjelenik egy karika a Fragment jobb oldal\u00e1n. Kattintsunk r\u00e1, majd a bal klikket lenyomva h\u00fazzuk a kurzort a m\u00e1sik Fragment f\u00f6l\u00e9 \u00e9s ott engedj\u00fck el. \u00cdgy l\u00e9trej\u00f6tt egy \u00fatvonal a CalculatorFragment \u00e9s a HistoryFragment k\u00f6z\u00f6tt. V\u00e9gezz\u00fck el ugyanezt visszafel\u00e9. Ha ezzel megvagyunk, akkor k\u00f6vetkez\u0151t kell l\u00e1tnunk:

"},{"location":"laborok/calculator/#calculatoroperator","title":"CalculatorOperator","text":"

A labor k\u00f6vetkez\u0151 szakasz\u00e1ban a CalculatorOperator nev\u0171 seg\u00e9doszt\u00e1lyt fogjuk implement\u00e1lni, aminek a feladata, hogy elt\u00e1rolja a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1t, \u00e9s kisz\u00e1m\u00edtsa a t\u00e1mogatott m\u0171veletek eredm\u00e9ny\u00e9t. Ezt a Kotlin Regex k\u00f6nyvt\u00e1r\u00e1nak seg\u00edts\u00e9g\u00e9vel v\u00e9gzi el.

Els\u0151 l\u00e9p\u00e9sk\u00e9nt hozzunk l\u00e9tre egy \u00faj util package-t (jobb klikk calculator package-en \u2192 New \u2192 package), benne Util nev\u0171 Kotlin objektummal. Ez az objektum fogja tartalmazni az olyan konstansokat \u00e9s seg\u00e9dv\u00e1ltoz\u00f3kat, amik a sz\u00e1mol\u00f3g\u00e9p m\u0171k\u00f6dtet\u00e9s\u00e9hez sz\u00fcks\u00e9gesek.

object Util {\nconst val COMMA = \".\"\n\nprivate const val numberPattern = \"0[.][0-9]+|[1-9][0-9]*[.][0-9]+|[1-9][0-9]*|0|^[\\\\s]\"\nval numberRegex = Regex(numberPattern)\n\nprivate const val halfOperation = \"[/*%+-]($numberPattern)|^[\\\\s]\"\nval halfOperationRegex = Regex(halfOperation)\n\nprivate const val operationSymbolPattern = \"[/*%+-]|^[\\\\s]\"\nval operationSymbol = Regex(operationSymbolPattern)\n}\n

Ha ezzel megvagyunk hozzunk l\u00e9tre egy \u00faj model package-t a calculator package-n bel\u00fcl, majd hozzunk l\u00e9tre benne egy \u00faj enum oszt\u00e1lyt OperationSymbol n\u00e9ven, ami a sz\u00e1mol\u00f3g\u00e9ppel elv\u00e9gezhet\u0151 m\u0171veleteket reprezent\u00e1lja. Az enum oszt\u00e1lyunk rendelkezzen egy konstruktorral, ami a m\u0171veletekhez a hozz\u00e1juk tartoz\u00f3 szimb\u00f3lumot rendeli String-k\u00e9nt.

enum class OperationSymbol(val symbol: String) {\nDIVISION(\"/\"),   // 0\nMULTIPLICATION(\"*\"),   // 1\nSUBTRACTION(\"-\"),   // 2\nADDITION(\"+\"),   // 3\nMODULO(\"-\");   // 4\n}\n

K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt eg\u00e9sz\u00edts\u00fck ki az oszt\u00e1lyt egy companion object-el benne egy olyan getByOrdinal() nev\u0171 seg\u00e9df\u00fcggv\u00e9nnyel, ami a sorrend szerinti index alapj\u00e1n visszaadja a megfelel\u0151 OperationSymbol-t. Teh\u00e1t 0 eset\u00e9n a DIVISION-t, 1 eset\u00e9n MULTIPLICATION-t \u00e9s \u00edgy tov\u00e1bb.

companion object {\nfun getByOrdinal(ordinal: Int): OperationSymbol? = OperationSymbol.entries.getOrNull(ordinal)\n}\n

Az enum oszt\u00e1lyhoz tartoz\u00f3 entries az oszt\u00e1lyban defini\u00e1lt enum objektumok t\u00f6mbj\u00e9t adja vissza.

Ezut\u00e1n a util package-ben hozzunk l\u00e9tre egy CalculatorOperator nev\u0171 Kotlin object-t (Singleton oszt\u00e1ly). Ez a Singleton felel a sz\u00e1mol\u00f3g\u00e9p vez\u00e9rl\u00e9s\u00e9\u00e9rt.

Els\u0151 l\u00e9p\u00e9sk\u00e9nt a CalculatorOperator oszt\u00e1lyon bel\u00fcl vegy\u00fcnk fel egy CalculatorState nev\u0171 data class-t. Ez a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak t\u00e1rol\u00e1s\u00e1\u00e9rt felel, ami a CalculatorOperator met\u00f3dusai sz\u00e1m\u00e1ra \u00edrhat\u00f3 \u00e9s olvashat\u00f3, m\u00e1s oszt\u00e1lyok sz\u00e1m\u00e1ra pedig csak olvashat\u00f3.

object CalculatorOperator {\n\nvar state: CalculatorState = CalculatorState()\nprivate set\n\ndata class CalculatorState(\nval input: String = \"\",\nval number1: Double = Double.NaN,\nval number2: Double = Double.NaN,\nval operation: OperationSymbol = OperationSymbol.ADDITION,\nval result: Double = Double.NaN\n)\n}\n

data class

A data class egy nagyon hasznos funkci\u00f3 a Kotlin nyelvben. Seg\u00edts\u00e9g\u00e9vel az els\u0151dleges konstruktorban (ami k\u00f6zvetlen\u00fcl az oszt\u00e1lyn\u00e9v ut\u00e1n \u00e1ll) deklar\u00e1lt v\u00e1ltoz\u00f3kra automatikusan gener\u00e1l\u00f3dnak a hashCode(), equals(), illetve toString() f\u00fcggv\u00e9nyek, melyek hasznosak a k\u00fcl\u00f6nb\u00f6z\u0151 adathalmazok kezel\u00e9s\u00e9re. B\u00e1r megengedett, az adat oszt\u00e1lyokn\u00e1l lehet\u0151leg ker\u00fclj\u00fck a v\u00e1ltoztathat\u00f3 v\u00e1ltoz\u00f3kat (var).

Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat, majd implement\u00e1ljuk a sz\u00e1mok bevitel\u00e9\u00e9rt felel\u0151s onNumberPressed() met\u00f3dust:

fun onNumberPressed(number: Int) {\nstate = state.copy(\ninput = state.input + \"$number\"\n)\n}\n

A sz\u00e1mok bevitele \u00fagy t\u00f6rt\u00e9nik, hogy az \u00e1llapot input attrib\u00fatum\u00e1t mindig egy hozz\u00e1f\u0171z\u00f6tt sz\u00e1mmal megv\u00e1ltoztatott \u00e9rt\u00e9kkel \"friss\u00edtj\u00fck\".

K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt implement\u00e1ljuk a m\u0171veletek elv\u00e9gz\u00e9s\u00e9\u00e9rt felel\u0151s met\u00f3dust:

fun onOperationPressed(operation: Int) {\nval input = state.input\nif (Util.numberRegex.matches(input)) {\nstate = state.copy(\nnumber1 = Util.numberRegex.find(input)!!.value.toDouble(),\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\nstate = state.copy(\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.operationSymbol.matches(input)) {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n}\n}\n
Ha \u00e9rdekel az onOperationPressed() m\u0171k\u00f6d\u00e9si elve, akkor ezt a f\u00fclet lenyitva tudod megismerni.

A m\u0171veletek kezel\u00e9se sor\u00e1n h\u00e1rom esetet k\u00fcl\u00f6nb\u00f6ztet\u00fcnk meg egym\u00e1st\u00f3l:

Az els\u0151 eset, amikor az input string m\u00e1r tartalmaz valamilyen sz\u00e1mot. Ekkor ezt a sz\u00e1mot elt\u00e1roljuk a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak (state) number1 attrib\u00fatum\u00e1ban, majd az input-ot fel\u00fcl\u00edrj\u00fck a bevitt m\u0171velet szimb\u00f3lum\u00e1val, illetve az operation attrib\u00fatumba is elmentj\u00fck a megfelel\u0151 OperationSymbol enum objektumot.

A m\u00e1sodik eset akkor t\u00f6rt\u00e9nik, amikor az input-ban egy szimb\u00f3lumot valamilyen sz\u00e1m is k\u00f6vet (pl. \"*5\"). Ekkor a number1 attrib\u00fatum m\u00e1r tartalmaz egy sz\u00e1mot (az 1. eset m\u00e1r lezajlott), \u00edgy az els\u0151 esetben felvett m\u0171veleti szimb\u00f3lummal \u00e9s az azt k\u00f6vet\u0151 sz\u00e1mmal m\u00e1r el is v\u00e9gezhet\u0151 egy m\u0171velet. A m\u0171velet eredm\u00e9ny\u00e9vel fel\u00fcl\u00edrjuk a number1 \u00e9rt\u00e9k\u00e9t, majd a kor\u00e1bbiakhoz hasonl\u00f3an elt\u00e1roljuk az \u00faj m\u0171velet szimb\u00f3lum\u00e1t.

A harmadik eset, akkor \u00e1ll fenn, amikor csak egy m\u0171veleti szimb\u00f3lum van az input-ban (pl. \"*\"), ami helyett m\u00e1st akarunk elv\u00e9gezni, ekkor csak sim\u00e1n lecser\u00e9lj\u00fck az operation-t \u00e9s az input-ot.

V\u00e9gs\u0151 esetben, amikor az input teljesen \u00fcres, akkor inicializ\u00e1ljuk az \u00faj m\u0171veleti \u00e9rt\u00e9kekkel a state-et.

M\u00e9g sz\u00fcks\u00e9g\u00fcnk van a countResult() met\u00f3dusra, aminek seg\u00edts\u00e9g\u00e9vel kisz\u00e1m\u00edthat\u00f3ak a m\u0171veletek eredm\u00e9nyei:

private fun countResult(number2: Double): Double {\nreturn when (state.operation) {\nOperationSymbol.DIVISION -> state.number1 / number2\nOperationSymbol.MULTIPLICATION -> state.number1 * number2\nOperationSymbol.MODULO -> state.number1 % number2\nOperationSymbol.ADDITION -> state.number1 + number2\nOperationSymbol.SUBTRACTION -> state.number1 - number2\n}\n}\n

V\u00e9gezet\u00fcl vegy\u00fck fel az el\u0151jelv\u00e1lt\u00e1s\u00e9rt, a tizedesjegy\u00e9rt, a m\u0171velet elv\u00e9gz\u00e9s\u00e9\u00e9rt (=) \u00e9s a t\u00f6rtl\u00e9s\u00e9rt felel\u0151s met\u00f3dusokat. Ezekben a met\u00f3dusokban a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak friss\u00edt\u00e9se a fentiekhez hasonl\u00f3an t\u00f6rt\u00e9nik.

fun onSignChange(): Double {\nval input = state.input\nreturn if (Util.numberRegex.matches(input)) {\nval number1 = -Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nresult = Double.NaN,\nnumber1 = number1,\ninput = \"\"\n)\nnumber1\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 =  -Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nnumber2 = number2,\ninput = \"\"\n)\nnumber2\n} else Double.NaN\n}\n\nfun addComa() {\nstate = state.copy(\ninput = state.input + Util.COMMA\n)\n}\n\nfun onEquivalence(): Double {\nval input = state.input\nreturn if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else if (!state.number2.isNaN()) {\nval result = countResult(state.number2)\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else Double.NaN\n}\n\nfun onDelete() {\nstate = state.copy(\nnumber1 = Double.NaN,\nnumber2 = Double.NaN,\nresult = Double.NaN,\ninput = \"\"\n)\n}\n
"},{"location":"laborok/calculator/#calculatorfragment","title":"CalculatorFragment","text":"

Els\u0151 l\u00e9p\u00e9sk\u00e9nt keress\u00fck meg a res/layout alatt tal\u00e1lhat\u00f3 fragment_calculator.xml f\u00e1jlt, ahol CalculatorFragment View komponenseit \u00e9s azok elrendez\u00e9s\u00e9t fogjuk meghat\u00e1rozni.

A laborra ford\u00edthat\u00f3 id\u0151 miatt csak ismerkedj\u00fcnk meg az XML fel\u00e9p\u00edt\u00e9si elv\u00e9vel \u00e9s m\u00e1soljuk \u00e1t a k\u00e9sz View hierarchi\u00e1t, ami az al\u00e1bbi r\u00e9szt k\u00f6vet\u0151en el\u00e9rhet\u0151 egyben, egy leny\u00edl\u00f3 r\u00e9szt kinyitva.

<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TableLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_weight=\"1\">\n...\n    </TableRow>\n...\n</TableLayout>\n

A CalculatorFragment-\u00fcnk t\u00e1bl\u00e1zatos elrendez\u00e9s\u00e9t egy TableLayout hat\u00e1rozza meg, aminek a sorait TableRow-k seg\u00edts\u00e9g\u00e9vel tudjuk megadni. A t\u00e1bl\u00e1zat mind a 6 sora ugyanolyan magass\u00e1g\u00fa, amit \u00fagy tudunk el\u00e9rni, hogy a TableRow-k eset\u00e9n az android:layout_weight=\"1\" attrib\u00fatumot felvessz\u00fck. Emellett minden gomb rendelkezik 5dp margin-nal, hogy ne legyenek t\u00fal k\u00f6zel egym\u00e1shoz.

<TextView\nandroid:id=\"@+id/consoleTextView\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/text_calculator_console\" android:textSize=\"40sp\"\nandroid:gravity=\"center_vertical|end\"\nandroid:fontFamily=\"sans-serif-medium\"/>\n

A t\u00e1bl\u00e1zat els\u0151 sor\u00e1ban a konzol szerep\u00e9t bet\u00f6lt\u0151 TextView szerepel. Az \u00e1ltala megjelen\u00edtett sz\u00f6veg a View-n bel\u00fcl jobb oldalt, f\u00fcgg\u0151legesen k\u00f6z\u00e9pre igaz\u00edtva l\u00e1that\u00f3 f\u00e9lk\u00f6v\u00e9r bet\u0171t\u00edpussal. Ez az android:gravity \u00e9s android:fontFamily attrib\u00fatumokkal \u00e9rhet\u0151 el.

 <Button\nandroid:id=\"@+id/deleteButton\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:textSize=\"22sp\"\nandroid:text=\"@string/button_text_delete\"\nstyle=\"@style/Widget.Material3.Button\"/>\n

A konzol alatt l\u00e9v\u0151 sorokban a m\u0171velet \u00e9s sz\u00e1m gombok k\u00f6vetkeznek. A gombok egyenletesen t\u00f6ltik ki a sorokban elfoglalhat\u00f3 teret, ami ugyancsak az android:layout_weight=\"1\" attrib\u00fatum felv\u00e9tel\u00e9vel \u00e9rhet\u0151 el.

CalculatorFragment teljes XML k\u00f3dja
<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TableLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_weight=\"1\">\n\n<TextView\nandroid:id=\"@+id/consoleTextView\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_weight=\"1\"\nandroid:clickable=\"true\"\nandroid:focusable=\"true\"\nandroid:fontFamily=\"sans-serif-medium\"\n\nandroid:gravity=\"center_vertical|end\"\nandroid:text=\"@string/text_calculator_console\"\nandroid:textSize=\"40sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/deleteButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_delete\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/signButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_sign\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/modulo_button\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_modulo\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationDivisionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_division\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number7Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_seven\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number8Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_eight\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number9Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_nine\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationMultiplicationButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_multiplication\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number4Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_four\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number5Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_five\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number6Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_six\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationSubtractionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_subtraction\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number1Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_one\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number2Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_two\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number3Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_three\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationAdditionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_addition\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number0Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_zero\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/commaButton\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_come\"\nandroid:textSize=\"22sp\" />\n\n<Button\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:enabled=\"false\"\nandroid:visibility=\"invisible\" />\n\n<Button\nandroid:id=\"@+id/operationEquivalenceButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_equivalence\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n</TableLayout>\n

T\u00e9rj\u00fcnk \u00e1t a CalculatorFragment oszt\u00e1lyra, ahol a f\u00e1jl tartalm\u00e1t cser\u00e9lj\u00fck le a k\u00f6vetkez\u0151re:

package hu.bme.aut.android.calculator\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.calculator.databinding.FragmentCalculatorBinding\n\nclass CalculatorFragment : Fragment() {\n\nprivate var _binding: FragmentCalculatorBinding? = null\nprivate val binding get() = _binding!!\n\noverride fun onCreateView(\ninflater: LayoutInflater, container: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\n_binding = FragmentCalculatorBinding.inflate(inflater, container, false)\nreturn binding.root\n}\n\noverride fun onDestroyView() {\nsuper.onDestroyView()\n_binding = null\n}\n}\n

Miel\u0151tt r\u00e1t\u00e9rn\u00e9nk a Fragment \u00e1ltal kezelt View komponensek inicializ\u00e1l\u00e1s\u00e1ra, vegy\u00fcnk fel m\u00e9g k\u00e9t Set-et, amikben a sz\u00e1m \u00e9s m\u0171velet gombokhoz tartoz\u00f3 View-k referenci\u00e1it fogjuk t\u00e1rolni. Bevezet\u00e9s\u00fckkel az inicaliz\u00e1l\u00e1s k\u00f6nnyebben elv\u00e9gezhet\u0151. Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat.

private lateinit var numberButtons: Set<Button>\n\nprivate lateinit var operationButtons: Set<Button>\n

Ha ezzel megvagyunk t\u00e1roljunk el egy referenci\u00e1t a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1r\u00f3l.

private val calcState get() = CalculatorOperator.state\n

Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1t. Majd t\u00e9rj\u00fcnk \u00e1t az onViewCreated() met\u00f3dusra, ahol megadhatjuk a View komponensek esem\u00e9nyekre adott viselked\u00e9s\u00e9t.

Els\u0151 l\u00e9p\u00e9sk\u00e9nt szervezz\u00fck ki a sz\u00e1mok megjelen\u00edt\u00e9s\u00e9nek elv\u00e9t egy setResult() nev\u0171 met\u00f3dusba. Ez seg\u00edti, hogy az eg\u00e9sz \u00e9s a val\u00f3s sz\u00e1mok k\u00fcl\u00f6nb\u00f6z\u0151 m\u00f3don jelenjenek meg a k\u00e9perny\u0151n.

private fun setResult(value: Double) {\nif (value.isNaN()) {\nbinding.consoleTextView.text = \"\"\n} else if (value % 1.0 == 0.0) {\nbinding.consoleTextView.text = value.toInt().toString()\n} else {\nbinding.consoleTextView.text = String.format(\"%.2f\",value)\n}\n}\n
Ezut\u00e1n implement\u00e1ljuk az initButtons() met\u00f3dust.

private fun initButtons() {\n// Init number and operation button sets\nwith(binding) {\nnumberButtons = setOf(\nnumber0Button, number1Button, number2Button,\nnumber3Button, number4Button, number5Button,\nnumber6Button, number7Button, number8Button,\nnumber9Button\n)\n\noperationButtons = setOf(\noperationDivisionButton,\noperationMultiplicationButton,\noperationSubtractionButton,\noperationAdditionButton,\nmoduloButton\n)\n}\n\n// Init click listeners for number buttons\nnumberButtons.forEachIndexed { number, button ->\nbutton.setOnClickListener {\nCalculatorOperator.onNumberPressed(number)\nbinding.consoleTextView.text = numberRegex.find(calcState.input)?.value ?: \"\"\n}\n}\n\n// Init click listeners for number buttons\noperationButtons.forEachIndexed { operation, button ->\nbutton.setOnClickListener {\nCalculatorOperator.onOperationPressed(operation)\nsetResult(calcState.result)\n}\n}\n\n// Init click listener for sign button\nbinding.signButton.setOnClickListener {\nsetResult(CalculatorOperator.onSignChange())\n}\n\n// Init click listener for delete button\nbinding.deleteButton.setOnClickListener {\nCalculatorOperator.onDelete()\nsetResult(calcState.result)\n}\n\n// Init click listener for comma button\nbinding.commaButton.setOnClickListener {\nCalculatorOperator.addComa()\nbinding.consoleTextView.text = calcState.input\n}\n\n// Init click listener for equivalence button\nbinding.operationEquivalenceButton.setOnClickListener {\nsetResult(CalculatorOperator.onEquivalence())\n}\n}\n

Itt el\u0151sz\u00f6r inicializ\u00e1ljuk Set-eket. Majd egy forEachIndexed ciklissal be\u00e1ll\u00edtjuk az esem\u00e9nykezel\u0151j\u00fcket. Ezut\u00e1n sorra elv\u00e9gezz\u00fck az esem\u00e9nykezel\u0151k be\u00e1ll\u00edt\u00e1s\u00e1t azokra a gombokra is, amikb\u0151l csak egy-egy p\u00e9ld\u00e1ny l\u00e9tezik.

A with egy olyan scope f\u00fcggv\u00e9ny, aminek seg\u00edts\u00e9g\u00e9vel azt tudjuk kifejezni, hogy: ezzel az objektummal csin\u00e1ld a k\u00f6vetkez\u0151t. \u00cdgy sok esetben kicsit \u00e1tl\u00e1that\u00f3bb\u00e1 lehet tenni a k\u00f3dot, mivel a context-k\u00e9nt megadott binding objektumra this-k\u00e9nt hivatkozhatunk. Vannak m\u00e1s scope f\u00fcggv\u00e9nyek is k\u00fcl\u00f6nb\u00f6z\u0151 felhaszn\u00e1l\u00e1si esetekre. R\u00f3luk ezen a linken lehet olvasni.

V\u00e9gezet\u00fcl h\u00edvjuk meg ezt az initButtons() met\u00f3dust az onViewCreated()-ben.

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\nsuper.onViewCreated(view, savedInstanceState)\n\ninitButtons()   }\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a CalculatorFragment egy bele\u00edrt sz\u00e1mmal (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/calculator/#recyclerview","title":"RecyclerView","text":"

A RecyclerView k\u00f6nyvt\u00e1r megk\u00f6nny\u00edti a nagy adathalmazok hat\u00e9kony megjelen\u00edt\u00e9s\u00e9t. Meg kell hat\u00e1rozni, hogy az egyes elemek hogyan n\u00e9zzenek ki. \u00cdgy az adathalmazt \u00e1tadva egy Adapter nev\u0171 komponensnek az dinamikusan l\u00e9trehozza az elemeket akkor, amikor \u00e9ppen sz\u00fcks\u00e9g van r\u00e1juk.

Ahogy a n\u00e9v is sugallja, a RecyclerView \u00fajrahasznos\u00edtja ezeket a View elemeket. Amikor egy elem elt\u0171nik a k\u00e9perny\u0151r\u0151l, a RecyclerView nem szabad\u00edtja fel, hanem \u00fajra felhaszn\u00e1lja azt a k\u00e9perny\u0151n megjelen\u0151 \u00faj elemhez. Ennek k\u00f6sz\u00f6nhet\u0151en a RecyclerView bevezet\u00e9s\u00e9vel javul a teljes\u00edtm\u00e9ny \u00e9s az alkalmaz\u00e1s v\u00e1laszideje, tov\u00e1bb\u00e1 cs\u00f6kkenti az energiafogyaszt\u00e1st.

A RecyclerView implement\u00e1l\u00e1sa \u00edgy h\u00e1rom l\u00e9p\u00e9sb\u0151l \u00e1ll:

  1. l\u00e9p\u00e9s: View elem layout-j\u00e1nak meghat\u00e1roz\u00e1sa
  2. l\u00e9p\u00e9s: Adapter oszt\u00e1ly implement\u00e1l\u00e1sa
  3. l\u00e9p\u00e9s: RecyclerView felv\u00e9tele a Fragment/Activity-ben \u00e9s inicializ\u00e1l\u00e1sa a megfelel\u0151 oszt\u00e1lyban

Hozzunk l\u00e9re egy \u00faj XML f\u00e1jlt view_history_item.xml n\u00e9ven ares/layout mapp\u00e1ban. A lista elem LinearLayout-ot haszn\u00e1l a komponensei v\u00edzszintes sorba t\u00f6rt\u00e9n\u0151 elrendez\u00e9s\u00e9hez (android:orientation=\"horizontal\"). A LinearLayout eset\u00e9n is rendelhet\u00fcnk s\u00falyoz\u00e1st (ar\u00e1nyokat) az egyes View komponensekhez az android:weightSum \u00e9s az android:layout_weight attrib\u00fatumok seg\u00edts\u00e9g\u00e9vel.

<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:id=\"@+id/historyView\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:orientation=\"horizontal\"\nandroid:gravity=\"center_vertical\"\nandroid:layout_margin=\"5dp\"\nandroid:weightSum=\"10\">\n\n<TextView\nandroid:id=\"@+id/operationTextView\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\ntools:text=\"1 + 1 = 2\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"7\"\nandroid:textSize=\"22sp\"\nandroid:layout_gravity=\"start\"\nandroid:fontFamily=\"sans-serif-medium\" />\n\n<Button\nandroid:id=\"@+id/loadButton\"\nstyle=\"?attr/materialButtonOutlinedStyle\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_margin=\"5dp\"\nandroid:text=\"@string/button_text_load\"\nandroid:layout_weight=\"3\"/>\n\n</LinearLayout>\n

K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck el a HistoryAdapter oszt\u00e1lyunkat. Ehhez hozzunk l\u00e9tre egy \u00faj adapter package-et, majd benne egy \u00faj Kotlin oszt\u00e1lyt HistoryAdapter n\u00e9ven. Az oszt\u00e1lyunk belsej\u00e9ben vegy\u00fcnk fel egy ViewHolder nev\u0171 inner class-t, aminek konstruktor\u00e1ban egy ViewHistoryItemBinding szerepeljen, amit majd ez egyes View referenci\u00e1k inicializ\u00e1l\u00e1s\u00e1hoz haszn\u00e1lunk fel. Import\u00e1ljuk a hi\u00e1nyz\u00f3 ViewHistoryItemBinding referenci\u00e1t.

package hu.bme.aut.android.calculator.adapter\n\nimport android.widget.Button\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport hu.bme.aut.android.calculator.databinding.ViewHistoryItemBinding\n\nclass HistoryAdapter {\n\ninner class ViewHolder(binding: ViewHistoryItemBinding) :\nRecyclerView.ViewHolder(binding.historyView) {\n\nval operationTextView: TextView\nval loadButton: Button\n\ninit {\noperationTextView = binding.operationTextView\nloadButton = binding.loadButton\n}\n}\n}\n

A HistoryAdapter-t sz\u00e1rmaztassuk le a RecyclerView.Adapter absztrakt oszt\u00e1lyb\u00f3l, \u00e9s az els\u0151dleges konstruktor\u00e1ban vegy\u00fcnk fel egy history v\u00e1ltoz\u00f3t, ami a sz\u00e1mol\u00f3g\u00e9p \u00e1ltal elmentett kor\u00e1bbi m\u0171veleteket tartalmazza. Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat.

class HistoryAdapter(\nprivate val history: List<CalculatorOperator.CalculatorState>,\nprivate val context: Context\n) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>()\n

Az AndroidStudio most hib\u00e1t jelez ez az\u00e9rt van, mert implement\u00e1lnunk kell az onCreateViewHolder(), onBindViewHolder() \u00e9s a getItemCount() met\u00f3dusokat.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\nval binding = ViewHistoryItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)\nreturn ViewHolder(binding)\n}\n\noverride fun onBindViewHolder(holder: ViewHolder, position: Int) {\nval operation = history[position]\nholder.operationTextView.text = context.getString(\nR.string.text_operation,\noperation.number1,\noperation.operation.symbol,\noperation.number2,\noperation.result\n)\nholder.loadButton.setOnClickListener {\n// TODO: implement event handler\n}\n}\n\noverride fun getItemCount(): Int = history.size\n

getString()

Ha a String er\u0151forr\u00e1sunkban valamilyen v\u00e1ltoz\u00f3 \u00e9rt\u00e9k\u00e9t szeretn\u00e9nk megjelen\u00edteni (form\u00e1z\u00e1s), akkor azt \u00fagy tehetj\u00fck meg, hogy az er\u0151forr\u00e1sunkban argumantumokat helyez\u00fcnk el. Eset\u00fcnkben egy ilyen argumentum p\u00e9ld\u00e1ul a %1$.2f, ami kett\u0151 tizedesjegyig jelen\u00edti meg a sz\u00e1mokat.

A Context-b\u0151l el\u00e9rhet\u0151 getString() met\u00f3dus seg\u00edts\u00e9g\u00e9vel pedig az er\u0151forr\u00e1s id-j\u00e1nak megad\u00e1s\u00e1t k\u00f6vet\u0151en felsorolhatjuk az argumentumainkat, amik alapj\u00e1n a getString() elv\u00e9gzi azok besz\u00far\u00e1s\u00e1t.

A lista elemre val\u00f3 kattint\u00e1s kezel\u00e9s\u00e9hez sz\u00fcks\u00e9g\u00fcnk lesz egy interface-re, amit majd a list\u00e1t megjelen\u00edt\u00f3 Fragment fog implement\u00e1lni. Vegy\u00fcnk fel egy bels\u0151 interface-t ClickListener n\u00e9ven, ami rendelkezzen egy onClick(loadedData: String) met\u00f3dussal. \u00c9s eg\u00e9sz\u00edts\u00fck ki az Adapter konstruktor\u00e1t egy onClickListener v\u00e1ltoz\u00f3val.

class HistoryAdapter(\nprivate val onClickListener: ClickListener,\nprivate val history: List<CalculatorOperator.CalculatorState>,\nprivate val context: Context\n) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {\n...\n
interface ClickListener {\nfun onClick(loadedData: String)\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a HistoryAdapter oszt\u00e1ly k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/calculator/#historyfragment","title":"HistoryFragment","text":"

T\u00e9rj\u00fcnk vissza a CalculatorOperator-ra, hogy kezelni tudja az elv\u00e9gzett m\u0171veletek ment\u00e9s\u00e9t. Eg\u00e9sz\u00edts\u00fck ki az oszt\u00e1lyt egy history v\u00e1ltoz\u00f3val, egy loadState() \u00e9s egy addStateToHsitory() met\u00f3dussal, illetve m\u00f3dos\u00edtsuk az onOperationPressed() \u00e9s onEquaivalence() met\u00f3dusokat.

object CalculatorOperator {\n\nval history = mutableListOf<CalculatorState>()\n\nfun loadState(value: String) {\nstate = state.copy(\ninput = \"\",\nnumber1 = value.toDouble(),\nnumber2 = 0.0,\nresult = 0.0,\noperation = OperationSymbol.ADDITION\n)\n}\n\nprivate fun addStateToHistory() {\nhistory.add(state)\n}\n...\n
fun onOperationPressed(operation: Int) {\nval input = state.input\nif (Util.numberRegex.matches(input)) {\nstate = state.copy(\nnumber1 = Util.numberRegex.find(input)!!.value.toDouble(),\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nnumber2 = number2,\nresult = countResult(number2),\n)\n\naddStateToHistory()\n\nstate = state.copy(\nnumber1 = state.result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput =  OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.operationSymbol.matches(input)) {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n}\n}\n
fun onEquivalence(): Double {\nval input = state.input\nreturn if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\n\nstate = state.copy(\nnumber2 = number2,\nresult = result\n)\n\naddStateToHistory()\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else if (!state.number2.isNaN()) {\nval result = countResult(state.number2)\naddStateToHistory()\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else Double.NaN\n}\n

Ha ezzel megvagyunk keress\u00fck meg a res/layout mapp\u00e1ban l\u00e9v\u0151 fragment_history.xml f\u00e1jlt \u00e9s m\u00f3dos\u00edtsuk a k\u00f6vetkez\u0151 m\u00f3don.

<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n\n<com.google.android.material.appbar.AppBarLayout\nandroid:id=\"@+id/historyAppBar\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\">\n\n<com.google.android.material.appbar.MaterialToolbar\nandroid:id=\"@+id/topAppBar\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"?attr/actionBarSize\"\napp:navigationIcon=\"@drawable/ic_arrow_back_24\"\napp:title=\"@string/app_bar_title_history\" />\n\n</com.google.android.material.appbar.AppBarLayout>\n\n<androidx.recyclerview.widget.RecyclerView\nandroid:id=\"@+id/historyRecyclerView\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\nandroid:clipToPadding=\"false\"\napp:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@id/historyAppBar\"\ntools:listitem=\"@layout/view_history_item\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

A HistoryFragment egy AppBar-t \u00e9s egy RecyclerView jelen\u00edt meg. Az AppBar rendelkezik egy vissza gombbal, amihez az ikont a labor elej\u00e9n m\u00e1r l\u00e9trehoztuk. A RecyclerView pedig egy f\u00fcgg\u0151legesen g\u00f6rgethet\u0151 list\u00e1t jelen\u00edt meg.

T\u00e9rj\u00fcnk \u00e1t a HistoryFragment oszt\u00e1lyra. Az oszt\u00e1lynak most a Fragment-b\u0151l val\u00f3 sz\u00e1rmaz\u00e1s mellett a HistoryAdapter.ClickListener interf\u00e9sz\u00e9t is implement\u00e1lnia kell. Az Adapter inicializ\u00e1l\u00e1sa most is egy lateinit var v\u00e1ltoz\u00f3 \u00e9s az onViewCreated() met\u00f3dus seg\u00edts\u00e9g\u00e9vel t\u00f6rt\u00e9nik. El\u0151sz\u00f6r inicializ\u00e1ljuk, \u00e9s ut\u00e1na a binding seg\u00edts\u00e9s\u00e9g\u00e9vel \u00f6sszek\u00f6tj\u00fck a Fragment \u00e1ltal megjelen\u00edtett RecyclerView komponenssel. V\u00e9gs\u0151 soron pedig hozz\u00e1rendel\u00fcnk egy esem\u00e9nykezele\u0151t az AppBar-ban l\u00e9v\u0151 vissza gombhoz.

package hu.bme.aut.android.calculator\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.calculator.adapter.HistoryAdapter\nimport hu.bme.aut.android.calculator.databinding.FragmentHistoryBinding\nimport hu.bme.aut.android.calculator.util.CalculatorOperator\n\nclass HistoryFragment: Fragment(), HistoryAdapter.ClickListener {\n\nprivate var _binding: FragmentHistoryBinding? = null\nprivate val binding get() = _binding!!\n\nprivate lateinit var adapter: HistoryAdapter\n\noverride fun onCreateView(\ninflater: LayoutInflater,\ncontainer: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\n_binding = FragmentHistoryBinding.inflate(inflater, container, false)\nreturn binding.root\n}\n\noverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {\nsuper.onViewCreated(view, savedInstanceState)\n\n// Init RecyclerView\nadapter = HistoryAdapter(this@HistoryFragment, CalculatorOperator.history, requireContext())\nbinding.historyRecyclerView.adapter = adapter\n\nbinding.topAppBar.setNavigationOnClickListener {\n// TODO: navigate back to CalculatorFragment\n}\n}\n\noverride fun onClick(loadedData: String) {\n// TODO: implement method\n}\n\noverride fun onDestroyView() {\nsuper.onDestroyView()\n_binding = null\n}\n}\n

Az\u00e9rt, hogy a visszat\u00f6lt\u00f6tt eredm\u00e9ny a felhaszn\u00e1l\u00f3 sz\u00e1m\u00e1ra is l\u00e1that\u00f3v\u00e1 v\u00e1ljon, eg\u00e9sz\u00edts\u00fck ki a CalculatorFragment onViewCreated f\u00fcggv\u00e9ny\u00e9t:

setResult(CalculatorOperator.state.number1)\n
"},{"location":"laborok/calculator/#navigacio","title":"Navig\u00e1ci\u00f3","text":"

Most m\u00e1r csak a Fragment-ek k\u00f6zti navig\u00e1ci\u00f3 v\u00e9gleges\u00edt\u00e9se van h\u00e1tra. A CalculatorFragment-r\u0151l a konzol\u00e9rt felel\u0151s TextView-ra kattintva t\u00e9rhet\u00fcnk \u00e1t a HistoryFragment-re. Ehhez t\u00e9rj\u00fcnk vissza a CalculatorFragment onViewCreated() met\u00f3dus\u00e1ra, ahol a View komponensek inicializ\u00e1l\u00e1st v\u00e9gezt\u00fck el. Vegy\u00fck fel a k\u00f6vetkez\u0151 esem\u00e9nykezel\u0151t \u00e9s enged\u00e9lyezz\u00fck, hogy a View kattinthat\u00f3 legyen:

with(binding.consoleTextView) {\nisClickable = true\nsetOnClickListener {\nval action = CalculatorFragmentDirections.actionCalculatorFragmentToHistoryFragment()\nfindNavController().navigate(action)\n}\n}\n

A navig\u00e1ci\u00f3 kezel\u00e9s\u00e9\u00e9rt az \u00fan. NavController felel, amit a findNavController() met\u00f3dussal \u00e9rhet\u00fcnk el, rajta pedig a navigate(action) met\u00f3dush\u00edv\u00e1ssal ki tudjuk v\u00e1ltani a navig\u00e1ci\u00f3t. Az action-ben most a navig\u00e1ci\u00f3s \"\u00fatvonal\" szerepel, de ha sz\u00fcks\u00e9ges, akkor ez kieg\u00e9sz\u00edthet\u0151 tov\u00e1bbi argumentumokkal (l\u00e1sd a dokument\u00e1ci\u00f3ban). A findNavController()-hez androidx.navigation.fragment.findNavController import\u00e1l\u00e1s\u00e1ra lesz sz\u00fcks\u00e9g\u00fcnk.

Ha ezzel megvagyunk t\u00e9rj\u00fcnk \u00e1t a HistoryFragment-re, ahol a vissza gombra fogjuk be\u00e1ll\u00edtani, hogy megnyom\u00e1sra l\u00e9pjen vissza a CalculatorFragment-re. Itt is az onViewCreated() met\u00f3dusban vagy\u00fck fel k\u00f6vetkez\u0151 esem\u00e9nykezel\u0151t:

binding.topAppBar.setNavigationOnClickListener {\nfindNavController().popBackStack()\n}\n

V\u00e9gezet\u00fcl implement\u00e1ljuk a HistoryFragment onClick() met\u00f3dus\u00e1t.

override fun onClick(loadedData: String) {\nval action = HistoryFragmentDirections.actionHistoryFragmentToCalculatorFragment()\nCalculatorOperator.loadState(loadedData)\nfindNavController().navigate(action)\n}\n

Ebben az esetben is a findNavController()-hez androidx.navigation.fragment.findNavController import\u00e1l\u00e1s\u00e1ra lesz sz\u00fcks\u00e9g\u00fcnk.

Ahhoz, hogy az adatok bet\u00f6lt\u00e9se megfelel\u0151en megt\u00f6rt\u00e9nnyen t\u00e9rj\u00fcnk vissza a HistoryAdapter onBindViewHolder() met\u00f3dus\u00e1ra, \u00e9s \u00e1ll\u00edtsuk be, hogy a Load gombra val\u00f3 kattint\u00e1s sor\u00e1n a HistoryFragment \u00e1ltal implement\u00e1lt onClick() met\u00f3dust h\u00edvja meg:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {\nval operation = history[position]\nholder.operationTextView.text = context.getString(\nR.string.text_operation,\noperation.number1,\noperation.operation.symbol,\noperation.number2,\noperation.result\n)\nholder.loadButton.setOnClickListener {\nif (operation.result % 1.0 == 0.0) {\nonClickListener.onClick(operation.result.toInt().toString())\n} else {\nonClickListener.onClick(String.format(\"%.10f\", operation.result))\n}\n}\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a HistoryFragment n\u00e9h\u00e1ny bejegyz\u00e9ssel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a HistoryFragment oszt\u00e1ly onClick() met\u00f3dus\u00e1nak k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/calculator/#onallo-resz-elozmenyek-torlese","title":"\u00d6n\u00e1ll\u00f3 r\u00e9sz - El\u0151zm\u00e9nyek t\u00f6rl\u00e9se","text":"
  1. Vegy\u00fcnk fel egy \u00faj t\u00f6rl\u00e9s Vector Asset-et a vissza gombhoz hasonl\u00f3an.
  2. A ResourceManager seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edts\u00fcnk egy menu er\u0151forr\u00e1st menu_top_app_bar n\u00e9ven, ami t\u00f6rl\u00e9s men\u00fc elemet tartalmazza. Hint
  3. Implement\u00e1ljunk egy a t\u00f6rl\u00e9s\u00e9rt felel\u0151s clearHistory() met\u00f3dust a CalculatorOperator-ban.
  4. A FragmentHistory-hoz tartoz\u00f3 XML layout f\u00e1jlban vagy\u00fck fel a Toolbar-hoz tartoz\u00f3 app:menu=\"@menu/menu_top_app_bar\" attrib\u00fatumot.
  5. A FragmentHistory-ban vegy\u00fck fel a menu esem\u00e9nykezel\u0151j\u00e9t.
binding.topAppBar.setOnMenuItemClickListener { menuItem ->\nwhen(menuItem.itemId) {\nR.id.delete -> {\nval size = CalculatorOperator.history.size\nCalculatorOperator.clearHistory()\nadapter.notifyItemRangeRemoved(0,size)\ntrue\n}\nelse -> false\n}\n}\n

Itt fontos megjegyezni, hogy az adapter-t \u00e9rtes\u00edten\u00fcnk kell arr\u00f3l, hogy milyen intervallumot \u00e9rintett a v\u00e1ltoztat\u00e1s. Ezt az adapter.notifyItemRangeRemoved()-al tudjuk elv\u00e9gezni. Ha esetleg \u00faj elemet venn\u00e9nk fel vagy valamilyen egy\u00e9b v\u00e1ltoztat\u00e1st csin\u00e1ln\u00e1nk az adapter \u00e1ltal megjelen\u00edtett adathalmazon, arr\u00f3l ugyan\u00edgy \u00e9rtes\u00edteni kell az adapter-t.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik az \u00fcres History k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a HistoryFragment oszt\u00e1ly setOnMenuItemClickListener met\u00f3dus\u00e1nak k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/calculator/#onallo-feladat-kontextus-fuggo-mezotorles","title":"\u00d6n\u00e1ll\u00f3 feladat - Kontextus-f\u00fcgg\u0151 mez\u0151t\u00f6rl\u00e9s","text":"

A jelenlegi alkalmaz\u00e1s a t\u00f6rl\u00e9sn\u00e9l a teljes sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1t t\u00f6rli. Val\u00f3s\u00edtsuk meg, hogy ha m\u00e1r az els\u0151 sz\u00e1m \u00e9s jelet megadtuk, \u00e9s a m\u00e1sodik sz\u00e1mra is elkezdt\u00fcnk \u00edrni, akkor a C helyett CE felirat legyen a gombon, \u00e9s ennek megnyom\u00e1sa csak a m\u00e1sodik sz\u00e1mot t\u00f6rli (a gomb ekkor visszav\u00e1lt a C m\u0171k\u00f6d\u00e9sre).

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a sz\u00e1mol\u00f3g\u00e9p CE* gombja (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a CalculatorFragment oszt\u00e1ly ehhez tartoz\u00f3 r\u00e9sze, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/compose/","title":"Labor04 - Felhaszn\u00e1l\u00f3i fel\u00fcletek k\u00e9sz\u00edt\u00e9se a Jetpack Compose seg\u00edts\u00e9g\u00e9vel (ComposeBasics)","text":""},{"location":"laborok/compose/#bevezetes","title":"Bevezet\u00e9s","text":"

A labor c\u00e9lja a Jetpack Compose haszn\u00e1lat\u00e1nak bemutat\u00e1sa: felhaszn\u00e1l\u00f3i fel\u00fcletek k\u00e9sz\u00edt\u00e9se egyszer\u0171, egym\u00e1sba \u00e1gyazhat\u00f3 composable met\u00f3dusok seg\u00edts\u00e9g\u00e9vel, XML le\u00edr\u00f3k haszn\u00e1lata n\u00e9lk\u00fcl. A labor sor\u00e1n egy egyszer\u0171 alkalmaz\u00e1st fogunk k\u00e9sz\u00edteni, amelyben bejelentkez\u00e9si \u00e9s f\u0151k\u00e9perny\u0151k tal\u00e1lhat\u00f3k.

Az alkalmaz\u00e1sban a t\u00e9nyleges bejelentkeztet\u00e9si logika most nem kap helyet, puszt\u00e1n a felhaszn\u00e1l\u00f3i fel\u00fclet l\u00e9trehoz\u00e1s\u00e1nak m\u00f3dj\u00e1ra koncentr\u00e1lunk.

A megval\u00f3s\u00edtand\u00f3 felhaszn\u00e1l\u00f3i fel\u00fcletet az al\u00e1bbi k\u00e9perny\u0151k\u00e9pek szeml\u00e9ltetik:

"},{"location":"laborok/compose/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/compose/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Checkout

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

"},{"location":"laborok/compose/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

  1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
  2. A projekt neve legyen ComposeBasics, a kezd\u0151 package pedig hu.bme.aut.android.composebasics.
  3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
  4. A minimum API szint legyen API24: Android 7.0.
  5. A Build configuration language Kotlin DSL legyen.

FILE PATH

A projekt mindenk\u00e9ppen a repository-ban l\u00e9v\u0151 ComposeBasics k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n a laborvezet\u0151 vezet\u00e9s\u00e9vel vizsg\u00e1ljuk meg a forr\u00e1s fel\u00e9p\u00edt\u00e9s\u00e9t:

  • Tekints\u00fck \u00e1t, hogyan m\u0171k\u00f6dnek a fel\u00fcletet le\u00edr\u00f3 composable function\u00f6k.
  • Buildelj\u00fck le a projektet, \u00e9s pr\u00f3b\u00e1ljuk ki az el\u0151n\u00e9zetet.
  • N\u00e9zz\u00fck meg, hogyan friss\u00fcl az el\u0151n\u00e9zet, ahogyan m\u00f3dos\u00edtjuk a k\u00f3dunkat.
"},{"location":"laborok/compose/#szoveges-eroforrasok-definialasa","title":"Sz\u00f6veges er\u0151forr\u00e1sok defini\u00e1l\u00e1sa","text":"

A strings.xml f\u00e1jl m\u0171k\u00f6d\u00e9s\u00e9t m\u00e1r ismerj\u00fck, t\u00f6lts\u00fck fel ezt el\u0151re a k\u00e9s\u0151bb sz\u00fcks\u00e9ges sz\u00f6veges c\u00edmk\u00e9kkel, hogy k\u00e9s\u0151bb a l\u00e9nyeges elemekre tudjunk koncentr\u00e1lni:

<resources>\n<string name=\"app_name\">ComposeBasics</string>\n<string name=\"textfield_label_email\">email</string>\n<string name=\"textfield_label_password\">password</string>\n<string name=\"button_label_login\">Log in</string>\n<string name=\"textfield_label_username\">username</string>\n<string name=\"snackbar_message_this_is_a\">This is a Snackbar</string>\n<string name=\"top_app_bar_title_home\">Home</string>\n<string name=\"button_label_logout\">Log out</string>\n<string name=\"dropdown_menu_item_label_settings\">Settings</string>\n<string name=\"dropdown_menu_item_label_profile\">Profile</string>\n</resources>\n
"},{"location":"laborok/compose/#fuggosegek","title":"F\u00fcgg\u0151s\u00e9gek","text":""},{"location":"laborok/compose/#fuggosegek-frissitese","title":"F\u00fcgg\u0151s\u00e9gek friss\u00edt\u00e9se","text":"

Az Android Studio a projekt l\u00e9trehoz\u00e1sakor felveszi ugyan a Compose-t a f\u00fcgg\u00e9segek k\u00f6z\u00e9, de n\u00e9mileg elavult verzi\u00f3kat haszn\u00e1l. Friss\u00edts\u00fck a modul szint\u0171 build.gradle.kts f\u00e1jlban a f\u00fcgg\u0151s\u00e9geket az al\u00e1bbiakra, majd szinkroniz\u00e1ljuk is a projektet:

Gradle Version Catalogs

Az Android Studio Iguana-t\u00f3l vagy Gradle 8.3-t\u00f3l kezd\u0151d\u0151en a f\u00fcgg\u0151s\u00e9gek kezel\u00e9s\u00e9re a Gradle bevezette a Version Catalog-ot.

A Gradle Version Catalogs lehet\u0151v\u00e9 teszi a f\u00fcgg\u0151s\u00e9gek \u00e9s b\u0151v\u00edtm\u00e9nyek sk\u00e1l\u00e1zhat\u00f3 m\u00f3don t\u00f6rt\u00e9n\u0151 hozz\u00e1ad\u00e1s\u00e1t \u00e9s karbantart\u00e1s\u00e1t a projekthez. Ahelyett, hogy a f\u00fcgg\u0151s\u00e9geket \u00e9s verzi\u00f3kat az egyes build f\u00e1jlokban be\u00e9getn\u00e9nk, egy k\u00f6zponti katal\u00f3gusban defini\u00e1ljuk \u0151ket, \u00e9s az egyes modulokban csak hivatkozunk r\u00e1juk. \u00cdgy friss\u00edt\u00e9s eset\u00e9n el\u00e9g egy helyen \u00e1t\u00edrnunk p\u00e9ld\u00e1ul a verzi\u00f3sz\u00e1mot.

A f\u00fcgg\u0151s\u00e9geink a Version Catalogban (libs.version.toml):

[versions]\nagp = \"8.5.2\"\nkotlin = \"1.9.0\"\ncoreKtx = \"1.13.1\"\njunit = \"4.13.2\"\njunitVersion = \"1.2.1\"\nespressoCore = \"3.6.1\"\nlifecycleRuntimeKtx = \"2.8.6\"\nactivityCompose = \"1.9.2\"\ncomposeBom = \"2024.09.02\"\n\n[libraries]\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"coreKtx\" }\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\nandroidx-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"junitVersion\" }\nandroidx-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espressoCore\" }\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycleRuntimeKtx\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"composeBom\" }\nandroidx-ui = { group = \"androidx.compose.ui\", name = \"ui\" }\nandroidx-ui-graphics = { group = \"androidx.compose.ui\", name = \"ui-graphics\" }\nandroidx-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\nandroidx-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-ui-test-junit4 = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\" }\nandroidx-material3 = { group = \"androidx.compose.material3\", name = \"material3\" }\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\njetbrains-kotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\n

Illetve a modul szint\u0171 build.gradle.kts f\u00e1jlban:

dependencies {\n\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.activity.compose)\n    implementation(platform(libs.androidx.compose.bom))\n    implementation(libs.androidx.ui)\n    implementation(libs.androidx.ui.graphics)\n    implementation(libs.androidx.ui.tooling.preview)\n    implementation(libs.androidx.material3)\n    testImplementation(libs.junit)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.espresso.core)\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(libs.androidx.ui.test.junit4)\n    debugImplementation(libs.androidx.ui.tooling)\n    debugImplementation(libs.androidx.ui.test.manifest)\n}\n

A fenti f\u00fcgg\u0151s\u00e9gekhez 34-es SDK-val kell ford\u00edtanunk a projektet, ha a legener\u00e1lt alkalmaz\u00e1sban kor\u00e1bbi lenne megadva, akkor friss\u00edts\u00fck ezt is a modul szint\u0171 build.gradle.kts f\u00e1jlunkban:

    compileSdk = 34\n
"},{"location":"laborok/compose/#fuggoseg-felvetele","title":"F\u00fcgg\u0151s\u00e9g felv\u00e9tele","text":"

Az \u00e1ltalunk haszn\u00e1lni k\u00edv\u00e1nt ikonokhoz sz\u00fcks\u00e9g\u00fcnk van a Material Icons Extended modulra, valamint a navig\u00e1ci\u00f3hoz a Navigation Component-re.

Vegy\u00fck fel a sz\u00fcks\u00e9ges referenci\u00e1kat a libs.versions.toml f\u00e1jlba:

[versions]\nmaterialIconsExtended = \"1.7.2\"\nnavigationCompose = \"2.8.1\"\n...\n\n[libraries]\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\", version.ref=\"materialIconsExtended\"}\nandroidx-navigation-compose = { group = \"androidx.navigation\", name = \"navigation-compose\", version.ref = \"navigationCompose\" }\n...\n

Majd a f\u00fcgg\u0151s\u00e9get a modul szint\u0171 build.gradle.kts f\u00e1jlba:

implementation(libs.androidx.material.icons.extended)\nimplementation(libs.androidx.navigation.compose)\n...\n
"},{"location":"laborok/compose/#elemi-ui-epitoelemek-elkeszitese","title":"Elemi UI \u00e9p\u00edt\u0151elemek elk\u00e9sz\u00edt\u00e9se","text":"

A fenti k\u00e9peken l\u00e1that\u00f3, hogy a bejelentkeztet\u00e9si form egyedi kin\u00e9zet\u0171 sz\u00f6vegmez\u0151kb\u0151l \u00e9s c\u00edmk\u00e9kb\u0151l \u00e9p\u00fclnek fel. A Compose alapelve - ahogyan a neve is t\u00fckr\u00f6zi, - hogy a felhaszn\u00e1l\u00f3i fel\u00fclet\u00fcnket hierarchikusan \u00e9p\u00edthetj\u00fck fel, \u00e9s a kisebb \u00e9p\u00edt\u0151elemekb\u0151l \u00f6sszetettebbeket \u00e1ll\u00edthatunk \u00f6ssze. Ez egyr\u00e9szt seg\u00edti a fejleszt\u0151i gondolkod\u00e1st, hiszen k\u00f6nnyen tudunk a felhaszn\u00e1l\u00f3i fel\u00fclet adott r\u00e9sz\u00e9re koncentr\u00e1lni, ezeket f\u00fcggetlen\u00fcl elk\u00e9sz\u00edteni, \u00e9s \u00edgy id\u0151vel a r\u00e9szekb\u0151l m\u00e1r k\u00f6nnyen \u00f6sszerakhat\u00f3 lesz a teljes k\u00edv\u00e1nt UI is. M\u00e1sr\u00e9szt, ez a megk\u00f6zel\u00edt\u00e9s seg\u00edti az \u00fajrafelhaszn\u00e1l\u00e1st, hiszen a kisebb fel\u00fcleti elemek k\u00f6nnyen \u00fajrafelhaszn\u00e1lhat\u00f3k az alkalmaz\u00e1s k\u00fcl\u00f6nb\u00f6z\u0151 r\u00e9szeiben is.

K\u00e9sz\u00edts\u00fcnk el\u0151sz\u00f6r egy igen \u00e1ltal\u00e1nos sz\u00f6vegmez\u0151t, amelyet majd az \u00e9ppen aktu\u00e1lis ig\u00e9nyeknek megfelel\u0151en gazdagon tudunk param\u00e9terezni. Tulajdonk\u00e9ppen a rendszer r\u00e9sz\u00e9t k\u00e9pez\u0151 TextField is sokr\u00e9t\u0171 funkcionalit\u00e1ssal rendelkezik, azonban szeretn\u00e9nk egy magasabb szint\u0171 komponenst, amely sz\u00e1munkra k\u00f6nnyebben haszn\u00e1lhat\u00f3, \u00e9s a hibajelz\u00e9s megjelen\u00edt\u00e9s\u00e9t is megoldja.

El\u0151sz\u00f6r hozzunk l\u00e9tre ehhez egy hu.bme.aut.android.composebasics.ui.common package-et. Ebbe fognak ker\u00fclni az alapvet\u0151 fontoss\u00e1g\u00fa UI \u00e9p\u00edt\u0151elemeink.

Ezen bel\u00fcl k\u00e9sz\u00edts\u00fcnk egy NormalTextField komponenst a k\u00f6vetkez\u0151 tartalommal:

package hu.bme.aut.android.composebasics.ui.common\n\n@ExperimentalMaterial3Api\n@Composable\nfun NormalTextField(\nmodifier: Modifier = Modifier,\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nenabled: Boolean = true,\nreadOnly: Boolean = false,\nisError: Boolean = false,\nonDone: (KeyboardActionScope.() -> Unit)?,\nleadingIcon: @Composable (() -> Unit)?,\ntrailingIcon: @Composable (() -> Unit)?\n) {\nOutlinedTextField(\nvalue = value.trim(),\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = if (isError) {\n{\nIcon(\nimageVector = Icons.Default.ErrorOutline,\ncontentDescription = null\n)\n}\n} else {\n{\nif (trailingIcon != null) {\ntrailingIcon()\n}\n}\n},\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nsingleLine = true,\nreadOnly = readOnly,\nisError = isError,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Text,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n)\n)\n}\n

A Kotlin nyelv megengedi, hogy a f\u00fcggv\u00e9nyparam\u00e9tereket f\u00fcggv\u00e9nyh\u00edv\u00e1skor neves\u00edtve adjuk meg, \u00edgy a param\u00e9terek sorrendje v\u00e1ltozhat, mivel a n\u00e9v alapj\u00e1n a ford\u00edt\u00f3 \u00f6ssze tudja kapcsolni a param\u00e9tereket a megadott \u00e9rt\u00e9kekkel. Egy m\u00e1sik hasznos tulajdons\u00e1ga a Kotlin nyelvnek, hogy a param\u00e9tereknek alap\u00e9rtelmezett (default) \u00e9rt\u00e9k adhat\u00f3 meg a f\u00fcggv\u00e9nydefin\u00edci\u00f3ban, \u00e9s ezzel elker\u00fclhetj\u00fck, hogy egy f\u00fcggv\u00e9nynek sok overloadolt v\u00e1ltozat\u00e1t kelljen elk\u00e9sz\u00edten\u00fcnk. A k\u00e9t funkci\u00f3t kombin\u00e1lva nagyon rugalmasan tudjuk az \u00edgy defini\u00e1lt f\u00fcggv\u00e9nyeket h\u00edvni, \u00e9s ezt a Compose technol\u00f3gia remek\u00fcl kihaszn\u00e1lja.

Tekints\u00fck \u00e1t a fenti k\u00f3dot! A komponens a konstruktoron kereszt\u00fcl sz\u00e1mos param\u00e9tert \u00e1t tud venni:

  • value: a sz\u00f6vegmez\u0151 tartalma; ezt egyszer\u0171en tov\u00e1bbadjuk a felhaszn\u00e1lt OutlinedTextField komponensnek, de az eleji/v\u00e9gi whitespace karaktereket a trim() seg\u00edts\u00e9g\u00e9vel lev\u00e1gjuk
  • label: a sz\u00f6vegmez\u0151 c\u00edmk\u00e9je, amely magyar\u00e1zza annak tartalm\u00e1t; ezt egy Text composable-be csomagolva tov\u00e1bbadjuk
  • onValueChange: esem\u00e9nykezel\u0151, amely a tartalom megv\u00e1ltoztat\u00e1sakor h\u00edv\u00f3dik; egyszer\u0171en tov\u00e1bbadjuk
  • leadingIcon \u00e9s traliningIcon: a sz\u00f6vegmez\u0151 elej\u00e9n \u00e9s v\u00e9g\u00e9n megjelen\u00edtend\u0151 ikonok, amelyeket egy \u00fajabb composable f\u00fcggv\u00e9nyk\u00e9nt lehet megadni; a komponens\u00fcnk be\u00e9p\u00edtett hibajelz\u00e9st val\u00f3s\u00edt meg, ez\u00e9rt ha hiba van be\u00e1ll\u00edtva, akkor a sz\u00f6veg v\u00e9g\u00e9n nem a be\u00e1ll\u00edtott ikon, hanem hibajelz\u00e9s jelenik meg
  • modifier: a megjelen\u00e9st m\u00f3dos\u00edt\u00f3 param\u00e9terek; itt tov\u00e1bbadjuk a megadottakat, \u00e9s m\u00e9g hozz\u00e1adjuk, hogy a t\u00e9ma szerinti minim\u00e1lis sz\u00e9less\u00e9g l\u00e9pjen \u00e9rv\u00e9nyre
  • enabled: enged\u00e9lyezve van-e a sz\u00f6vegmez\u0151?
  • readOnly: csak olvashat\u00f3-e a sz\u00f6vegmez\u0151?
  • isError: ha a sz\u00f6vegmez\u0151 tartalma nem \u00e9rv\u00e9nyes, akkor be\u00e1ll\u00edthatjuk true \u00e9rt\u00e9kre, \u00e9s a sz\u00f6vegmez\u0151 v\u00e9g\u00e9n egy hibajelz\u0151 ikon fog megjelenni.
  • onDone: esem\u00e9nykezel\u0151, hogy mi t\u00f6rt\u00e9njen, ha a szerkeszt\u00e9st a felhaszn\u00e1l\u00f3 befejezte

A modifier \u00e9rt\u00e9kek\u00e9nt a komponens felhaszn\u00e1l\u00e1sakor nagyon sok param\u00e9ter megadhat\u00f3. Erre sz\u00e1mos p\u00e9ld\u00e1t l\u00e1thatunk az Android hivatalos dokument\u00e1ci\u00f3j\u00e1ban: https://developer.android.com/jetpack/compose/modifiers

A felhaszn\u00e1lt OutlinedTextField komponensen tov\u00e1bbi jellemz\u0151ket is be\u00e1ll\u00edtottunk, amelyeket egy\u00e9bk\u00e9nt a NormalTextField nem tud k\u00edv\u00fclr\u0151l fel\u00fclb\u00edr\u00e1lhat\u00f3v\u00e1 tenni. Ezek jelent\u00e9se:

  • singleLine: csak egy sort lehet beg\u00e9pelni a sz\u00f6vegmez\u0151be
  • keyboardOptions: ez \u00e1ll\u00edtja be, hogy milyen jelleg\u0171 billenty\u0171zet jelenjen meg a k\u00e9perny\u0151n, \u00e9s milyen IME gyorsgomb tartozzon a szerkeszt\u0151h\u00f6z. Itt mindig egyszer\u0171 sz\u00f6veges billenty\u0171zetet \u00e9s \"k\u00e9sz\" gombot v\u00e1lasztunk. Ha emailt vagy telefonsz\u00e1mot g\u00e9peltetn\u00e9nk be, akkor megjelen\u00edthet\u00fcnk ehhez alkalmasabb billenty\u0171zetet is.
  • keyboardActions: mi t\u00f6rt\u00e9njen az egyes IME akci\u00f3k kiv\u00e1lt\u00e1sakor. Itt csak a kor\u00e1bban megadott onDone esem\u00e9nykezel\u0151t h\u00edvjuk meg.

Ezzel elk\u00e9sz\u00fclt az els\u0151 composable komponens\u00fcnk, de mivel m\u00e9g sok hi\u00e1nyzik a felhaszn\u00e1l\u00f3i fel\u00fcletb\u0151l, ez\u00e9rt ezt csak sok\u00e1 tudn\u00e1nk val\u00f3j\u00e1ban kipr\u00f3b\u00e1lni. Szerencs\u00e9re a Compose technol\u00f3gia lehet\u0151s\u00e9get ad r\u00e1, hogy fejleszt\u00e9s k\u00f6zben is pontos el\u0151n\u00e9zetet kapjunk a komponenseinkb\u0151l. Ezt c\u00e9lszer\u0171en \u00fagy tessz\u00fck meg, hogy defini\u00e1lunk egy el\u0151n\u00e9zeti f\u00fcggv\u00e9nyt, amely a k\u00edv\u00e1nt param\u00e9terez\u00e9ssel megh\u00edvja a composable f\u00fcggv\u00e9ny\u00fcnket, majd erre a f\u00fcggv\u00e9nyre is r\u00e1tessz\u00fck a @Composable \u00e9s az @ExperimentalMaterial3Api annot\u00e1ci\u00f3kat, illetve az el\u0151n\u00e9zet gener\u00e1l\u00e1s\u00e1\u00e9rt felel\u0151s @Preview annot\u00e1ci\u00f3t is. Pr\u00f3b\u00e1ljuk ki a komponens\u00fcnket az al\u00e1bbi tesztf\u00fcggv\u00e9nnyel, amit betehet\u00fcnk a NormalTextField f\u00e1jlj\u00e1ba:

@ExperimentalMaterial3Api\n@Preview\n@Composable\nfun NormalTextViewPreview() {\nNormalTextField(\nvalue = \"Csetneki P\u00e9ter\",\nlabel = \"N\u00e9v\",\nonValueChange = {},\nleadingIcon = {},\ntrailingIcon = {},\nonDone = {}\n)\n}\n

Ne feledj\u00fck, hogy a Preview csak egy build ut\u00e1n tekinthet\u0151 meg.

El\u0151n\u00e9zeti f\u00fcggv\u00e9nyb\u0151l t\u00f6bbet is l\u00e9trehozhatunk, hogy l\u00e1ssuk, hogyan n\u00e9z ki a komponens\u00fcnk k\u00fcl\u00f6nb\u00f6z\u0151 param\u00e9terez\u00e9sek eset\u00e9n. Vizsg\u00e1ljuk meg a hibajelz\u00e9ssel ell\u00e1tott megjelen\u00e9st is:

@ExperimentalMaterial3Api\n@Preview\n@Composable\nfun NormalTextViewErrorPreview() {\nNormalTextField(\nvalue = \"abc\",\nlabel = \"Mennyis\u00e9g (kg)\",\nonValueChange = {},\nleadingIcon = {},\ntrailingIcon = {},\nonDone = {},\nisError = true\n)\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a k\u00e9t el\u0151n\u00e9zet a sz\u00f6vegmez\u0151 komponensr\u0151l \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet. A n\u00e9v mez\u0151be a saj\u00e1t neved ker\u00fclj\u00f6n.

A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A fentihez hasonl\u00f3an a ui.common package-be k\u00e9sz\u00edts\u00fcnk egy \u00fajabb komponenst PasswordTextField n\u00e9ven az al\u00e1bbi tartalommal:

package hu.bme.aut.android.composebasics.ui.common\n\n@ExperimentalMaterial3Api\n@Composable\nfun PasswordTextField(\nmodifier: Modifier = Modifier,\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nenabled: Boolean = true,\nreadOnly: Boolean = false,\nisError: Boolean = false,\nonDone: (KeyboardActionScope.() -> Unit)?,\nleadingIcon: @Composable (() -> Unit)?,\nisVisible: Boolean = true,\nonVisibilityChanged: () -> Unit,\n) {\nval visibilityIcon = if (isVisible) {\nIcons.Rounded.VisibilityOff\n} else {\nIcons.Rounded.Visibility\n}\nOutlinedTextField(\nvalue = value.trim(),\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = if (isError) {\n{\nIcon(\nimageVector = Icons.Default.ErrorOutline,\ncontentDescription = null\n)\n}\n} else {\n{\nIconButton(onClick = onVisibilityChanged) {\nIcon(imageVector = visibilityIcon, contentDescription = null)\n}\n}\n},\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nsingleLine = true,\nreadOnly = readOnly,\nisError = isError,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Password,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n),\nvisualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(),\n)\n}\n

Ez a komponens csak k\u00e9t apr\u00f3 dologban t\u00e9r el az el\u0151z\u0151t\u0151l:

  1. Mivel jelszavak beg\u00e9pel\u00e9s\u00e9hez haszn\u00e1ljuk, a jelsz\u00f3 kitakar\u00e1sa vagy mutat\u00e1sa is \u00e1ll\u00edthat\u00f3 a komponensben. Ezt \u00fagy val\u00f3s\u00edtjuk meg, hogy nem lehet k\u00fcl\u00f6n ikont megadni a sz\u00f6vegmez\u0151 v\u00e9g\u00e9hez, hanem ott egy csukott vagy nyitott szem jelenik meg, \u00e9s az erre t\u00f6rt\u00e9n\u0151 kattint\u00e1ssal lehet a l\u00e1that\u00f3s\u00e1got \u00e1ll\u00edtani. A l\u00e1that\u00f3s\u00e1g \u00e1llapota \u00e9s az esem\u00e9nykezel\u0151 param\u00e9terekk\u00e9nt vannak megadva, teh\u00e1t a l\u00e1that\u00f3s\u00e1g \u00e1llapot\u00e1t \u00e9s az esem\u00e9nykezel\u0151t a komponens bennfoglal\u00f3 komponens\u00e9ben kell megval\u00f3s\u00edtani.

  2. A komponensnek a l\u00e1that\u00f3s\u00e1g \u00e1llapot\u00e1t\u00f3l f\u00fcgg\u0151en egy vizu\u00e1lis transzform\u00e1ci\u00f3 is be van \u00e1ll\u00edtva, hogy a tartalm\u00e1t ne k\u00f6zvetlen, hanem kitakartan jelen\u00edtse meg.

"},{"location":"laborok/compose/#az-alkalmazas-fo-kepernyoinek-elkeszitese","title":"Az alkalmaz\u00e1s f\u0151 k\u00e9perny\u0151inek elk\u00e9sz\u00edt\u00e9se","text":"

Most, hogy a k\u00e9perny\u0151k minden fontos alkot\u00f3r\u00e9sze a rendelkez\u00e9s\u00fcnkre \u00e1ll, elkezdhetj\u00fck maguknak a k\u00e9perny\u0151knek az elk\u00e9sz\u00edt\u00e9s\u00e9t. Kezdj\u00fck a bejelentkez\u0151 k\u00e9perny\u0151vel!

A k\u00e9perny\u0151knek \u00e9s a hozz\u00e1juk kapcsol\u00f3d\u00f3 k\u00f3doknak hozzunk l\u00e9tre egy k\u00f6z\u00f6s hu.bme.aut.android.composebasics.feature package-et, majd ezen bel\u00fcl a bejelentkez\u0151 k\u00e9perny\u0151 a login package-be ker\u00fclj\u00f6n! K\u00e9sz\u00edts\u00fck el a k\u00e9perny\u0151 k\u00f3dj\u00e1t LoginScreen n\u00e9ven, majd adjuk meg a k\u00f6vetkez\u0151 k\u00f3dot:

package hu.bme.aut.android.composebasics.feature.login\n\n@ExperimentalMaterial3Api\n@Composable\nfun LoginScreen(\nmodifier: Modifier = Modifier,\nonLoginClick: (String) -> Unit\n) {\nvar usernameValue by remember { mutableStateOf(\"\") }\nvar isUsernameError by remember { mutableStateOf(false) }\n\nvar passwordValue by remember { mutableStateOf(\"\") }\nvar isPasswordVisible by remember { mutableStateOf(false) }\nvar isPasswordError by remember { mutableStateOf(false) }\n\nBox(\nmodifier = modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\ncontentAlignment = Alignment.Center\n) {\nColumn(horizontalAlignment = Alignment.CenterHorizontally) {\nNormalTextField(\nvalue = usernameValue,\nlabel = stringResource(id = R.string.textfield_label_username),\nonValueChange = { newValue ->\nusernameValue = newValue\nisUsernameError = false\n},\nisError = isUsernameError,\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Person,\ncontentDescription = null\n)\n},\ntrailingIcon = { },\nonDone = { }\n)\nSpacer(modifier = Modifier.height(16.dp))\nPasswordTextField(\nvalue = passwordValue,\nlabel = stringResource(id = R.string.textfield_label_password),\nonValueChange = { newValue ->\npasswordValue = newValue\nisPasswordError = false\n},\nisError = isPasswordError,\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Key,\ncontentDescription = null\n)\n},\nisVisible = isPasswordVisible,\nonVisibilityChanged = { isPasswordVisible = !isPasswordVisible },\nonDone = { }\n)\nSpacer(modifier = Modifier.height(16.dp))\nButton(\nonClick = {\nif (usernameValue.isEmpty()) {\nisUsernameError = true\n} else if (passwordValue.isEmpty()) {\nisPasswordError = true\n} else {\nonLoginClick(usernameValue)\n}\n},\nmodifier = Modifier.width(TextFieldDefaults.MinWidth),\nshape = RectangleShape\n) {\nText(text = stringResource(id = R.string.button_label_login))\n}\n}\n}\n}\n

Egy fontos eddig nem l\u00e1tott elem, hogy a felhaszn\u00e1l\u00f3i fel\u00fclet elemeinek \u00e1llapott\u00e1rol\u00e1s\u00e1ra (pl. sz\u00f6vegmez\u0151 tartalma, l\u00e1that\u00f3-e valami, jel\u00f6l\u0151n\u00e9gyzet be van pip\u00e1lva stb.) MutableState t\u00edpus\u00fa t\u00e1rol\u00f3kat kell l\u00e9trehoznunk. Ezt a mutableStateOf() factory-met\u00f3dussal tudjuk megtenni, \u00e9s ennek meg kell adni a kezd\u0151\u00e1llapotot. Mindezt az inicializ\u00e1ci\u00f3t lazy bet\u00f6lt\u00e9ssel akarjuk v\u00e9gezni, hogy a fel\u00fclet fel\u00e9p\u00edt\u00e9se k\u00f6zben t\u00f6rt\u00e9njen. Ehhez haszn\u00e1ljuk a remember kulcssz\u00f3t.

Felt\u0171nnek m\u00e9g k\u00fcl\u00f6nb\u00f6z\u0151 kont\u00e9nerelemek, amelyek seg\u00edts\u00e9g\u00e9vel a fel\u00fcleti elemek elrendez\u00e9s\u00e9t tudjuk meghat\u00e1rozni. Ilyen a kor\u00e1bban m\u00e1r \u00e9rintett Box. Ez alkalmas a teljes k\u00e9perny\u0151tartalmak befoglal\u00e1s\u00e1ra. Ezzel \u00e1ll\u00edtjuk be a h\u00e1tteret a Material t\u00e9m\u00e1nk szerintire, illetve hogy a k\u00e9perny\u0151 teljes eg\u00e9sz\u00e9t t\u00f6ltse ki a befoglalt tartalom. Ezen bel\u00fcl l\u00e1tunk egy Column elemet, amellyel egy oszlopba vannak rendezve egym\u00e1s al\u00e1 a sz\u00f6vegmez\u0151k \u00e9s a bejelentkez\u0151 gomb. A v\u00edzszintes igaz\u00edt\u00e1s az oszlopon k\u00f6z\u00e9pre van \u00e1ll\u00edtva. A norm\u00e1l \u00e9s a jelszavas saj\u00e1t sz\u00f6vegmez\u0151k, valamint a bejelentkeztet\u0151 gomb k\u00f6z\u00f6tt t\u00e9relv\u00e1laszt\u00f3 Spacer komponenseket tal\u00e1lunk.

\u00d6sszess\u00e9g\u00e9ben azt figyelhetj\u00fck meg, hogy a logika egy r\u00e9sze m\u00e1r itt fel van oldva, hiszen az \u00e1llapot egyes r\u00e9szeit itt kezelj\u00fck, \u00e9s ehhez kapcsol\u00f3d\u00f3an esem\u00e9nykezel\u0151ket is adunk tov\u00e1bb az \u00e9p\u00edt\u0151elemk\u00e9nt szolg\u00e1l\u00f3 kisebb komponenseknek. Viszont vannak olyan dolgok, mint pl. a login gomb esem\u00e9nykezel\u0151je, amelyek m\u00e9g mindig fel\u00fclr\u0151l j\u00f6nnek. Alapvet\u0151en a Compose-ban \u00fagy kell gondolkodnunk, hogy az \u00e1llapotot, amire t\u00f6bb fel\u00fcleti elemnek sz\u00fcks\u00e9ge van, azt feljebb kell emeln\u00fcnk egy k\u00f6z\u00f6s \u0151sbe. Ezt az Android terminol\u00f3gia \u00fagy h\u00edvja, hogy state hoisting Pl. a beg\u00e9pelt felhaszn\u00e1l\u00f3nevet a sz\u00f6vegmez\u0151 is haszn\u00e1lja, illetve a befoglal\u00f3 bejelentkez\u0151 k\u00e9perny\u0151n\u00e9l is sz\u00fcks\u00e9g van r\u00e1. Maga a bejelentkez\u0151 k\u00e9perny\u0151 a legfels\u0151 komponens a hierarchi\u00e1ban, amelyik haszn\u00e1lja, ez\u00e9rt itt tudjuk ezt az \u00e1llapotot kezelni. A navig\u00e1ci\u00f3 viszont, hogy mi t\u00f6rt\u00e9njen a gombokra kattint\u00e1skor, az m\u00e1r m\u00e1s komponenseket is \u00e9rint, ez\u00e9rt azt fentebbi szinten kell kezelni, ez\u00e9rt ez m\u00e9g mindig param\u00e9terk\u00e9nt \u00e9rkezik a k\u00e9perny\u0151t megtestes\u00edt\u0151 komponenshez.

Aki fejlesztett m\u00e1r a React webes keretrendszerben, annak ismer\u0151s lehet ez a koncepci\u00f3, mert nagyon hasonl\u00f3 a React komponensek m\u0171k\u00f6d\u00e9s\u00e9hez.

N\u00e9zz\u00fck is meg az elk\u00e9sz\u00fclt komponenst:

@ExperimentalMaterial3Api\n@Preview(showBackground = true)\n@Composable\nfun LoginScreenPreview() {\nLoginScreen(\nonLoginClick = { }\n)\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az el\u0151n\u00e9zet a bejelentkez\u0151 k\u00e9perny\u0151r\u0151l \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A m\u00e1sodik elk\u00e9sz\u00edtend\u0151 k\u00e9perny\u0151nk az alkalmaz\u00e1s \"f\u0151k\u00e9perny\u0151je\", amit sikeres bejelentkez\u00e9s ut\u00e1n l\u00e1t a felhaszn\u00e1l\u00f3. Viszont itt m\u00e1r r\u00e9szben \u00e9rinten\u00fcnk kell a k\u00e9perny\u0151k k\u00f6zti navig\u00e1ci\u00f3 k\u00e9rd\u00e9s\u00e9t is, hiszen a k\u00e9perny\u0151nek lesz egy men\u00fcje, ahonnan majd m\u00e1s k\u00e9perny\u0151kre lehet navig\u00e1lni. Ehhez egy navigation package-et hozzunk l\u00e9tre, \u00e9s ebbe ker\u00fclj\u00f6n az al\u00e1bbi Screen oszt\u00e1ly. Itt sealed class-t alkalmazunk a lehets\u00e9ges k\u00e9perny\u0151k le\u00edr\u00e1s\u00e1ra, mert csak el\u0151re megadott sz\u00e1m\u00fa k\u00e9perny\u0151nk van, \u00e9s a f\u0151k\u00e9perny\u0151 argumentumot is kaphat. A sealed class kicsit hasonl\u00edt az enumhoz, de t\u00e1mogatja ezt a fontos k\u00fcl\u00f6nbs\u00e9get is. Az oszt\u00e1ly el\u0151tt defini\u00e1lt konstansokat k\u00e9s\u0151bb fogjuk haszn\u00e1lni, amikor teljesen \u00f6sszerakjuk a navig\u00e1ci\u00f3s gr\u00e1fot.

package hu.bme.aut.android.composebasics.navigation\n\nconst val ROOT_GRAPH_ROUTE = \"root\"\nconst val AUTH_GRAPH_ROUTE = \"auth\"\nconst val MAIN_GRAPH_ROUTE = \"main\"\n\nsealed class Screen(val route: String) {\nobject Login: Screen(route = \"login\")\nobject Home: Screen(route = \"home/{${Args.username}}\") {\nfun passUsername(username: String) = \"home/$username\"\nobject Args {\nconst val username = \"username\"\n}\n}\nobject Profile: Screen(route = \"profile\")\nobject Settings: Screen(route = \"settings\")\n}\n

sealed class

A Kotlin sealed class-ai olyan oszt\u00e1lyok, amelyekb\u0151l korl\u00e1tozott az \u00f6r\u00f6kl\u00e9s, \u00e9s ford\u00edt\u00e1si id\u0151ben minden lesz\u00e1rmazott oszt\u00e1lya ismert. Ezeket az oszt\u00e1lyokat az enumokhoz hasonl\u00f3 m\u00f3don tudjuk alkalmazni. Jelen esetben a Home val\u00f3j\u00e1ban nem a Screen k\u00f6zvetlen lesz\u00e1rmazottja, hanem anonim lesz\u00e1rmazott oszt\u00e1lya, mivel a felhaszn\u00e1l\u00f3n\u00e9v param\u00e9terk\u00e9nt t\u00f6rt\u00e9n\u0151 kezel\u00e9s\u00e9t is tartalmazza.

Maga a f\u0151k\u00e9perny\u0151 egy feature.home subpackage-be ker\u00fclj\u00f6n. El\u0151sz\u00f6r itt is egy seg\u00e9doszt\u00e1lyt hozunk l\u00e9tre. Jelen esetben a men\u00fcpontokat fogjuk enumban modellezni. Minden men\u00fcpontra jellemz\u0151 a neve, az ikonja, illetve egy azonos\u00edt\u00f3, ahova navig\u00e1l:

package hu.bme.aut.android.composebasics.feature.home\n\nenum class MenuItemUiModel(\nval text: @Composable () -> Unit,\nval icon: @Composable () -> Unit,\nval screenRoute: String\n) {\nPROFILE(\ntext = { Text(text = stringResource(id = R.string.dropdown_menu_item_label_profile))},\nicon = {\nIcon(imageVector = Icons.Default.Person, contentDescription = null)\n},\nscreenRoute = Screen.Profile.route\n),\nSETTINGS(\ntext = { Text(text = stringResource(id = R.string.dropdown_menu_item_label_settings))},\nicon = {\nIcon(imageVector = Icons.Default.Settings, contentDescription = null)\n},\nscreenRoute = Screen.Settings.route\n)\n}\n

A men\u00fcben szerepelnek profil \u00e9s be\u00e1ll\u00edt\u00e1s lehet\u0151s\u00e9gek is, amelyekr\u0151l kor\u00e1bban nem volt sz\u00f3. Ezek nem lesznek igazi kidolgozott k\u00e9perny\u0151k, de p\u00e9ldak\u00e9pp szerepelnek itt, hogy bemutassuk, hogyan lehetne a f\u0151men\u00fcb\u0151l tov\u00e1bbi oldalakra is elnavig\u00e1lni. L\u00e1that\u00f3, hogy itt a men\u00fcpontokn\u00e1l meghivatkoztuk a kor\u00e1bban a Screen oszt\u00e1lyban defini\u00e1lt k\u00e9perny\u0151ket is. A le\u00edrt men\u00fcpontokb\u00f3l m\u00e9g fel kell \u00e9p\u00edten\u00fcnk a men\u00fct is. Elvileg ezt megtehetn\u00e9nk a teljes f\u0151k\u00e9perny\u0151 r\u00e9szek\u00e9nt, de \u00e1tl\u00e1that\u00f3bb strukt\u00far\u00e1t kapunk, ha ezt k\u00fcl\u00f6n composable komponensbe szervezz\u00fck. Ahogyan \u00e1ltal\u00e1ban v\u00e9ve a met\u00f3dusokn\u00e1l sem \u00e1tl\u00e1that\u00f3 a t\u00fal hossz\u00fa, \u00fagy a fel\u00fcleti komponenseinket is \u00e9rdemes kisebb, jobban kezelhet\u0151 egys\u00e9gekre osztani. K\u00e9sz\u00edts\u00fcnk teh\u00e1t egy Menu komponenst:

package hu.bme.aut.android.composebasics.feature.home\n\n@Composable\nfun Menu(\nexpanded: Boolean,\nitems: Array<MenuItemUiModel>,\nonDismissRequest: () -> Unit,\nonClick: (String) -> Unit,\nmodifier: Modifier = Modifier\n) {\nDropdownMenu(\nmodifier = modifier.padding(5.dp),\nexpanded = expanded,\nonDismissRequest = onDismissRequest\n) {\nitems.forEachIndexed { index, item ->\nDropdownMenuItem(\ntext = item.text,\nleadingIcon = item.icon,\nonClick = { onClick(item.screenRoute) },\nmodifier = Modifier.clip(RoundedCornerShape(5.dp))\n)\nif (index != items.lastIndex) {\nHorizontalDivider(modifier = Modifier\n.height(10.dp)\n.padding(vertical = 5.dp))\n}\n}\n}\n}\n

L\u00e1tjuk, hogy a men\u00fcelemek l\u00e1trehoz\u00e1sa is ciklussal t\u00f6rt\u00e9nik, \u00e9s a men\u00fcpontok igen k\u00f6nnyen b\u0151v\u00edthet\u0151ek. A bej\u00e1r\u00e1sn\u00e1l a men\u00fcpontok index\u00e9t is felhaszn\u00e1ljuk, hogy a men\u00fcpontok ut\u00e1n - az utols\u00f3 kiv\u00e9tel\u00e9vel - elv\u00e1laszt\u00f3t is gener\u00e1ljunk.

Most r\u00e1t\u00e9rhet\u00fcnk a t\u00e9nyleges f\u0151k\u00e9perny\u0151 l\u00e9trehoz\u00e1s\u00e1ra:

package hu.bme.aut.android.composebasics.feature.home\n\n@ExperimentalMaterial3Api\n@Composable\nfun HomeScreen(\nargument: String,\nmodifier: Modifier = Modifier,\nonLogout: () -> Unit,\nonMenuItemClick: (String) -> Unit\n) {\n\nval snackbarHostState = remember { SnackbarHostState() }\n\nvar expandedMenu by remember { mutableStateOf(false) }\n\nval scope = rememberCoroutineScope()\n\nval context = LocalContext.current\n\nScaffold(\nsnackbarHost = { SnackbarHost(snackbarHostState) },\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = stringResource(id = R.string.top_app_bar_title_home))\n},\nactions = {\nIconButton(onClick = onLogout) {\nIcon(\nimageVector = Icons.AutoMirrored.Filled.Logout,\ncontentDescription = null\n)\n}\nIconButton(onClick = { expandedMenu = !expandedMenu }) {\nIcon(imageVector = Icons.Default.MoreVert, contentDescription = null)\n}\nMenu(\nexpanded = expandedMenu,\nitems = MenuItemUiModel.entries.toTypedArray(),\nonDismissRequest = { expandedMenu = false },\nonClick = {\nonMenuItemClick(it)\nexpandedMenu = false\n},\n)\n}\n)\n},\nfloatingActionButton = {\nFloatingActionButton(onClick = {\nscope.launch {\nsnackbarHostState.showSnackbar(message = context.getString(R.string.snackbar_message_this_is_a))\n}\n}) {\nIcon(imageVector = Icons.Default.Add, contentDescription = null)\n}\n},\nmodifier = modifier\n) {\nBox(\nmodifier = Modifier\n.padding(it)\n.fillMaxSize(),\n) {\nText(\ntext = \"Hello, $argument!\",\ntextAlign = TextAlign.Center,\nmodifier = Modifier.align(Alignment.Center)\n)\n}\n}\n}\n

A k\u00e9perny\u0151n t\u00f6bb \u00fajdons\u00e1got is felfedezhet\u00fcnk:

  1. A Scaffold elem szolg\u00e1l komplexebb Material st\u00edlus\u00fa k\u00e9perny\u0151k fel\u00e9p\u00edt\u00e9s\u00e9re. A param\u00e9terez\u00e9s\u00e9b\u0151l l\u00e1that\u00f3, hogy ez az elem be\u00e9p\u00edtetten t\u00e1mogat t\u00f6bb gyakran megszokott k\u00e9perny\u0151elemet, mint a SnackBar, TopBar vagy a FloatingActionButton. Ezeket a param\u00e9terez\u00e9ssel adjuk meg neki, \u00e9s gondoskodik a megfelel\u0151 elrendez\u00e9sr\u0151l.

  2. A k\u00e9perny\u0151n SnackBar is lesz, \u00e9s ennek az \u00e1llapot\u00e1t nem MutableState, hanem SnackbarHostState t\u00edpusk\u00e9nt tudjuk l\u00e9trehozni.

  3. A SnackBar \u00fczenetek megjelen\u00edt\u00e9s\u00e9t coroutine fogja v\u00e9gezni, \u00e9s ehhez scope-ot Compose k\u00f6rnyezetben a rememberCoroutineScope() f\u00fcggv\u00e9nnyel tudunk k\u00e9rni.

  4. A LocalContext.current kifejez\u00e9ssel kaphatunk egy kontextust Compose k\u00f6rnyezetben, amellyel a rendszerszint\u0171 er\u0151forr\u00e1sokhoz - pl. a sz\u00f6veges c\u00edmk\u00e9khez - hozz\u00e1f\u00e9rhet\u00fcnk.

A k\u00e9perny\u0151 t\u00f6bbi r\u00e9sze a kor\u00e1bbi p\u00e9ld\u00e1k alapj\u00e1n m\u00e1r k\u00f6nnyen \u00e9rthet\u0151.

N\u00e9zz\u00fck meg, hogyan fest az elk\u00e9sz\u00edtett f\u0151k\u00e9perny\u0151:

@ExperimentalMaterial3Api\n@Preview(showBackground = true)\n@Composable\nfun HomeScreenPreview() {\nHomeScreen(\nargument = \"Felhaszn\u00e1l\u00f3\",\nonLogout = {},\nonMenuItemClick = {}\n)\n}\n
"},{"location":"laborok/compose/#a-kepernyok-kozotti-navigacio-elkeszitese","title":"A k\u00e9perny\u0151k k\u00f6z\u00f6tti navig\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se","text":"

Most m\u00e1r csak \u00f6ssze kell k\u00f6tn\u00fcnk a megl\u00e9v\u0151 k\u00e9perny\u0151ket a navig\u00e1ci\u00f3s szab\u00e1lyokkal. Ehhez navig\u00e1ci\u00f3s gr\u00e1fokat fogunk defini\u00e1lni. Egyr\u00e9szt defini\u00e1lunk egy gr\u00e1fot az authentik\u00e1ci\u00f3 el\u0151tti k\u00e9perny\u0151kre, itt most csak a bejelentkez\u00e9s lesz, de egy val\u00f3s alkalmaz\u00e1sban lehetne pl. egy regisztr\u00e1ci\u00f3s k\u00e9perny\u0151nk is. Ezeket a kor\u00e1bban l\u00e9trehozott navigation package-be tegy\u00fck. Az authentik\u00e1ci\u00f3 el\u0151tti gr\u00e1f a k\u00f6vetkez\u0151k\u00e9ppen n\u00e9z ki:

package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\nfun NavGraphBuilder.authNavGraph(\nnavController: NavHostController\n) {\nnavigation(\nstartDestination = Screen.Login.route,\nroute = AUTH_GRAPH_ROUTE\n) {\ncomposable(\nroute = Screen.Login.route\n) {\nLoginScreen(\nonLoginClick = {\nnavController.navigate(Screen.Home.passUsername(it))\n}\n)\n}\n}\n}\n

A k\u00f3db\u00f3l azt tudjuk meg\u00e1llap\u00edtani, hogy a navig\u00e1ci\u00f3s gr\u00e1f a bejelentkeztet\u00e9si k\u00e9perny\u0151n kezd\u0151dik, \u00e9s neki is van egy \u00fatvonalazonos\u00edt\u00f3ja, amelyet most a kor\u00e1bban defini\u00e1lt AUTH_GRAPH_ROUTE konstanssal adtunk meg. A navig\u00e1ci\u00f3ban composable fel\u00fcleti elemeket adhatunk meg: mindegyikhez tartozik egy-egy \u00fatvonal, ezekhez a Screen oszt\u00e1lyb\u00f3l hivatkozzuk meg a megfelel\u0151 \u00fatvonalat. L\u00e1that\u00f3, hogy a hierarchikusan \u00f6ssze\u00e1ll\u00edtott felhaszn\u00e1l\u00f3i fel\u00fcletek \"utols\u00f3\" param\u00e9terei itt kapnak konkr\u00e9t \u00e9rt\u00e9tet. Konkr\u00e9tan a bejelentkez\u00e9s gomb esem\u00e9nykezel\u0151je van itt lambda-kifejez\u00e9sk\u00e9nt megadva. Ez a lambda-kifejez\u00e9s val\u00f3j\u00e1ban a navig\u00e1ci\u00f3s kontrollert h\u00edvja meg, \u00e9s azzal navig\u00e1ltat a megfelel\u0151 \u00fatvonalra, amit a kontroller a navig\u00e1ci\u00f3s gr\u00e1f alapj\u00e1n felold. Figyelj\u00fck meg, hogy a bejelentkez\u00e9s ut\u00e1n a f\u0151k\u00e9perny\u0151 \u00fatvonal\u00e1ba a felhaszn\u00e1l\u00f3nevet mint param\u00e9tert is belek\u00f3doljuk. Azt is l\u00e1thatjuk, hogy t\u00e9nyleges bejelentkeztet\u0151 logika itt nem t\u00f6rt\u00e9nik, de ha erre lenne sz\u00fcks\u00e9g\u00fcnk, azt itt megtehetn\u00e9nk, hiszen itt van megadva a bejelentkez\u00e9s gomb esem\u00e9nykezel\u0151je.

A m\u00e1sik navig\u00e1ci\u00f3s gr\u00e1f a bejelentkez\u00e9s ut\u00e1ni navig\u00e1ci\u00f3t \u00edrja le:

package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\nfun NavGraphBuilder.mainNavGraph(\nnavController: NavHostController\n) {\nnavigation(\nstartDestination = Screen.Home.route,\nroute = MAIN_GRAPH_ROUTE\n) {\ncomposable(\nroute = Screen.Home.route,\narguments = listOf(\nnavArgument(Screen.Home.Args.username) {\ntype = NavType.StringType\n}\n)\n) {\nHomeScreen(\nargument = navController.currentBackStackEntry?.arguments\n?.getString(Screen.Home.Args.username) ?: \"\",\nonLogout = {\nnavController.popBackStack(route = Screen.Login.route, inclusive = false)\n},\nonMenuItemClick = { navController.navigate(it) }\n)\n}\ncomposable(route = Screen.Profile.route) {\nBox(\nmodifier = Modifier.fillMaxSize(),\ncontentAlignment = Alignment.Center\n) {\nText(text = \"Profile\")\n}\n}\ncomposable(route = Screen.Settings.route) {\nBox(\nmodifier = Modifier.fillMaxSize(),\ncontentAlignment = Alignment.Center\n) {\nText(text = \"Settings\")\n}\n}\n}\n}\n

V\u00e9g\u00fcl a kett\u0151t egyes\u00edten\u00fcnk kell:

package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\n@Composable\nfun NavGraph(\nnavController: NavHostController\n) {\nNavHost(\nnavController = navController,\nstartDestination = AUTH_GRAPH_ROUTE,\nroute = ROOT_GRAPH_ROUTE\n) {\nauthNavGraph(navController = navController)\nmainNavGraph(navController = navController)\n}\n}\n

Figyelj\u00fck meg, hogy ebben a gr\u00e1fban a f\u0151k\u00e9perny\u0151re \u00e9rkezve hogyan lehet felhaszn\u00e1l\u00f3nevet kinyerni! Illetve azt is meg\u00e1llap\u00edthatjuk, hogy a f\u0151k\u00e9perny\u0151re \u00e9rkezve a backstackr\u0151l t\u00f6rl\u0151dik a bejelentkeztet\u0151 k\u00e9perny\u0151 \u00fatvonala. Ez \u00edgy logikus, hiszen ha m\u00e1r sikeresen bel\u00e9pt\u00fcnk, nem szeretn\u00e9nk, hogy a back gombra kattintva v\u00e9letlen kil\u00e9pj\u00fcnk az alkalmaz\u00e1sb\u00f3l. A gr\u00e1fban a profil \u00e9s be\u00e1ll\u00edt\u00e1s oldalak nincsenek kidolgozva, ez\u00e9rt ide csak egy-egy Box elemet vett\u00fcnk fel placeholder sz\u00f6veggel.

M\u00e1r csak a MainActivity-be kell bek\u00f6tn\u00fcnk a navig\u00e1ci\u00f3 szerint feloldott felsz\u00edn megjelen\u00edt\u00e9s\u00e9t. Itt t\u00f6rt\u00e9nik az alkalmaz\u00e1s t\u00e9m\u00e1j\u00e1nak a megad\u00e1sa is:

package hu.bme.aut.android.composebasics\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.safeDrawingPadding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.ui.Modifier\nimport androidx.navigation.compose.rememberNavController\nimport hu.bme.aut.android.composebasics.navigation.NavGraph\nimport hu.bme.aut.android.composebasics.ui.theme.ComposeBasicsTheme\n\nclass MainActivity : ComponentActivity() {\n@OptIn(ExperimentalMaterial3Api::class)\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nenableEdgeToEdge()\nsetContent {\nComposeBasicsTheme() {\nval navController = rememberNavController()\nBox(modifier = Modifier.safeDrawingPadding()) {\nNavGraph(navController = navController)\n}\n}\n}\n}\n}\n

EdgeToEdge

Android 15-t\u0151l (API 35) az alkalmaz\u00e1sunk k\u00e9pes a rendszer UI (StatusBar, NavigationBar, soft keyboard, stb.) al\u00e1 is rajzolni. Ezzel val\u00f3s\u00edtott\u00e1k meg azt, hogy a k\u00e9sz\u00fcl\u00e9k teljes k\u00e9perny\u0151j\u00e9t haszn\u00e1lni tudjuk a sz\u00e9l\u00e9t\u0151l a sz\u00e9l\u00e9ig. Ez hasznos lehet sz\u00e1mtalan esetben, amikor \"teljes k\u00e9perny\u0151s\" alkalmaz\u00e1st szeretn\u00e9nk \u00edrni, nem korl\u00e1toz minket az elfed\u0151 rendszer UI. A funkci\u00f3 term\u00e9szetesen alacsonyabb API szinteken is el\u00e9rhet\u0151, erre val\u00f3 a fent is l\u00e1that\u00f3 enableEdgeToEdge f\u00fcggv\u00e9nyh\u00edv\u00e1s.

Ez viszont amennyire hasznos, annyi probl\u00e9m\u00e1t is tud okozni, ha e miatt valami vez\u00e9rl\u0151nk becs\u00faszik mondjuk a szoftveres billenty\u0171zet al\u00e1, amit \u00edgy nem tudunk el\u00e9rni. Ennek kik\u00fcsz\u00f6b\u00f6l\u00e9s\u00e9re tal\u00e1lt\u00e1k ki az inseteket. Ennek sz\u00e1mos be\u00e1ll\u00edt\u00e1sa van, amellyel nem kell nek\u00fcnk k\u00e9zzel megtippelni, hogy p\u00e9ld\u00e1ul a status bar h\u00e1ny dp magas, k\u00fcl\u00f6n\u00f6sen, hogy ezek az \u00e9rt\u00e9kek fut\u00e1sid\u0151ben v\u00e1ltozhatnak (l\u00e1sd szoftveres billenty\u0171zet). A sz\u00e1mos be\u00e1ll\u00edt\u00e1s k\u00f6z\u00fcl mi most a fent l\u00e1that\u00f3 safeDrawindPadding-et haszn\u00e1ljuk, ami mint neve is mutatja, pont akkora paddinget \u00e1ll\u00edt mindenhova, hogy semmit se takarjon ki a rendszer UI. (Term\u00e9szetesen ez nem csak az Activity-ben, hanem minden Screenen \u00e9s Composable-\u00f6n k\u00f6l\u00fcn is haszn\u00e1lhat\u00f3.)

A funkci\u00f3 egyik j\u00f3 demonstr\u00e1ci\u00f3ja, hogy a LoginScreen vez\u00e9rl\u0151i, amik a teljes oldal k\u00f6zep\u00e9re vannak helyezve, a szoftveres billenty\u0171zet megjelen\u00e9sekor nem takar\u00f3dnak le, hanem a szabadon marad\u00f3 hely k\u00f6zep\u00e9re cs\u00fasznak.

Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st!

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az alkalmaz\u00e1s f\u0151k\u00e9perny\u0151je bel\u00e9p\u00e9s ut\u00e1n a saj\u00e1t neveddel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

"},{"location":"laborok/compose/#onallo-feladat-sotet-mod","title":"\u00d6n\u00e1ll\u00f3 feladat - S\u00f6t\u00e9t m\u00f3d","text":"

A Compose alkalmaz\u00e1s be\u00e9p\u00edtetten t\u00e1mogatja az \u00e9jszakai m\u00f3dot. Keresd meg az emul\u00e1lt k\u00e9sz\u00fcl\u00e9k be\u00e1ll\u00edt\u00e1sai k\u00f6zt a s\u00f6t\u00e9t t\u00e9ma haszn\u00e1lat\u00e1t, \u00e9s kapcsold be! (Settings -> Display -> Dark theme) Pr\u00f3b\u00e1ld ki \u00edgy az alkalmaz\u00e1st!

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az alkalmaz\u00e1s dark mode-ban (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

"},{"location":"laborok/compose/#onallo-feladat-regisztracio-gomb","title":"\u00d6n\u00e1ll\u00f3 feladat - Regisztr\u00e1ci\u00f3 gomb","text":"

Adj hozz\u00e1 a login oldal alj\u00e1hoz egy teljes oldal sz\u00e9less\u00e9g\u0171 gombot, ahol az \u00faj felhaszn\u00e1l\u00f3 a regisztr\u00e1ci\u00f3 oldalra navig\u00e1lhatna. A gomb \u00fajrahaszn\u00e1lhat\u00f3 komponensk\u00e9nt legyen megval\u00f3s\u00edtva. Az al\u00e1bbi k\u00e9p mutatja az elk\u00e9sz\u00edtend\u0151 fel\u00fcletet:

Seg\u00edts\u00e9g: a Surface \u00e9s a Text composable function\u00f6k a seg\u00edts\u00e9gedre lehetnek a megold\u00e1sban.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az a login k\u00e9perny\u0151 a gombbal \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

"},{"location":"laborok/di/","title":"Labor08 - F\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1s a Dagger \u00e9s a Hilt seg\u00edts\u00e9g\u00e9vel, tesztel\u00e9s (Todo)","text":""},{"location":"laborok/di/#bevezetes","title":"Bevezet\u00e9s","text":"

A kor\u00e1bbi laborokon m\u00e1r elsaj\u00e1t\u00edtottuk, hogyan lehet az Android alkalmaz\u00e1sunkat laz\u00e1n csatolt r\u00e9teges architekt\u00far\u00e1val megval\u00f3s\u00edtani. Ez egy\u00e9rtelm\u0171en seg\u00edti az alkalmaz\u00e1s rugalmas fejleszt\u00e9s\u00e9t, esetleg az egyes r\u00e9tegek is lecser\u00e9lhet\u0151k, de ha megn\u00e9zz\u00fck a k\u00f3dot, m\u00e9g mindig viszonylag jelent\u0151s f\u00fcgg\u00e9st tal\u00e1lunk, hiszen ahol egy m\u00e1sik r\u00e9tegbeli komponenst hozunk l\u00e9tre, ott a p\u00e9ld\u00e1nyos\u00edt\u00e1s a k\u00f3dba van \"\u00e9getve\", a m\u00e1sik r\u00e9teg lecser\u00e9l\u00e9s\u00e9hez itt is m\u00f3dos\u00edtanunk kellene a k\u00f3dot. Ezekre a probl\u00e9m\u00e1kra ny\u00fajt megold\u00e1st a f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1s (dependency injection). Ez egy \u00e1ltal\u00e1nos szoftverfejleszt\u00e9si technika, amelyet nemcsak Androidon, hanem m\u00e1s platformokon is haszn\u00e1lunk. Ebben a laborban az Androidon haszn\u00e1lhat\u00f3 Dagger \u00e9s Hilt k\u00f6nyvt\u00e1rakat ismerj\u00fck meg, amellyel Androidon tudunk f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1st v\u00e9gezni. A k\u00e9t k\u00f6nyvt\u00e1rat gyakran egy\u00fctt haszn\u00e1ljuk, a Dagger alapvet\u0151bb, alacsonyabb szint\u0171 funkci\u00f3kat ny\u00fajt, a Hilt pedig erre \u00e9p\u00fcl r\u00e1, hogy k\u00f6nnyebb\u00e9 tegye a fejleszt\u00e9st.

A labornak egy m\u00e1sik fontos t\u00e9m\u00e1ja az Android alkalmaz\u00e1sok tesztel\u00e9se.

"},{"location":"laborok/di/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/di/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

A Hilt megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektbe fogjuk integr\u00e1lni a k\u00fcl\u00f6nb\u00f6z\u0151 szolg\u00e1ltat\u00e1sokat, ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl. Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a projektet.

FILE PATH

A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

"},{"location":"laborok/di/#a-dagger-es-a-hilt-inicializalasa","title":"A Dagger \u00e9s a Hilt inicializ\u00e1l\u00e1sa","text":"

A Dagger/Hilt feladata teh\u00e1t az lesz, hogy az alkalmaz\u00e1sunk egym\u00e1st\u00f3l f\u00fcgg\u0151 komponenseit laz\u00e1bban csatolt m\u00f3don k\u00f6ti \u00f6ssze. A gyakorlatban ez azt jelenti, hogy ha egy bizonyos t\u00edpus\u00fa f\u00fcgg\u0151s\u00e9gre van sz\u00fcks\u00e9g\u00fcnk, \u00e9s az adott f\u00fcgg\u0151s\u00e9g meg van jel\u00f6lve mint injekt\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9g, akkor a k\u00f6nyvt\u00e1rak el fogj\u00e1k v\u00e9gezni nek\u00fcnk az adott f\u00fcgg\u0151s\u00e9g megkeres\u00e9s\u00e9t \u00e9s be\u00e1ll\u00edt\u00e1s\u00e1t. Ez t\u00f6rt\u00e9nhet p\u00e9ld\u00e1ul egy konstruktornak t\u00f6rt\u00e9n\u0151 param\u00e9ter\u00e1tad\u00e1son kereszt\u00fcl. Ennek el\u0151nye, hogy ha egy komponenst lecser\u00e9l\u00fcnk - p\u00e9ld\u00e1ul ahogyan a Firebase laboron lecser\u00e9lt\u00fck a mem\u00f3riabeli implement\u00e1ci\u00f3kat \u00e9les, Firebase-ben m\u0171k\u00f6d\u0151kre - akkor nem sz\u00fcks\u00e9ges a k\u00f3dunkat m\u00f3dos\u00edtani, csup\u00e1n meg kell adni a Daggernek/Hiltnek, hogy az el\u00e9rhet\u0151 implement\u00e1ci\u00f3k k\u00f6z\u00fcl melyik legyen az, amelyiket sz\u00fcks\u00e9g eset\u00e9n alkalmazza.

T\u00f6bbf\u00e9le f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00f3 keretrendszer l\u00e9tezik Androidon, \u00e9s az Android platformon k\u00edv\u00fcl is, ezek n\u00e9mileg m\u00e1s elveken m\u0171k\u00f6dnek. A Dagger a legjobb teljes\u00edtm\u00e9ny \u00e9rdek\u00e9ben \u00fagy m\u0171k\u00f6dik, hogy nem fut\u00e1s k\u00f6zben oldja fel a f\u00fcgg\u0151s\u00e9geket, hanem a ford\u00edt\u00e1si folyamatba avatkozik bele, \u00e9s m\u00e1r ak\u00f6zben felt\u00e9rk\u00e9pezi a f\u00fcgg\u0151s\u00e9gi viszonyok jel\u00f6l\u00e9s\u00e9re alkalmazott annot\u00e1ci\u00f3kat. Ez\u00e9rt a projekt inicializ\u00e1l\u00e1s\u00e1nak r\u00e9szek\u00e9nt sz\u00fcks\u00e9ges felvenn\u00fcnk egy gradle plugint is a folyamatba.

El\u0151sz\u00f6r a libs.versions.toml f\u00e1jlunkba vegy\u00fck fel az \u00faj f\u00fcgg\u0151s\u00e9gekhez kapcsol\u00f3d\u00f3 bejegyz\u00e9seket:

[versions]\nhilt = \"2.51.1\"\nhilt-navigation-compose = \"1.2.0\"\n\n[libraries]\nhilt-android = { module = \"com.google.dagger:hilt-android\", version.ref = \"hilt\" }\nhilt-compiler = { module = \"com.google.dagger:hilt-compiler\", version.ref = \"hilt\" }\nhilt-navigation-compose = { module = \"androidx.hilt:hilt-navigation-compose\", version.ref = \"hilt-navigation-compose\"}\n\n[plugins]\ngoogle-dagger-hilt-android = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\"}\n

Majd a projekt szint\u0171 build.gradle.kts f\u00e1jlba vegy\u00fck fel a a k\u00f6vetkez\u0151 sort a pluginek k\u00f6z\u00e9:

alias(libs.plugins.google.dagger.hilt.android) apply false\n

Majd a modul szint\u0171 build.gradle f\u00e1jlban alkalmazzuk a plugint:

plugins {\n...\n\nalias(libs.plugins.google.dagger.hilt.android)\n}\n

\u00c9s vegy\u00fck m\u00e9g fel a sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket, majd szinkroniz\u00e1ljuk a projektet:

// Hilt\nimplementation(libs.hilt.android)\nimplementation(libs.hilt.navigation.compose)\nksp(libs.hilt.compiler)\n

Ezzel a build folyamat \u00e9s a f\u00fcgg\u0151s\u00e9gek rendben vannak. Most glob\u00e1lisan, az alkalmaz\u00e1s szintj\u00e9n inicializ\u00e1lnunk kell a Daggert, hogy l\u00e9trej\u00f6jj\u00f6n egy kontextus, amelyben a f\u00fcgg\u0151s\u00e9geket menedzseli. Ehhez a TodoApplication oszt\u00e1lyra tegy\u00fck r\u00e1 a @HiltAndroidApp annot\u00e1ci\u00f3t:

@HiltAndroidApp\nclass TodoApplication : Application() {\n// ...\n}\n

Majd nyissuk meg a MainActivity oszt\u00e1lyt is, ezen pedig az @AndroidEntryPoint annot\u00e1ci\u00f3t helyezz\u00fck el:

@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nTodoTheme {\nNavGraph()\n}\n}\n}\n}\n

Ezzel a k\u00f6z\u00f6s inicializ\u00e1ci\u00f3s feladatok elk\u00e9sz\u00fcltek, de m\u00e9g t\u00e9nyleges injekt\u00e1lhat\u00f3 komponenseket \u00e9s injekt\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9geket nem hoztunk l\u00e9tre. Most elkezdj\u00fck a \"bedr\u00f3tozott\" f\u00fcgg\u0151s\u00e9gi viszonyokat f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1sra cser\u00e9lni.

"},{"location":"laborok/di/#az-adatbazismodul-elkeszitese","title":"Az adatb\u00e1zismodul elk\u00e9sz\u00edt\u00e9se","text":"

A Dagger \u00e9s a Hilt \u00e1ltal kezelt komponensek a jobb \u00e1tl\u00e1that\u00f3s\u00e1g \u00e9rdek\u00e9ben modulokra oszthat\u00f3k. Minden modul komponenseket hoz l\u00e9tre, amelyeket a megjel\u00f6lt injekt\u00e1l\u00e1si pontokon a k\u00f6nyvt\u00e1rak fel fognak haszn\u00e1lni. Az els\u0151 modulunk a TodoDatabase \u00e9s a TodoDao l\u00e9trehoz\u00e1s\u00e1t fogja elv\u00e9gezni. Hozzunk l\u00e9tre ennek a modulnak egy data.di package-et, \u00e9s ebbe vegy\u00fck fel a modult megval\u00f3s\u00edt\u00f3 oszt\u00e1lyunkat:

@Module\n@InstallIn(SingletonComponent::class)\nobject DatabaseModule {\n\n@Provides\n@Singleton\nfun provideDatabaseInstance(\n@ApplicationContext context: Context\n): TodoDatabase = Room.databaseBuilder(\ncontext,\nTodoDatabase::class.java,\n\"todo_database\"\n).fallbackToDestructiveMigration().build()\n\n@Provides\n@Singleton\nfun provideTodoDao(\ndb: TodoDatabase\n): TodoDao = db.dao\n}\n

Ebben a @Module annot\u00e1ci\u00f3 azt jelenti, hogy az oszt\u00e1ly komponensekkel j\u00e1rul hozz\u00e1 a Dagger/Hilt \u00e1ltal kezelt objektumgr\u00e1fhoz. Az oszt\u00e1lyban legy\u00e1rtott komponensek teh\u00e1t f\u00fcgg\u0151s\u00e9gk\u00e9nt injekt\u00e1lhat\u00f3ak lesznek m\u00e1s komponensekbe, illetve maguk is hivatkozhatnak f\u00fcgg\u0151s\u00e9gekre. Az @InstallIn(SingletonComponent::class) a SingletonComponenthez k\u00f6ti a modult, aminek az lesz az eredm\u00e9nye, hogy a komponensek injekt\u00e1l\u00e1sa az eg\u00e9sz alkalmaz\u00e1son bel\u00fcl m\u0171k\u00f6dik majd.

A met\u00f3dusokon lev\u0151 @Provides hat\u00e1rozza meg, hogy a met\u00f3dusok \"factory\" met\u00f3dusk\u00e9nt szolg\u00e1ljanak, \u00e9s az \u00e1ltaluk visszaadott objektumok a Dagger \u00e1ltal kezelt komponensekk\u00e9 v\u00e1ljanak. A @Singleton annot\u00e1ci\u00f3 azt adja meg, hogy ezekb\u0151l a komponensekb\u0151l egyetlen p\u00e9ld\u00e1ny k\u00e9sz\u00fclj\u00f6n, \u00e9s az eg\u00e9sz alkalmaz\u00e1son kereszt\u00fcl mindenhol ezt haszn\u00e1ljuk f\u00fcgg\u0151s\u00e9gk\u00e9nt. Legt\u00f6bbsz\u00f6r elegend\u0151 egy p\u00e9ld\u00e1ny, \u00e9s ez\u00e9rt ez a c\u00e9lravezet\u0151 megold\u00e1s.

Most, hogy a komponenseket legy\u00e1rtottuk, arr\u00f3l kell gondoskodni, elt\u00e1vol\u00edtsuk ezeknek a komponenseknek a kor\u00e1bbi m\u00f3don t\u00f6rt\u00e9n\u0151 l\u00e9trehoz\u00e1s\u00e1t, \u00e9s megjel\u00f6lj\u00fck azokat a helyeket, ahova a Dagger \u00e9s Hilt k\u00f6nyvt\u00e1raknak ezeket a komponenseket f\u00fcgg\u0151s\u00e9gk\u00e9nt injekt\u00e1lniuk kell. Kor\u00e1bban a TodoDatabase l\u00e9trehoz\u00e1sa a TodoApplication oszt\u00e1lyban volt. Innen elt\u00e1vol\u00edtjuk a p\u00e9ld\u00e1nyos\u00edt\u00e1st, viszont a repository komponens\u00fcnket egyel\u0151re nem b\u00edztuk a Dagger/Hilt p\u00e1rosra, ez\u00e9rt ezt m\u00e9g mindig l\u00e9tre kell hozni, ehhez viszont sz\u00fcks\u00e9g van a TodoDao-ra, amit viszont m\u00e1r ide is injekt\u00e1lhatunk.

A TodoApplication oszt\u00e1lyunk most \u00edgy fest:

@HiltAndroidApp\nclass TodoApplication : Application() {\n\n@Inject\nlateinit var dao: TodoDao\n\ncompanion object {\nlateinit var repository: TodoRepositoryImpl\n}\n\noverride fun onCreate() {\nsuper.onCreate()\n\nrepository = TodoRepositoryImpl(dao)\n}\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, a fenti l\u00e9p\u00e9sekhez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/di/#a-repository-modul-elkeszitese","title":"A repository modul elk\u00e9sz\u00edt\u00e9se","text":"

A fentihez hasonl\u00f3an most a repository-t is a Dagger/Hilt kezel\u00e9s\u00e9re b\u00edzzuk, \u00e9s megsz\u00fcntetj\u00fck a k\u00e9zi l\u00e9trehoz\u00e1st. Ehhez el\u0151sz\u00f6r szint\u00e9n egy modult kell l\u00e9trehoznunk. Tegy\u00fck ezt szint\u00e9n a data.di package-be:

@Module\n@InstallIn(SingletonComponent::class)\nabstract class RepositoryModule {\n\n@Binds\n@Singleton\nabstract fun bindTodoRepository(\ntodoRepositoryImpl: TodoRepositoryImpl\n): TodoRepository\n\n}\n

Ez a modul n\u00e9mileg m\u00e1sk\u00e9pp m\u0171k\u00f6dik, mint a kor\u00e1bbi. Egyr\u00e9szt az oszt\u00e1ly absztrakt, illetve nem a @Provides annot\u00e1ci\u00f3t haszn\u00e1ltuk, hanem a @Binds-et, \u00e9s az ezzel megjel\u00f6lt met\u00f3dus azt hat\u00e1rozza meg, hogy amikor TodoRepository t\u00edpus\u00fa f\u00fcgg\u0151s\u00e9gre van sz\u00fcks\u00e9g\u00fcnk, akkor annak a konkr\u00e9t TodoRepositoryImpl t\u00edpus\u00fa implement\u00e1ci\u00f3j\u00e1t kell haszn\u00e1lni. Most m\u00e1r kivehetj\u00fck a TodoApplication oszt\u00e1lyb\u00f3l a repository kezel\u00e9s\u00e9t is, \u00edgy az oszt\u00e1lyunk \u00fcres lesz, de a Hilt inicializ\u00e1ci\u00f3ja miatt tov\u00e1bbra is sz\u00fcks\u00e9g van r\u00e1, hogy megtartsuk:

@HiltAndroidApp\nclass TodoApplication : Application()\n

Viszont a TodoRepositoryImpl p\u00e9ld\u00e1nyos\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9g van a TodoDao oszt\u00e1lyre, \u00e9s mivel innent\u0151l ezt a Dagger/Hilt v\u00e9gzi, ez\u00e9rt az @Inject annot\u00e1ci\u00f3 haszn\u00e1lat\u00e1val jelezni kell, hogy p\u00e9ld\u00e1nyos\u00edt\u00e1skor a TodoDao-t f\u00fcgg\u0151s\u00e9gk\u00e9nt szeretn\u00e9nk injekt\u00e1lni. \u00cdrjuk \u00e1t az oszt\u00e1ly fejl\u00e9c\u00e9t az al\u00e1bbira:

class TodoRepositoryImpl @Inject constructor(\nprivate val dao: TodoDao\n) : TodoRepository {\n// ...\n}\n

A repository viszont szorosan volt csatolva a viewmodelekhez, amelyeket a h\u00e1rom feature-h\u00f6z \u00edrtunk. M\u00e9g ezeket is \u00e1t kell alak\u00edtanunk hozz\u00e1, hogy az alkalmaz\u00e1s \u00fajra haszn\u00e1lhat\u00f3 legyen. Jelenleg mindh\u00e1rom viewmodel oszt\u00e1lyunkban az al\u00e1bbihoz hasonl\u00f3 factory-k vannak:

    companion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodosViewModel(\ntodoOperations = todoOperations\n)\n}\n}\n}\n

Ezeket t\u00f6r\u00f6ln\u00fcnk kell, viszont gondoskodnunk kell r\u00f3la, hogy a TodoUseCases is inicializ\u00e1l\u00f3djon. Jelenlegi k\u00e9sz\u00fclts\u00e9gben ezt nem tudjuk m\u00e9g injekt\u00e1lni, csak a TodoRepository komponenst, ennek seg\u00edts\u00e9g\u00e9vel viszont p\u00e9ld\u00e1nyos\u00edthat\u00f3 a TodoUseCases. Illetve m\u00e9g el kell helyezni a viewmodel oszt\u00e1lyokon a @HiltViewModel annot\u00e1ci\u00f3t, hogy a Hilt \u00e1ltal menedzselt viewmodellekk\u00e9 v\u00e1ljanak. A TodosViewModel v\u00e9g\u00fcl \u00edgy alakul:

@HiltViewModel\nclass TodosViewModel @Inject constructor(\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(TodosState())\nval state = _state.asStateFlow()\n\ninit {\ntodoOperations = TodoUseCases(repository)\nloadTodos()\n}\nprivate fun loadTodos() {\n\nviewModelScope.launch {\n_state.update { it.copy(isLoading = true) }\ntry {\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.update { it.copy(\nisLoading = false,\ntodos = todos\n) }\n}\n} catch (e: Exception) {\n_state.update {  it.copy(\nisLoading = false,\nerror = e\n) }\n}\n}\n}\n}\n

Hasonl\u00f3 \u00e1talak\u00edt\u00e1sokat kell v\u00e9gezn\u00fcnk a CheckTodoViewModel oszt\u00e1lyon:

@HiltViewModel\nclass CheckTodoViewModel @Inject constructor(\nprivate val savedState: SavedStateHandle,\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(CheckTodoState())\nval state: StateFlow<CheckTodoState> = _state\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CheckTodoEvent) {\nwhen (event) {\nCheckTodoEvent.EditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = true\n)\n}\n}\n\nCheckTodoEvent.StopEditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = false\n)\n}\n}\n\nis CheckTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(title = newValue)\n)\n}\n}\n\nis CheckTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(description = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo?.copy(priority = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectDate -> {\nval newValue = event.date.toString()\n_state.update {\nit.copy(\ntodo = it.todo?.copy(dueDate = newValue)\n)\n}\n}\n\nCheckTodoEvent.DeleteTodo -> {\nonDelete()\n}\n\nCheckTodoEvent.UpdateTodo -> {\nonUpdate()\n}\n}\n}\n\ninit {\ntodoOperations = TodoUseCases(repository)\nload()\n}\n\nprivate fun load() {\nval todoId = checkNotNull<Int>(savedState[\"id\"])\nviewModelScope.launch {\n_state.update { it.copy(isLoadingTodo = true) }\ntry {\nval todo = todoOperations.loadTodo(todoId)\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\n_state.update {\nit.copy(\nisLoadingTodo = false,\ntodo = todo.getOrThrow().asTodoUi()\n)\n}\n}\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onUpdate() {\nviewModelScope.launch(Dispatchers.IO) {\ntry {\ntodoOperations.updateTodo(\n_state.value.todo?.asTodo()!!\n)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onDelete() {\nviewModelScope.launch {\ntry {\ntodoOperations.deleteTodo(state.value.todo!!.id)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

Majd a CreateTodoViewModel oszt\u00e1lyon is:

@HiltViewModel\nclass CreateTodoViewModel @Inject constructor(\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(CreateTodoState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\ninit {\ntodoOperations = TodoUseCases(repository)\n}\n\nfun onEvent(event: CreateTodoEvent) {\nwhen(event) {\nis CreateTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(title = newValue)\n) }\n}\nis CreateTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(description = newValue)\n) }\n}\nis CreateTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update { it.copy(\ntodo = it.todo.copy(priority = newValue)\n) }\n}\nis CreateTodoEvent.SelectDate -> {\nval newValue = event.date\n_state.update { it.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n) }\n}\nCreateTodoEvent.SaveTodo -> {\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

Majd v\u00e9g\u00fcl az \u00f6sszes screen oszt\u00e1lyban v\u00e1ltoztassuk meg a viewmodel l\u00e9treoz\u00e1s\u00e1nak m\u00f3dj\u00e1t \u00fagy, hogy a hiltViewModel() h\u00edv\u00e1st haszn\u00e1ljuk. Pl. a CheckTodoScreen eddig \u00edgy n\u00e9zett ki:

fun CheckTodoScreen(\nonNavigateBack: () -> Unit,\nviewModel: CheckTodoViewModel = viewModel(factory = CheckTodoViewModel.Factory)\n) {\n// ...\n}\n

A k\u00f6vetkez\u0151k\u00e9ppen m\u00f3dos\u00edtsuk:

fun CheckTodoScreen(\nonNavigateBack: () -> Unit,\nviewModel: CheckTodoViewModel = hiltViewModel()\n) {\n// ...\n}\n
"},{"location":"laborok/di/#a-usecases-modul-elkeszitese","title":"A usecases modul elk\u00e9sz\u00edt\u00e9se","text":"

M\u00e1r csak a usecases modult kell elk\u00e9sz\u00edten\u00fcnk. Ehhez k\u00e9sz\u00edts\u00fcnk el\u0151sz\u00f6r egy domain.di package-et, \u00e9s ebbe hozzuk l\u00e9tre az al\u00e1bbit:

@Module\n@InstallIn(SingletonComponent::class)\nobject TodoUseCaseModule {\n\n@Provides\n@Singleton\nfun provideLoadTodosUseCase(\nrepository: TodoRepository\n): LoadTodosUseCase = LoadTodosUseCase(repository)\n\n@Provides\n@Singleton\nfun provideTodoUseCases(\nrepository: TodoRepository,\nloadTodos: LoadTodosUseCase\n): TodoUseCases = TodoUseCases(repository, loadTodos)\n\n}\n

A TodoUseCases oszt\u00e1lyt pedig \u00edgy alak\u00edtsuk \u00e1t:

class TodoUseCases(\nval repository: TodoRepository,\nval loadTodos: LoadTodosUseCase\n) {\n\nval loadTodo = LoadTodoUseCase(repository)\nval saveTodo = SaveTodoUseCase(repository)\nval updateTodo = UpdateTodoUseCase(repository)\nval deleteTodo = DeleteTodoUseCase(repository)\n}\n

Most m\u00e1r a viewmodel oszt\u00e1lyokba nem sz\u00fcks\u00e9ges a TodoRepository injekt\u00e1l\u00e1sa \u00e9s a TodoUseCases k\u00e9zi l\u00e9trehoz\u00e1sa, hiszen a TodoUseCases k\u00f6zvetlen is injekt\u00e1lhat\u00f3v\u00e1 v\u00e1lt. M\u00f3dos\u00edtsuk ennek megfelel\u0151en a viewmodel oszt\u00e1lyokat!

A CheckTodoViewModel k\u00f3dja:

@HiltViewModel\nclass CheckTodoViewModel @Inject constructor(\nprivate val savedState: SavedStateHandle,\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(CheckTodoState())\nval state: StateFlow<CheckTodoState> = _state\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CheckTodoEvent) {\nwhen (event) {\nCheckTodoEvent.EditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = true\n)\n}\n}\n\nCheckTodoEvent.StopEditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = false\n)\n}\n}\n\nis CheckTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(title = newValue)\n)\n}\n}\n\nis CheckTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(description = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo?.copy(priority = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectDate -> {\nval newValue = event.date.toString()\n_state.update {\nit.copy(\ntodo = it.todo?.copy(dueDate = newValue)\n)\n}\n}\n\nCheckTodoEvent.DeleteTodo -> {\nonDelete()\n}\n\nCheckTodoEvent.UpdateTodo -> {\nonUpdate()\n}\n}\n}\n\ninit {\nload()\n}\n\nprivate fun load() {\nval todoId = checkNotNull<Int>(savedState[\"id\"])\nviewModelScope.launch {\n_state.update { it.copy(isLoadingTodo = true) }\ntry {\nval todo = todoOperations.loadTodo(todoId)\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\n_state.update {\nit.copy(\nisLoadingTodo = false,\ntodo = todo.getOrThrow().asTodoUi()\n)\n}\n}\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onUpdate() {\nviewModelScope.launch(Dispatchers.IO) {\ntry {\ntodoOperations.updateTodo(\n_state.value.todo?.asTodo()!!\n)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onDelete() {\nviewModelScope.launch {\ntry {\ntodoOperations.deleteTodo(state.value.todo!!.id)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

A CreateTodoViewModel \u00edgy fest:

@HiltViewModel\nclass CreateTodoViewModel @Inject constructor(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(CreateTodoState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CreateTodoEvent) {\nwhen(event) {\nis CreateTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(title = newValue)\n) }\n}\nis CreateTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(description = newValue)\n) }\n}\nis CreateTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update { it.copy(\ntodo = it.todo.copy(priority = newValue)\n) }\n}\nis CreateTodoEvent.SelectDate -> {\nval newValue = event.date\n_state.update { it.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n) }\n}\nCreateTodoEvent.SaveTodo -> {\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

A TodosViewModel \u00e1talak\u00edtott verzi\u00f3ja pedig az al\u00e1bbi:

@HiltViewModel\nclass TodosViewModel @Inject constructor(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(TodosState())\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\nprivate fun loadTodos() {\n\nviewModelScope.launch {\n_state.update { it.copy(isLoading = true) }\ntry {\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.update { it.copy(\nisLoading = false,\ntodos = todos\n) }\n}\n} catch (e: Exception) {\n_state.update {  it.copy(\nisLoading = false,\nerror = e\n) }\n}\n}\n}\n}\n

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, a fenti l\u00e9p\u00e9sekhez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/di/#az-alkalmazas-tesztelese","title":"Az alkalmaz\u00e1s tesztel\u00e9se","text":"

Az elk\u00e9sz\u00fclt alkalmaz\u00e1snak most egy-egy r\u00e9sz\u00e9t automatiz\u00e1lt tesztekkel fogjuk ellen\u0151rizni. Az automatiz\u00e1lt teszteket k\u00e9t f\u0151 t\u00edpusra oszthatjuk Androidon:

  • Lok\u00e1lis tesztek: ezek olyan tesztek amelyek b\u00e1rmif\u00e9le virtualiz\u00e1lt k\u00f6rnyezet n\u00e9lk\u00fcl, puszt\u00e1n Java k\u00f3dnak a fejleszt\u0151i g\u00e9pen t\u00f6rt\u00e9n\u0151 futtat\u00e1s\u00e1val v\u00e9grehajthat\u00f3k.
  • Instrument\u00e1lt tesztek: ezek a tesztek az emul\u00e1toron futnak.
"},{"location":"laborok/di/#lokalis-tesztek-futtatasa","title":"Lok\u00e1lis tesztek futtat\u00e1sa","text":"

A lok\u00e1lis tesztek k\u00f6zt is megk\u00fcl\u00f6nb\u00f6ztethet\u00fcnk k\u00fcl\u00f6nb\u00f6z\u0151 t\u00edpusokat aszerint, hogy architektur\u00e1lisan mekkora r\u00e9sz\u00e9t \u00e9rintik a k\u00f3dnak. A k\u00f3d kis egys\u00e9geit, gyakorlatban met\u00f3dusait ellen\u0151rz\u0151 teszteket unitteszteknek nevezz\u00fck. Ha a teszt n\u00e9h\u00e1ny oszt\u00e1ly k\u00f3dj\u00e1n is \u00e1th\u00edv, akkor integr\u00e1ci\u00f3s tesztr\u0151l besz\u00e9l\u00fcnk, ha pedig valamilyen komplexebb, sok komponenst \u00e9rint\u0151 folyamatot tesztel, akkor rendszertesztnek nevezz\u00fck.

A lok\u00e1lis tesztek k\u00f6z\u00fcl leggyakrabban unitteszteket k\u00e9sz\u00edt\u00fcnk, a nagyobb egys\u00e9geket pedig gyakrabban m\u00e1r instrument\u00e1lt tesztekkel ellen\u0151rizz\u00fck. Most a lok\u00e1lis tesztek k\u00f6z\u00fcl a unittesztekre koncentr\u00e1lunk. Ezek a legk\u00f6nnyebben elk\u00e9sz\u00edthet\u0151ek \u00e9s sz\u00e1mos el\u0151ny\u00fck van:

  • Szisztematikus m\u00f3dszert adnak a rendszer teljes letesztel\u00e9s\u00e9hez
  • Gyorsan lefutnak
  • Mivel minden teszt egy kis egys\u00e9gre vonatkozik, a meghi\u00fasul\u00f3 teszt j\u00f3l r\u00e1mutat a probl\u00e9ma hely\u00e9re is

A unittesztek eset\u00e9n fontos kih\u00edv\u00e1s, hogy a f\u00fcgg\u0151s\u00e9geket izol\u00e1ljuk, lev\u00e1lasszuk, hiszen ha azok is megh\u00edv\u00f3dn\u00e1nak, akkor nem unittesztr\u0151l besz\u00e9ln\u00e9nk, hanem integr\u00e1ci\u00f3s tesztr\u0151l, \u00e9s a fenti el\u0151ny\u00f6k nem teljes\u00fcln\u00e9nek. K\u00fcl\u00f6n\u00f6sen az az el\u0151ny veszne el, hogy a teszt j\u00f3l mutatja a hiba hely\u00e9t. Ez\u00e9rt a tesztekben a f\u00fcgg\u0151s\u00e9geket valamilyen test double objektummal, tipikusan mock objektummal cser\u00e9lj\u00fck le. A mock objektum egy \"buta\", de \"felprogramozhat\u00f3\" komponens, ami \u00e9ppen csak annyit csin\u00e1l, amit a teszt idej\u00e9re elv\u00e1runk t\u0151le, azaz tipikusan valamilyen be\u00e9getett adatot ad vissza. Ezen k\u00edv\u00fcl a seg\u00edts\u00e9g\u00e9vel ellen\u0151rizhet\u0151 az is, hogy a lecser\u00e9lt f\u00fcgg\u0151s\u00e9gen a tesztelt k\u00f3dr\u00e9szlet t\u00e9nyleg elv\u00e9gezte a v\u00e1rt h\u00edv\u00e1st.

Az alkalmaz\u00e1sban nincsenek t\u00fal bonyolult \u00fczleti logika r\u00e9szek, de a tesztel\u00e9s technik\u00e1j\u00e1t j\u00f3l meg tudjuk figyelni. Most a TodoRepositoryImpl oszt\u00e1lyt fogjuk tesztelni. Konvenci\u00f3 szerint oszt\u00e1lyokhoz k\u00e9sz\u00edt\u00fcnk tesztoszt\u00e1lyokat, \u00e9s a tesztoszt\u00e1lyokban minden tesztelt met\u00f3dus egy lehets\u00e9ges lefut\u00e1s\u00e1hoz k\u00e9sz\u00edt\u00fcnk egy tesztmet\u00f3dust. A tesztoszt\u00e1lyokat a tesztelt oszt\u00e1lyokkal azonos package-be tessz\u00fck, \u00e9s nev\u00fckben a Test ut\u00f3tagot haszn\u00e1ljuk.

El\u0151sz\u00f6r fel kell venn\u00fcnk a tesztel\u00e9shez haszn\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9geket a projektbe! Mivel a kapott v\u00e1zban eddig nem voltak tesztek, \u00edgy ezek a f\u00fcgg\u0151s\u00e9gek teljesen hi\u00e1nyoztak. A lok\u00e1lis tesztekhez a testImplementation scope-ot kell haszn\u00e1lnunk. Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket, el\u0151sz\u00f6r a libs.versions.toml f\u00e1jllal kezdve:

[versions]\njunit = \"4.13.2\"\nmockitoCore = \"5.11.0\"\nmockitoInline = \"5.2.0\"\nmockitoKotlin = \"5.2.1\"\n\n[libraries]\njunit = { module = \"junit:junit\", version.ref = \"junit\" }\nmockito-inline = { module = \"org.mockito:mockito-inline\", version.ref = \"mockitoInline\" }\nmockito-core = { module = \"org.mockito:mockito-core\", version.ref = \"mockitoCore\" }\nmockito-kotlin = { module = \"org.mockito.kotlin:mockito-kotlin\", version.ref = \"mockitoKotlin\" }\n

Majd folytassuk a modulszint\u0171 build.gradle.kts f\u00e1jllal:

// Testing\ntestImplementation(libs.junit)\ntestImplementation(libs.mockito.core)\ntestImplementation(libs.mockito.inline)\ntestImplementation(libs.mockito.kotlin)\n

Hozzuk l\u00e9tre a data.datasource package-et ez\u00e9rt a test k\u00f6nyvt\u00e1rban is! A lok\u00e1lis tesztek a test k\u00f6nyvt\u00e1rban vannak, az androidTest k\u00f6nyvt\u00e1r pedig az instrument\u00e1lt tesztek helye.

A l\u00e9trehozott package-ben hozzunk l\u00e9tre egy TodoRepositoryImplTest oszt\u00e1lyt az al\u00e1bbi m\u00f3don:

package hu.bme.aut.android.todo.data.datasource\n\nimport hu.bme.aut.android.todo.data.dao.TodoDao\nimport hu.bme.aut.android.todo.data.entities.TodoEntity\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.datetime.LocalDate\nimport org.junit.Assert\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mock\nimport org.mockito.junit.MockitoJUnitRunner\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\n\n@RunWith(MockitoJUnitRunner::class)\nclass TodoRepositoryImplTest {\n\n@Mock\nlateinit var todoDao: TodoDao\n\n@Test\nfun testGetAllTodos() {\nval sampleTodo = TodoEntity(\n1,\n\"Test app\",\nPriority.HIGH,\nLocalDate(2024, 4, 30),\n\"Write unit tests for our Todo app\"\n)\nval mockDao = mock<TodoDao> {\non { getAllTodos() } doReturn (flowOf(listOf(sampleTodo)))\n}\nval todoRepositoryImpl = TodoRepositoryImpl(mockDao)\nval result = todoRepositoryImpl.getAllTodos()\nrunBlocking {\nAssert.assertTrue(result.first().contains(sampleTodo))\nverify(mockDao, times(1)).getAllTodos()\n}\n}\n}\n

N\u00e9zz\u00fck \u00e1t a laborvezet\u0151vel egy\u00fctt, hogyan m\u0171k\u00f6dik a teszt!

Alapvet\u0151en minden teszt h\u00e1rom l\u00e9p\u00e9sb\u0151l \u00e1ll:

  1. A test fixture, azaz a tesztelni k\u00edv\u00e1nt k\u00f3dr\u00e9szlethez sz\u00fcks\u00e9ges kezdeti \u00e1llapot fel\u00e1ll\u00edt\u00e1sa. Ha egy \"sikeres\" lefut\u00e1st szeretn\u00e9nk tesztelni, akkor ennek megfelel\u0151en k\u00e9sz\u00edtj\u00fck el\u0151 a k\u00f6rnyezetet. Ha pedig egy hiba ut\u00e1na elv\u00e1rt hat\u00e1st, pl. exception dob\u00f3dik, hiba\u00fczenet \u00edr\u00f3dik ki stb. szeretn\u00e9nk tesztelni, akkor ennek megfelel\u0151en. Idetartozik a f\u00fcgg\u0151s\u00e9gek kiv\u00e1lt\u00e1sa is.
  2. A tesztelni k\u00edv\u00e1nt k\u00f3dr\u00e9szlet futtat\u00e1sa.
  3. Az elv\u00e1rt eredm\u00e9nyek megfogalmaz\u00e1sa, annak ellen\u0151rz\u00e9se, hogy teljes\u00fcltek-e (assertions). Ha mock objektumokat haszn\u00e1ltunk, akkor idetartozik annak ellen\u0151rz\u00e9se is, hogy rajtuk megh\u00edv\u00f3dtak-e azok a met\u00f3dusok, amelyek megh\u00edv\u00f3d\u00e1s\u00e1ra sz\u00e1m\u00edtottunk.

A p\u00e9ld\u00e1nkban a TodoRepositoryImpl oszt\u00e1ly getAllTodos met\u00f3dusa csup\u00e1n annyit tesz, hogy tov\u00e1bbh\u00edvja a TodoDao oszt\u00e1ly getAllTodos met\u00f3dus\u00e1t, majd ennek eredm\u00e9ny\u00e9t visszaadja. A teszt\u00fcnk ez\u00e9rt nem lesz t\u00fal bonyolult. Alapvet\u0151en abb\u00f3l \u00e1ll, hogy k\u00e9sz\u00edten\u00fcnk kell egy TodoDao mock objektumot, amely be\u00e9getett TodoEntity list\u00e1t fog visszaadni. Ezt a mockot kell odaadnunk a TodoRepositoryImp f\u00fcgg\u0151s\u00e9g\u00e9nek, majd meg kell h\u00edvnunk a tesztelni k\u00edv\u00e1nt met\u00f3dust, \u00e9s meg kell vizsg\u00e1lni, hogy a be\u00e9getett list\u00e1t adta-e vissza, valamint a mockunknak az azonos nev\u0171 met\u00f3dusa szint\u00e9n h\u00edv\u00f3dott-e.

Futtassuk le a tesztet!

"},{"location":"laborok/di/#instrumentalt-tesztek-futtatasa","title":"Instrument\u00e1lt tesztek futtat\u00e1sa","text":"

Bonyolultabb teszteket sem lehetetlen lok\u00e1lis tesztk\u00e9nt futtatni, de a f\u00fcgg\u0151s\u00e9gek sz\u00f6vev\u00e9nyess\u00e9ge miatt ez egy j\u00f3val bonyolultabb feladat lenne. Praktikusabb ez\u00e9rt ha \u00f6sszetettebb folyamatok tesztel\u00e9s\u00e9hez az emul\u00e1tort is seg\u00edts\u00e9g\u00fcl h\u00edvjuk. Ilyen p\u00e9ld\u00e1ul a Compose seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edtett UI tesztel\u00e9se. Az Android \u00e1ltal biztos\u00edtott eszk\u00f6z\u00f6kkel l\u00e9tre tudjuk hozni a komponenseinket egy emul\u00e1lt k\u00f6rnyezetben, \u00e9s a teszt k\u00f3dj\u00e1b\u00f3l interakci\u00f3kat is ki tudunk v\u00e1ltani (pl. \u00edrjunk be sz\u00f6veget egy mez\u0151be, kattintsunk egy gombon stb.). Ez a fajta tesztel\u00e9s l\u00e1that\u00f3an j\u00f3val k\u00f6zelebb \u00e1ll ahhoz a m\u00f3dhoz, ahogyan az alkalmaz\u00e1s majd t\u00e9nylegesen futni fog. Logikusan bel\u00e9that\u00f3 ugyanakkor az is, hogy ezek a tesztek j\u00f3val bonyolultabbak, \u00e9s lassabban is fognak futni.

Az instrument\u00e1lt teszteket az androidTest k\u00f6nyvt\u00e1rban lehet l\u00e9trehozni. Mivel ezek nagyobb l\u00e9pt\u00e9k\u0171 tesztek is lehetnek, nem felt\u00e9tlen tartoznak logikailag egy komponenshez. Amennyiben azonban odatartoznak, javasolt ezeket is azonos package-be tenni \u00e9s a lok\u00e1lis tesztek\u00e9hez hasonl\u00f3 elnevez\u00e9si konvenci\u00f3 szerint elnevezni.

El\u0151sz\u00f6r itt is a f\u00fcgg\u0151s\u00e9gek felv\u00e9tel\u00e9vel kezd\u00fcnk, a libs.versions.toml f\u00e1jllal:

[versions]\nespressoCore = \"3.6.1\"\nhiltAndroidTesting = \"2.51.1\"\nhiltAndroidCompiler = \"2.51.1\"\njunitVersion = \"1.2.1\"\n\n\n[libraries]\nandroidx-espresso-core = { module = \"androidx.test.espresso:espresso-core\", version.ref = \"espressoCore\" }\nandroidx-junit = { module = \"androidx.test.ext:junit\", version.ref = \"junitVersion\" }\nandroidx-ui-test-junit4 = { module = \"androidx.compose.ui:ui-test-junit4\" }\nandroidx-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\" }\nandroidx-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\" }\nhilt-android-testing = { module = \"com.google.dagger:hilt-android-testing\", version.ref = \"hiltAndroidTesting\" }\nhilt-android-compiler = { module = \"com.google.dagger:hilt-android-compiler\", version.ref = \"hiltAndroidCompiler\" }\n

Majd hivatkozzuk is meg ezeket a modulszint\u0171 build.gradle.kts f\u00e1jlban:

// Instrumented testing\nandroidTestImplementation(libs.androidx.junit)\nandroidTestImplementation(libs.androidx.espresso.core)\nandroidTestImplementation(libs.androidx.ui.test.junit4)\ndebugImplementation(libs.androidx.ui.tooling)\ndebugImplementation(libs.androidx.ui.test.manifest)\n\n// Hilt for testing\nandroidTestImplementation(libs.hilt.android.testing)\nkspAndroidTest(libs.hilt.android.compiler)\n

A p\u00e9ld\u00e1nkban azt fogjuk tesztelni, hogy ha \u00faj teend\u0151 l\u00e9trehoz\u00e1s\u00e1n\u00e1l a d\u00e1tumv\u00e1laszt\u00f3 ikonj\u00e1ra kattintunk, akkor val\u00f3ban el\u0151ugrik a d\u00e1tumv\u00e1laszt\u00f3 komponens. Miel\u0151tt a t\u00e9nyleges tesztet meg\u00edrjuk, gondoskodnunk kell r\u00f3la, hogy a tesztb\u0151l majd a felhaszn\u00e1l\u00f3i fel\u00fcleten a d\u00e1tumv\u00e1laszt\u00f3 ikonj\u00e1t meg tudjuk hivatkozni. Ha lenne rajta megjelen\u00edtett sz\u00f6veg, a tesztb\u0151l ez alapj\u00e1n is lehetne hivatkozni, de jelen esetben csak egy ikonr\u00f3l van sz\u00f3. \u00dagy tudjuk azonos\u00edthat\u00f3v\u00e1 tenni, hogy a Modifier\u00e9n kereszt\u00fcl egy test taggel l\u00e1tjuk el. M\u00f3dos\u00edtsuk eszerint a TodoEditor oszt\u00e1lyban a DatePicker komponens h\u00edv\u00e1s\u00e1t:

DatePicker(\npickedDate = pickedDate,\nonClick = onDatePickerClicked,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction)\n.testTag(\"datePickerIcon\"),\nenabled = enabled\n)\n

Most m\u00e1r elk\u00e9sz\u00edthetj\u00fck a tesztet! Mivel a teszt a CreateTodoScreen oszt\u00e1lyhoz k\u00f6thet\u0151, ez pedig a feature.todo_create package-ben van, el\u0151sz\u00f6r hozzuk l\u00e9tre ezt a package-et az androidTest mapp\u00e1ban is.

Majd k\u00e9sz\u00edts\u00fck el a CreateTodoScreenTest oszt\u00e1lyunkat:

package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport hu.bme.aut.android.todo.MainActivity\nimport org.junit.Rule\nimport org.junit.Test\n\nclass CreateTodoScreenTest {\n@get:Rule\nval composeTestRule = createAndroidComposeRule<MainActivity>()\n\n@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)\n@Test\nfun testDatePickerDialogIsShownWhenClickedOnDatePickerIcon() {\ncomposeTestRule.activity.setContent {\nCreateTodoScreen(\nonNavigateBack = { })\n}\n\ncomposeTestRule.onNodeWithText(\"Select date\").assertIsNotDisplayed()\n\ncomposeTestRule.onNode(hasTestTag(\"datePickerIcon\")).performClick()\n\ncomposeTestRule.onNodeWithText(\"Select date\").assertIsDisplayed()\n}\n}\n

A teszt f\u0151 eleme a createAndroidComposeRule h\u00edv\u00e1s, amely egy teszt rule-t ad vissza. Ezen kereszt\u00fcl renderelhetj\u00fck a k\u00edv\u00e1nt Compose tartalmat, \u00e9s ezen kereszt\u00fcl t\u00f6rt\u00e9nik a k\u00edv\u00e1nt akci\u00f3k kiv\u00e1lt\u00e1sa \u00e9s az elv\u00e1rt eredm\u00e9ny ellen\u0151rz\u00e9se is. Ha valamilyen elemnek a megjelen\u00e9s\u00e9t akarjuk tesztelni, \u00e9rdemes azt is megfogalmazni, hogy a kiv\u00e1ltott interakci\u00f3 el\u0151tt m\u00e9g nincs megjelen\u00edtve.

Futtassuk le a tesztet! Figyelj\u00fck meg, hogy v\u00e9gigk\u00f6vethet\u0151 az emul\u00e1toron is a teszt fut\u00e1sa.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lefutott teszt, a hozz\u00e1 tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/di/#onallo-feladat-1-dependency-injection-befejezese","title":"\u00d6n\u00e1ll\u00f3 feladat 1. - Dependency Injection befejez\u00e9se","text":"

A TodoUseCases oszt\u00e1lyban egyel\u0151re csak a LoadTodosUseCase f\u00fcgg\u0151s\u00e9get hozzuk l\u00e9tre a modulban, \u00e9s injekt\u00e1ljuk a Dagger/Hilt seg\u00edts\u00e9g\u00e9vel, a t\u00f6bbi usecase most is manu\u00e1lisan p\u00e9ld\u00e1nyosodik a repository \u00e1tad\u00e1s\u00e1val. Folytasd az \u00e1talak\u00edt\u00e1st, \u00e9s hozd l\u00e9tre az \u00f6sszes usecase-t a usecase modulban, hogy ut\u00e1na m\u00e1r a Dagger/Hilt kezelje \u0151ket!

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1talak\u00edtott k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/di/#onallo-feladat-2-ujabb-teszt-keszitese","title":"\u00d6n\u00e1ll\u00f3 feladat 2. - \u00dajabb teszt k\u00e9sz\u00edt\u00e9se","text":"

K\u00e9sz\u00edts\u00fcnk egy tesztet arra vonatkoz\u00f3an, hogy ha a priorit\u00e1sv\u00e1laszt\u00f3ra kattintunk, akkor az \u00f6sszes lehets\u00e9ges v\u00e1laszthat\u00f3 priorit\u00e1s megjelenik a k\u00e9perny\u0151n!

Seg\u00edts\u00e9g az implement\u00e1ci\u00f3hoz:

  • Most el\u00e9g a TodoEditor komponensb\u0151l kiindulni, nem sz\u00fcks\u00e9ges a teljes Screen tesztel\u00e9se.
  • A TodoEditor megh\u00edv\u00e1sakor ki kell t\u00f6lten\u00fcnk a k\u00f6telez\u0151 param\u00e9tereit, de mivel ezekre a tesztben nem t\u00e1maszkodunk, ez\u00e9rt \u00fcres sztringeket, \u00fcres f\u00fcggv\u00e9nyeket haszn\u00e1lhatunk helyett\u00fck.
  • L\u00e1ssuk el test taggel a leg\u00f6rd\u00fcl\u0151 list\u00e1t.
  • Fogalmazzuk meg, hogy a v\u00e1lasztott alap\u00e9rtelmezett priorit\u00e1s l\u00e1that\u00f3 a k\u00e9perny\u0151n, de a t\u00f6bbi sz\u00f6vege nem.
  • Emul\u00e1ljunk kattint\u00e1st!
  • Fogalmazzuk meg, hogy most a t\u00f6bbi priorit\u00e1s is megjelent!
  • A teszt a k\u00f6z\u00f6s p\u00e9lda szerint is m\u0171k\u00f6dik, de itt el\u00e9g lehet az egyszer\u0171bb createComposeRule(), majd a composeTestRule.setContent is.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lefutott teszt, a hozz\u00e1 tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/firebase/","title":"Labor07 - Firebase","text":""},{"location":"laborok/firebase/#bevezeto","title":"Bevezet\u0151","text":"

A labor sor\u00e1n a megl\u00e9v\u0151 feladatkezel\u0151 alkalmaz\u00e1s ker\u00fcl tov\u00e1bbfejleszt\u00e9sre a Firebase Backend as a Service (BaaS) felhaszn\u00e1l\u00e1s\u00e1val. A feladat c\u00e9lja, hogy szeml\u00e9ltesse, hogyan lehet k\u00f6z\u00f6s backendet haszn\u00e1l\u00f3 alkalmaz\u00e1st fejleszteni saj\u00e1t backend k\u00f3d fejleszt\u00e9se n\u00e9lk\u00fcl.

A Firebase manaps\u00e1g az egyik legn\u00e9pszer\u0171bb Backend as a Service megold\u00e1s Android, iOS \u00e9s web kliensek t\u00e1mogat\u00e1s\u00e1val, mely sz\u00e1mos szolg\u00e1ltat\u00e1st biztos\u00edt, p\u00e9ld\u00e1ul: - real-time adatb\u00e1ziskezel\u00e9s - storage - authentik\u00e1ci\u00f3 - push \u00e9rtes\u00edt\u00e9sek - analytics - crash reporting

Tov\u00e1bbi \u00e1ltal\u00e1nos inform\u00e1ci\u00f3k a Firebase-r\u0151l: https://firebase.google.com/.

A laborfoglalkoz\u00e1s c\u00e9lja, hogy bemutassa a Firebase legfontosabb szolg\u00e1ltat\u00e1sait egy komplett alkalmaz\u00e1s megval\u00f3s\u00edt\u00e1sa keret\u00e9ben. Az alkalmaz\u00e1s az al\u00e1bbi f\u0151 funkci\u00f3kat t\u00e1mogatja: - regisztr\u00e1ci\u00f3, bejelentkez\u00e9s - \u00fczenetek list\u00e1z\u00e1sa - \u00fczenet\u00edr\u00e1s - k\u00e9pek csatol\u00e1sa \u00fczenetekhez - \u00fczenetek megjelen\u00edt\u00e9se val\u00f3s id\u0151ben - crash reporting - analitika

Az anyag r\u00e9szletes meg\u00e9rt\u00e9s\u00e9hez javasoljuk, hogy figyelje a laborvezet\u0151 utas\u00edt\u00e1sait \u00e9s labor ut\u00e1n is 10-20 percet sz\u00e1njon a k\u00f3dr\u00e9szek meg\u00e9rt\u00e9s\u00e9re.

"},{"location":"laborok/firebase/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/firebase/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

A Firebase megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektbe fogjuk integr\u00e1lni a k\u00fcl\u00f6nb\u00f6z\u0151 szolg\u00e1ltat\u00e1sokat. Ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl is, valamilyen probl\u00e9ma eset\u00e9n a kezd\u0151projektet err\u0151l a linkr\u0151l \u00e9rhet\u0151 el.

Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a kicsomagolt projektet.

FILE PATH

A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

"},{"location":"laborok/firebase/#projekt-elokeszitese-konfiguracio","title":"Projekt el\u0151k\u00e9sz\u00edt\u00e9se, konfigur\u00e1ci\u00f3","text":"

Els\u0151 l\u00e9p\u00e9sk\u00e9nt l\u00e9tre kell hozni egy Firebase projektet a Firebase admin fel\u00fclet\u00e9n (Firebase console), majd egy Android Studio projektet \u00e9s a kett\u0151t \u00f6ssze kell k\u00f6tni: - Navig\u00e1ljunk a Firebase console fel\u00fclet\u00e9re: https://console.firebase.google.com/ ! - Jelentkezz\u00fcnk be! - Hozzunk l\u00e9tre egy \u00faj projektet a Create project elemet v\u00e1lasztva!

  • A projekt neve legyen BMETodoNEPTUN_KOD, ahol a NEPTUN_KOD hely\u00e9re a saj\u00e1t Neptun k\u00f3dunkat helyettes\u00edts\u00fck!
  • Az analitik\u00e1t most m\u00e9g nem sz\u00fcks\u00e9ges konfigur\u00e1lni.

projekt n\u00e9v

A Neptun k\u00f3dra az\u00e9rt van sz\u00fcks\u00e9g, mert ugyanazon laborg\u00e9p kulcs\u00e1val ugyanolyan nev\u0171 projektet nem hozhatunk l\u00e9tre t\u00f6bbsz\u00f6r, \u00e9s t\u00f6bb laborcsoport l\u00e9v\u00e9n ebb\u0151l probl\u00e9ma ad\u00f3dhatna. Ugyanerre lesz majd sz\u00fcks\u00e9g a package n\u00e9v eset\u00e9n is.

Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n fuss\u00e1k \u00e1t a laborvezet\u0151vel k\u00f6z\u00f6sen a Firebase console fel\u00fclet\u00e9t az al\u00e1bbi elemekre kit\u00e9rve: - Authentication, Firestore \u00e9s Storage.

N\u00e9zz\u00fck \u00e1t a megnyitott projektet! K\u00fcl\u00f6n\u00f6s figyelemmel vizsg\u00e1ljuk \u00e1t a projekt mostani fel\u00e9p\u00edt\u00e9s\u00e9t, az \u00faj r\u00e9szeket (todo_auth, data package), illetve hogyan lehets\u00e9ges ebben \u00e1t\u00e1llni a Firebase szolg\u00e1ltat\u00e1saira.

Adjuk hozz\u00e1 az AndroidManifest.xml f\u00e1jlhoz az internet haszn\u00e1lati enged\u00e9lyt:

<uses-permission android:name=\"android.permission.INTERNET\" />\n
"},{"location":"laborok/firebase/#firebase-inicilaizacio-authentication","title":"Firebase inicilaiz\u00e1ci\u00f3, Authentication","text":"

Ezek ut\u00e1n v\u00e1lasszuk Android Studioban a Tools -> Firebase men\u00fcpontot, melynek hat\u00e1s\u00e1ra jobb oldalt megny\u00edlik a Firebase Assistant funkci\u00f3.

A Firebase Assistant akkor fogja megtal\u00e1lni a Firebase console-on l\u00e9trehozott projektet, ha Android Studioba is ugyanazzal a Google accounttal vagyunk bejelentkezve, mint amivel a console-on l\u00e9trehoztuk a projektet. Ellen\u0151rizz\u00fck ezt mindk\u00e9t helyen! Amennyiben a Firebase Assistant-ot nem siker\u00fcl be\u00fczemelni, manu\u00e1lisan is \u00f6sszek\u00f6thet\u0151 a k\u00e9t projekt. A le\u00edr\u00e1sban ismertetni fogjuk a l\u00e9p\u00e9seket, amelyeket az Assistant v\u00e9gez el.

V\u00e1lasszuk az Assistant-ban az Authentication szakaszt \u00e9s azon bel\u00fcl az Authenticate using a custom authentication system-t, majd a Connect to Firebase gombot. Ezt k\u00f6vet\u0151en egy dialog ny\u00edlik meg, ahol ha megfelel\u0151ek az accountok, a m\u00e1sodik szakaszt (Choose an existing Firebase or Google project) v\u00e1lasztva kiv\u00e1laszthatjuk a projektet, amit a Firebase console-on m\u00e1r l\u00e9trehoztunk. Itt egy\u00e9bk\u00e9nt lehet\u0151s\u00e9g van \u00faj projektet is l\u00e9trehozni. (Ha els\u0151re hib\u00e1t l\u00e1tunk a projekttel val\u00f3 \u00f6sszekapcsol\u00e1sn\u00e1l, pr\u00f3b\u00e1ljuk \u00fajra, m\u00e1sodszorra \u00e1ltal\u00e1ban sikeresen megt\u00f6rt\u00e9nik az Android Studio projekt szinkroniz\u00e1l\u00e1sa a Firebase projekttel.)

A h\u00e1tt\u00e9rben val\u00f3j\u00e1ban annyi t\u00f6rt\u00e9nik, hogy az alkalmaz\u00e1sunk package neve \u00e9s az al\u00e1\u00edr\u00f3 kulcs SHA-1 hash-e alapj\u00e1n hozz\u00e1ad\u00f3dik egy Android alkalmaz\u00e1s a Firebase console-on l\u00e9v\u0151 projekt\u00fcnkh\u00f6z, \u00e9s az ahhoz tartoz\u00f3 konfigur\u00e1ci\u00f3s (google-services.json) f\u00e1jl let\u00f6lt\u0151dik a projekt\u00fcnk k\u00f6nyvt\u00e1r\u00e1ba az alap\u00e9rtelmezett (app) modul al\u00e1.

Ezt a l\u00e9p\u00e9ssorozatot manu\u00e1lisan is v\u00e9grehajthatjuk a Firebase console-on az Add Firebase to your Android app-et v\u00e1lasztva. A debug kulcs SHA-1 lenyomata ilyenkor a jobb oldalon tal\u00e1lhat\u00f3 Gradle f\u00fcl\u00f6n gener\u00e1lhat\u00f3. A fenti sorban kattintsunk az Execute Gradle Task men\u00fcpontra, majd a felugr\u00f3 ablakban \u00cdrjuk be a gradle signingreport-ot, \u00e9s nyomjunk egy entert. Ezek ut\u00e1naz als\u00f3 Run ablakban megtal\u00e1lhat\u00f3 az SHA-1 kulcs.

K\u00f6vetkez\u0151 l\u00e9p\u00e9sben szint\u00e9n az Assistant-ban az Authenticate using a custom authentication system alatt v\u00e1lasszuk az Add the Firebase Authentication SDK to your app elemet, itt l\u00e1that\u00f3 is, hogy milyen m\u00f3dos\u00edt\u00e1sok t\u00f6rt\u00e9nnek a projekt \u00e9s modul szint\u0171 build.gradle.kts f\u00e1jlokban.

Sajnos a Firebase plugin nincs rendszeresen friss\u00edtve, \u00e9s \u00edgy el\u0151fordul, hogy a f\u00fcgg\u0151s\u00e9gek r\u00e9gi verzi\u00f3j\u00e1t adja hozz\u00e1 a build.gradle.kts f\u00e1jlokhoz. Ez\u00e9rt most friss\u00edteni fogjuk az im\u00e9nt automatikusan felvett f\u00fcgg\u0151s\u00e9geket, valamint innent\u0151l manu\u00e1lisan fogjuk hozz\u00e1adni az \u00fajabbakat az Assistant haszn\u00e1lata helyett. Fontos, hogy mindenb\u0151l az itt le\u00edrt verzi\u00f3t haszn\u00e1ljuk.

Ellen\u0151rizz\u00fck a projekt szint\u0171 build.gradle f\u00e1jlban a google-services-t, hogy az al\u00e1bbi verzi\u00f3val rendelkezik:

classpath 'com.google.gms:google-services:4.4.2'\n

A Firebase BoM seg\u00edts\u00e9g\u00e9vel egys\u00e9gesen tudjuk kezelni az \u00f6sszes firebase k\u00f6nyvt\u00e1runk verzi\u00f3sz\u00e1m\u00e1t.

Cser\u00e9lj\u00fck le a modul szint\u0171 build.gradle-ben a firebase-auth verzi\u00f3t a k\u00f6vetkez\u0151re:

val firebaseBom = platform(\"com.google.firebase:firebase-bom:33.5.1\")\nimplementation(firebaseBom)\nimplementation(\"com.google.firebase:firebase-auth-ktx\")\n

A gener\u00e1lt projektv\u00e1z t\u00f6bbi \u00e1ltal\u00e1nos f\u00fcgg\u0151s\u00e9ge (pl. appcompat \u00e9s ktx-core k\u00f6nyvt\u00e1rak) is elavult lehet, ezt az Android Studio jelzi is s\u00f6t\u00e9ts\u00e1rga h\u00e1tt\u00e9rrel. Ezekre r\u00e1\u00e1llva a kurzorral az Alt-Enter gyorsbillenyt\u0171vel kiv\u00e1laszthatjuk ezeknek a friss\u00edt\u00e9s\u00e9t.

Ahhoz, hogy az e-mail alap\u00fa regisztr\u00e1ci\u00f3 \u00e9s authentik\u00e1ci\u00f3 megfelel\u0151en m\u0171k\u00f6dj\u00f6n, a Firebase console-ban az Authentication -> Sign-in method alatt az Email/Password providert enged\u00e9lyezni kell.

K\u00e9sz\u00edts\u00fck el a megfelel\u0151 Service oszt\u00e1lyt. Hozzunk l\u00e9tre a data/auth package-en bel\u00fcl a FirebaseAuthService oszt\u00e1lyt! Val\u00f3s\u00edtsuk meg az AuthService interf\u00e9sz egyes met\u00f3dusait! Ehhez sz\u00fcks\u00e9g\u00fcnk lesz egy FirebaseAuth objektumra, melyet k\u00fcls\u0151 forr\u00e1sb\u00f3l fogunk megkapni:

package hu.bme.aut.android.todo.data.auth  import com.google.firebase.auth.FirebaseAuth  import com.google.firebase.auth.UserProfileChangeRequest  import hu.bme.aut.android.todo.domain.model.User  import kotlinx.coroutines.channels.awaitClose  import kotlinx.coroutines.flow.Flow  import kotlinx.coroutines.flow.callbackFlow  import kotlinx.coroutines.tasks.await\n\nclass FirebaseAuthService(private val firebaseAuth: FirebaseAuth) : AuthService {\noverride val currentUserId: String? get() = firebaseAuth.currentUser?.uid\noverride val hasUser: Boolean get() = firebaseAuth.currentUser != null\noverride val currentUser: Flow<User?> get() = callbackFlow {\nthis.trySend(currentUserId?.let { User(it) })\nval listener =\nFirebaseAuth.AuthStateListener { auth ->\nthis.trySend(auth.currentUser?.let { User(it.uid) })\n}\nfirebaseAuth.addAuthStateListener(listener)\nawaitClose { firebaseAuth.removeAuthStateListener(listener) }\n}\n\n\noverride suspend fun signUp(email: String, password: String) {\nfirebaseAuth.createUserWithEmailAndPassword(email,password)\n.addOnSuccessListener {  result ->\nval user = result.user\nval profileChangeRequest = UserProfileChangeRequest.Builder()\n.setDisplayName(user?.email?.substringBefore('@'))\n.build()\nuser?.updateProfile(profileChangeRequest)\n}.await()\n}\n\noverride suspend fun authenticate(email: String, password: String) {\nfirebaseAuth.signInWithEmailAndPassword(email, password).await()\n}\n\noverride suspend fun sendRecoveryEmail(email: String) {\nfirebaseAuth.sendPasswordResetEmail(email).await()\n}\n\noverride suspend fun deleteAccount() {\nfirebaseAuth.currentUser!!.delete().await()\n}\n\noverride suspend fun signOut() {\nfirebaseAuth.signOut()\n}\n}\n
N\u00e9zz\u00fck \u00e1t, hogyan tudtuk az \u00e1ltalunk defini\u00e1lt AuthService interf\u00e9szhez illeszteni a Firebase \u00e1ltal biztos\u00edtott API-t!

Sokszor a biztos\u00edtott API k\u00f6zvetlen\u00fcl megfeleltethet\u0151 az \u00e1ltalunk defini\u00e1lt szolg\u00e1ltat\u00e1sokkal, mint p\u00e9ld\u00e1ul a currentUser vagy hasUser mez\u0151kn\u00e9l. Itt egyed\u00fcl arra kell figyeln\u00fcnk, hogy ne maradjon le a get() defin\u00edci\u00f3, akkor ugyanis a Service l\u00e9trej\u00f6ttekor t\u00f6rt\u00e9nne egy \u00e9rt\u00e9kad\u00e1s, nem pedig minden egyes kiolvas\u00e1sn\u00e1l egy f\u00fcggv\u00e9nyh\u00edv\u00e1s.

Ha szerencs\u00e9nk van, akkor a felhaszn\u00e1lt API be\u00e9p\u00edtetten t\u00e1mogatni fogja a Kotlin k\u00fcl\u00f6nb\u00f6z\u0151 funkcionalit\u00e1sait, mint p\u00e9ld\u00e1ul a Kotlinos t\u00edpusok, contract-ok \u00e9s coroutine-ok. Sokszor viszont Java alap\u00fa k\u00f6nyvt\u00e1rakat kapunk, amiket nek\u00fcnk kell adapt\u00e1lni Kotlin k\u00f6rnyezetre. Erre egy j\u00f3 p\u00e9lda a currentUser mez\u0151. Ez a callbackFlow met\u00f3dust haszn\u00e1lja, melynek seg\u00edts\u00e9g\u00e9vel a callback jelleg\u0171 API-t tudjuk \u00e1talak\u00edtani Flow jelleg\u0171 eredm\u00e9nny\u00e9. A blokkon bel\u00fcl egy listenert regisztr\u00e1lunk be, mellyel meg tudjuk figyelni, ha v\u00e1ltoz\u00e1s t\u00f6rt\u00e9nik az aktu\u00e1lis felhaszn\u00e1l\u00f3k k\u00f6r\u00e9ben. Ekkor a trySend() met\u00f3dussal tudjuk a Flow-ra feliratkoz\u00f3 fel\u00e9 elk\u00fcldeni az \u00faj felhaszn\u00e1l\u00f3 adatait. \u00c9rdemes arra is figyelni, hogy ez a listener csak a feliratkoz\u00e1s ut\u00e1n kezd el \u00e9rt\u00e9keket kik\u00fcldeni. Annak \u00e9rdek\u00e9ben, hogy a UI egyb\u0151l megkapja az aktu\u00e1lis \u00e9rt\u00e9ket, a feliratkoz\u00e1s el\u0151tt kik\u00fcldj\u00fck az aktu\u00e1lis felhaszn\u00e1l\u00f3 adatait is.

A t\u00f6bbi utas\u00edt\u00e1sn\u00e1l a Firebase egy Task t\u00edpus\u00fa eredm\u00e9ny objektummal t\u00e9r vissza. A Java vil\u00e1g\u00e1ban erre fel tudunk iratkozni, \u00e9s az eredm\u00e9ny\u00e9t egy Callback-ben le tudjuk kezelni. Szerencs\u00e9re azonban a Firebase \u00e1ltal\u00e1nos k\u00f6nytv\u00e1r\u00e1ban megtal\u00e1lhat\u00f3 egy Kotlin kieg\u00e9sz\u00edt\u0151 met\u00f3dus, mellyel a Task bev\u00e1rhat\u00f3 coroutine kontextusban az await() kulcssz\u00f3val. A teljess\u00e9g kedv\u00e9\u00e9rt n\u00e9zz\u00fck meg az al\u00e1bbi p\u00e9ld\u00e1t, hogyan lehet egy Callback jelleg\u0171 API-t \u00e1talak\u00edtani suspend fajt\u00e1j\u00fa f\u00fcggv\u00e9nny\u00e9:

override suspend fun authenticate(email: String, password: String) = suspendCoroutine { continuation ->\nfirebaseAuth\n.signInWithEmailAndPassword(email, password)\n.addOnSuccessListener { continuation.resume(Unit) }\n.addOnFailureListener { continuation.resumeWithException(it) }\n}\n

A suspendCoroutine met\u00f3dus le fogja futtatni a benne megadott blokkot, majd addig v\u00e1rakoztatja a coroutine-t, ameddig a blokkban megkapott Continuation objektumon kereszt\u00fcl nem jelezz\u00fck a h\u00edv\u00e1s v\u00e9geredm\u00e9ny\u00e9t. Ezzel a f\u00fcggv\u00e9nnyel k\u00f6nnyed\u00e9n \u00e1t tudjuk alak\u00edtani a Callback jelleg\u0171 m\u0171k\u00f6d\u00e9seket suspend alap\u00fara. Arra azonban figyelj\u00fcnk, hogy minden esetben megh\u00edv\u00f3djon a Continuation valamelyik resume met\u00f3dusa, ellenkez\u0151 esetben ugyanis befagy az adott coroutine, sose fog tudni tov\u00e1bbl\u00e9pni. Hasonl\u00f3an hasznos f\u00fcggv\u00e9ny a suspendCancellableCoroutine, mellyel azokat az eseteket is le tudjuk kezelni, ha a coroutine-t a folyamat k\u00f6zben t\u00f6rlik.

\u00c1ll\u00edtsuk \u00e1t az alkalmaz\u00e1sunkat, hogy ezt az \u00faj FirebaseAuthService-t haszn\u00e1lja! Ehhez m\u00f3dos\u00edtsuk a TodoApplication oszt\u00e1lyunkat:

package hu.bme.aut.android.todo  import android.app.Application  import com.google.firebase.auth.FirebaseAuth  import hu.bme.aut.android.todo.data.auth.AuthService  import hu.bme.aut.android.todo.data.auth.FirebaseAuthService  import hu.bme.aut.android.todo.data.todos.MemoryTodoService  import hu.bme.aut.android.todo.data.todos.TodoService\n\nclass TodoApplication : Application(){\noverride fun onCreate() {\nsuper.onCreate()\nauthService = FirebaseAuthService(FirebaseAuth.getInstance())\ntodoService = MemoryTodoService()\n}\n\ncompanion object{\nlateinit var authService: AuthService\nlateinit var todoService: TodoService\n}\n}\n

Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Hozzunk l\u00e9tre egy \u00faj felhaszn\u00e1l\u00f3t!

jelsz\u00f3

Ugyan nem kapunk semmi visszajelz\u00e9st, de a Firebase nem fogad el 6 karaktern\u00e9l r\u00f6videbb jelsz\u00f3t. \u00cdgy amennyiben r\u00f6vid a jelszavunk, \u00fagy t\u0171nhet, hogy a gombnyom\u00e1s hat\u00e1s\u00e1ra nem t\u00f6rt\u00e9nik semmi, nem m\u0171k\u00f6dik a regisztr\u00e1ci\u00f3. Ilyenkor ellen\u0151rizz\u00fck, hogy mindenk\u00e9ppen legal\u00e1bb 6 hossz\u00fa jelsz\u00f3t adtunk-e meg.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik Firebase Authentication oldal\u00e1n a beregisztr\u00e1lt felhaszn\u00e1l\u00f3, illetve a FirebaseAuthService forr\u00e1sk\u00f3dja, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/firebase/#feladatok-listazasa-keszitese","title":"Feladatok list\u00e1z\u00e1sa, k\u00e9sz\u00edt\u00e9se","text":"

K\u00f6vetkez\u0151 l\u00e9p\u00e9sben a feladatok list\u00e1z\u00e1s\u00e1t fogjuk implement\u00e1lni a projekten bel\u00fcl.

Adjuk hozz\u00e1 a projekthez a Cloud Firestore t\u00e1mogat\u00e1st:

implementation(\"com.google.firebase:firebase-firestore-ktx\")\n

Kapcsoljuk be a Cloud Firestore-t a Firebase console-on is . Az adatb\u00e1zist test mode-ban fogjuk haszn\u00e1lni, \u00edgy egyel\u0151re publikusan \u00edrhat\u00f3/olvashat\u00f3 lesz, de cser\u00e9be nem kell konfigur\u00e1lnunk a hozz\u00e1f\u00e9r\u00e9s-szab\u00e1lyoz\u00e1st. Ezt term\u00e9szetesen k\u00e9s\u0151bb mindenk\u00e9pp meg kellene tenni egy \u00e9les projektben.

Locationnek v\u00e1lasszunk egy hozz\u00e1nk k\u00f6zel es\u0151 opci\u00f3t.

Hozzuk l\u00e9tre a todos package-en bel\u00fcl a firebase package-et. Ebben k\u00e9t oszt\u00e1lyt fogunk defini\u00e1lni: a Firestore-ban t\u00e1rolt adatobjektum oszt\u00e1ly modellj\u00e9t, illetve a kommunik\u00e1ci\u00f3t megval\u00f3s\u00edt\u00f3 service k\u00f3dj\u00e1t.

Hozzuk el\u0151sz\u00f6r l\u00e9tre az adatot reprezent\u00e1l\u00f3 oszt\u00e1lyt FirebaseTodo n\u00e9ven:

package hu.bme.aut.android.todo.data.todos.firebase\n\nimport com.google.firebase.Timestamp\nimport com.google.firebase.firestore.DocumentId\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.datetime.*\nimport java.time.Instant\nimport java.time.LocalDateTime\nimport java.time.ZoneId\nimport java.util.Date\n\ndata class FirebaseTodo(\n@DocumentId val id: String = \"\",\nval title: String = \"\",\nval priority: Priority = Priority.NONE,\nval dueDate: Timestamp = Timestamp.now(),\nval description: String = \"\"\n)\n\nfun FirebaseTodo.asTodo() = Todo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = LocalDateTime\n.ofInstant(Instant.ofEpochSecond(dueDate.seconds), ZoneId.systemDefault())\n.toKotlinLocalDateTime()\n.date,\ndescription = description,\n)\n\nfun Todo.asFirebaseTodo() = FirebaseTodo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = Timestamp(Date.from(dueDate.atStartOfDayIn(TimeZone.currentSystemDefault()).toJavaInstant())),\ndescription = description,\n)\n
Ebben a f\u00e1jlban defini\u00e1ltuk a k\u00e9t \u00e1talak\u00edt\u00f3 f\u00fcggv\u00e9nyt is, mellyel a Firebase \u00e9s az alkalmaz\u00e1s t\u00f6bbi r\u00e9sz\u00e9ben haszn\u00e1lt Todo oszt\u00e1ly k\u00f6z\u00f6tt tudunk \u00e1talak\u00edtani. Az egyed\u00fcli bonyolult r\u00e9sz a Firebase \u00e1ltal haszn\u00e1lt Timestamp oszt\u00e1ly haszn\u00e1lata az id\u0151pont elt\u00e1rol\u00e1s\u00e1ra, erre most r\u00e9szletesen nem t\u00e9r\u00fcnk ki.

Hozzuk l\u00e9tre a feladatok t\u00e1rol\u00e1s\u00e1t v\u00e9gz\u0151 FirebaseTodoService oszt\u00e1lyt is ebben a package-ben:

package hu.bme.aut.android.todo.data.todos.firebase\n\nimport com.google.firebase.firestore.FirebaseFirestore\nimport com.google.firebase.firestore.ktx.snapshots\nimport com.google.firebase.firestore.ktx.toObjects\nimport com.google.firebase.firestore.ktx.toObject\nimport hu.bme.aut.android.todo.data.auth.AuthService\nimport hu.bme.aut.android.todo.data.todos.TodoService\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.tasks.await\n\nclass FirebaseTodoService(\nprivate val firestore: FirebaseFirestore,\nprivate val authService: AuthService\n) : TodoService {\n\noverride val todos: Flow<List<Todo>> = authService.currentUser.flatMapLatest { user ->\nif (user == null) flow { emit(emptyList()) }\nelse currentCollection(user.id)\n.snapshots()\n.map { snapshot ->\nsnapshot\n.toObjects<FirebaseTodo>()\n.map {\nit.asTodo()\n}\n}\n}\n\noverride suspend fun getTodo(id: String): Todo? =\nauthService.currentUserId?.let {\ncurrentCollection(it).document(id).get().await().toObject<FirebaseTodo>()?.asTodo()\n}\n\noverride suspend fun saveTodo(todo: Todo) {\nauthService.currentUserId?.let {\ncurrentCollection(it).add(todo.asFirebaseTodo()).await()\n}\n}\n\noverride suspend fun updateTodo(todo: Todo) {\nauthService.currentUserId?.let {\ncurrentCollection(it).document(todo.id).set(todo.asFirebaseTodo()).await()\n}\n}\n\noverride suspend fun deleteTodo(id: String) {\nauthService.currentUserId?.let {\ncurrentCollection(it).document(id).delete().await()\n}\n}\n\nprivate fun currentCollection(userId: String) =\nfirestore.collection(USER_COLLECTION).document(userId).collection(TODO_COLLECTION)\n\ncompanion object {\nprivate const val USER_COLLECTION = \"users\"\nprivate const val TODO_COLLECTION = \"todos\"\n}\n}\n
V\u00e9g\u00fcl ne felejts\u00fck el befriss\u00edteni a TodoApplication oszt\u00e1lyunkat, hogy a Firestoreban t\u00e1rolt feladatokat haszn\u00e1lja az alkalmaz\u00e1s:

package hu.bme.aut.android.todo\n\nimport android.app.Application\nimport com.google.firebase.auth.FirebaseAuth\nimport com.google.firebase.firestore.FirebaseFirestore\nimport hu.bme.aut.android.todo.data.auth.AuthService\nimport hu.bme.aut.android.todo.data.auth.FirebaseAuthService\nimport hu.bme.aut.android.todo.data.todos.TodoService\nimport hu.bme.aut.android.todo.data.todos.firebase.FirebaseTodoService\n\nclass TodoApplication : Application(){\noverride fun onCreate() {\nsuper.onCreate()\nauthService = FirebaseAuthService(FirebaseAuth.getInstance())\ntodoService = FirebaseTodoService(FirebaseFirestore.getInstance(), authService)\n}\n\ncompanion object{\nlateinit var authService: AuthService\nlateinit var todoService: TodoService\n}\n}\n

Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1sunkat! Ellen\u0151rizz\u00fck, hogy t\u00e9nyleg l\u00e9trej\u00f6nnek az adatb\u00e1zisban is a feladatok.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik Firebase Firestore oldal\u00e1n a l\u00e9trehozott feladat, illetve a fut\u00f3 alkalmaz\u00e1s, melyben az egyik l\u00e9trehozott feladat tartalmazza a Neptun-k\u00f3dot. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

Messaging, Crashlytics, Analytics

A k\u00f6vezkez\u0151 technol\u00f3gi\u00e1k \u00e1tfut\u00e1si ideje sajnos hosszabb, \u00edgy az eredm\u00e9nyre nem ritk\u00e1n \u00f3r\u00e1kat is v\u00e1rni kell. (A notificationnek n\u00e9h\u00e1ny perc alatt meg kell j\u00f6nnie.)

"},{"location":"laborok/firebase/#push-ertesitesek","title":"Push \u00e9rtes\u00edt\u00e9sek","text":"

Adjuk hozz\u00e1 a projekt\u00fcnkh\u00f6z a Firebase Messaging f\u00fcgg\u0151s\u00e9get:

implementation(\"com.google.firebase:firebase-messaging-ktx\")\n

Csup\u00e1n ennyi elegend\u0151 a push alapvet\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez, ha \u00edgy \u00fajraford\u00edtjuk az alkalmaz\u00e1st, a Firebase fel\u00fclet\u00e9r\u0151l vagy API-j\u00e1val k\u00fcld\u00f6tt push \u00fczeneteket automatikusan megkapj\u00e1k a mobil kliensek \u00e9s egy Notification-ben megjelen\u00edtik.

Pr\u00f3b\u00e1ljuk ki a push k\u00fcld\u00e9st a Firebase console-r\u00f3l (Cloud messaging men\u00fcpont alatt Send your first message), \u00e9s vizsg\u00e1ljuk meg, hogyan \u00e9rkezik meg telefonra, ha nem fut az alkalmaz\u00e1s. (Amikor fut az alkalmaz\u00e1s, akkor t\u0151l\u00fcnk v\u00e1rja az \u00fczenet lekezel\u00e9s\u00e9t az API.) A Notification szekci\u00f3 alatt \u00edrjuk be az \u00fczenet c\u00edm\u00e9t \u00e9s sz\u00f6veg\u00e9t, a Target r\u00e9szn\u00e9l pedig v\u00e1lasszuk ki az alkalmaz\u00e1st, hogy minden fut\u00f3 p\u00e9ld\u00e1ny megkapja az \u00fczenetet.

Term\u00e9szetesen lehet\u0151s\u00e9g van saj\u00e1t push \u00fczenet feldolgoz\u00f3 szolg\u00e1ltat\u00e1s k\u00e9sz\u00edt\u00e9s\u00e9re is egy FirebaseMessagingService l\u00e9trehoz\u00e1s\u00e1val, melyr\u0151l tov\u00e1bbi r\u00e9szletek itt olvashat\u00f3k.

"},{"location":"laborok/firebase/#crashlytics","title":"Crashlytics","text":"

A Firebase Console-on el\u0151sz\u00f6r navig\u00e1ljunk a Crashlytics men\u00fcpontra, \u00e9s kapcsoljuk be a funkci\u00f3t. V\u00e1lasszuk az \u00faj Firebase alkalmaz\u00e1s integr\u00e1ci\u00f3j\u00e1t.

Adjuk hozz\u00e1 a projekthez a f\u00fcgg\u0151s\u00e9geket a projekt szint\u0171 build.gradle.kts f\u00e1jlba:

id(\"com.google.firebase.crashlytics\") version \"3.0.2\" apply false\n

Ezekkel a m\u00f3dos\u00edt\u00e1sokkal egy Gradle plugint adtunk hozz\u00e1 a projekt\u00fcnkh\u00f6z, amit a modul szint\u0171 build.gradle f\u00e1jl elej\u00e9n be kell kapcsolnunk a m\u00e1r megl\u00e9v\u0151k ut\u00e1n:

id(\"com.google.firebase.crashlytics\")\n

V\u00e9g\u00fcl pedig sz\u00fcks\u00e9g\u00fcnk van k\u00e9t egyszer\u0171 Gradle f\u00fcgg\u0151s\u00e9gre is, amit a megl\u00e9v\u0151 Firebase f\u00fcgg\u0151s\u00e9gek mell\u00e9 helyezhet\u00fcnk, a modul szint\u0171 build.gradle f\u00e1jlban:

implementation(\"com.google.firebase:firebase-crashlytics-ktx\")\nimplementation(\"com.google.firebase:firebase-analytics-ktx\")\n

Vegy\u00fcnk fel egy \u00faj akci\u00f3t TodosScreen TodoAppBar r\u00e9sz\u00e9be, amivel az alkalmaz\u00e1st hib\u00e1val be tudjuk z\u00e1rni:

TodoAppBar(\ntitle = stringResource(id = StringResources.app_bar_title_todos),\nactions = {\nIconButton(onClick = {\nviewModel.signOut()\nonSignOut()\n}) {\nIcon(imageVector = Icons.Default.Logout, contentDescription = null)\n}\nIconButton(onClick = {\nthrow RuntimeException(\"Test crash!\")\n}) {\nIcon(imageVector = Icons.Default.Close, contentDescription = null)\n}\n}\n)\n

V\u00e9g\u00fcl a Firebase console-ban is enged\u00e9lyezz\u00fck a funkci\u00f3t a Crashlytics men\u00fcpont alatt.

Pr\u00f3b\u00e1ljuk ki saj\u00e1t hibajelz\u00e9sek k\u00e9sz\u00edt\u00e9s\u00e9t a men\u00fc esem\u00e9nykezel\u0151j\u00e9ben. Vizsg\u00e1ljuk meg, meg\u00e9rkezik-e a Firebase Console-ba a hiba\u00fczenet!

"},{"location":"laborok/firebase/#analitika","title":"Analitika","text":"

Most enged\u00e9lyezz\u00fck az analitik\u00e1t a Firebase console Analytics men\u00fcpontja alatt!

Az SDK-t m\u00e1r be\u00e1ll\u00edtottuk, ez\u00e9rt a megfelel\u0151 Account kiv\u00e1laszt\u00e1sa ut\u00e1n a Nextre, majd a Finishre kattinhatunk.

Ezut\u00e1n az alkalmaz\u00e1s m\u00e1r napl\u00f3z alapvet\u0151 analitik\u00e1kat, haszn\u00e1lati statisztik\u00e1kat, melyek ugyanezen men\u00fcpont alatt lesznek el\u00e9rhet\u0151k.

Emellett term\u00e9szetesen lehet\u0151s\u00e9g van az analitika kib\u0151v\u00edt\u00e9s\u00e9re \u00e9s testreszab\u00e1s\u00e1ra is. Az ehhez sz\u00fcks\u00e9ges Firebase f\u00fcgg\u0151s\u00e9get a Crashlyticsn\u00e9l m\u00e1r felvett\u00fck.

K\u00e9sz\u00edts\u00fcnk saj\u00e1t analitika \u00fczeneteket egy \u00fajabb akci\u00f3b\u00f3l k\u00fcldve, ami szint\u00e9n a TopAppBar-ba ker\u00fcl:

IconButton(onClick = {\n    val bundle = Bundle()\n    bundle.putString(\"demo_key\", \"idabc\")\n    bundle.putString(\"data_key\", \"mydata\")\n\n    FirebaseAnalytics.getInstance(context)\n        .logEvent(FirebaseAnalytics.Event.LOGIN, bundle)\n}) {\n    Icon(imageVector = Icons.Default.Message, contentDescription = null)\n}\n

Fontos kiemelni, hogy nem garant\u00e1lt, hogy az analitika val\u00f3s id\u0151ben l\u00e1tszik a Firebase console-on. 30 percig vagy ak\u00e1r tov\u00e1bb is tarthat, mire egy-egy esem\u00e9ny itt megjelenik.

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, illetve a k\u00e9t \u00faj akci\u00f3gomb forr\u00e1sk\u00f3dja, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/firebase/#onallo-feladatok","title":"\u00d6n\u00e1ll\u00f3 feladatok","text":""},{"location":"laborok/firebase/#automatikus-bejelentkezes","title":"Automatikus bejelentkez\u00e9s","text":"

Val\u00f3s\u00edtsuk meg, hogy a bejelentkez\u0151 k\u00e9perny\u0151 helyett egyb\u0151l a lista oldalra ugorjunk, ha a felhaszn\u00e1l\u00f3 kijelentkez\u00e9s helyett csak bez\u00e1rta az alkalmaz\u00e1st!

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, az automatikus bejelentkez\u00e9st megval\u00f3s\u00edt\u00f3 forr\u00e1sk\u00f3d, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/firebase/#navigacio-esemeny-jelzese","title":"Navig\u00e1ci\u00f3 esem\u00e9ny jelz\u00e9se","text":"

K\u00fcldj\u00fcnk egy analitikai esem\u00e9nyt abban az esetben, ha a felhaszn\u00e1l\u00f3 megnyitja valamelyik feladat\u00e1t! Az esem\u00e9ny tartalmazza a feladat azonos\u00edt\u00f3j\u00e1t is. Figyelj\u00fcnk arra, hogy megfelel\u0151 n\u00e9vvel k\u00fcldj\u00fck el az esem\u00e9nyt!

BEADAND\u00d3 (1 pont)

K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, a navig\u00e1ci\u00f3 sor\u00e1n az esem\u00e9nyt kik\u00fcld\u0151 forr\u00e1sk\u00f3d, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

"},{"location":"laborok/network/","title":"Labor09 - Network \u00e9s Paging (Unsplash)","text":""},{"location":"laborok/network/#bevezetes","title":"Bevezet\u00e9s","text":"

Ezen a laboron megismerked\u00fcnk Android a h\u00e1l\u00f3zati h\u00edv\u00e1sok elv\u00e9gz\u00e9s\u00e9nek egyik legelterjedtebb megval\u00f3s\u00edt\u00e1s\u00e1val (Retrofit), a Paging Library-vel, illetve azzal, hogy a Compose keretrendszerben hogyan tudunk k\u00fcl\u00f6nb\u00f6z\u0151 k\u00e9perny\u0151m\u00e9reteket t\u00e1mogatni.

"},{"location":"laborok/network/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

"},{"location":"laborok/network/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
  1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

  2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

    Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

  3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

  4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a repository-ban l\u00e9v\u0151 projektet.

"},{"location":"laborok/network/#meglevo-projekt-attekintese","title":"Megl\u00e9v\u0151 projekt \u00e1ttekint\u00e9se","text":"

A megl\u00e9v\u0151 projektben megtal\u00e1lhat\u00f3 az elk\u00e9sz\u00edtett fel\u00fclet, illetve a hozz\u00e1 tartoz\u00f3 Viewmodellek. Tekints\u00fck \u00e1t a laborvezet\u0151vel a megl\u00e9v\u0151 k\u00f3dot!

"},{"location":"laborok/network/#adat-es-halozati-reteg","title":"Adat- \u00e9s h\u00e1l\u00f3zati r\u00e9teg","text":""},{"location":"laborok/network/#konyvtarak","title":"K\u00f6nyvt\u00e1rak","text":"

Retrofit

A Retrofit egy \u00e1ltal\u00e1nos c\u00e9l\u00fa HTTP k\u00f6nyvt\u00e1r Java \u00e9s K\u00f6tlin k\u00f6rnyezetben. Sz\u00e9les k\u00f6rben haszn\u00e1lj\u00e1k, sz\u00e1mos projektben bizony\u00edtott m\u00e1r (kv\u00e1zi ipari standard). Az\u00e9rt haszn\u00e1ljuk, hogy ne kelljen alacsony sz\u00ednt\u0171 h\u00e1l\u00f3zati h\u00edv\u00e1sokat implement\u00e1lni.

Seg\u00edts\u00e9g\u00e9vel el\u00e9g egy interface-ben annot\u00e1ci\u00f3k seg\u00edts\u00e9g\u00e9vel le\u00edrni az API-t (ez pl. a Swagger eszk\u00f6zzel gener\u00e1lhat\u00f3 is), majd e m\u00f6g\u00e9 k\u00e9sz\u00edt a Retrofit egy olyan oszt\u00e1lyt, mely a sz\u00fcks\u00e9ges h\u00e1l\u00f3zati h\u00edv\u00e1sokat elv\u00e9gzi. A Retrofit a h\u00e1tt\u00e9rben az OkHttp3-at haszn\u00e1lja, valamint az objektumok JSON form\u00e1tumba t\u00f6rt\u00e9n\u0151 soros\u00edt\u00e1s\u00e1t a Moshi libraryvel v\u00e9gzi. Ez\u00e9rt ezeket is be kell hivatkozni.

Paging 3.0

A Paging Library az Android Jetpack r\u00e9sze. Seg\u00edt az adatok oldalank\u00e9nti bet\u00f6lt\u00e9s\u00e9ben \u00e9s megjelen\u00edt\u00e9s\u00e9ben nagyobb adatk\u00e9szletb\u0151l, helyi t\u00e1rol\u00f3b\u00f3l vagy h\u00e1l\u00f3zatr\u00f3l. Ez a megk\u00f6zel\u00edt\u00e9s lehet\u0151v\u00e9 teszi az alkalmaz\u00e1sunk sz\u00e1m\u00e1ra, hogy mind a h\u00e1l\u00f3zati s\u00e1vsz\u00e9less\u00e9get, mind pedig a rendszer er\u0151forr\u00e1sait hat\u00e9konyabban haszn\u00e1lja.

A Paging Library haszn\u00e1lat\u00e1nak el\u0151nyei:

  • Az oldalank\u00e9nti adatok mem\u00f3ri\u00e1ban t\u00f6rt\u00e9n\u0151 gyors\u00edt\u00f3t\u00e1raz\u00e1sa. Ez biztos\u00edtja, hogy az alkalmaz\u00e1s hat\u00e9konyan haszn\u00e1lja a rendszer er\u0151forr\u00e1sait oldalank\u00e9nti adatokkal val\u00f3 munka sor\u00e1n.
  • Be\u00e9p\u00edtett k\u00e9r\u00e9sek duplik\u00e1l\u00f3d\u00e1s\u00e1nak megakad\u00e1lyoz\u00e1sa, hogy az alkalmaz\u00e1s hat\u00e9konyan haszn\u00e1lja a h\u00e1l\u00f3zati s\u00e1vsz\u00e9less\u00e9get \u00e9s a rendszer er\u0151forr\u00e1sait.
  • Konfigur\u00e1lhat\u00f3 RecyclerView adapterek, amelyek automatikusan lek\u00e9rik az adatokat, amikor a felhaszn\u00e1l\u00f3 g\u00f6rget a bet\u00f6lt\u00f6tt adatok v\u00e9g\u00e9re.
  • Els\u0151oszt\u00e1ly\u00fa t\u00e1mogat\u00e1s Kotlin coroutines \u00e9s Flow, valamint LiveData \u00e9s RxJava sz\u00e1m\u00e1ra.
  • Be\u00e9p\u00edtett hibakezel\u00e9s-t\u00e1mogat\u00e1s, bele\u00e9rtve a friss\u00edt\u00e9si \u00e9s \u00fajrapr\u00f3b\u00e1l\u00e1si k\u00e9pess\u00e9geket.

A Paging Library f\u0151bb elemei:

  • PagingData - egy t\u00e1rol\u00f3 'oldalazott' adatok sz\u00e1m\u00e1ra. Az adatok friss\u00edt\u00e9sekor k\u00fcl\u00f6n PagingData tart\u00e1lyt haszn\u00e1lunk.
  • PagingSource - a PagingSource az alap oszt\u00e1ly az adatok r\u00e9szletekben val\u00f3 bet\u00f6lt\u00e9s\u00e9hez PagingData streambe.
  • Pager.flow - egy Flow-t hoz l\u00e9tre, amely a PagingConfig \u00e9s egy f\u00fcggv\u00e9ny alapj\u00e1n konstru\u00e1lja a megval\u00f3s\u00edtott PagingSource-t.
  • RemoteMediator - seg\u00edt az oldalaz\u00e1s megval\u00f3s\u00edt\u00e1s\u00e1ban h\u00e1l\u00f3zatr\u00f3l \u00e9s adatb\u00e1zisb\u00f3l.
  • Coil

    A Coil (Coroutine Image Loader) egy k\u00e9p bet\u00f6lt\u0151 k\u00f6nyvt\u00e1r Androidra, amelyet a Kotlin koroutinokra \u00e9p\u00fcl. A Coil haszn\u00e1lat\u00e1nak el\u0151nyei:

    • Gyors: A Coil sz\u00e1mos optimaliz\u00e1l\u00e1st v\u00e9gez, bele\u00e9rtve a mem\u00f3ria- \u00e9s lemezt\u00e1rol\u00f3 gyors\u00edt\u00f3t\u00e1raz\u00e1st, az \u00e1tm\u00e9retez\u00e9st a mem\u00f3ri\u00e1ban, az automatikus k\u00e9r\u00e9sek sz\u00fcneteltet\u00e9s\u00e9t/le\u00e1ll\u00edt\u00e1s\u00e1t \u00e9s m\u00e9g sok m\u00e1st.
    • K\u00f6nny\u0171: A Coil kb. 2000 met\u00f3dust ad az APK-hoz (azoknak az alkalmaz\u00e1soknak, amelyek m\u00e1r haszn\u00e1lj\u00e1k az OkHttp \u00e9s a Coroutines k\u00f6nyvt\u00e1rakat), ami hasonl\u00f3 a Picasso-hoz \u00e9s jelent\u0151sen kevesebb, mint a Glide \u00e9s a Fresco k\u00f6nyvt\u00e1rak.
    • K\u00f6nnyen haszn\u00e1lhat\u00f3: A Coil API-ja a Kotlin nyelv funkci\u00f3it haszn\u00e1lja a k\u00f6nny\u0171 haszn\u00e1lat \u00e9s a minim\u00e1lis boilerplate k\u00f3d \u00e9rdek\u00e9ben.
    • Modern: A Coil a Kotlin nyelv\u0171s\u00e9get helyezi el\u0151t\u00e9rbe \u00e9s a modern k\u00f6nyvt\u00e1rakat haszn\u00e1lja, bele\u00e9rtve a Coroutines-t, az OkHttp-t, az Okio-t \u00e9s az AndroidX Lifecycles-t.
    "},{"location":"laborok/network/#fuggosegek","title":"F\u00fcgg\u0151s\u00e9gek","text":"

    A Retrofit, Paging \u00e9s Coil k\u00f6nyvt\u00e1rak haszn\u00e1lat\u00e1hoz a k\u00f6vetkez\u0151 f\u00fcgg\u0151s\u00e9gek sz\u00fcks\u00e9gesek (ezek m\u00e1r szerepelnek a projektben, ne vegy\u00fck fel \u0151ket \u00fajra):

        [versions]\nretrofit = \"2.11.0\"\npaging= \"3.3.2\"\ncoil = \"2.5.0\"\n\n[libraries]\nretrofit = { group = \"com.squareup.retrofit2\", name = \"retrofit\", version.ref = \"retrofit\" }\nretrofit-moshi = { group = \"com.squareup.retrofit2\", name = \"converter-moshi\", version.ref = \"retrofit\" }\npaging = { group = \"androidx.paging\", name = \"paging-compose\", version.ref = \"paging\" }\ncoil = { group = \"io.coil-kt\", name = \"coil-compose\", version.ref = \"coil\" }\n\ndependencies {\nimplementation(libs.retrofit)\nimplementation(libs.retrofit.moshi)\nimplementation(libs.paging)\nimplementation(libs.coil)\n}\n

    A data.model package-be hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt, melyek az API haszn\u00e1lat\u00e1hoz sz\u00fcks\u00e9gesek:

    UnsplashPhoto.kt:

    @Entity(tableName = \"photos\")\ndata class UnsplashPhoto(\n@PrimaryKey(autoGenerate = false)\nval id: String,\nval likes: Int,\n@Embedded\nval user: UserData,\n@Embedded\nval urls: Urls\n)\n\ndata class UserData(\nval username: String,\nval name: String,\n@Json(name = \"total_likes\")\nval totalLikes: Int,\n@Json(name = \"total_photos\")\nval totalPhotos: Int,\n@Json(name = \"profile_image\") @Embedded\nval profileImage: UserProfileImage,\n)\n\ndata class UserProfileImage(\nval small: String,\nval medium: String,\nval large: String\n)\n\ndata class Urls(\nval regular: String,\nval full: String\n)\n

    Az @Embedded annot\u00e1ci\u00f3val megjelel\u0151t mez\u0151k oszt\u00e1lyainak mez\u0151i k\u00f6zvetlen\u00fcl hivatkozhat\u00f3k az SQL querykben.

    A Moshi automatikusan megoldja majd az egyes tagv\u00e1ltoz\u00f3k szerializ\u00e1l\u00e1s\u00e1t, kiv\u00e9ve ott, ahol elt\u00e9r a n\u00e9v. Ezt a Json annot\u00e1ci\u00f3val jelezhetj\u00fck.

    SearchResult.kt:

    data class SearchResult(\n@Json(name = \"results\") val photos: List<UnsplashPhoto>\n)\n

    Hozzuk l\u00e9tre az els\u0151 DAO-t a data.local.dao package-ben:

    UnsplashPhotoDao.kt:

    @Dao\ninterface UnsplashPhotoDao {\n@Insert\nsuspend fun insertPhotos(photos: List<UnsplashPhoto>)\n\n@Query(\"SELECT EXISTS (SELECT * FROM photos WHERE id = :id)\")\nfun exists(id: String): Flow<Boolean>\n\n@Query(\"DELETE FROM photos\")\nsuspend fun deleteAllPhotos()\n\n@Query(\"SELECT * FROM photos\")\nfun getAllPhotos(): PagingSource<Int, UnsplashPhoto>\n\n@Query(\"SELECT * FROM photos WHERE id = :id\")\nfun getPhotoById(id: String): Flow<UnsplashPhoto>\n}\n

    Majd a data.local.database package-ben hozzuk l\u00e9tre az adatb\u00e1zist (az UnsplashPhotoRemoteKeysDao oszt\u00e1lyt k\u00e9s\u0151bb hozzuk l\u00e9tre):

    UnsplashDatabase.kt:

    @Database(entities = [UnsplashPhoto::class, UnsplashPhotoRemoteKeys::class], version = 1)\nabstract class UnsplashDatabase : RoomDatabase() {\nabstract val photosDao: UnsplashPhotoDao\nabstract val remoteKeysDao: UnsplashPhotoRemoteKeysDao\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, az adat\u00e1bzishoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/network/#a-retrofit-interfesz","title":"A Retrofit interf\u00e9sz","text":"

    Az Unsplash API h\u00e1rom v\u00e9gpontj\u00e1t fogjuk haszn\u00e1lni a k\u00e9pek, egy adott k\u00e9p lek\u00e9r\u00e9s\u00e9re, valamint a keres\u00e9s v\u00e9grehajt\u00e1s\u00e1ra. Hozzuk l\u00e9tre a lenti interface-t a data.remote.api package-ben.

    UnsplashApi.kt:

    interface UnsplashApi {\n\n@GET(\"/photos\")\nsuspend fun getPhotosFromEditorialFeed(\n@Query(\"page\") page: Int = 1,\n@Query(\"per_page\") perPage: Int = 10,\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n): Response<List<UnsplashPhoto>>\n\n@GET(\"/photos/{id}\")\nsuspend fun getPhotoById(\n@Path(\"id\") id: String,\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n): Response<UnsplashPhoto>\n\n@GET(\"/search/photos\")\nsuspend fun getSearchResults(\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n@Query(\"page\") page: Int = 1,\n@Query(\"per_page\") perPage: Int = 10,\n@Query(\"query\") searchTerms: String,\n): Response<SearchResult>\n\n}\n

    Ha a Reponse-t nem tudn\u00e1 beimport\u00e1lni az Android Studio, vegy\u00fck fel k\u00e9zzel az import retrofit2.Response sort.

    A Retrofit annot\u00e1ci\u00f3i seg\u00edts\u00e9g\u00e9vel egyszer\u0171en tudjuk defini\u00e1lni a k\u00e9r\u00e9seinket. Cser\u00e9lj\u00fck le az itt szerepl\u0151 ACCESS_KEY stringet az Unsplashen regisztr\u00e1ci\u00f3 ut\u00e1n el\u00e9rhet\u0151 saj\u00e1t kulcsunkra. Az https://unsplash.com/oauth/applications oldalon regisztr\u00e1ci\u00f3 ut\u00e1n egy \u00faj applik\u00e1ci\u00f3t kell l\u00e9trehozni, \u00e9s azt megnyitva tal\u00e1lhatjuk meg a kulcsot.

    "},{"location":"laborok/network/#a-pagingsource-osztaly","title":"A PagingSource oszt\u00e1ly","text":"

    Az els\u0151 l\u00e9p\u00e9s egy PagingSource implement\u00e1ci\u00f3 meghat\u00e1roz\u00e1sa, hogy az adatforr\u00e1s azonos\u00edthat\u00f3 legyen. A PagingSource API oszt\u00e1lya tartalmazza a load() met\u00f3dust, amelyet fel\u00fcl kell \u00edrni, hogy jelentse, hogyan lehet lapozott adatokat visszanyerni a megfelel\u0151 adatforr\u00e1sb\u00f3l.

    Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.paging package-ben:

    SearchPagingSource.kt:

    class SearchPagingSource(\nprivate val api: UnsplashApi,\nprivate val searchTerms: String\n): PagingSource<Int, UnsplashPhoto>() {\n\noverride fun getRefreshKey(state: PagingState<Int, UnsplashPhoto>): Int? = state.anchorPosition\n\noverride suspend fun load(params: LoadParams<Int>): LoadResult<Int, UnsplashPhoto> {\nval currentPage = params.key ?: 1\nreturn try {\nval response = api.getSearchResults(searchTerms = searchTerms, perPage = INITIAL_PAGE_SIZE, page = currentPage)\nif (response.isSuccessful) {\nval photos = response.body()?.photos ?: emptyList()\nval endOfPaginationReached = photos.isEmpty()\nLoadResult.Page(\ndata = photos,\nprevKey = if (currentPage == 1) null else currentPage - 1,\nnextKey = if (endOfPaginationReached) null else currentPage + 1\n)\n} else throw Exception(response.errorBody()?.string() ?: \"Unsuccessful request.\")\n} catch (e: Exception) {\nLog.e(\"error\",e.stackTraceToString())\nLoadResult.Error(e)\n}\n}\n}\n

    A PagingSource<Key, Value> k\u00e9t t\u00edpusparam\u00e9tert tartalmaz: Key \u00e9s Value. A kulcs meghat\u00e1rozza az azonos\u00edt\u00f3t, amelyet az adat bet\u00f6lt\u00e9s\u00e9hez haszn\u00e1lnak, \u00e9s az \u00e9rt\u00e9k maga az adat t\u00edpusa.

    Egy tipikus PagingSource implement\u00e1ci\u00f3 a konstruktor\u00e1ban megadott param\u00e9tereket tov\u00e1bb\u00edtja a load() met\u00f3dusnak, hogy bet\u00f6lthesse a megfelel\u0151 adatokat egy lek\u00e9rdez\u00e9shez.

    A LoadParams objektum inform\u00e1ci\u00f3kat tartalmaz az elv\u00e9gzend\u0151 bet\u00f6lt\u00e9si m\u0171veletr\u0151l. Ide tartozik a bet\u00f6ltend\u0151 kulcs \u00e9s a bet\u00f6ltend\u0151 elemek sz\u00e1ma.

    A LoadResult objektum az adatbet\u00f6lt\u00e9s eredm\u00e9ny\u00e9t tartalmazza. A LoadResult egy z\u00e1rt oszt\u00e1ly, amely k\u00e9t form\u00e1ban jelenhet meg att\u00f3l f\u00fcgg\u0151en, hogy a load() h\u00edv\u00e1s sikeres volt-e vagy sem:

    • Sikeres esetben LoadResult.Page objektum
    • Sikertelen esetben LoadResult.Error objektum.

    A try-catch blokkban l\u00e1that\u00f3, hogy hogyan tudjuk haszn\u00e1lni a Retrofit interf\u00e9sz\u00fcnket h\u00e1l\u00f3zati h\u00edv\u00e1s elv\u00e9gz\u00e9s\u00e9re.

    A PagingSource implement\u00e1ci\u00f3ja emellett tartalmaznia kell egy getRefreshKey() met\u00f3dust, amely egy PagingState objektumot v\u00e1r param\u00e9terk\u00e9nt, \u00e9s visszaadja a kulcsot, amelyet \u00e1t kell adni a load() met\u00f3dusnak, amikor az adat friss\u00edt\u00e9se vagy \u00e9rv\u00e9nytelen\u00edt\u00e9se t\u00f6rt\u00e9nik az els\u0151 bet\u00f6lt\u00e9s ut\u00e1n. A Paging k\u00f6nyvt\u00e1r automatikusan megh\u00edvja ezt a met\u00f3dust az adat k\u00e9s\u0151bbi friss\u00edt\u00e9sekor.

    "},{"location":"laborok/network/#a-remotemediator-osztaly","title":"A RemoteMediator oszt\u00e1ly","text":"

    Jobb felhaszn\u00e1l\u00f3i \u00e9lm\u00e9nyt biztos\u00edthatunk azzal, ha gondoskodunk arr\u00f3l, hogy az alkalmaz\u00e1sunk haszn\u00e1lhat\u00f3 legyen akkor is, ha az internetkapcsolat instabil vagy ha a felhaszn\u00e1l\u00f3 offline. Az egyik m\u00f3dja ennek, hogy egyszerre lapozunk a h\u00e1l\u00f3zatr\u00f3l \u00e9s egy helyi adatb\u00e1zisb\u00f3l is. Ezzel az alkalmaz\u00e1s az UI-t egy helyi adatb\u00e1zisb\u00f3l vez\u00e9rli \u00e9s csak akkor k\u00e9r le adatokat a h\u00e1l\u00f3zatr\u00f3l, ha m\u00e1r nincs t\u00f6bb adat az adatb\u00e1zisban.

    A Paging k\u00f6nyvt\u00e1r a RemoteMediator komponenst biztos\u00edtja ehhez az esethez. A RemoteMediator a Paging k\u00f6nyvt\u00e1r jelz\u00e9sek\u00e9nt m\u0171k\u00f6dik, amikor az alkalmaz\u00e1s kimer\u00fcl az el\u0151t\u00e1rolt adatb\u00f3l. Ezt a jelz\u00e9st lehet haszn\u00e1lni tov\u00e1bbi adatok bet\u00f6lt\u00e9s\u00e9re a h\u00e1l\u00f3zatr\u00f3l, \u00e9s azokat helyi adatb\u00e1zisban t\u00e1rolni, ahol egy PagingSource bet\u00f6ltheti \u00e9s a felhaszn\u00e1l\u00f3i fel\u00fcleten megjelen\u00edtheti.

    Ha tov\u00e1bbi adatokra van sz\u00fcks\u00e9g, a Paging k\u00f6nyvt\u00e1r megh\u00edvja a RemoteMediator implement\u00e1ci\u00f3j\u00e1b\u00f3l a load() met\u00f3dust. Ez egy suspending f\u00fcggv\u00e9ny, \u00edgy hossz\u00fa ideig fut\u00f3 munk\u00e1t v\u00e9gezhet biztons\u00e1gosan. Ez a f\u00fcggv\u00e9ny \u00e1ltal\u00e1ban az \u00faj adatokat egy h\u00e1l\u00f3zati forr\u00e1sb\u00f3l szedi le, majd elmenti helyi t\u00e1rol\u00f3ba.

    Ez a folyamat \u00faj adatokkal m\u0171k\u00f6dik, de id\u0151vel az adatok t\u00e1rol\u00e1sa az adatb\u00e1zisban \u00e9rv\u00e9nytelen\u00edt\u00e9st ig\u00e9nyelhet, p\u00e9ld\u00e1ul amikor a felhaszn\u00e1l\u00f3 k\u00e9zzel ind\u00edtja el a friss\u00edt\u00e9st. Ezt a LoadType tulajdons\u00e1g jelzi, amelyet \u00e1t kell adni a load() met\u00f3dusnak. A LoadType t\u00e1j\u00e9koztatja a RemoteMediator-t arr\u00f3l, hogy a megl\u00e9v\u0151 adatokat friss\u00edteni kell-e, vagy olyan tov\u00e1bbi adatokat kell-e lek\u00e9rni, amelyeket a megl\u00e9v\u0151 list\u00e1hoz kell hozz\u00e1adni.

    \u00cdgy a RemoteMediator biztos\u00edtja, hogy az alkalmaz\u00e1s azokat az adatokat t\u00f6ltse be, amelyeket a felhaszn\u00e1l\u00f3k a megfelel\u0151 sorrendben szeretn\u00e9nek l\u00e1tni.

    Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.paging package-ben:

    UnsplashRemoteMediator.kt:

    @ExperimentalPagingApi\nclass UnsplashRemoteMediator(\nprivate val api: UnsplashApi,\nprivate val db: UnsplashDatabase\n): RemoteMediator<Int, UnsplashPhoto>() {\n\noverride suspend fun load(\nloadType: LoadType,\nstate: PagingState<Int, UnsplashPhoto>\n): MediatorResult {\nreturn try {\nval page = when (loadType) {\nLoadType.REFRESH -> {\nval remoteKeys = getRemoteKeysForClosestToPosition(state)\nremoteKeys?.nextKey?.minus(1) ?: INITIAL_PAGE\n}\nLoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)\nLoadType.APPEND -> {\nval remoteKeys = getRemoteKeysForLastItem(state) ?: throw InvalidObjectException(\"Result is empty\")\nremoteKeys.nextKey ?: return MediatorResult.Success(endOfPaginationReached = true)\n}\n}\n\nval response = api.getPhotosFromEditorialFeed(page = page, perPage = INITIAL_PAGE_SIZE)\nvar endOfPaginationReached = false\nif (response.isSuccessful) {\nval photos = response.body() ?: emptyList()\nendOfPaginationReached = photos.isEmpty()\ndb.withTransaction {\nif (loadType == LoadType.REFRESH) {\ndb.photosDao.deleteAllPhotos()\ndb.remoteKeysDao.deleteAllKeys()\n}\nval prevKey = if (page == INITIAL_PAGE) null else page - 1\nval nextKey = if (photos.isEmpty()) null else page + 1\nval keys = photos.map { photo ->\nUnsplashPhotoRemoteKeys(\nid = photo.id,\nprevKey = prevKey,\nnextKey = nextKey\n)\n}\ndb.remoteKeysDao.insertAllKeys(keys)\ndb.photosDao.insertPhotos(photos)\n}\n}\n\nMediatorResult.Success(endOfPaginationReached = endOfPaginationReached)\n\n} catch (e: IOException) {\nMediatorResult.Error(e)\n} catch (e: HttpException) {\nMediatorResult.Error(e)\n} catch (e: InvalidObjectException) {\nMediatorResult.Error(e)\n}\n}\n\nprivate suspend fun getRemoteKeysForLastItem(\nstate: PagingState<Int, UnsplashPhoto>\n): UnsplashPhotoRemoteKeys? {\nreturn state.lastItemOrNull()?.let { photo ->\ndb.withTransaction {\ndb.remoteKeysDao.getKeysById(photo.id)\n}\n}\n}\n\nprivate suspend fun getRemoteKeysForClosestToPosition(\nstate: PagingState<Int, UnsplashPhoto>\n): UnsplashPhotoRemoteKeys? {\nreturn state.anchorPosition?.let { position ->\nstate.closestItemToPosition(position)?.id?.let { id ->\ndb.withTransaction {\ndb.remoteKeysDao.getKeysById(id)\n}\n}\n}\n}\n}\n

    A load() met\u00f3dus visszat\u00e9r\u00e9si \u00e9rt\u00e9ke egy MediatorResult objektum. A MediatorResult lehet MediatorResult.Error (amely tartalmazza az hiba le\u00edr\u00e1s\u00e1t) vagy MediatorResult.Success (amely tartalmaz egy jelz\u00e9st arr\u00f3l, hogy van-e m\u00e9g t\u00f6bb adat bet\u00f6lt\u00e9sre).

    A load() met\u00f3dusnak a k\u00f6vetkez\u0151 l\u00e9p\u00e9seket kell v\u00e9grehajtania:

    • Meg kell hat\u00e1rozni, hogy melyik oldalt kell a h\u00e1l\u00f3zatr\u00f3l bet\u00f6lteni a bet\u00f6lt\u00e9si t\u00edpus \u00e9s az eddig bet\u00f6lt\u00f6tt adatok alapj\u00e1n.
    • Kiv\u00e1ltani a h\u00e1l\u00f3zati k\u00e9r\u00e9st.
    • V\u00e9grehajtani a bet\u00f6lt\u00e9si m\u0171velet kimenet\u00e9t\u0151l f\u00fcgg\u0151 cselekv\u00e9seket:
    • Ha a bet\u00f6lt\u00e9s sikeres \u00e9s az kapott elemek list\u00e1ja nem \u00fcres, akkor t\u00e1rolja el a lista elemeit az adatb\u00e1zisban, majd t\u00e9rjen vissza a MediatorResult.Success (endOfPaginationReached = false) \u00e9rt\u00e9kkel. Az adatok t\u00e1rol\u00e1sa ut\u00e1n \u00e9rv\u00e9nytelen\u00edtse az adatforr\u00e1st, hogy \u00e9rtes\u00edtse a Paging k\u00f6nyvt\u00e1rat az \u00faj adatokr\u00f3l.
    • Ha a bet\u00f6lt\u00e9s sikeres \u00e9s a kapott elemek list\u00e1ja \u00fcres vagy az utols\u00f3 oldal indexe, akkor t\u00e9rjen vissza a MediatorResult.Success (endOfPaginationReached = true) \u00e9rt\u00e9kkel. Az adatok t\u00e1rol\u00e1sa ut\u00e1n \u00e9rv\u00e9nytelen\u00edtse az adatforr\u00e1st, hogy \u00e9rtes\u00edtse a Paging k\u00f6nyvt\u00e1rat az \u00faj adatokr\u00f3l.
    • Ha a k\u00e9r\u00e9s hib\u00e1t okoz, akkor t\u00e9rjen vissza a MediatorResult.Error \u00e9rt\u00e9kkel.
    "},{"location":"laborok/network/#tavoli-kulcsok","title":"T\u00e1voli kulcsok","text":"

    Kezeln\u00fcnk kell azt a helyzetet, amikor a helyi gyors\u00edt\u00f3t\u00e1rban t\u00e1rolt adatok elavultak a t\u00e1voli adatforr\u00e1shoz k\u00e9pest.

    A t\u00e1voli kulcsok lehet\u0151v\u00e9 teszik, hogy inform\u00e1ci\u00f3t menthess\u00fcnk el a legut\u00f3bbi oldalr\u00f3l, amelyet a szerverr\u0151l k\u00e9rtek le. Az alkalmaz\u00e1s felhaszn\u00e1lhatja ezt az inform\u00e1ci\u00f3t a k\u00f6vetkez\u0151 bet\u00f6ltend\u0151 adatoldal azonos\u00edt\u00e1s\u00e1hoz \u00e9s k\u00e9r\u00e9s\u00e9hez.

    A t\u00e1voli kulcsok olyan kulcsok, amelyeket a RemoteMediator implement\u00e1ci\u00f3 arra haszn\u00e1l, hogy k\u00f6z\u00f6lje a backend szolg\u00e1ltat\u00e1ssal, melyik adatot kell legk\u00f6zelebb bet\u00f6lteni. A legegyszer\u0171bb esetben minden lapozott adat elemhez tartozik egy t\u00e1voli kulcs, amelyre k\u00f6nnyen hivatkozhat. Azonban ha a t\u00e1voli kulcsok nem feleltethet\u0151ek meg a konkr\u00e9t elemeknek, nek\u00fcnk kell \u0151ket kezelni a load h\u00edv\u00e1sban.

    Amikor a t\u00e1voli kulcsok nem k\u00f6zvetlen\u00fcl kapcsol\u00f3dnak a listaelemekhez, c\u00e9lszer\u0171 \u0151ket k\u00fcl\u00f6n t\u00e1bl\u00e1zatban t\u00e1rolni a helyi adatb\u00e1zisban. Defini\u00e1lni kell egy Room entit\u00e1st, amely egy t\u00e1voli kulcsokb\u00f3l \u00e1ll\u00f3 t\u00e1bl\u00e1zatot reprezent\u00e1l.

    Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.local.model package-ben:

    UnsplashPhotoRemoteKeys.kt:

    @Entity(tableName = \"remote_keys\")\ndata class UnsplashPhotoRemoteKeys(\n@PrimaryKey val id: String,\nval prevKey: Int?,\nval nextKey: Int?\n)\n

    Emellett defini\u00e1lni kell egy DAO-t a RemoteKey entit\u00e1sra.

    Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.local.dao package-ben:

    UnsplashPhotoRemoteKeysDao.kt:

    @Dao\ninterface UnsplashPhotoRemoteKeysDao {\n\n@Insert(onConflict = OnConflictStrategy.REPLACE)\nsuspend fun insertAllKeys(keys: List<UnsplashPhotoRemoteKeys>)\n\n@Query(\"SELECT * FROM remote_keys WHERE id = :id\")\nsuspend fun getKeysById(id: String): UnsplashPhotoRemoteKeys\n\n@Query(\"DELETE FROM remote_keys\")\nsuspend fun deleteAllKeys()\n}\n

    "},{"location":"laborok/network/#pagingutil","title":"PagingUtil","text":"

    Hozzuk l\u00e9tre a PagingUtil oszt\u00e1lyt az util package-ben a paging konfigur\u00e1l\u00e1s\u00e1ra \u00e9s placeholderek be\u00e1ll\u00edt\u00e1s\u00e1ra:

    PagingUtil.kt:

    object PagingUtil {\nconst val INITIAL_PAGE_SIZE = 10\nconst val INITIAL_PAGE = 1\n\nfun <T: Any> LazyGridScope.items(\nitems: LazyPagingItems<T>,\nkey: ((item: T) -> Any)? = null,\nitemContent: @Composable LazyGridItemScope.(value: T?) -> Unit\n) {\nitems(\ncount = items.itemCount,\nkey = if (key == null) null else { index ->\nval item = items.peek(index)\nif (item == null) {\nPagingPlaceholderKey(index)\n} else {\nkey(item)\n}\n}\n) { index ->\nitemContent(items[index])\n}\n}\n\ndata class PagingPlaceholderKey(private val index: Int) : Parcelable {\noverride fun writeToParcel(parcel: Parcel, flags: Int) {\nparcel.writeInt(index)\n}\n\noverride fun describeContents(): Int {\nreturn 0\n}\n\ncompanion object {\n@Suppress(\"unused\")\n@JvmField\nval CREATOR: Parcelable.Creator<PagingPlaceholderKey> =\nobject : Parcelable.Creator<PagingPlaceholderKey> {\noverride fun createFromParcel(parcel: Parcel) =\nPagingPlaceholderKey(parcel.readInt())\n\noverride fun newArray(size: Int) = arrayOfNulls<PagingPlaceholderKey?>(size)\n}\n}\n}\n}\n

    A data.repository package-be vegy\u00fck fel a lenti DataSource oszt\u00e1lyt, melyen kereszt\u00fcl a ViewModel el\u00e9ri a k\u00e9r\u00e9seinket:

    UnsplashPhotoDataSource.kt:

    @ExperimentalPagingApi\nclass UnsplashPhotoDataSource(\nprivate val api: UnsplashApi,\nprivate val db: UnsplashDatabase\n) {\n\nfun getAllPhotos(): Flow<PagingData<UnsplashPhoto>> {\nreturn Pager(\nconfig = PagingConfig(pageSize = INITIAL_PAGE_SIZE),\nremoteMediator = UnsplashRemoteMediator(api, db),\npagingSourceFactory = {  db.photosDao.getAllPhotos() }\n).flow\n}\n\nfun getPhotoByIdFromDatabase(id: String): Flow<UnsplashPhoto> = db.photosDao.getPhotoById(id)\n\nfun getSearchResults(searchTerms: String): Flow<PagingData<UnsplashPhoto>> {\nreturn Pager(\nconfig = PagingConfig(pageSize = INITIAL_PAGE_SIZE),\npagingSourceFactory = {\nSearchPagingSource(\napi = api,\nsearchTerms = searchTerms\n)\n}\n).flow\n}\n\nsuspend fun exists(id: String): Boolean = db.photosDao.exists(id).first()\n\nsuspend fun getPhotoByIdFromApi(id: String): Flow<UnsplashPhoto> {\nval response = api.getPhotoById(id)\nreturn if (response.isSuccessful) {\nflow { emit(response.body() ?: throw NullPointerException()) }\n} else throw Exception(\"Unsuccessful request\")\n}\n}\n

    Ha a flow-emit r\u00e9szn\u00e9l hib\u00e1t kapunk, cser\u00e9lj\u00fck le az importot a k\u00f6vetkez\u0151re: import kotlinx.coroutines.flow.flow.

    Itt l\u00e1thatjuk a Pager oszt\u00e1ly haszn\u00e1lat\u00e1t a lapozott adatok folyam\u00e1nak be\u00e1ll\u00edt\u00e1s\u00e1hoz.

    V\u00e9g\u00fcl hozzuk l\u00e9tre a saj\u00e1t Application oszt\u00e1lyunkat a gy\u00f6k\u00e9r package-ben:

    UnsplashApplication.kt:

    @ExperimentalPagingApi\nclass UnsplashApplication : Application() {\ncompanion object {\nlateinit var photoDataSource: UnsplashPhotoDataSource\n}\n\noverride fun onCreate() {\nsuper.onCreate()\nval db = Room.databaseBuilder(\nthis.baseContext,\nUnsplashDatabase::class.java,\n\"unsplash_db\"\n).build()\n\nval client = OkHttpClient.Builder()\n.readTimeout(15, TimeUnit.SECONDS)\n.connectTimeout(15, TimeUnit.SECONDS)\n.build()\n\nval retrofit = Retrofit.Builder()\n.baseUrl(\"https://api.unsplash.com/\")\n.client(client)\n.addConverterFactory(MoshiConverterFactory.create())\n.build()\n\nval api = retrofit.create(UnsplashApi::class.java)\n\nphotoDataSource = UnsplashPhotoDataSource(api,db)\n\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s list\u00e1z\u00f3 k\u00e9perny\u0151je (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a Paging-hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s r\u00e9szletes k\u00e9perny\u0151je (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a Paging-hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/network/#onallo-feladat-1-kulonbozo-kepernyomeretek-tamogatasa","title":"\u00d6n\u00e1ll\u00f3 feladat 1 - K\u00fcl\u00f6nb\u00f6z\u0151 k\u00e9perny\u0151m\u00e9retek t\u00e1mogat\u00e1sa","text":"

    Szeretn\u00e9nk felk\u00e9sz\u00edteni az alkalmaz\u00e1sunkat, hogy k\u00fcl\u00f6nb\u00f6z\u0151 m\u00e9ret\u0171 k\u00e9perny\u0151k eset\u00e9n m\u00e1shogy, az adott k\u00e9sz\u00fcl\u00e9k sz\u00e1m\u00e1ra optim\u00e1lis m\u00f3don jelenleg meg. Ezzez els\u0151k\u00e9nt a util package-ben hozzunk l\u00e9tre egy oszt\u00e1lyt a kateg\u00f3ri\u00e1inkra:

    WindowSize.kt:

    enum class WindowSize { Compact, Medium, Expanded }\n

    Ezt k\u00f6vet\u0151en hozzuk l\u00e9tre a marad\u00e9k k\u00e9t elrendez\u00e9s\u00fcnket a feature.photos_feed.screensbysize package-ben:

    PhotosFeedScreen_Compact.kt:

    @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@Composable\nfun PhotosFeedScreen_Compact(\nrefreshState: PullRefreshState,\nrefreshing: Boolean,\nonPhotoItemClick: (String) -> Unit,\nphotos: LazyPagingItems<UnsplashPhotoUiModel>,\nvalue: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\n) {\nval state = rememberLazyListState()\nScaffold(\nmodifier = modifier,\ntopBar = {\nSearchTopAppBar(\nisScrollInProgress = state.isScrollInProgress,\nvalue = value,\nonValueChange = onValueChange\n)\n}\n) {\nBox(\nmodifier = modifier\n.fillMaxSize()\n.pullRefresh(refreshState)\n.padding(it)\n) {\nif (!refreshing) {\nLazyColumn(\nmodifier = modifier.fillMaxSize()\n) {\nitems(photos) { photo ->\nphoto?.let { model ->\nPhotoItem(\nphoto = model,\nonClick = onPhotoItemClick\n)\n}\n}\n}\n}\nPullRefreshIndicator(\nrefreshing = refreshing,\nstate = refreshState,\nmodifier = Modifier.align(Alignment.TopCenter)\n)\n}\n}\n}\n

    PhotosFeedScreen_Expanded.kt:

    @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@Composable\nfun PhotosFeedScreen_Expanded(\nonPhotoItemClick: (String) -> Unit,\nphotos: LazyPagingItems<UnsplashPhotoUiModel>,\nvalue: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\nloadedPhoto: UnsplashPhotoUiModel? = null,\n) {\nval density = LocalDensity.current\nval configuration = LocalConfiguration.current\nval screenWidth = configuration.screenWidthDp.dp\nval state = rememberLazyGridState()\n\nRow(modifier = modifier.fillMaxSize()) {\nRow(\nmodifier = Modifier.fillMaxSize()\n) {\nScaffold(\nmodifier = Modifier.fillMaxSize(),\ntopBar = {\nSearchTopAppBar(\nisScrollInProgress = state.isScrollInProgress,\nvalue = value,\nonValueChange = onValueChange\n)\n}\n) {\nLazyVerticalGrid(\nstate = state,\ncolumns = GridCells.Adaptive(240.dp),\nmodifier = Modifier\n.fillMaxSize()\n.padding(it)\n.weight(1f)\n) {\nitems(photos) { photo ->\nphoto?.let { model ->\nPhotoItem(\nphoto = model,\nonClick = onPhotoItemClick\n)\n}\n}\n}\n}\n\nAnimatedVisibility(\nvisible = loadedPhoto != null && screenWidth >= 1000.dp,\nenter = slideInHorizontally {\nwith(density) { 40.dp.roundToPx() }\n} + expandHorizontally(\nexpandFrom = Alignment.End\n) + fadeIn(\ninitialAlpha = 0.3f\n),\nexit = slideOutHorizontally() + shrinkHorizontally() + fadeOut(),\nmodifier = Modifier\n.fillMaxSize()\n.padding(5.dp)\n.weight(1f)\n) {\nPhotoDetails(\nloadedPhoto = loadedPhoto!!,\nwindowSize = WindowSize.Expanded\n)\n}\n}\n}\n}\n

    Friss\u00edts\u00fck a Screen\u00fcnket, hogy haszn\u00e1lja a fenti Composable-\u00f6ket:

    PhotosFeedScreen.kt:

    @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@ExperimentalPagingApi\n@Composable\nfun PhotosFeedScreen(\nmodifier: Modifier = Modifier,\nwindowSize: WindowSize = WindowSize.Compact,\nonPhotoItemClick: (String) -> Unit = {},\nviewModel: PhotosFeedViewModel = viewModel(factory = PhotosFeedViewModel.Factory)\n) {\n\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval photos = state.photos?.collectAsLazyPagingItems()\nval selectedPhoto = state.photo?.collectAsStateWithLifecycle(null)\n\nval configuration = LocalConfiguration.current\nval screenWidth = configuration.screenWidthDp.dp\n\nval refreshState = rememberPullRefreshState(\nrefreshing = state.isLoading,\nonRefresh = viewModel::refreshPhotos\n)\n\nif (photos != null) {\nwhen(windowSize) {\nWindowSize.Compact -> {\nPhotosFeedScreen_Compact(\nrefreshState = refreshState,\nrefreshing = state.isLoading,\nonPhotoItemClick = onPhotoItemClick,\nphotos = photos,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\nWindowSize.Medium -> {\nPhotosFeedScreen_Medium(\nrefreshState = refreshState,\nrefreshing = state.isLoading,\nonPhotoItemClick = onPhotoItemClick,\nphotos = photos,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\nWindowSize.Expanded -> {\nPhotosFeedScreen_Expanded(\nonPhotoItemClick = if (screenWidth >= 1000.dp) {\nviewModel::loadSelectedPhoto\n} else onPhotoItemClick,\nphotos = photos,\nloadedPhoto = selectedPhoto?.value,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\n}\n} else if (state.isError) {\nBox(modifier = modifier.fillMaxSize()) {\nText(text = state.throwable?.message.toString())\n}\n}\n}\n

    A PhotoDetails.kt-ban sz\u00fcntess\u00fck meg a h\u00e1rom windowsize-ot tartalmaz\u00f3 sor kommentez\u00e9s\u00e9t.

    Friss\u00edts\u00fck a NavGraph-ot: NavGraph.kt:

    @ExperimentalMaterial3Api\n@ExperimentalPagingApi\n@ExperimentalMaterialApi\n@Composable\nfun NavGraph(\nwindowSize: WindowSize = WindowSize.Compact,\n) {\nval navController = rememberNavController()\n\nNavHost(\nnavController = navController,\nstartDestination = Screen.PhotosFeed.route\n) {\ncomposable(route = Screen.PhotosFeed.route) {\nPhotosFeedScreen(\nwindowSize = windowSize,\nonPhotoItemClick = { photoId ->\nnavController.navigate(Screen.LoadedPhoto.passPhotoId(photoId))\n}\n)\n}\ncomposable(\nroute = Screen.LoadedPhoto.route,\narguments = listOf(\nnavArgument(\"photoId\") {\ntype = NavType.StringType\n}\n)\n) {\nLoadedPhotoScreen()\n}\n}\n}\n

    Majd az Activity-t is \u00e1ll\u00edtsuk be ezek haszn\u00e1lat\u00e1ra:

    MainActivity.kt:

    class MainActivity : ComponentActivity() {\n@OptIn(\nExperimentalMaterial3WindowSizeClassApi::class,\nExperimentalMaterial3Api::class,\nExperimentalPagingApi::class,\nExperimentalMaterialApi::class\n)\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nUnsplashTheme {\nval windowSize = when (calculateWindowSizeClass(this).widthSizeClass) {\nWindowWidthSizeClass.Compact -> WindowSize.Compact\nWindowWidthSizeClass.Medium -> WindowSize.Medium\nWindowWidthSizeClass.Expanded -> WindowSize.Expanded\nelse -> WindowSize.Compact\n}\n\nNavGraph(windowSize = windowSize)\n}\n}\n}\n}\n
    Alt+Enterrel vegy\u00fck fel az annot\u00e1ci\u00f3ra kattintva a hi\u00e1nyz\u0151 f\u00fcgg\u0151s\u00e9get.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s a m\u00e1s m\u00e9retben megjelen\u0151 k\u00e9pekkel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a m\u00e1s m\u00e9ret\u0171 k\u00e9perny\u0151kh\u00f6z tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/network/#onallo-feladat-2-dependency-injection","title":"\u00d6n\u00e1ll\u00f3 feladat 2 - Dependency Injection","text":"

    \u00cdrjuk \u00e1t a projektet \u00fagy, hogy az el\u0151z\u0151 laboron megismert Dependency Injection keretrendszereket haszn\u00e1lja!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a dependency injectionj\u00f6z tartoz\u00f3 relev\u00e1ns k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/permissions/","title":"Labor11 - Fut\u00e1sidej\u0171 enged\u00e9lyek (Contacts)","text":"

    A labor c\u00e9lja, hogy bemutassa, hogyan lehet a Compose keretrendszerben fut\u00e1sidej\u0171 enged\u00e9lyeket kezelni.

    "},{"location":"laborok/permissions/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/permissions/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
    2. A projekt neve legyen Contacts, a kezd\u0151 package pedig hu.bme.aut.android.contacts.
    3. A projektet a repository-n bel\u00fcl egy k\u00fcl\u00f6n mapp\u00e1ban hozzuk l\u00e9tre.
    4. Nyelvnek v\u00e1lasszuk a Kotlin-t.
    5. A minimum API szint legyen 24 (Android 7.0).

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Contacts k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

    "},{"location":"laborok/permissions/#verziok-frissitese","title":"Verzi\u00f3k friss\u00edt\u00e9se","text":"

    Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket a modul szint\u0171 build.gradle f\u00e1jlunkba, majd a laborvezet\u0151vel tekints\u00fck \u00e1t \u0151ket.

    dependencies {\n// Compose Bill of Materials\nval composeBom = platform(\"androidx.compose:compose-bom:2024.05.00\")\nimplementation(composeBom)\nandroidTestImplementation(composeBom)\n\n// Compose\nimplementation(\"androidx.compose.material3:material3\")\nimplementation(\"androidx.compose.ui:ui\")\nimplementation(\"androidx.compose.ui:ui-tooling-preview\")\nimplementation(\"androidx.compose.material:material-icons-extended\")\n\n// Compose testing\nandroidTestImplementation(\"androidx.compose.ui:ui-test-junit4\")\ndebugImplementation(\"androidx.compose.ui:ui-test-manifest\")\ndebugImplementation(\"androidx.compose.ui:ui-tooling\")\n\n// Core\nimplementation(\"androidx.core:core-ktx:1.13.1\")\nimplementation(\"androidx.activity:activity-compose:1.9.0\")\n\n// Lifecycle, Viewmodel\nval lifecycle_version = \"2.7.0\"\nimplementation(\"androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version\")\nimplementation(\"androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version\")\n\n// Navigation\nimplementation(\"androidx.navigation:navigation-compose:2.7.7\")\n\n// Permissions\nimplementation(\"com.google.accompanist:accompanist-permissions:0.35.0-alpha\")\n\n// Coil\nimplementation(\"io.coil-kt:coil-compose:2.5.0\")\n\n//Testing\ntestImplementation(\"junit:junit:4.13.2\")\nandroidTestImplementation(\"androidx.test.ext:junit:1.1.5\")\nandroidTestImplementation(\"androidx.test.espresso:espresso-core:3.5.1\")\n}\n

    Ezek mellett ellen\u0151rizz\u00fck a kotlin plugin \u00e9s a compose verzi\u00f3j\u00e1t. A labor k\u00e9sz\u00edt\u00e9sekor a k\u00f6vetkez\u0151ek voltak \u00e9rv\u00e9nyben:

    • Projekt szint\u0171 build.gradle:
      plugins {  \n  ...\n  id 'org.jetbrains.kotlin.android' version '1.8.10' apply false  \n}\n
    • Modul szint\u0171 build.gradle:
      android {\n    ...\n    composeOptions {\n        kotlinCompilerExtensionVersion '1.4.3'\n    }\n}\n

    A modul szint\u0171 build.gradle f\u00e1jlban \u00e1lll\u00edtsuk \u00e1t a compileSdk \u00e9rt\u00e9k\u00e9t 34-re!

    V\u00e9g\u00fcl vegy\u00fck fel el\u0151re az alkalmaz\u00e1shoz sz\u00fcks\u00e9ges sz\u00f6veges er\u0151forr\u00e1sokat:

    strings.xml:

    <resources>\n<string name=\"app_name\">Contacts</string>\n<string name=\"button_label_request_permission\">Grant permission</string>\n<string name=\"permission_grant_ask_beginning\">\"The\"</string>\n<string name=\"permission_grant_ask_separator_before_last_permission\">, and</string>\n<string name=\"permission_grant_ask_separator\">,</string>\n<string name=\"permission_grant_ask_singular_permission\">\" permission is\"</string>\n<string name=\"permission_grant_ask_multiple_permissions\">\" permissions are\"</string>\n<string name=\"permission_grant_ask_rationale\">\" important. Please grant all of them for the app to function properly.\"</string>\n<string name=\"permission_grant_ask_denied\">\" denied. The app cannot function without them. Please grant all of them.\"</string>\n<string name=\"contact_data_label_name\">Name</string>\n<string name=\"contact_data_label_phonenumber\">Number</string>\n<string name=\"contact_data_label_email\">Email</string>\n<string name=\"some_error_message\">Error</string>\n</resources>\n

    "},{"location":"laborok/permissions/#kontaktok-listaja","title":"Kontaktok list\u00e1ja","text":"

    Hozzunk l\u00e9tre egy \u00faj domain package-t l\u00e9tre a projekt\u00fcnk gy\u00f6ker\u00e9ben, mely az alkalmaz\u00e1sunk adatr\u00e9teg\u00e9nek r\u00e9szeit fogja tartalmazni, majd ezen bel\u00fcl hozzunk l\u00e9tre egy model package-et, mely az adatmodellek oszt\u00e1ly megfelel\u0151it fogja tartalmazni. Ebben hozzuk l\u00e9tre az al\u00e1bbi f\u00e1jlt:

    Contact.kt:

    import android.graphics.Bitmap\n\ndata class Contact(\nval id: String = \"\",\nval name: String = \"\",\nvar phoneNumber: String = \"\",\nvar emailAddress: String = \"\",\nvar photo: Bitmap? = null\n)\n

    "},{"location":"laborok/permissions/#navigacio-kialakitasa","title":"Navig\u00e1ci\u00f3 kialak\u00edt\u00e1sa","text":"

    Az el\u0151z\u0151 laborhoz hasonl\u00f3an alak\u00edtsuk ki a projektben a navig\u00e1ci\u00f3n\u00e1l haszn\u00e1lt oszt\u00e1lyokat!

    Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban l\u00e9tre egy \u00faj package-et navigation n\u00e9ven, majd hozzuk l\u00e9tre benne az \u00fatvonalakat reprezent\u00e1l\u00f3 Screen oszt\u00e1lyt:

    sealed class Screen(val route: String) {  }\n
    Illetve hozzuk l\u00e9tre a navig\u00e1ci\u00f3t v\u00e9gz\u0151 Composable f\u00fcggv\u00e9nyt is a NavGraph.kt f\u00e1jlban:
    @Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = \"\"\n) {\n\n}\n}\n

    A NavGraph Composable szerepe, hogy karban tartsa az \u00fatvonalakat, itt fogjuk a navig\u00e1ci\u00f3s esem\u00e9nyeket feldolgozni.

    V\u00e9g\u00fcl friss\u00edts\u00fck a MainActivity tartalm\u00e1t \u00fagy, hogy a NavGraph Composable-t haszn\u00e1lja:

    class MainActivity : ComponentActivity() {\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nContactsTheme {\nNavGraph()\n}\n}\n}\n}\n
    "},{"location":"laborok/permissions/#use-casek","title":"Use-casek","text":"

    A n\u00e9vjegyek kezel\u00e9s\u00e9t \u00e9rint\u0151 m\u0171veletek use-casekbe fogjuk kiszervezni. Az els\u0151 h\u00e1rom use-case, amire sz\u00fcks\u00e9g\u00fcnk lesz: - N\u00e9vjegyek list\u00e1z\u00e1sa - H\u00edv\u00e1s ind\u00edt\u00e1sa - SMS k\u00fcld\u00e9se

    Ut\u00f3bbi k\u00e9t funkci\u00f3ra nem k\u00e9sz\u00edt\u00fcnk saj\u00e1t implement\u00e1ci\u00f3t, hanem a k\u00e9sz\u00fcl\u00e9ken el\u00e9rhet\u0151 alkalmaz\u00e1sokat fogjuk elind\u00edtani implicit intent seg\u00edts\u00e9g\u00e9vel. A kontextuson v\u00e9gzett egy\u00e9ni f\u00fcggv\u00e9nyh\u00edv\u00e1sokat ezt k\u00f6vet\u0151en fogjuk implement\u00e1lni.

    Vegy\u00fck fel az al\u00e1bbi use-case-eket a domain.usecase package-be.

    LoadContactsUseCase.kt:

    class LoadContactsUseCase(\nprivate val context: Context\n) {\noperator fun invoke(): Flow<ArrayList<Contact>> = context.getContacts()\n}\n

    MakeACallUseCase.kt:

    class MakeACallUseCase(\nprivate val context: Context\n) {\noperator fun invoke(phoneNumber: String) {\nval callIntent = Intent(Intent.ACTION_CALL).apply {\ndata = Uri.parse(\"tel:$phoneNumber\")\nflags = FLAG_ACTIVITY_NEW_TASK\n}\ncontext.startActivity(callIntent)\n}\n}\n

    SendSMSUseCase.kt:

    class SendSMSUseCase(\nprivate val context: Context\n) {\noperator fun invoke(phoneNumber: String) {\nval intent = Intent(Intent.ACTION_VIEW).apply {\ndata = Uri.parse(\"sms:$phoneNumber\")\nflags = Intent.FLAG_ACTIVITY_NEW_TASK\n}\ncontext.startActivity(intent)\n}\n}\n

    R\u00f6viden tekints\u00fck \u00e1t a use-case-eket a laborvezet\u0151vel.

    "},{"location":"laborok/permissions/#nevjegymuveletek","title":"N\u00e9vjegym\u0171veletek","text":"

    Hozzuk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban a data package-et, majd benne a n\u00e9vjegyek kezel\u00e9s\u00e9vel kapcsolatos m\u0171veleteket elv\u00e9gz\u0151 seg\u00e9doszt\u00e1lyt. A m\u0171veletekben a ContentProvider \u00e1ltal adott adatok el\u00e9r\u00e9s\u00e9hez a ContentResolvert \u00e9s Cursort haszn\u00e1lunk.

    ContactsOperations.kt:

    object ContactsOperations {\n\nfun Context.getContacts(): Flow<ArrayList<Contact>> = flow {\nval contacts = ArrayList<Contact>()\nthis@getContacts.contentResolver?.performQuery(\nuri = ContactsContract.Contacts.CONTENT_URI,\nsortOrder = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + \" ASC\"\n).use { cursor ->\nif (cursor != null && cursor.count > 0) {\nval idColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID)\nval nameColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)\nwhile (cursor.moveToNext()) {\nval contactId = cursor.getString(idColumnIndex)\nval name = cursor.getString(nameColumnIndex)\nif (name != null) {\ncontacts.add(Contact(contactId, name))\n}\n}\n}\n}\ncontacts.forEach {\nit.phoneNumber = getContactPhoneNumber(it.id)\nit.emailAddress = getContactEmail(it.id)\nit.photo = getContactPhoto(it.id)\n}\nemit(contacts)\n}.flowOn(Dispatchers.IO)\n\nprivate fun Context.getContactName(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval displayNameColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)\ncursor.getString(displayNameColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactPhoneNumber(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval phoneNumberColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)\ncursor.getString(phoneNumberColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactEmail(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Email.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval emailColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)\ncursor.getString(emailColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactPhoto(id: String): Bitmap? {\nvar photo = BitmapFactory.decodeResource(this.resources, R.drawable.ic_launcher_foreground)\nContactsContract.Contacts.openContactPhotoInputStream(\nthis.contentResolver,\nContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id.toLong())\n).use {\nif (it != null) {\nphoto = BitmapFactory.decodeStream(it)\n}\n}\nreturn photo\n}\n\nprivate fun ContentResolver.performQuery(\n@RequiresPermission.Read uri: Uri,\nprojection: Array<String>? = null,\nselection: String? = null,\nselectionArgs: Array<String>? = null,\nsortOrder: String? = null\n): Cursor? {\nreturn query(uri, projection, selection, selectionArgs, sortOrder)\n}\n\n}\n

    "},{"location":"laborok/permissions/#engedelykezeles","title":"Enged\u00e9lykezel\u00e9s","text":""},{"location":"laborok/permissions/#hatter","title":"H\u00e1tt\u00e9r","text":"

    Android 6.0 (API level 23, Marshmallow) verzi\u00f3t\u00f3l kezdve a felhaszn\u00e1l\u00f3 fut\u00e1sid\u0151ben adhatja meg, vagy utas\u00edthatja el az alkalmaz\u00e1s \u00e1ltal k\u00e9rt enged\u00e9lyeket, \u00e9s nem az alkalmaz\u00e1s telep\u00edt\u00e9sekor vagy friss\u00edt\u00e9sekor. D\u00f6nthet \u00fagy, hogy bizonyos enged\u00e9lyeket nem ad meg egy alkalmaz\u00e1snak, \u00edgy nagyobb fok\u00fa ir\u00e1ny\u00edt\u00e1s ker\u00fcl a kez\u00e9be. Az alkalmaz\u00e1senged\u00e9lyeket k\u00e9s\u0151bb b\u00e1rmikor m\u00f3dos\u00edthatja a rendszerszint\u0171 alkalmaz\u00e1s be\u00e1ll\u00edt\u00e1sokn\u00e1l.

    Az enged\u00e9lyek k\u00e9t kateg\u00f3ri\u00e1ba vannak sorolva: normal \u00e9s dangerous.

    A normal kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyek nem jelentenek k\u00f6zvetlen kock\u00e1zatot a felhaszn\u00e1l\u00f3 szem\u00e9lyes adataira, ezeket az enged\u00e9lyeket a rendszer automatikusan megadja az alkalmaz\u00e1snak, ha sz\u00fcks\u00e9ge van r\u00e1.

    A dangerous kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyek lehet\u0151s\u00e9get adhatnak az alkalmaz\u00e1snak a felhaszn\u00e1l\u00f3 szem\u00e9lyes adataihoz val\u00f3 hozz\u00e1f\u00e9r\u00e9shez. Ebben az esetben a felhaszn\u00e1l\u00f3nak kell megadni az enged\u00e9lyt az alkalmaz\u00e1s sz\u00e1m\u00e1ra. Ennek a k\u00f6zvetlen k\u00f6vetkezm\u00e9nye az, hogy az alkalmaz\u00e1sokat fel kell k\u00e9sz\u00edteni arra az esetre, ha nincs megadva egy adott funkci\u00f3 m\u0171k\u00f6d\u00e9s\u00e9hez elengedhetetlen enged\u00e9ly.

    Ezen az oldalon tal\u00e1lhat\u00f3 az \u00f6sszes enged\u00e9ly kateg\u00f3ri\u00e1nk\u00e9nt.

    Az AndroidManifest.xml f\u00e1jlban kateg\u00f3ri\u00e1t\u00f3l f\u00fcggetlen\u00fcl meg kell adni az alkalmaz\u00e1s sz\u00e1m\u00e1ra sz\u00fcks\u00e9ges \u00f6sszes enged\u00e9lyt, de ennek hat\u00e1sa elt\u00e9r a futtat\u00f3 rendszer verzi\u00f3j\u00e1t\u00f3l \u00e9s a target SDK verzi\u00f3t\u00f3l f\u00fcgg\u0151en:

    • Ha az eszk\u00f6z Android 5.1 (API level 22) vagy alacsonyabb verzi\u00f3t futtat, VAGY az alkalmaz\u00e1s target SDK szintje 22 vagy kisebb, akkor a rendszer telep\u00edt\u00e9skor k\u00e9ri el az \u00f6sszes sz\u00fcks\u00e9ges enged\u00e9lyt. Ha a felhaszn\u00e1l\u00f3 nem fogadja el egyben az \u00f6sszes k\u00e9r\u00e9st, akkor a telep\u00edt\u00e9si folyamat le\u00e1ll.

    • Ha az eszk\u00f6z Android 6.0 (API level 23) vagy nagyobb verzi\u00f3t futtat \u00c9S az alkalmaz\u00e1s target SDK szintje 23 vagy nagyobb, akkor az alkalmaz\u00e1s a fut\u00e1sa sor\u00e1n fogja elk\u00e9rni a dangerous kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyeket, a normal enged\u00e9lyeket pedig a rendszer automatikusan megadja. Ebben az esetben a felhaszn\u00e1l\u00f3 b\u00e1rmikor b\u00e1rmelyik enged\u00e9lyt megadhatja, vagy visszavonhatja. Megtagadott enged\u00e9lyekkel az alkalmaz\u00e1s limit\u00e1lt funkcionalit\u00e1ssal futhat tov\u00e1bb, erre a helyzetre is fel kell k\u00e9sz\u00fclni.

    Megjegyz\u00e9s: a Google Play 2018 augusztus\u00e1t\u00f3l megk\u00f6veteli a legal\u00e1bb 26-os target SDK verzi\u00f3t \u00faj alkalmaz\u00e1sokra, 2018 november\u00e9t\u0151l pedig m\u00e1r megl\u00e9v\u0151 alkalmaz\u00e1sok friss\u00edt\u00e9seire is. Ezzel a legal\u00e1bb 6.0-s Androidot futtat\u00f3 eszk\u00f6z\u00f6k\u00f6n elker\u00fclhetetlenn\u00e9 v\u00e1lt a fut\u00e1sidej\u0171 enged\u00e9lyek kezel\u00e9se.

    Az enged\u00e9lyek kezel\u00e9s\u00e9re most az Accompanist k\u00f6nyvt\u00e1rat fogjuk most haszn\u00e1lni, mely a Jetpack Compose keretrenszerben t\u00e1mogatja az enged\u00e9lyek k\u00f6nny\u0171 kezel\u00e9s\u00e9t.

    "},{"location":"laborok/permissions/#engedelyek-kezelese","title":"Enged\u00e9lyek kezel\u00e9se","text":"

    Az alkalmaz\u00e1sunknak sz\u00fcks\u00e9ge lesz enged\u00e9lyekre a kapcsolatok el\u00e9r\u00e9s\u00e9hez, szerkeszt\u00e9s\u00e9hez, valamint h\u00edv\u00e1s ind\u00edt\u00e1s\u00e1hoz, ezeket vegy\u00fck fel a manifest f\u00e1jlba az application tagen k\u00edv\u00fclre:

        <uses-feature android:name=\"android.hardware.telephony\" android:required=\"false\" />\n<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>\n<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>\n<uses-permission android:name=\"android.permission.CALL_PHONE\"/>\n

    Az enged\u00e9lykezel\u00e9s fontosabb elemei: - A rememberPermissionState API \u00e1ltal tudunk a felhaszn\u00e1l\u00f3t\u00f3l egy enged\u00e9lyre r\u00e1k\u00e9rdezni, \u00e9s az enged\u00e9ly st\u00e1tusz\u00e1t ellen\u0151rizni. - Egyes esetekben sz\u00fcks\u00e9g lehet arra, hogy t\u00e1j\u00e9koztassuk a felhaszn\u00e1l\u00f3t arr\u00f3l, hogy mi\u00e9rt k\u00e9r az alkalmaz\u00e1s bizonyos dangerous enged\u00e9lyeket. Ez n\u00f6velheti a felhaszn\u00e1l\u00f3 bizalm\u00e1t az alkalmaz\u00e1ssal szemben. - Ha egy jogosults\u00e1got a felhaszn\u00e1l\u00f3 egyszer elutas\u00edtott, a shouldShowRationale v\u00e1ltoz\u00f3 \u00e9rt\u00e9ke alapj\u00e1n eld\u00f6nthet\u0151, hogy a k\u00e9rd\u00e9ses enged\u00e9ly \u00fajra elk\u00e9r\u00e9se a rendszer szerint szorul-e r\u00e9szletes magyar\u00e1zatra.

    Hozzuk l\u00e9tre a gy\u00f6k\u00e9r k\u00f6nyvt\u00e1rban az util package-t, majd benne az al\u00e1bbi, enged\u00e9lykezel\u00e9sek kiszolg\u00e1l\u00f3 seg\u00e9doszt\u00e1lyt.

    PermissionsUtil.kt:

    object PermissionsUtil {\n@ExperimentalPermissionsApi\nfun getTextToShowGivenPermissions(\npermissions: List<PermissionState>,\nshouldShowRationale: Boolean,\ncontext: Context\n): String {\nval revokedPermissionsSize = permissions.size\nif (revokedPermissionsSize == 0) return \"\"\nval textToShow = StringBuilder()\n\nwith(context) {\ntextToShow.apply {\nappend(getString(R.string.permission_grant_ask_beginning))\n}\n\nfor (i in permissions.indices) {\nval permission = permissions[i].permission\n.replace(Regex(\"[a-z]|[.]\"), \"\")\ntextToShow.append(\" $permission\")\nwhen {\nrevokedPermissionsSize > 1 && i == revokedPermissionsSize - 2 -> {\ntextToShow.append(getString(R.string.permission_grant_ask_separator_before_last_permission))\n}\nelse -> {\ntextToShow.append(getString(R.string.permission_grant_ask_separator))\n}\n}\n}\ntextToShow.append(\nif (revokedPermissionsSize == 1) {\ngetString(R.string.permission_grant_ask_singular_permission)\n} else getString(R.string.permission_grant_ask_multiple_permissions)\n)\ntextToShow.append(\nif (shouldShowRationale) {\ngetString(R.string.permission_grant_ask_rationale)\n} else {\ngetString(R.string.permission_grant_ask_denied)\n}\n)\n}\n\nreturn textToShow.toString()\n}\n}\n

    "},{"location":"laborok/permissions/#lista-felulet-es-logika","title":"Lista fel\u00fclet \u00e9s logika","text":"

    Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl a feature package-et, mely az egyes oldalak Composable \u00e9s ViewModel oszt\u00e1lyait fogja tartalmazni k\u00fcl\u00f6n packagenk\u00e9nt, majd hozzuk l\u00e9tre ebben a contact_list package-t.

    El\u0151sz\u00f6r foglalkozzunk az oldalhoz tartoz\u00f3 ViewModel oszt\u00e1llyal. Hozzuk l\u00e9tre a ContactsListViewModel.kt f\u00e1jlt, majd m\u00e1soljuk be az al\u00e1bbi k\u00f3dr\u00e9szletet:

    ContactsViewModel.kt:

    class ContactsViewModel(\nprivate val loadContactsUseCase: LoadContactsUseCase,\nprivate val makeCall: MakeACallUseCase,\nprivate val sendSMS: SendSMSUseCase,\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(ContactsState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: ContactsEvent) {\nwhen(event) {\nis ContactsEvent.MakeCall -> {\nval phoneNumber = event.phoneNumber\nviewModelScope.launch {\nmakeCall(phoneNumber)\n}\n}\nis ContactsEvent.SendSms -> {\nval phoneNumber = event.phoneNumber\nviewModelScope.launch {\nsendSMS(phoneNumber)\n}\n}\n}\n}\n\nfun loadContacts() {\nviewModelScope.launch(Dispatchers.IO) {\n_state.update { it.copy(isLoading = true) }\nloadContactsUseCase().catch { e ->\n_state.update { it.copy(\nisLoading = false,\nerror = e\n) }\n}.collect { contacts ->\n_state.update { it.copy(\nisLoading = false,\ncontacts = contacts\n) }\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[APPLICATION_KEY] as Application).baseContext\nContactsViewModel(\nloadContactsUseCase = LoadContactsUseCase(context),\nmakeCall = MakeACallUseCase(context),\nsendSMS = SendSMSUseCase(context)\n)\n}\n}\n}\n\n}\n\ndata class ContactsState(\nval isLoading: Boolean = false,\nval error: Throwable? = null,\nval isError: Boolean = error != null,\nval contacts: List<Contact> = emptyList()\n)\n\nsealed class ContactsEvent {\ndata class MakeCall(val phoneNumber: String): ContactsEvent()\ndata class SendSms(val phoneNumber: String): ContactsEvent()\n}\n
    Az el\u0151z\u0151 laborhoz hasonl\u00f3an a fel\u00fcletet le\u00edr\u00f3 \u00e1llapot oszt\u00e1lyt sealed class-k\u00e9nt deklar\u00e1ljuk, \u00e9s j\u00f3l elk\u00fcl\u00f6n\u00edtett \u00e1llapot oszt\u00e1lyokat vesz\u00fcnk fel, \u00edgy is jelezve, hogy az egyes \u00e1llapotokban az oldalunkon mit kell megjelen\u00edteni. Ezeket egy MutableStateFlow seg\u00edts\u00e9g\u00e9vel kezelj\u00fck, melyet egy csak olvashat\u00f3 v\u00e1ltozat\u00e1ban osztunk meg az oldalt reprezent\u00e1l\u00f3 Composable-el.

    Mivel a ViewModel k\u00e9pes t\u00fal\u00e9lni az \u0151t l\u00e9trehoz\u00f3 komponenst, ez\u00e9rt a k\u00f3db\u00f3l mi nem a konstruktor h\u00edv\u00e1s\u00e1val fogjuk l\u00e9trehozni a p\u00e9ld\u00e1nyt, hanem a keretrendszernek tudunk \u00e1tadni egy speci\u00e1lis factory met\u00f3dust, amit a rendszer az els\u0151 alkalommal meg fog h\u00edvni. Ezt a met\u00f3dust szervezt\u00fck ki a companion object r\u00e9szbe, ami jelenleg csak l\u00e9trehoz egy p\u00e9ld\u00e1nyt, a k\u00e9s\u0151bbiekben azonban hasznos lesz k\u00fcl\u00f6nb\u00f6z\u0151 k\u00fcls\u0151 \u00e9rt\u00e9kek inicializ\u00e1l\u00e1s\u00e1ra.

    K\u00fcl\u00f6n oszt\u00e1lyokat hozunk l\u00e9tre a n\u00e9vjegyeken v\u00e9gzett esem\u00e9nyek rendszerez\u00e9s\u00e9re, illetve az \u00e1llapot t\u00e1rol\u00e1s\u00e1ra.

    A k\u00e9pek \u00e9s sz\u00f6vegek kezel\u00e9s\u00e9hez hozzuk l\u00e9tre az al\u00e1bbi seg\u00e9doszt\u00e1lyokat a ui.model package-ben:

    UiIcon.kt:

    import androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\n\nsealed class UiIcon {\n@Composable\nfun AsImage(\nmodifier: Modifier = Modifier,\ntint: Color = LocalContentColor.current,\n@StringRes contentDescriptionResId: Int? = null,\n) {\nwhen (this) {\nis ImageRes -> {\nIcon(\npainter = painterResource(id = drawableResId),\ntint = tint,\nmodifier = modifier,\ncontentDescription = if (contentDescriptionResId == null) {\nnull\n} else stringResource(id = contentDescriptionResId)\n)\n}\nis VectorImage -> {\nIcon(\nimageVector = imageVector,\ntint = tint,\nmodifier = modifier,\ncontentDescription = if (contentDescriptionResId == null) {\nnull\n} else stringResource(id = contentDescriptionResId)\n)\n}\n}\n}\n}\n\ndata class ImageRes(@DrawableRes val drawableResId: Int): UiIcon()\n\ndata class VectorImage(val imageVector: ImageVector): UiIcon()\n

    UiText.kt:

    import android.content.Context\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport hu.bme.aut.android.contacts.R\n\nsealed class UiText {\n\ndata class DynamicString(val text: String) : UiText()\n\ndata class StringResource(\n@StringRes val id: Int,\nval formatArgs: ArrayList<Any> = arrayListOf()\n) : UiText()\n\n@Composable\nfun AsText(\nmodifier: Modifier = Modifier,\ncolor: Color = Color.Unspecified,\nfontSize: TextUnit = TextUnit.Unspecified,\nfontStyle: FontStyle? = null,\nfontWeight: FontWeight? = null,\nfontFamily: FontFamily? = null,\nletterSpacing: TextUnit = TextUnit.Unspecified,\ntextDecoration: TextDecoration? = null,\ntextAlign: TextAlign? = null,\nlineHeight: TextUnit = TextUnit.Unspecified,\noverflow: TextOverflow = TextOverflow.Clip,\nsoftWrap: Boolean = true,\nmaxLines: Int = Int.MAX_VALUE,\nonTextLayout: (TextLayoutResult) -> Unit = {},\nstyle: TextStyle = LocalTextStyle.current\n) {\nText(\ntext = when(this) {\nis DynamicString -> {\ntext\n}\nis StringResource -> {\nstringResource(id = id, formatArgs)\n}\n},\nmodifier = modifier,\ncolor = color,\nfontSize = fontSize,\nfontStyle = fontStyle,\nfontWeight = fontWeight,\nfontFamily = fontFamily,\nletterSpacing = letterSpacing,\ntextDecoration = textDecoration,\ntextAlign = textAlign,\nlineHeight = lineHeight,\noverflow = overflow,\nsoftWrap = softWrap,\nmaxLines = maxLines,\nonTextLayout = onTextLayout,\nstyle = style\n)\n}\n\nfun asString(context: Context): String {\nreturn when(this) {\nis DynamicString -> {\ntext\n}\nis StringResource -> {\ncontext.getString(id,formatArgs)\n}\n}\n}\n\n}\n\n\nfun Throwable?.toUiText(): UiText {\nval message: String = this?.message.orEmpty()\nreturn if (message.isBlank()) {\nUiText.StringResource(R.string.some_error_message)\n} else {\nUiText.DynamicString(message)\n}\n}\n

    A ui.common packagebe hozzuk l\u00e9tre az al\u00e1bbi, listaelemeket reprez\u00e1nt\u00e1l\u00f3 f\u00e1jlt, \u00e9s tekints\u00fck \u00e1t az elrendez\u00e9s\u00e9t:

    ContactListItem.kt:

    import androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Call\nimport androidx.compose.material.icons.filled.Person\nimport androidx.compose.material.icons.filled.Sms\nimport androidx.compose.material3.Divider\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.contacts.domain.model.Contact\nimport hu.bme.aut.android.contacts.ui.model.VectorImage\n\n@ExperimentalMaterial3Api\n@Composable\nfun ContactListItem(\ncontact: Contact,\nmodifier: Modifier = Modifier,\nonMakeCall: (String) -> Unit,\nonSendSms: (String) -> Unit\n) {\nListItem(\nheadlineContent = { Text(text = contact.name) },\nsupportingContent = { Text(text = contact.phoneNumber) },\nleadingContent = {\nif (contact.photo != null) {\nImage(\nbitmap = contact.photo!!.asImageBitmap(),\ncontentDescription = null,\nmodifier = Modifier\n.size(50.dp)\n.clip(RoundedCornerShape(5.dp)),\ncontentScale = ContentScale.Crop\n)\n} else {\nIcon(\nimageVector = Icons.Default.Person,\ncontentDescription = null,\nmodifier = Modifier\n.size(50.dp)\n.clip(RoundedCornerShape(5.dp))\n.background(MaterialTheme.colorScheme.secondaryContainer),\ntint = MaterialTheme.colorScheme.background\n)\n}\n},\ntrailingContent = {\nRow {\nIconButton(onClick = { onMakeCall(contact.phoneNumber) }) {\nVectorImage(Icons.Default.Call)\n.AsImage(tint = MaterialTheme.colorScheme.primary)\n}\nSpacer(modifier = Modifier.width(5.dp))\nIconButton(onClick = { onSendSms(contact.phoneNumber) }) {\nVectorImage(Icons.Default.Sms)\n.AsImage(tint = MaterialTheme.colorScheme.primary)\n}\n}\n},\nmodifier = modifier\n)\nDivider(color = MaterialTheme.colorScheme.primaryContainer)\n}\n

    Ezt k\u00f6vet\u0151en l\u00e9trehozhatjuk a k\u00e9perny\u0151nkh\u00f6z tartoz\u00f3 elrendez\u00e9st a feature.contact_list package-ben. Itt megfigyelhet\u0151 a kor\u00e1bban eml\u00edtett rememberMultiplePermissionsState haszn\u00e1lata, illetve az enged\u00e9lyek megl\u00e9t\u00e9nek ellen\u0151rz\u00e9se. Az enged\u00e9lyek elk\u00e9r\u00e9se egy AlertDialog seg\u00edts\u00e9g\u00e9vel t\u00f6rt\u00e9nik.

    ContactsScreen.kt:

    import androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLifecycleOwner\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.rememberMultiplePermissionsState\nimport hu.bme.aut.android.contacts.R\nimport hu.bme.aut.android.contacts.ui.common.ContactListItem\nimport hu.bme.aut.android.contacts.ui.model.VectorImage\nimport hu.bme.aut.android.contacts.ui.model.toUiText\nimport hu.bme.aut.android.contacts.util.PermissionsUtil.getTextToShowGivenPermissions\nimport androidx.compose.foundation.lazy.items\n\n@ExperimentalPermissionsApi\n@ExperimentalMaterial3Api\n@Composable\nfun ContactsScreen(\nmodifier: Modifier = Modifier,\nviewModel: ContactsViewModel = viewModel(factory = ContactsViewModel.Factory),\nonListItemClick: (String) -> Unit,\nonFabClick: () -> Unit\n) {\nval context = LocalContext.current\nval lifecycle = LocalLifecycleOwner.current.lifecycle\n\nval contactsPermissions = rememberMultiplePermissionsState(\npermissions = listOf(\nandroid.Manifest.permission.READ_CONTACTS,\nandroid.Manifest.permission.WRITE_CONTACTS,\nandroid.Manifest.permission.CALL_PHONE\n)\n)\n\nif (contactsPermissions.allPermissionsGranted) {\n\nLaunchedEffect(key1 = contactsPermissions.allPermissionsGranted) {\nviewModel.loadContacts()\n}\n\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nScaffold(\nmodifier = modifier,\nfloatingActionButton = {\nLargeFloatingActionButton(onClick = onFabClick) {\nVectorImage(Icons.Default.Add).AsImage()\n}\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nif (state.isLoading) {\nCircularProgressIndicator(color = MaterialTheme.colorScheme.primary)\n} else if (state.isError) {\nText(text = state.error.toUiText().asString(context))\n} else {\nLazyColumn(\nmodifier = Modifier\n.fillMaxSize()\n) {\nitems(state.contacts) { contact ->\nContactListItem(\ncontact = contact,\nmodifier = Modifier.clickable {\nonListItemClick(contact.id)\n},\nonMakeCall = { viewModel.onEvent(ContactsEvent.MakeCall(it)) },\nonSendSms = { viewModel.onEvent(ContactsEvent.SendSms(it)) }\n)\n}\n}\n}\n}\n}\n\n} else {\nAlertDialog(\nonDismissRequest = { },\nconfirmButton = {\nButton(onClick = { contactsPermissions.launchMultiplePermissionRequest() }) {\nText(stringResource(id = R.string.button_label_request_permission))\n}\n},\ntext = {\nText(\ngetTextToShowGivenPermissions(\ncontactsPermissions.revokedPermissions,\ncontactsPermissions.shouldShowRationale,\ncontext\n)\n)\n}\n)\n}\n}\n
    Ezt k\u00f6vet\u0151en friss\u00edthetj\u00fck a Screen oszt\u00e1lyunk az \u00faj \u00fatvonallal:

    object Contacts: Screen(route = \"contacts\")\n

    Illetve kieg\u00e9sz\u00edthetj\u00fck a NavGraph oszt\u00e1lyt is a listan\u00e9zet\u00fcnkkel:

    @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.Contacts.route\n) {\ncomposable(route = Screen.Contacts.route) {\nContactsScreen(\nonListItemClick = {\n//TODO\n},\nonFabClick = {\n//TODO\n}\n)\n}\n}\n}\n

    Ha hib\u00e1t dob az Android Studio, adjuk hozz\u00e1 a hi\u00e1nyz\u00f3 annot\u00e1ci\u00f3kat.

    "},{"location":"laborok/permissions/#vcf-file-importalasa","title":"VCF file import\u00e1l\u00e1sa","text":"

    A k\u00f6vetkez\u0151 l\u00e9p\u00e9seket csak emul\u00e1toron sz\u00fcks\u00e9ges elv\u00e9gezni, ha nem \u00e1llnak rendelkez\u00e9sre n\u00e9vjegyek: - T\u00f6lts\u00fck le a repository-ban el\u00e9rhet\u0151 .vcf f\u00e1jlt: n\u00e9vjegyek. - A Device Explorer tool haszn\u00e1lat\u00e1val m\u00e1soljuk be a let\u00f6lt\u00f6tt f\u00e1jlt az sdcard/Download k\u00f6nyvt\u00e1rba. - Nyissuk meg az emul\u00e1tor gy\u00e1ri N\u00e9vjegyek alkalmaz\u00e1s\u00e1t, \u00e9s import\u00e1ljuk a vcf f\u00e1jlt.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lista n\u00e9zet n\u00e9vjegyekkel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a folyamatban lev\u0151 h\u00edv\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a megnyitott SMS k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/permissions/#onallo-feladatok","title":"\u00d6n\u00e1ll\u00f3 feladatok","text":""},{"location":"laborok/permissions/#kontaktok-hozzaadasa","title":"Kontaktok hozz\u00e1ad\u00e1sa","text":"

    Vegy\u00fcnk fel egy \u00faj use-case-t a n\u00e9vjegyek ment\u00e9s\u00e9re:

    SaveContactUseCase.kt:

    class SaveContactUseCase (\nprivate val context: Context\n) {\noperator fun invoke(\nname: String,\nphoneNumber: String,\nemailAddress: String,\nphotoUri: Uri?\n) {\ncontext.addNewContact(name, phoneNumber, emailAddress, photoUri)\n}\n}\n

    Eg\u00e9sz\u00edts\u00fck ki a ContactOperations oszt\u00e1lyunkat az al\u00e1bbi, hozz\u00e1ad\u00e1shoz sz\u00fcks\u00e9ges f\u00fcggv\u00e9nyekkel:

    fun Context.addNewContact(\nname: String,\nphoneNumber: String,\nemailAddress: String,\nphotoUri: Uri?\n) {\nval id = 0\nval contentProviderOperations = arrayListOf<ContentProviderOperation>()\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)\n.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)\n.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber)\n.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MAIN)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, emailAddress)\n.withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_OTHER)\n.build()\n)\n\nif (photoUri != null) {\nval photo = if (Build.VERSION.SDK_INT < 28) {\nMediaStore.Images.Media.getBitmap(contentResolver, photoUri)\n} else {\nval source = ImageDecoder.createSource(contentResolver, photoUri)\nImageDecoder.decodeBitmap(source)\n}\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bitmapToByteArray(photo))\n.build()\n)\n}\n\ncontentResolver.applyBatch(ContactsContract.AUTHORITY, contentProviderOperations)\n}\n\n\n\nprivate fun bitmapToByteArray(bitmap: Bitmap): ByteArray {\nval stream = ByteArrayOutputStream()\nbitmap.compress(Bitmap.CompressFormat.PNG, 90, stream)\nreturn stream.toByteArray()\n}\n

    Hozzuk l\u00e9tre a ui.common package-ben az al\u00e1bbi oszt\u00e1lyt az adataink \u00fajrafelhaszn\u00e1lhat\u00f3 m\u00f3don t\u00f6rt\u00e9n\u0151 megjelen\u00edt\u00e9s\u00e9hez:

    ContactDataItem.kt:

    @ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDataItem(\nmodifier: Modifier = Modifier,\nenabled: Boolean = false,\nleadingIcon: UiIcon,\nlabel: UiText,\nvalue: String,\nonValueChange: (String) -> Unit\n) {\nSurface(\nmodifier = modifier\n.fillMaxWidth()\n.padding(bottom = 10.dp),\ncolor = MaterialTheme.colorScheme.primaryContainer,\nshape = RoundedCornerShape(5.dp)\n) {\nRow(\nmodifier = Modifier\n.fillMaxWidth()\n.height(IntrinsicSize.Min),\nverticalAlignment = Alignment.CenterVertically,\nhorizontalArrangement = Arrangement.Start\n) {\nleadingIcon.AsImage(modifier = Modifier.padding(15.dp))\nColumn(\nmodifier = Modifier\n.height(IntrinsicSize.Min)\n.fillMaxWidth()\n.padding(end = 15.dp)\n) {\nTextField(\nvalue = value,\nlabel = {\nlabel.AsText(\nstyle = MaterialTheme.typography.labelSmall,\nfontWeight = FontWeight.Black,\nmaxLines = 1\n)\n},\nonValueChange = onValueChange,\nsingleLine = true,\nshape = RectangleShape,\ncolors = TextFieldDefaults.textFieldColors(\nfocusedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,\ndisabledTextColor = MaterialTheme.colorScheme.onPrimaryContainer,\ncontainerColor = Color.Transparent,\nfocusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\nunfocusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\ndisabledLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\nfocusedIndicatorColor = Color.Transparent,\nunfocusedIndicatorColor = Color.Transparent,\ndisabledIndicatorColor = Color.Transparent,\nerrorIndicatorColor = Color.Transparent,\n),\nenabled = enabled,\nmodifier = Modifier.fillMaxWidth()\n)\n}\n}\n}\n}\n\n@Preview(showBackground = true)\n@ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDataItem_Preview() {\nvar value by remember {\nmutableStateOf(\"data value\")\n}\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Warning),\nlabel = UiText.DynamicString(\"data name\"),\nvalue = value,\nonValueChange = { value = it }\n)\n}\n

    A feature.contact_add package-ben hozzuk l\u00e9tre a hozz\u00e1 tartoz\u00f3 ViewModel oszt\u00e1lyt:

    AddNewContactViewModel.kt:

    class AddNewContactViewModel(\nprivate val saveContact: SaveContactUseCase\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(AddNewContactState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: AddNewContactEvent) {\nwhen(event) {\nis AddNewContactEvent.ChangeContactName -> {\n_state.update { it.copy(contactName = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactNumber -> {\n_state.update { it.copy(contactNumber = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactEmail -> {\n_state.update { it.copy(contactEmail = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactPhoto -> {\n_state.update { it.copy(contactPhoto = event.newValue) }\n}\n}\n}\n\nfun addContact() {\nviewModelScope.launch(Dispatchers.IO) {\nsaveContact(\nstate.value.contactName,\nstate.value.contactNumber,\nstate.value.contactEmail,\nstate.value.contactPhoto\n)\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application).baseContext\nAddNewContactViewModel(saveContact = SaveContactUseCase(context))\n}\n}\n}\n}\n\ndata class AddNewContactState(\nval contactName: String = \"\",\nval contactNumber: String = \"\",\nval contactEmail: String = \"\",\nval contactPhoto: Uri? = null\n)\n\nsealed class AddNewContactEvent {\ndata class ChangeContactName(val newValue: String): AddNewContactEvent()\ndata class ChangeContactNumber(val newValue: String): AddNewContactEvent()\ndata class ChangeContactEmail(val newValue: String): AddNewContactEvent()\n\ndata class ChangeContactPhoto(val newValue: Uri): AddNewContactEvent()\n}\n

    Ezt k\u00f6vet\u0151en a fel\u00fcletet is elk\u00e9sz\u00edthetj\u00fck:

    AddNewContactScreen.kt:

    @ExperimentalFoundationApi\n@ExperimentalMaterial3Api\n@Composable\nfun AddNewContactScreen(\nmodifier: Modifier = Modifier,\nonNavigateBack: () -> Unit,\nviewModel: AddNewContactViewModel = viewModel(factory = AddNewContactViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nScaffold(\nmodifier = modifier.fillMaxSize(),\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = \"Contacts\")\n},\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nVectorImage(Icons.Default.ArrowBack).AsImage()\n}\n},\ncolors = TopAppBarDefaults.smallTopAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n},\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = {\nviewModel.addContact()\nonNavigateBack()\n}\n) {\nIcon(\nimageVector = Icons.Default.Save,\ncontentDescription = null\n)\n}\n}\n) { padding ->\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nPhotoPicker(\naddImageUri = {\nviewModel.onEvent(AddNewContactEvent.ChangeContactPhoto(it))\n},\nimageUri = state.contactPhoto\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Person),\nlabel = UiText.StringResource(R.string.contact_data_label_name),\nvalue = state.contactName,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactName(it)) }\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Phone),\nlabel = UiText.StringResource(R.string.contact_data_label_phonenumber),\nvalue = state.contactNumber,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactNumber(it)) }\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Email),\nlabel = UiText.StringResource(R.string.contact_data_label_email),\nvalue = state.contactEmail,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactEmail(it)) }\n)\n}\n}\n\n}\n\n@Composable\nfun PhotoPicker(\naddImageUri: (Uri) -> Unit,\nimageUri: Uri?\n) {\nval photoPicker = rememberLauncherForActivityResult(\ncontract = ActivityResultContracts.PickVisualMedia()\n) {\nif (it != null) {\naddImageUri(it)\n}\n}\n\nBox(\ncontentAlignment = Alignment.Center,\nmodifier = Modifier\n.padding(15.dp)\n.size(120.dp)\n.border(\nborder = BorderStroke(2.dp, MaterialTheme.colorScheme.primary),\nshape = RoundedCornerShape(100.dp)\n)\n.clip(shape = RoundedCornerShape(100.dp))\n) {\nIcon(\nimageVector = Icons.Default.Image,\ncontentDescription = null,\ntint = MaterialTheme.colorScheme.primary,\nmodifier = Modifier.size(40.dp)\n)\nAsyncImage(\nmodifier = Modifier\n.size(120.dp)\n.clip(shape = RoundedCornerShape(100.dp))\n.clickable {\nphotoPicker.launch(\nPickVisualMediaRequest(\nActivityResultContracts.PickVisualMedia.ImageOnly\n)\n)\n},\nmodel = ImageRequest.Builder(LocalContext.current)\n.data(imageUri)\n.crossfade(enable = true)\n.build(),\ncontentDescription = null,\ncontentScale = ContentScale.Crop,\n)\n}\n\n}\n

    Eg\u00e9sz\u00edts\u00fck ki a Screen oszt\u00e1lyunkat az \u00fajabb \u00fatvonallal:

    object AddContact: Screen(route = \"add_contact\")\n

    A Navgraph-ban kezelj\u00fck a FAB-unk \u00e9rint\u00e9s\u00e9t, \u00e9s vegy\u00fck fel az \u00faj k\u00e9perny\u0151t:

    onFabClick = {\nnavController.navigate(Screen.AddContact.route)\n}\n
    composable(route = Screen.AddContact.route) {\nAddNewContactScreen(\nonNavigateBack = {\nnavController.popBackStack(route = Screen.Contacts.route, inclusive = false)\n}\n)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a saj\u00e1t neveddel kit\u00f6lt\u00f6tt hozz\u00e1ad\u00e1si k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/permissions/#kontaktok-szerkesztese","title":"Kontaktok szerkeszt\u00e9se","text":"

    Vegy\u00fcnk fel egy \u00faj use-case-t a n\u00e9vjegyek r\u00e9szleteinek megjelen\u00edt\u00e9s\u00e9re:

    GetContactDetailsUseCase.kt:

    class GetContactDetailsUseCase (\nprivate val context: Context\n) {\noperator fun invoke(id: String): Contact {\nreturn context.getContactDetails(id)\n}\n}\n

    Eg\u00e9sz\u00edts\u00fck ki a ContactOperations oszt\u00e1lyunkat az al\u00e1bbiakkal:

    ContactsOperations.kt:

        fun Context.getContactDetails(id: String): Contact {\nreturn Contact(\nid = id,\nname = getContactName(id),\nphoneNumber = getContactPhoneNumber(id),\nemailAddress = getContactEmail(id),\nphoto = getContactPhoto(id)\n)\n}\n

    A feature.contact_details package-ben hozzuk l\u00e9tre a hozz\u00e1 tartoz\u00f3 ViewModel oszt\u00e1lyt:

    ContactDetailsViewModel.kt:

    class ContactDetailsViewModel(\ngetContactDetails: GetContactDetailsUseCase,\nprivate val makeCall: MakeACallUseCase,\nprivate val sendSMS: SendSMSUseCase,\nsavedStateHandle: SavedStateHandle\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(ContactDetailsState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: ContactDetailsEvent) {\nwhen(event) {\nis ContactDetailsEvent.MakeCall -> {\nmakeCall(state.value.contact.phoneNumber)\n}\nis ContactDetailsEvent.SendSms -> {\nsendSMS(state.value.contact.phoneNumber)\n}\n}\n}\n\ninit {\nval id = checkNotNull<String>(savedStateHandle[\"id\"])\nviewModelScope.launch(Dispatchers.IO) {\ntry {\n_state.update { it.copy(isLoading = true) }\nval contact = getContactDetails(id)\n_state.update { it.copy(\nisLoading = false,\ncontact = contact\n) }\n} catch (e: IOException) {\n_state.update { it.copy(\nisLoading = false,\nerror = e\n) }\n}\n\n}\n}\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application).baseContext\nval savedStateHandle = createSavedStateHandle()\nContactDetailsViewModel(\ngetContactDetails = GetContactDetailsUseCase(context),\nmakeCall = MakeACallUseCase(context),\nsendSMS = SendSMSUseCase(context),\nsavedStateHandle = savedStateHandle\n)\n}\n}\n}\n}\n\ndata class ContactDetailsState(\nval isLoading: Boolean = false,\nval error: Throwable? = null,\nval isError: Boolean = error != null,\nval contact: Contact = Contact()\n)\n\nsealed class ContactDetailsEvent {\nobject MakeCall: ContactDetailsEvent()\nobject SendSms: ContactDetailsEvent()\n\n}\n

    Hozzuk l\u00e9tre a ui.common package-ben az al\u00e1bbi oszt\u00e1lyt a gombjainknak:

    ActionButton.kt:

    @Composable\nfun ActionButton(\nmodifier: Modifier = Modifier,\nonClick: () -> Unit,\nicon: UiIcon,\nlabel: UiText? = null\n) {\n\nSurface(\nmodifier = modifier\n.clickable { onClick() },\nshape = RoundedCornerShape(5.dp),\ncolor = MaterialTheme.colorScheme.secondary\n) {\nColumn(\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nicon.AsImage(\nmodifier = Modifier\n.padding(\nstart = 10.dp,\nend = 10.dp,\ntop = 5.dp,\nbottom = if (label == null) 5.dp else 0.dp\n)\n.size(if (label == null) 40.dp else 20.dp),\ntint = MaterialTheme.colorScheme.onSecondary\n)\n\nlabel?.AsText(\nmodifier = Modifier.padding(bottom = 5.dp),\ncolor = MaterialTheme.colorScheme.onSecondary\n)\n}\n}\n}\n\n@Preview\n@Composable\nfun ActionButton_Preview() {\nActionButton(\nonClick = { /*TODO*/ },\nicon = VectorImage(Icons.Default.Warning),\nlabel = UiText.DynamicString(\"Warning\")\n)\n}\n

    Ezt k\u00f6vet\u0151en a fel\u00fcletet is elk\u00e9sz\u00edthetj\u00fck:

    ContactDetailsScreen.kt:

    @ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDetailsScreen(\nmodifier: Modifier = Modifier,\nonNavigateBack: () -> Unit,\nviewModel: ContactDetailsViewModel = viewModel(factory = ContactDetailsViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval context = LocalContext.current\n\nScaffold(\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = \"Contacts\")\n},\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nVectorImage(Icons.Default.ArrowBack).AsImage()\n}\n},\ncolors = TopAppBarDefaults.smallTopAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nif (state.isLoading) {\nCircularProgressIndicator(color = MaterialTheme.colorScheme.primary)\n} else if (state.isError) {\nText(text = state.error.toUiText().asString(context))\n} else {\nval contact = state.contact\nval photo = contact.photo\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nif (photo != null) {\nImage(\nbitmap = photo.asImageBitmap(),\ncontentDescription = null,\nmodifier = Modifier\n.padding(15.dp)\n.size(120.dp)\n.clip(shape = RoundedCornerShape(100.dp))\n.background(Color.Black),\nalignment = Alignment.Center\n)\n}\nText(\ntext = contact.name,\nstyle = MaterialTheme.typography.titleLarge,\ntextAlign = TextAlign.Center\n)\nSpacer(modifier = Modifier.height(15.dp))\nRow(modifier = Modifier\n.fillMaxWidth()\n.padding(horizontal = 5.dp)) {\nActionButton(\nonClick = { viewModel.onEvent(ContactDetailsEvent.MakeCall) },\nicon = VectorImage(Icons.Default.Call),\nmodifier = Modifier.weight(1f)\n)\nActionButton(\nonClick = { viewModel.onEvent(ContactDetailsEvent.SendSms) },\nicon = VectorImage(Icons.Default.Sms),\nmodifier = Modifier\n.weight(1f)\n.padding(horizontal = 5.dp)\n)\nActionButton(\nonClick = { /*TODO*/ },\nicon = VectorImage(Icons.Default.Mail),\nmodifier = Modifier.weight(1f)\n)\n}\nSpacer(modifier = Modifier.height(15.dp))\nContactDataItem(\nleadingIcon = VectorImage(Icons.Default.Home),\nlabel = UiText.StringResource(R.string.contact_data_label_phonenumber),\nvalue = contact.phoneNumber,\nonValueChange = { },\nmodifier = Modifier.padding(horizontal = 5.dp)\n)\nContactDataItem(\nleadingIcon = VectorImage(Icons.Default.Home),\nlabel = UiText.StringResource(R.string.contact_data_label_email),\nvalue = contact.emailAddress,\nonValueChange = { },\nmodifier = Modifier.padding(horizontal = 5.dp)\n)\n}\n}\n}\n}\n}\n\n@ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Preview(showBackground = true)\n@Composable\nfun ContactDetailsScreen_Preview() {\nContactDetailsScreen(onNavigateBack = { })\n}\n

    Eg\u00e9sz\u00edts\u00fck ki a Screen oszt\u00e1lyunkat az \u00fajabb \u00fatvonallal:

    object ContactDetails: Screen(route = \"contact/{id}\") {\nfun passId(id: String) = \"contact/$id\"\n}\n

    A Navgraph-ban kezelj\u00fck a listaelemek \u00e9rint\u00e9s\u00e9t, \u00e9s vegy\u00fck fel az \u00faj k\u00e9perny\u0151t:

    onListItemClick = { id ->\nnavController.navigate(Screen.ContactDetails.passId(id))\n},\n
    composable(\nroute = Screen.ContactDetails.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.StringType\n}\n)\n) {\nContactDetailsScreen(\nonNavigateBack = {\nnavController.popBackStack(route = Screen.Contacts.route, inclusive = false)\n}\n)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a saj\u00e1t neveddel kit\u00f6lt\u00f6tt r\u00e9szletek k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/persistence/","title":"Labor06 - Perzisztens adatt\u00e1rol\u00e1s a Room k\u00f6nyvt\u00e1rral","text":""},{"location":"laborok/persistence/#bevezetes","title":"Bevezet\u00e9s","text":"

    A labor c\u00e9lja a perzisztens adatt\u00e1rol\u00e1s megismer\u00e9se ORM technik\u00e1val, a Room k\u00f6nyvt\u00e1r seg\u00edts\u00e9g\u00e9vel. A labor egy\u00fattal azt is bemutatja, hogy egy modern, \u00f6sszetett alkalamz\u00e1s k\u00fcl\u00f6nb\u00f6z\u0151 r\u00e9szeit (adatel\u00e9r\u00e9s, \u00fczleti logika, felhaszn\u00e1l\u00f3i fel\u00fclet) hogyan tudunk megfelel\u0151 r\u00e9tegez\u00e9ssel, \u00e1ttekinthet\u0151 \u00e9s j\u00f3l karban tarthat\u00f3 architekt\u00far\u00e1val kifejleszteni.

    Ezeknek az elveknek a megismer\u00e9s\u00e9hez az \u00f6t\u00f6dik laboron megismert Todo alkalmaz\u00e1s kidolgozottabb verzi\u00f3j\u00e1t k\u00e9sz\u00edtj\u00fck el.

    "},{"location":"laborok/persistence/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/persistence/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    A Room megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektben fogunk dolgozni, ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl. Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a projektet.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

    "},{"location":"laborok/persistence/#a-reteges-architektura-kialakitasa","title":"A r\u00e9teges architekt\u00fara kialak\u00edt\u00e1sa","text":"

    Egy \u00f6sszetettebb alkalmaz\u00e1son bel\u00fcl a k\u00f3dot r\u00e9tegekbe szervezz\u00fck, hogy a m\u0171k\u00f6d\u00e9s j\u00f3l \u00e1tl\u00e1that\u00f3 legyen, illetve hogy az alkalmaz\u00e1s egyes r\u00e9szei kev\u00e9sb\u00e9 f\u00fcggjenek a t\u00f6bbit\u0151l. A r\u00e9teges architekt\u00far\u00e1nkban lesz egy domain modell \u00e9s \u00fczleti logika, amely a megval\u00f3s\u00edtott funkcionalit\u00e1s megjelen\u00e9st\u0151l \u00e9s adatb\u00e1ziskezel\u00e9st\u0151l f\u00fcggetlen r\u00e9sz\u00e9t k\u00e9pezi. A felhaszn\u00e1l\u00f3i fel\u00fcleten bevitt adatoknak k\u00fcl\u00f6n modellje lesz, \u00e9s ezt majd \u00e1t kell alak\u00edtanunk a f\u00fcggetlen domain modellre. Az adatb\u00e1zisba ment\u00e9shez szint\u00e9n k\u00fcl\u00f6n modellt haszn\u00e1lunk, \u00e9s ebben az esetben is konverzi\u00f3ra lesz sz\u00fcks\u00e9g\u00fcnk a domain modell \u00e9s az adatb\u00e1zismodell k\u00f6z\u00f6tt. Az \u00edgy kialak\u00edtott r\u00e9tegez\u00e9ssel a megjelen\u00edt\u00e9s \u00e9s az adatb\u00e1zismodell sem f\u00fcggenek egym\u00e1st\u00f3l, csup\u00e1n a f\u00fcggetlen domain modellt\u0151l. \u00cdgy mind a megjelen\u00e9s, mind az adab\u00e1ziskezel\u0151 r\u00e9teg k\u00f6nnyebben m\u00f3dos\u00edthat\u00f3 a m\u00e1sikt\u0151l f\u00fcggetlen\u00fcl.

    "},{"location":"laborok/persistence/#a-domainmodell-es-az-uzleti-logika-elkeszitese","title":"A domainmodell \u00e9s az \u00fczleti logika elk\u00e9sz\u00edt\u00e9se","text":"

    El\u0151sz\u00f6r a domain r\u00e9teggel foglalkozunk, ez tulajdonk\u00e9ppen m\u00e1r rendelkez\u00e9sre \u00e1ll a domain.model package-ben. Ez a domain (a megoldand\u00f3 feladat) nagyj\u00e1b\u00f3l technol\u00f3giaf\u00fcggetlen r\u00e9sze, amelybe m\u00e9g nem vegy\u00fclnek a konkr\u00e9t adatt\u00e1rol\u00e1si technol\u00f3gi\u00e1val vagy megjelen\u00edt\u00e9ssel kapcsolatos r\u00e9szletek. Ezzel a k\u00f6zb\u00fcls\u0151 r\u00e9teggel az alkalmaz\u00e1sunk komponensei laz\u00e1bban csatoltt\u00e1 v\u00e1lnak, \u00e9s megk\u00f6nny\u00edtik, hogy kev\u00e9s m\u00f3dos\u00edt\u00e1ssal lecser\u00e9lj\u00fck ak\u00e1r az adatb\u00e1ziskezel\u00e9s\u00e9rt felel\u0151s Roomot, ak\u00e1r a megjelen\u00edt\u00e9st. Az itt megval\u00f3s\u00edtott \u00fczleti logika m\u0171veletek nem f\u00fcggenek k\u00f6zvetlen a Roomt\u00f3l, csak a reposiory komponensekt\u0151l, \u00e9s mivel a tennival\u00f3k f\u00fcggetlen domainmodellj\u00e9vel dolgoznak, a megjelen\u00edt\u00e9st\u0151l is f\u00fcggetlenek.

    Term\u00e9szetesen m\u00e1s architekt\u00far\u00e1val is lehet m\u0171k\u00f6d\u0151k\u00e9pes alkalmaz\u00e1st k\u00e9sz\u00edteni, de ez a megold\u00e1s v\u00e1lt Android platformon konvencion\u00e1liss\u00e1, ez\u00e9rt ha ezt k\u00f6vetj\u00fck, akkor k\u00f6nnyebben tudunk egy\u00fctt dolgozni m\u00e1s fejleszt\u0151kkel. A hivatalos dokument\u00e1ci\u00f3 is szentel ennek a k\u00e9rd\u00e9snek egy fejezetet: https://developer.android.com/topic/architecture/domain-layer

    Tekints\u00fck \u00e1t a domain.model package-et, n\u00e9zz\u00fck meg, hogyan \u00e9p\u00fcl fel a domainmodell!

    "},{"location":"laborok/persistence/#a-felhasznaloi-felulet-elkeszitese","title":"A felhaszn\u00e1l\u00f3i fel\u00fclet elk\u00e9sz\u00edt\u00e9se","text":"

    Most a felhaszn\u00e1l\u00f3i fel\u00fclet modellj\u00e9vel haladunk tov\u00e1bb. Ezek a kor\u00e1bban l\u00e9trehozott domainmodellhez igen hasonlatosak, de a rugalmasabb architekt\u00fara \u00e9s a laza csatol\u00e1s megval\u00f3s\u00edt\u00e1sa miatt k\u00fcl\u00f6n modelleket k\u00e9sz\u00edt\u00fcnk a fel\u00fcleten megjelen\u00edtett adatokhoz. Ez egy ilyen egyszer\u0171 alkalmaz\u00e1sn\u00e1l el\u0151sz\u00f6r indokolatlan duplik\u00e1ci\u00f3nak t\u0171nhet, az az \u00e9rz\u00e9s\u00fcnk, hogy bizonyos dolgokat t\u00f6bbsz\u00f6r implement\u00e1lunk. Azonban ahogy egy alkalmaz\u00e1s fejl\u0151dik, b\u0151v\u00fcl, egy ilyen laz\u00e1n csatolt \u00e9s \u00e1tl\u00e1that\u00f3 architekt\u00fara mindenk\u00e9pp kifizet\u0151d\u0151v\u00e9 v\u00e1lik.

    A felhaszn\u00e1l\u00f3i fel\u00fclet modellje is m\u00e1r rendelkez\u00e9sre \u00e1ll a kiindul\u00f3 projektben. Tekints\u00fck \u00e1t a ui.model package-et! Ebben m\u00e1r rendelkez\u00e9sre \u00e1ll a PriorityUi \u00e9s a TodoUi oszt\u00e1ly, illetve konverzi\u00f3s logika is van a f\u00e1jloban a domain modellbe/modellb\u0151l t\u00f6rt\u00e9n\u0151 konvert\u00e1l\u00e1shoz. Tal\u00e1lunk m\u00e9g itt egy UiText oszt\u00e1lyt a felhaszn\u00e1l\u00f3i fel\u00fcleten megjelen\u0151 sz\u00f6veges \u00fczenetek k\u00f6nnyebb kezel\u00e9s\u00e9hez.

    A fent \u00e1ttekintett UI modellekre \u00e9p\u00fclnek a felhaszn\u00e1l\u00f3i fel\u00fclet megjelen\u00edtett r\u00e9szei. Ezek a ui.common package-ben m\u00e1r szint\u00e9n rendelkez\u00e9sre \u00e1llnak. Tekints\u00fck \u00e1t ezeket is!

    Most az elemi fel\u00fcleti elemekkel v\u00e9gezt\u00fcnk, most j\u00f6nnek a t\u00e9nyleges k\u00e9perny\u0151k. Ezek a feature package-ben vannak. Ezen bel\u00fcl h\u00e1rom f\u0151 funkci\u00f3t fogunk megk\u00fcl\u00f6nb\u00f6ztetni: l\u00e9trehoz\u00e1s, list\u00e1z\u00e1s, megjelen\u00edt\u00e9s. Ezek egy-egy subpackage-ben vannak, \u00e9s a kiindul\u00f3 projektben ez is mind rendelkez\u00e9sre \u00e1llnak. Tekints\u00fck \u00e1t ezeket is, \u00e9s eleven\u00edts\u00fck fel a funkci\u00f3jukat.

    A fel\u00fcleti elemek elk\u00e9sz\u00edt\u00e9se ut\u00e1n gondoskodni kell a k\u00f6zt\u00fck t\u00f6rt\u00e9n\u0151 navig\u00e1ci\u00f3r\u00f3l is. Ez is m\u00e1r rendelkez\u00e9sre \u00e1ll a navigation package-ben. N\u00e9zz\u00fck \u00e1t ezeket is!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a teend\u0151k list\u00e1ja a fut\u00f3 alkalmaz\u00e1sban, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/persistence/#az-adatreteg-elkeszitese","title":"Az adatr\u00e9teg elk\u00e9sz\u00edt\u00e9se","text":"

    Most elk\u00e9sz\u00edtj\u00fck az adatb\u00e1zis kezel\u00e9s\u00e9\u00e9rt felel\u0151s komponenseket. Most is n\u00e9hol \u00fagy t\u0171nhet majd, hogy bizonyos dolgokat \"dupl\u00e1n\" val\u00f3s\u00edtunk meg, azonban ennek az el\u0151nyei egy val\u00f3s komplex alkalmaz\u00e1sban mindig \u00e9rv\u00e9nyes\u00fclnek, ez\u00e9rt \u00e9rdemes megismern\u00fcnk, \u00e9s haszn\u00e1lnunk ezt az architektur\u00e1lis szervez\u00e9st.

    Az els\u0151 l\u00e9p\u00e9s, hogy a Roomot mint f\u00fcgg\u0151s\u00e9get vegy\u00fck fel a projekt\u00fcnkbe. Ehhez el\u0151sz\u00f6r a projekt szint\u0171 build.gradle.kts f\u00e1jlban \u00e1ll\u00edtsuk be a haszn\u00e1lni k\u00edv\u00e1nt kapt plugin verzi\u00f3j\u00e1t a f\u00fcgg\u0151s\u00e9gek k\u00f6zt:

    kotlin(\"kapt\") version \"1.9.10\" apply false\n

    Majd a modul szint\u0171 build.gradle.kts f\u00e1jlban enged\u00e9lyezz\u00fck a kapt plugint:

    plugins {\nid(\"com.android.application\")\nid(\"org.jetbrains.kotlin.android\")\nkotlin(\"kapt\")\n}\n

    \u00c9s ugyanitt vegy\u00fck is fel a Room k\u00f6nyvt\u00e1rat:

        // Room\nval room_version = \"2.6.0\"\nimplementation(\"androidx.room:room-runtime:$room_version\")\nkapt(\"androidx.room:room-compiler:$room_version\")\nimplementation(\"androidx.room:room-ktx:$room_version\")\n

    Most sz\u00fcks\u00e9g\u00fcnk van az elmentett tennival\u00f3k adatmodellj\u00e9re. Mivel a megk\u00f6zel\u00edt\u00e9s\u00fcnkben a Room k\u00f6nyvt\u00e1rat haszn\u00e1ljuk, ez azt jelenti, hogy egy olyan oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, amellyel a szoftver\u00fcnkben fut\u00e1sid\u0151ben egy teend\u0151 j\u00f3l modellezhet\u0151, \u00e9s ezt az oszt\u00e1lyt megfeleltetj\u00fck az SQLite adatb\u00e1zisunk egy t\u00e1bl\u00e1j\u00e1val. Ez \u00edgy k\u00e9nyelmes, hiszen a rel\u00e1ci\u00f3s adatmodell kiforrott, k\u00f6zismert, ez\u00e9rt az adatokat gyakran t\u00e1bl\u00e1kban akarjuk t\u00e1rolni, ugyanakkor a programunkban az objektumorient\u00e1lt szeml\u00e9letben mozgunk otthonosan, \u00e9s az adatokat ez\u00e9rt objektumokban szeretj\u00fck t\u00e1rolni. Ezeket az oszt\u00e1lyokat a szoftverfejleszt\u00e9si terminol\u00f3gi\u00e1ban entit\u00e1soknak szoktuk nevezni.

    Hozzunk l\u00e9tre ez\u00e9rt egy data.entities package-et, \u00e9s ebbe vegy\u00fck fel a k\u00f6vetkez\u0151t:

    @Entity(tableName = \"todo_table\")\ndata class TodoEntity(\n@PrimaryKey(autoGenerate = true) val id: Int,\nval title: String,\nval priority: Priority,\nval dueDate: LocalDate,\nval description: String\n)\n\nfun TodoEntity.asTodo(): Todo = Todo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = dueDate,\ndescription = description\n)\n\nfun Todo.asTodoEntity(): TodoEntity = TodoEntity(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = dueDate,\ndescription = description\n)\n

    Ebben a k\u00f3dban a Room k\u00f6nyvt\u00e1r annot\u00e1ci\u00f3val meg van jel\u00f6lve, hogy az oszt\u00e1ly egy entit\u00e1s lesz, \u00e9s a todo_table nev\u0171 t\u00e1bl\u00e1ba lesznek a p\u00e9ld\u00e1nyai lek\u00e9pezve, valamint az id nev\u0171 tagv\u00e1ltoz\u00f3j\u00e1nak megfelel\u0151 oszlop lesz az els\u0151dleges kulcs, \u00e9s ennek \u00e9rt\u00e9keit besz\u00far\u00e1skor fogja egyedi \u00e9rt\u00e9kk\u00e9nt gener\u00e1lni a k\u00f6rnyezet, vagyis nem kell nek\u00fcnk gondoskodnunk r\u00f3la, hogy minden \u00faj teend\u0151 \u00faj egyedi azonos\u00edt\u00f3t kapjon.

    A k\u00f6vetkez\u0151 l\u00e9p\u00e9s, hogy az entit\u00e1shoz kapcsol\u00f3d\u00f3 alapm\u0171veleteket is t\u00e1mogassuk a Room k\u00f6nyvt\u00e1r seg\u00edts\u00e9g\u00e9vel. Ezt egy DAO (Data Access Object) komponenssel fogjuk megval\u00f3s\u00edtani. A DAO egy - szint\u00e9n nem csak Android alatt alkalmazott - tervez\u00e9si minta, amelynek a l\u00e9nyege, hogy az egy entit\u00e1shoz kapcsol\u00f3d\u00f3 \u00f6sszes adatb\u00e1zism\u0171veleteket egy komponensbe gy\u0171jtj\u00fck \u00f6ssze. Ez egyr\u00e9szt j\u00f3l \u00e1ttekinthet\u0151, illetve ha az adatb\u00e1zist le szeretn\u00e9nk cser\u00e9lni m\u00e1s technol\u00f3gi\u00e1ra, akkor elvileg elegend\u0151 lenne a DAO komponens m\u00f3dos\u00edt\u00e1sa, b\u00e1r ilyen jelleg\u0171 m\u00f3dos\u00edt\u00e1sra manaps\u00e1g \u00e1ltal\u00e1ban nincs sz\u00fcks\u00e9g.

    Hozzunk l\u00e9tre egy data.dao package-et, \u00e9s ebbe vegy\u00fck fel az al\u00e1bbit:

    @Dao\ninterface TodoDao {\n\n@Insert(onConflict = OnConflictStrategy.REPLACE)\nsuspend fun insertTodo(todo: TodoEntity)\n\n@Query(\"SELECT * FROM todo_table\")\nfun getAllTodos(): Flow<List<TodoEntity>>\n\n@Query(\"SELECT * FROM todo_table WHERE id = :id\")\nfun getTodoById(id: Int): Flow<TodoEntity>\n\n@Update\nsuspend fun updateTodo(todo: TodoEntity)\n\n@Query(\"DELETE FROM todo_table WHERE id = :id\")\nsuspend fun deleteTodo(id: Int)\n}\n

    L\u00e1thatjuk, hogy egyr\u00e9szt maga az interf\u00e9sz is meg van jel\u00f6lve, mint DAO komponens, m\u00e1sr\u00e9szt az egyes m\u0171veleteken is Room annot\u00e1ci\u00f3k vannak. A Room az annot\u00e1ci\u00f3b\u00f3l, illetve az annot\u00e1lt met\u00f3dus param\u00e9tereib\u0151l \u00e9s visszat\u00e9r\u00e9si \u00e9rt\u00e9k\u00e9b\u0151l ki tudja k\u00f6vetkeztetni a sz\u00e1nd\u00e9kunkat. Besz\u00e9lj\u00fck \u00e1t az egyes met\u00f3dusok jelent\u00e9s\u00e9t a gyakorlatvezet\u0151vel! Mivel ez a komponens egy interf\u00e9sz, ezt nem mi fogjuk implement\u00e1lni, hanem a Room k\u00e9sz\u00edti el fut\u00e1sid\u0151ben az implement\u00e1ci\u00f3j\u00e1t.

    Ezut\u00e1n egy repository komponenst k\u00e9sz\u00edt\u00fcnk. Ez n\u00e9mileg \u00fagy t\u0171nik, mintha nem adna hozz\u00e1 t\u00fal sokat a DAO-hoz, azonban fontos c\u00e9lja, hogy a fels\u0151bb r\u00e9tegeket f\u00fcggetlen\u00edtse a Roomt\u00f3l, hogy ne k\u00f6zvetlen att\u00f3l f\u00fcggjenek. Tulajdonk\u00e9ppen a kiindul\u00f3 projektben m\u00e1r l\u00e9tezik egy ilyen komponens, de ezt \u00e1t kell alak\u00edtanunk, mert a kor\u00e1bbi verzi\u00f3 m\u00e9g nem haszn\u00e1lt f\u00fcggetlen dom\u00e9n- \u00e9s adatb\u00e1zismodelleket.

    El\u0151sz\u00f6r k\u00e9sz\u00edts\u00fcnk egy data.repository package-et, \u00e9s ebbe mozgassuk \u00e1t az interf\u00e9szt, majd cser\u00e9lj\u00fck le az al\u00e1bbira:

    interface TodoRepository {\nfun getAllTodos(): Flow<List<TodoEntity>>\n\nfun getTodoById(id: Int): Flow<TodoEntity>\n\nsuspend fun insertTodo(todo: TodoEntity)\n\nsuspend fun updateTodo(todo: TodoEntity)\n\nsuspend fun deleteTodo(id: Int)\n}\n

    Majd pedig ennek az implement\u00e1ci\u00f3j\u00e1t is k\u00e9sz\u00edts\u00fck el:

    class TodoRepositoryImpl(private val dao: TodoDao) : TodoRepository {\n\noverride fun getAllTodos(): Flow<List<TodoEntity>> = dao.getAllTodos()\n\noverride fun getTodoById(id: Int): Flow<TodoEntity> = dao.getTodoById(id)\n\noverride suspend fun insertTodo(todo: TodoEntity) { dao.insertTodo(todo) }\n\noverride suspend fun updateTodo(todo: TodoEntity) { dao.updateTodo(todo) }\n\noverride suspend fun deleteTodo(id: Int) { dao.deleteTodo(id) }\n}\n

    A kor\u00e1bbi implement\u00e1ci\u00f3t, a MemoryTodoRepository oszt\u00e1lyt most t\u00f6r\u00f6lj\u00fck ki, erre nem lesz m\u00e1r sz\u00fcks\u00e9g.

    A feature package-ekben most a viewmodelek is elt\u00f6rtek, mert egyr\u00e9szt a repository m\u00e1sik package-be ker\u00fclt, m\u00e1sr\u00e9szt a kor\u00e1bbi repository implement\u00e1ci\u00f3t t\u00f6r\u00f6lt\u00fck. Ezt nem tudjuk k\u00f6nnyen kijav\u00edtani, mert az \u00faj repository a konstruktor\u00e1ban a DAO komponenst v\u00e1rja, azt viszont nem konstruktorh\u00edv\u00e1ssal hozzuk l\u00e9tre, hanem a Room gy\u00e1rtja majd le, ez\u00e9rt ehhez m\u00e9g kell n\u00e9mi k\u00f3dot \u00edrnunk. M\u00e1sr\u00e9szt pedig nem is k\u00edv\u00e1natos, hogy a viewmodel k\u00f6zvetlen a repository-t h\u00edvja, hiszen ahogy fentebb indokoltuk, nem el\u0151ny\u00f6s, ha a felhaszn\u00e1l\u00f3i fel\u00fclet komponensek k\u00f6zvetlen az adatb\u00e1ziskezel\u00e9si r\u00e9teggel is f\u00fcggnek egym\u00e1st\u00f3l. Ez\u00e9rt majd a dom\u00e9nmodellhez kapcsol\u00f3d\u00f3 \u00fczletilogika-komponenseket fogunk bevezetni. De el\u0151bb fejezz\u00fck be az adatb\u00e1ziskezel\u00e9si r\u00e9teg implement\u00e1ci\u00f3j\u00e1t!

    M\u00e9g h\u00e1rom feladatunk van az adatb\u00e1ziskezel\u0151 r\u00e9teg kialak\u00edt\u00e1s\u00e1ban. Az els\u0151, hogy a let\u00e1rolni k\u00edv\u00e1nt Java-t\u00edpusok \u00e9s az SQLite be\u00e9p\u00edtett t\u00edpusai k\u00f6zt nem teljes az egyez\u00e9s. Ezt konverterekkel kell \u00e1thidalnunk. K\u00e9sz\u00edts\u00fcnk egy data.converters package-et, \u00e9s ebbe el\u0151sz\u00f6r a d\u00e1tumokkal kapcsolatos konverterek implement\u00e1ci\u00f3j\u00e1t:

    object LocalDateConverter {\n\n@TypeConverter\nfun LocalDate.asString(): String = this.toString()\n\n@TypeConverter\nfun String.asLocalDateTime(): LocalDate = this.toLocalDate()\n}\n

    A met\u00f3dusokon lev\u0151 @TypeConverter annot\u00e1ci\u00f3 jelzi a Room sz\u00e1m\u00e1ra, hogy ezeket a f\u00fcggv\u00e9nyeket konverzi\u00f3hoz haszn\u00e1lhatja, a szignat\u00far\u00e1b\u00f3l pedig egy\u00e9rtelm\u0171en kik\u00f6vetkeztethet\u0151, hogy milyen t\u00edpusok k\u00f6zt tud vel\u00fck konvert\u00e1lni. Most a priorit\u00e1s enumer\u00e1ci\u00f3t is t\u00e1mogassuk a megfelel\u0151 konverterekkel:

    object TodoPriorityConverter {\n\n@TypeConverter\nfun Priority.asString(): String = this.name\n\n@TypeConverter\nfun String.asPriority(): Priority {\nreturn when(this) {\nPriority.LOW.name -> Priority.LOW\nPriority.MEDIUM.name -> Priority.MEDIUM\nPriority.HIGH.name -> Priority.HIGH\nelse -> Priority.LOW\n}\n}\n}\n

    A m\u00e1sodik l\u00e9p\u00e9s, hogy az elk\u00e9sz\u00fclt komponensekb\u0151l \u00f6ssze kell \u00e1ll\u00edtanunk az adatb\u00e1ziskezel\u00e9s glob\u00e1lis be\u00e1ll\u00edt\u00e1sait \u00f6sszefog\u00f3 RoomDatabase implement\u00e1ci\u00f3nkat. Ezt tegy\u00fck a data package gy\u00f6ker\u00e9be:

    @Database(entities = [TodoEntity::class], version = 1)\n@TypeConverters(TodoPriorityConverter::class, LocalDateConverter::class)\nabstract class TodoDatabase : RoomDatabase() {\nabstract val dao: TodoDao\n}\n

    Figyelj\u00fck meg az annot\u00e1ci\u00f3kat! Itt meg vannak hivatkozva a haszn\u00e1lni k\u00edv\u00e1nt entit\u00e1sok \u00e9s konverterek, illetve az adatb\u00e1ziss\u00e9ma egy verzi\u00f3sz\u00e1mot is kap. Ez az\u00e9rt hasznos, mert ahogy fejl\u0151dik az alkalmaz\u00e1s, az adatb\u00e1zis s\u00e9m\u00e1ja is v\u00e1ltozhat, fejl\u0151dhet. Ilyen esetekben arra is lehet\u0151s\u00e9get ad a Room, hogy migr\u00e1ci\u00f3kat biztos\u00edtsunk a r\u00e9gebbi adatb\u00e1ziss\u00e9m\u00e1kr\u00f3l t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9sre. Ha telep\u00edtve van az alkalmaz\u00e1s r\u00e9gi verzi\u00f3ja, amely m\u00e1r mentett el adatokat az eszk\u00f6zre, \u00e9s friss\u00edtj\u00fck az alkalmaz\u00e1st, akkor a k\u00f6vetkez\u0151 indul\u00e1s ut\u00e1n a Room megvizsg\u00e1lja, hogy t\u00f6rt\u00e9nt-e v\u00e1ltoz\u00e1s az adatb\u00e1zis verzi\u00f3j\u00e1ban, \u00e9s sz\u00fcks\u00e9g eset\u00e9n futtatja a migr\u00e1ci\u00f3kat.

    Az utols\u00f3 l\u00e9p\u00e9s az adatb\u00e1ziskezel\u00e9s implement\u00e1ci\u00f3j\u00e1hoz, hogy az alkalmaz\u00e1s indul\u00e1sakor inicializ\u00e1ljuk az adatb\u00e1zist. Ehhez egy Application oszt\u00e1llyal kell kieg\u00e9sz\u00edten\u00fcnk az alkalmaz\u00e1sunkat. Az Application oszt\u00e1ly a teljes alkalmaz\u00e1s \u00e9letciklus-esem\u00e9nyeit tudja kezelni, illetve arra is alkalmas, hogy itt glob\u00e1lis adatokat ments\u00fcnk el, amelyeket majd az alkalmaz\u00e1s tetsz\u0151leges komponenseib\u0151l el\u00e9rhet\u0151v\u00e9 akarunk tenni. Ezt az alkalmaz\u00e1s \"root package\"-\u00e9be, a MainActivity mell\u00e9 tegy\u00fck:

    class TodoApplication : Application() {\n\ncompanion object {\nprivate lateinit var db: TodoDatabase\n\nlateinit var repository: TodoRepositoryImpl\n}\n\noverride fun onCreate() {\nsuper.onCreate()\ndb = Room.databaseBuilder(\napplicationContext,\nTodoDatabase::class.java,\n\"todo_database\"\n).fallbackToDestructiveMigration().build()\n\nrepository = TodoRepositoryImpl(db.dao)\n}\n}\n

    L\u00e1that\u00f3, hogy az alkalmaz\u00e1s indul\u00e1sakor l\u00e9trehozzuk az adatb\u00e1zist \u00e9s a TodoRepositoryImpl-et, majd ezeket az oszt\u00e1ly companion objectj\u00e9be el is mentj\u00fck. Hogy az Application oszt\u00e1ly t\u00e9nyleg az elv\u00e1s\u00e1runk szerint m\u0171k\u00f6dj\u00fcnk, m\u00e9g meg is kell hivatkozni a Manifest.xml f\u00e1jl application elem\u00e9ben. Cser\u00e9lj\u00fck az application elem nyit\u00f3 tagj\u00e9t az al\u00e1bbira:

        <application\nandroid:name=\".TodoApplication\"\nandroid:allowBackup=\"true\"\nandroid:dataExtractionRules=\"@xml/data_extraction_rules\"\nandroid:fullBackupContent=\"@xml/backup_rules\"\nandroid:icon=\"@mipmap/ic_launcher\"\nandroid:label=\"@string/app_name\"\nandroid:roundIcon=\"@mipmap/ic_launcher_round\"\nandroid:supportsRtl=\"true\"\nandroid:theme=\"@style/Theme.Todo\"\ntools:targetApi=\"31\">\n

    Ezzel \u00edgy m\u00e1r \u00f6ssze\u00e1llt az adatb\u00e1ziskezel\u0151 r\u00e9teg, de m\u00e9g fel kell oldanunk a komponensek k\u00f6zti kommunik\u00e1ci\u00f3t.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a TodoDatabase k\u00f3dr\u00e9sz\u00e9lete, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/persistence/#uzleti-logika","title":"\u00dczleti logika","text":"

    Most k\u00e9sz\u00edts\u00fck el a domain.usecases package-et. Ebbe ker\u00fclnek az egyes \u00fczletilogika-m\u0171veletek megval\u00f3s\u00edt\u00e1sai. Kezdj\u00fck a tennival\u00f3 l\u00e9trehoz\u00e1s\u00e1val:

    class SaveTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(todo: Todo) {\nrepository.insertTodo(todo.asTodoEntity())\n}\n\n}\n

    Ennek a k\u00f3dr\u00e9szletnek a szerepe, hogy - ak\u00e1rcsak a domainmodell - lev\u00e1lasztja az \u00fczleti logik\u00e1t az adatr\u00e9tegr\u0151l. Jelen esetben az \u00fczleti logik\u00e1nk igen egyszer\u0171, \u00e9s ez\u00e9rt ezek a m\u0171veletek tulajdonk\u00e9ppen csak megh\u00edvj\u00e1k az adatr\u00e9teget a repository komponenseken kereszt\u00fcl, illetve konvert\u00e1lj\u00e1k a domainmodelleket entit\u00e1sokk\u00e1. Egy \u00f6sszetettebb alkalmaz\u00e1sban ez nem felt\u00e9tlen van \u00edgy, \u00e9s ez a r\u00e9teg ak\u00e1r bonyolultabb is lehet, t\u00f6bb adatm\u0171veletb\u0151l nagyobb l\u00e9pt\u00e9k\u0171, \u00f6sszetettebb m\u0171veleteket val\u00f3s\u00edthat meg.

    A fentihez hasonl\u00f3 k\u00e9sz\u00edts\u00fck el a m\u00f3dos\u00edt\u00e1s use case oszt\u00e1ly\u00e1t:

    class UpdateTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(todo: Todo) {\nrepository.updateTodo(todo.asTodoEntity())\n}\n\n}\n

    Majd a lek\u00e9rdez\u00e9st:

    class LoadTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(id: Int): Result<Todo> {\nreturn try {\nResult.success(repository.getTodoById(id).first().asTodo())\n} catch (e: IOException) {\nResult.failure(e)\n}\n}\n\n}\n

    A t\u00f6rl\u00e9st:

    class DeleteTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(id: Int) {\nrepository.deleteTodo(id)\n}\n\n}\n

    \u00c9s legyen egy list\u00e1z\u00e1sunk is, amikor minden tennival\u00f3t bet\u00f6lt\u00fcnk:

    class LoadTodosUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(): Result<List<Todo>> {\nreturn try {\nval todos = repository.getAllTodos().first()\nResult.success(todos.map { it.asTodo() })\n} catch (e: IOException) {\nResult.failure(e)\n}\n}\n}\n

    V\u00e9g\u00fcl ezeket \u00f6sszefogjuk egy oszt\u00e1ly tagv\u00e1ltoz\u00f3iban:

    class TodoUseCases(repository: TodoRepository) {\nval loadTodos = LoadTodosUseCase(repository)\nval loadTodo = LoadTodoUseCase(repository)\nval saveTodo = SaveTodoUseCase(repository)\nval updateTodo = UpdateTodoUseCase(repository)\nval deleteTodo = DeleteTodoUseCase(repository)\n}\n

    Most m\u00f3dos\u00edtanunk kell a viewmodeljeinket. Kezdj\u00fck a l\u00e9trehoz\u00e1s m\u0171velettel! El\u0151sz\u00f6r is konstruktorban m\u00e1r nem a repository-t kapjuk meg, hanem a TodoUseCases oszt\u00e1lyra kapunk referenci\u00e1t:

    class TodoCreateViewModel(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n...\n}\n

    Majd ennek megfelel\u0151en m\u00f3dos\u00edtsuk a ment\u00e9st \u00e9s az inicializ\u00e1l\u00e1st is:

        private fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(TodoCreateUiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(TodoCreateUiEvent.Failure(e.toUiText()))\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodoCreateViewModel(\ntodoOperations = todoOperations\n)\n}\n}\n}\n

    Folytassuk a r\u00e9szletez\u0151 n\u00e9zettel, itt is hasonl\u00f3ak lesznek a v\u00e1ltoz\u00e1sok:

    class TodoDetailViewModel(\nprivate val todoOperations: TodoUseCases,\nprivate val savedStateHandle: SavedStateHandle) : ViewModel() {\n...\n}\n

    Majd:

        private fun loadTodos() {\nval id = checkNotNull<Int>(savedStateHandle[\"id\"])\nviewModelScope.launch {\ntry {\n_state.value = TodoDetailState.Loading\nval todo = todoOperations.loadTodo(id)\n_state.value = TodoDetailState.Result(\ntodo = todo.getOrThrow().asTodoUi()\n)\n} catch (e: Exception) {\n_state.value = TodoDetailState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nval savedStateHandle = createSavedStateHandle()\nTodoDetailViewModel(\ntodoOperations,\nsavedStateHandle\n)\n}\n}\n}\n

    V\u00e9g\u00fcl a list\u00e1z\u00f3 n\u00e9zet k\u00f6vetkezik:

    class TodoListViewModel(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n...\n}\n

    Majd:

        fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.value = TodoListState.Result(\ntodoList = todos\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodoListViewModel(\ntodoOperations\n)\n}\n}\n}\n

    Most m\u00e1r kipr\u00f3b\u00e1lhat\u00f3 az alkalmaz\u00e1s, \u00e9s a l\u00e9trehozott teend\u0151k t\u00e9nylegesen az adatb\u00e1zisba ment\u0151dnek.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a teend\u0151k list\u00e1ja, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt, illetve egy teend\u0151 c\u00edm\u00e9ben.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/persistence/#onallo-feladat-1","title":"\u00d6n\u00e1ll\u00f3 feladat 1","text":"

    Val\u00f3s\u00edtsd meg az \u00f6sszes tennival\u00f3 t\u00f6rl\u00e9s\u00e9t, pl. az AppBaron elhelyezett gombbal! A laboron l\u00e1tott architekt\u00far\u00e1hoz hasonl\u00f3an r\u00e9tegr\u0151l-r\u00e9tegre val\u00f3s\u00edtsd meg a sz\u00fcks\u00e9ges funkci\u00f3kat.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a mindent t\u00f6r\u00f6l funkci\u00f3, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/persistence/#onallo-feladat-2","title":"\u00d6n\u00e1ll\u00f3 feladat 2","text":"

    Hossz\u00fa kattint\u00e1sra leny\u00edl\u00f3 men\u00fcb\u0151l lehessen megosztani a tennival\u00f3kat m\u00e1s alkalmaz\u00e1sokkal sz\u00f6veges \u00fczenetk\u00e9nt. Az \u00fczenet tartalmazza a tennival\u00f3 jellemz\u0151it.

    Seg\u00edts\u00e9g: https://developer.android.com/training/sharing/send

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a megoszt\u00e1s funkci\u00f3, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/tictactoe/","title":"Labor02 - Egyszer\u0171 felhaszn\u00e1l\u00f3i fel\u00fclet t\u00f6bb Activity seg\u00edts\u00e9g\u00e9vel (TicTacToe)","text":""},{"location":"laborok/tictactoe/#bevezetes","title":"Bevezet\u00e9s","text":"

    A labor c\u00e9lja egy t\u00f6bb Activity-b\u0151l \u00e1ll\u00f3 Android alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9se, valamint az egyszer\u0171 rajzol\u00e1s bemutat\u00e1sa egy TicTacToe j\u00e1t\u00e9k seg\u00edts\u00e9g\u00e9vel.

    A labor sor\u00e1n a k\u00f6vetkez\u0151 funkci\u00f3kat fogjuk megval\u00f3s\u00edtani:

    • Men\u00fc Activity
    • J\u00e1t\u00e9kt\u00e9r Activity
    • TicTacToe n\u00e9zet
    • J\u00e1t\u00e9k logika elkezd\u00e9se

    A laborhoz kapcsol\u00f3d\u00f3 \u00f6n\u00e1ll\u00f3 feladat:

    • J\u00e1t\u00e9k logika megval\u00f3s\u00edt\u00e1sa: gy\u0151zelem ellen\u0151rz\u00e9se

    A megval\u00f3s\u00edtand\u00f3 j\u00e1t\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9t az al\u00e1bbi k\u00e9perny\u0151k\u00e9pek szeml\u00e9ltetik:

    "},{"location":"laborok/tictactoe/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/tictactoe/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Checkout

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    Android, Java, Kotlin

    Az Android hagyom\u00e1nyosan Java nyelven volt fejleszthet\u0151, azonban az ut\u00f3bbi \u00e9vekben a Google \u00e1t\u00e1llt a Kotlin nyelvre. Ez egy sokkal modernebb nyelv, mint a Java, sok olyan nyelvi elemet ad, amit k\u00e9nyelmes haszn\u00e1lni, valamint \u00faj nyelvi szab\u00e1lyokat, amikkel p\u00e9ld\u00e1ul elker\u00fclhet\u0151ek a Java nyelven gyakori NullPointerException jelleg\u0171 hib\u00e1k.

    M\u00e1sr\u00e9szr\u0151l viszont a nyelv sok mindenben t\u00e9r el a hagyom\u00e1nyosan C jelleg\u0171 szintaktik\u00e1t k\u00f6vet\u0151 nyelvekt\u0151l, amit majd l\u00e1tni is fogunk. A labor el\u0151tt \u00e9rdemes megismerkedni a nyelvvel, egyr\u00e9szt a fent l\u00e1that\u00f3 linken, m\u00e1sr\u00e9szt ezt az \u00f6sszefoglal\u00f3 cikket \u00e1tolvasva.

    "},{"location":"laborok/tictactoe/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Views Activity lehet\u0151s\u00e9get.
    2. A projekt neve legyen TicTacToe, a kezd\u0151 package hu.bme.aut.android.tictactoe, a ment\u00e9si hely pedig a kicheckoutolt repository-n bel\u00fcl a TicTacToe mappa.
    3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
    4. A minimum API szint legyen API24: Android 7.0.
    5. A Build configuration language Kotlin DSL legyen.

    FILE PATH

    A projekt mindenk\u00e9ppen a repository-ban l\u00e9v\u0151 TicTacToe k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n a laborvezet\u0151 vezet\u00e9s\u00e9vel vizsg\u00e1lja meg a forr\u00e1s fel\u00e9p\u00edt\u00e9s\u00e9t.

    A projekt l\u00e9trehoz\u00e1sakor, a ford\u00edt\u00f3 keretrendszernek rengeteg f\u00fcgg\u0151s\u00e9get kell let\u00f6ltenie. Am\u00edg ez nem t\u00f6rt\u00e9nt meg, addig a projektben neh\u00e9zkes navig\u00e1lni, hi\u00e1nyzik a k\u00f3dkieg\u00e9sz\u00edt\u00e9s, stb... \u00c9ppen ez\u00e9rt ezt tan\u00e1csos kiv\u00e1rni, azonban ez ak\u00e1r 5 percet is ig\u00e9nybe vehet az els\u0151 alkalommal! Az ablak alj\u00e1n l\u00e1that\u00f3 inform\u00e1ci\u00f3s s\u00e1vot kell figyelni.

    "},{"location":"laborok/tictactoe/#az-alkalmazas-mukodese","title":"Az alkalmaz\u00e1s m\u0171k\u00f6d\u00e9se","text":"

    A megval\u00f3s\u00edtand\u00f3 alkalmaz\u00e1s m\u0171k\u00f6d\u00e9si elve a k\u00f6vetkez\u0151:

    1. Az alkalmaz\u00e1s ind\u00edt\u00e1sakor a MainActivity jelenik meg.
    2. A MainActivity-r\u0151l lehet \u00faj j\u00e1t\u00e9kot ind\u00edtani az \u00daj j\u00e1t\u00e9k men\u00fcpont hat\u00e1s\u00e1ra, ez \u00e1tnavig\u00e1l a GameActivity-re.
    3. A MainActivity-r\u0151l meg lehet tekinteni az Eredm\u00e9nyek-et, ami jelenleg csak egy Toast-ot dob fel egy \u00fczenettel (ezt a funkci\u00f3t opcion\u00e1lisan k\u00e9s\u0151bb meg lehet val\u00f3s\u00edtani, ha a perzisztencia t\u00e9mak\u00f6rt m\u00e1r vett\u00fck el\u0151ad\u00e1son).
    4. A MainActivity-r\u0151l meg lehet n\u00e9zni az alkalmaz\u00e1s k\u00e9sz\u00edt\u0151ir\u0151l sz\u00f3l\u00f3 inform\u00e1ci\u00f3kat az Inf\u00f3 men\u00fct v\u00e1lasztva. Ez a funkci\u00f3 \u00e1tnavig\u00e1l az AboutActivity-re, ami dial\u00f3gus form\u00e1ban fog megjelenni.
    "},{"location":"laborok/tictactoe/#szoveges-eroforrasok","title":"Sz\u00f6veges er\u0151forr\u00e1sok","text":"

    Navig\u00e1ljunk a res/values/strings.xml-re, ahol a projekt sz\u00f6veges er\u0151forr\u00e1sai tal\u00e1lhat\u00f3ak. Haszn\u00e1ljuk a k\u00f6vetkez\u0151 sz\u00f6veges er\u0151forr\u00e1sokat:

    <resources>\n<string name=\"app_name\">TicTacToe</string>\n<string name=\"btn_start\">\u00daj j\u00e1t\u00e9k</string>\n<string name=\"btn_highscore\">Eredm\u00e9nyek</string>\n<string name=\"btn_about\">Inf\u00f3</string>\n<string name=\"toast_highscore\">Eredm\u00e9nyek</string>\n<string name=\"txt_about\">Made by Hallgat\u00f3</string>\n</resources>\n
    "},{"location":"laborok/tictactoe/#szukseges-tovabbi-activityk-letrehozasa","title":"Sz\u00fcks\u00e9ges tov\u00e1bbi Activityk l\u00e9trehoz\u00e1sa","text":"

    A fentiek alapj\u00e1n l\u00e1that\u00f3 teh\u00e1t, hogy a megl\u00e9v\u0151 MainActivity mellett m\u00e9g k\u00e9t m\u00e1sik Activity-t, a GameActivity-t \u00e9s az AboutActivity-t kell l\u00e9trehoznunk.

    Activity l\u00e9trehoz\u00e1sakor tipikusan az al\u00e1bbi forr\u00e1s \u00e1llom\u00e1nyok v\u00e1ltoznak:

    • L\u00e9trej\u00f6n az Activity-hez tartoz\u00f3 Kotlin f\u00e1jl.
    • L\u00e9trej\u00f6n az Activity-hez tartoz\u00f3 layout XML.
    • Az AndroidManifest.xml-be beker\u00fcl az Activity az <application> tag-en bel\u00fcl.

    Az Activity l\u00e9trehoz\u00e1st azonban megk\u00f6nny\u00edti az Android Studio \u00e9s a fenti l\u00e9p\u00e9seket nem kell egyes\u00e9vel elv\u00e9geznie a fejleszt\u0151nek.

    1. A megl\u00e9v\u0151 Activity-t tartalmaz\u00f3 package-re jobb eg\u00e9rgombbal kattintva v\u00e1lasszuk a New -> Activity -> Empty Views Activity opci\u00f3t \u00e9s hozzuk l\u00e9tre a m\u00e1sik k\u00e9t Activity-t (AboutActivity, GameActivity), Source Language-nek v\u00e1lasszuk a Kotlint.
    2. L\u00e9trehoz\u00e1s ut\u00e1n a res/values/strings.xml-ben a <resources> tagen bel\u00fcl vegy\u00fck fel a k\u00e9t \u00faj Activity c\u00edm\u00e9t:
          <string name=\"title_activity_about\">Az alkalmaz\u00e1sr\u00f3l</string>\n<string name=\"title_activity_game\">J\u00e1t\u00e9k</string>\n
    3. \u00c1ll\u00edtsuk be a Manifestben azt, hogy az AboutActivity dial\u00f3gus form\u00e1ban jelenjen meg, a theme attrib\u00fatum be\u00e1ll\u00edt\u00e1s\u00e1val

      A k\u00f3dkieg\u00e9sz\u00edt\u00e9s seg\u00edt megtal\u00e1lni a megfelel\u0151 t\u00e9m\u00e1t a lehet\u0151s\u00e9gek k\u00f6z\u00fcl, kezdj\u00fck el a kezd\u0151 bet\u0171ket be\u00edrni!

          <activity\nandroid:name=\".AboutActivity\"\nandroid:exported=\"false\"\nandroid:label=\"@string/title_activity_about\"\nandroid:parentActivityName=\".MainActivity\"\nandroid:theme=\"@style/Theme.AppCompat.Light.Dialog\">\n<meta-data\nandroid:name=\"android.support.PARENT_ACTIVITY\"\nandroid:value=\".MainActivity\" />\n</activity>\n

      A fenti k\u00f3dr\u00e9szletben az AboutActivity c\u00edm\u00e9t is be\u00e1ll\u00edtjuk a label attrib\u00fatum be\u00e1ll\u00edt\u00e1s\u00e1val

    4. \u00c1ll\u00edtsuk be a GameActivity c\u00edm\u00e9t is

          <activity\nandroid:name=\".GameActivity\"\nandroid:exported=\"false\"\nandroid:label=\"@string/title_activity_game\" />\n

    L\u00e9trehoz\u00e1s ut\u00e1n ellen\u0151rizz\u00fck a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel a l\u00e9trej\u00f6tt k\u00f3dokat!

    "},{"location":"laborok/tictactoe/#mainactivity-felulet","title":"MainActivity fel\u00fclet","text":"

    A MainActivity a fenti \u00e1bra alapj\u00e1n h\u00e1rom men\u00fcpontot tartalmaz k\u00f6z\u00e9pre igaz\u00edtva. Ezt a fel\u00fcletet a hozz\u00e1 tartoz\u00f3 res/layout/activity_main.xml-ben hozhatjuk l\u00e9tre. Mivel az AndroidStudio m\u00e1r alap\u00e9rtelmezetten ConstraintLayout alap\u00fa n\u00e9zetet gener\u00e1l, \u00edgy most ezt fogjuk haszn\u00e1lni a megval\u00f3s\u00edt\u00e1sra. Az anyagban ennek m\u0171k\u00f6d\u00e9se csak k\u00e9s\u0151bb k\u00f6vetkezik, \u00edgy al\u00e1bb megtal\u00e1lhat\u00f3 a k\u00e9sz XML le\u00edr\u00f3. Akinek van kedve, a gif alapj\u00e1n kipr\u00f3b\u00e1lhatja a haszn\u00e1lat\u00e1t:

    Tipp: Shift + Kattint\u00e1ssal lehet t\u00f6bb elemet kijel\u00f6lni

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".MainActivity\">\n\n<Button\nandroid:id=\"@+id/btnStart\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_start\"\napp:layout_constraintBottom_toTopOf=\"@+id/btnHighScores\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:layout_constraintVertical_chainStyle=\"packed\" />\n\n<Button\nandroid:id=\"@+id/btnHighScores\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_highscore\"\napp:layout_constraintBottom_toTopOf=\"@+id/btnAbout\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@+id/btnStart\" />\n\n<Button\nandroid:id=\"@+id/btnAbout\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_about\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@+id/btnHighScores\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n

    N\u00e9zz\u00fck \u00e1t a laborvezet\u0151vel a fel\u00fclet fel\u00e9p\u00edt\u00e9s\u00e9t!

    "},{"location":"laborok/tictactoe/#viewbinding","title":"ViewBinding","text":"

    A fel\u00fcleti elemeink el\u00e9r\u00e9s\u00e9re \"n\u00e9zetk\u00f6t\u00e9st\", azaz ViewBinding-ot fogunk haszn\u00e1lni. A ViewBinding a k\u00f3d\u00edr\u00e1st k\u00f6nny\u00edti meg sz\u00e1munkra. Amennyiben ezt haszn\u00e1ljuk, az automatikusan gener\u00e1l\u00f3d\u00f3 binding oszt\u00e1lyokon kereszt\u00fcl k\u00f6zvetlen referenci\u00e1n kereszt\u00fcl tudunk el\u00e9rni minden ID-val rendelkez\u0151 er\u0151forr\u00e1st az XML f\u00e1jljainkban.

    El\u0151sz\u00f6r is be kell kapcsolnunk a modulunkra a ViewBinding-ot. Az app modulhoz tartoz\u00f3 build.gradle.kts f\u00e1jlban az android tagen bel\u00fclre illessz\u00fck be az enged\u00e9lyez\u00e9st:

    android {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

    Majd nyomjunk a fels\u0151 k\u00e9k s\u00e1von jobb oldalon megjelen\u0151 Sync Now gombra. Ezzel a gradle bet\u00f6lti sz\u00fcks\u00e9ges v\u00e1ltoztat\u00e1sokat.

    ViewBinding

    Ebben az esetben a modul minden egyes XML layout f\u00e1jlj\u00e1hoz gener\u00e1l\u00f3dik egy \u00fagynevezett binding oszt\u00e1ly. Minden binding oszt\u00e1ly tartalmaz referenci\u00e1t az adott XML layout er\u0151forr\u00e1s gy\u00f6k\u00e9r elem\u00e9re \u00e9s az \u00f6sszes ID-val rendelkez\u0151 view-ra. A gener\u00e1lt oszt\u00e1ly neve \u00fagy \u00e1ll el\u0151, hogy az XML layout nev\u00e9t Pascal form\u00e1tumba alak\u00edtja a rendszer \u00e9s a v\u00e9g\u00e9re illeszti, hogy Binding. Azaz p\u00e9ld\u00e1ul a activity_login.xml er\u0151forr\u00e1sf\u00e1jlb\u00f3l az al\u00e1bbi binding oszt\u00e1ly gener\u00e1l\u00f3dik: ActivityLoginBinding.

    <LinearLayout ... >\n<TextView android:id=\"@+id/name\" />\n<ImageView android:cropToPadding=\"true\" />\n<Button android:id=\"@+id/button\"\nandroid:background=\"@drawable/rounded_button\" />\n</LinearLayout>\n

    A gener\u00e1lt oszt\u00e1lynak k\u00e9t mez\u0151je van. A name id-val rendelkez\u0151 TextView \u00e9s a button id-j\u00fa Button. A layout-ban szerepl\u0151 ImageView-nak nincs id-ja, ez\u00e9rt nem szerepel a binding oszt\u00e1lyban.

    Minden gener\u00e1lt oszt\u00e1ly tartalmaz egy getRoot() met\u00f3dust, amely direkt referenciak\u00e9nt szolg\u00e1l a layout gy\u00f6ker\u00e9re. A p\u00e9ld\u00e1ban a getRoot() met\u00f3dus a LinearLayout-tal t\u00e9r vissza.

    Ezzel ut\u00e1n m\u00e1r a teljes modulunkban automatikusan el\u00e9rhet\u0151v\u00e9 v\u00e1lt a ViewBinding. Haszn\u00e1lat\u00e1hoz az Activity-nkben csak p\u00e9ld\u00e1nyos\u00edtanunk kell a binding objektumot, amin kereszt\u00fcl majd el\u00e9rhetj\u00fck az er\u0151forr\u00e1sainkat. A binding p\u00e9ld\u00e1ny m\u0171k\u00f6d\u00e9s\u00e9hez h\u00e1rom dolgot kell tenn\u00fcnk:

    1. A gener\u00e1lt binding oszt\u00e1ly statikus inflate f\u00fcggv\u00e9ny\u00e9vel p\u00e9ld\u00e1nyos\u00edtjuk a binding oszt\u00e1lyunkat az Activity-hez,
    2. Szerz\u00fcnk egy referenci\u00e1t a gy\u00f6k\u00e9r n\u00e9zetre a getRoot() f\u00fcggv\u00e9nnyel,
    3. Ezt a gy\u00fck\u00e9relemet odaadjuk a setContentView() f\u00fcggv\u00e9nynek, hogy ez legyen az akt\u00edv view a k\u00e9perny\u0151n:
    package hu.bme.aut.android.tictactoe\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport hu.bme.aut.android.tictactoe.databinding.ActivityMainBinding\n\nclass MainActivity : AppCompatActivity() {\nprivate lateinit var binding: ActivityMainBinding\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nbinding = ActivityMainBinding.inflate(layoutInflater)\nsetContentView(binding.root)\n}\n}\n

    lateinit

    A lateinit kulcssz\u00f3val megjel\u00f6lt property-ket a ford\u00edt\u00f3 megengedi inicializ\u00e1latlanul hagyni az oszt\u00e1ly konstruktor\u00e1nak lefut\u00e1sa ut\u00e1nig, an\u00e9lk\u00fcl, hogy nullable-k\u00e9nt k\u00e9ne azokat megjel\u00f6ln\u00fcnk (ami k\u00e9s\u0151bb k\u00e9nyelmetlenn\u00e9 tenn\u00e9 a haszn\u00e1latukat, mert mindig ellen\u0151rizn\u00fcnk k\u00e9ne, hogy null-e az \u00e9rt\u00e9k\u00fck). Ez praktikus olyan esetekben, amikor egy oszt\u00e1ly inicializ\u00e1l\u00e1sa nem a konstruktor\u00e1ban t\u00f6rt\u00e9nik (p\u00e9ld\u00e1ul ahogy az Activity-k eset\u00e9ben az onCreate-ben), mert k\u00e9s\u0151bb az esetleges null eset lekezel\u00e9se n\u00e9lk\u00fcl haszn\u00e1lhatjuk majd a property-t. A lateinit haszn\u00e1lat\u00e1val \u00e1tv\u00e1llaljuk a felel\u0151ss\u00e9get a ford\u00edt\u00f3t\u00f3l, hogy a property-t az els\u0151 haszn\u00e1lata el\u0151tt inicializ\u00e1lni fogjuk - ellenkez\u0151 esetben kiv\u00e9telt kapunk.

    Cser\u00e9lj\u00fck le a fenti minta alapj\u00e1n a m\u00e1sik k\u00e9t Activity k\u00f3dj\u00e1t is ViewBinding alap\u00fa megold\u00e1sra!

    "},{"location":"laborok/tictactoe/#highscore-gomb-esemenykezelo","title":"Highscore gomb esem\u00e9nykezel\u0151","text":"

    Az Eredm\u00e9nyek men\u00fcpontra kattintva egy Toast \u00fczenetet kell megjelen\u00edteni. Ehhez meg kell keresni az Eredm\u00e9nyek men\u00fcpont gombj\u00e1t \u00e9s be kell \u00e1ll\u00edtani neki az al\u00e1bbi esem\u00e9nykezel\u0151t a MainActivity onCreate() f\u00fcggv\u00e9ny\u00e9n bel\u00fcl:

    binding.btnHighScores.setOnClickListener {\nToast.makeText(\nthis@MainActivity,\ngetString(R.string.toast_highscore),\nToast.LENGTH_LONG\n).show()\n}\n

    onClickListener

    A setOnClickListener f\u00fcggv\u00e9ny val\u00f3j\u00e1ban egy View.OnClickListener interf\u00e9szt megval\u00f3s\u00edt\u00f3 objektumot v\u00e1r param\u00e9terk\u00e9nt, amelynek egyetlen megval\u00f3s\u00edtand\u00f3 f\u00fcggv\u00e9nye van. Ezt l\u00e9trehozhatn\u00e1nk a Java-s anonim oszt\u00e1lyok st\u00edlus\u00e1ban is, de helyette kihaszn\u00e1ljuk, hogy a f\u00fcggv\u00e9nyek els\u0151rend\u0171 tagjai a Kotlin nyelvnek, \u00edgy rendelkez\u00fcnk igazi f\u00fcggv\u00e9ny t\u00edpusokkal. Jelen esetben a param\u00e9terben egy olyan lambda kifejez\u00e9st adunk \u00e1t, amely fejl\u00e9ce megegyezik az elv\u00e1rt interf\u00e9sz egyetlen f\u00fcggv\u00e9ny\u00e9nek fejl\u00e9c\u00e9vel, a SAM conversion nyelvi funkci\u00f3 pedig a h\u00e1tt\u00e9rben a lambda alapj\u00e1n l\u00e9trehozza a megfelel\u0151 View.OnClickListener p\u00e9ld\u00e1nyt.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a highscores Toast \u00fczenet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/tictactoe/#aboutactivity-felulet","title":"AboutActivity fel\u00fclet","text":"

    Ahogy kor\u00e1bban eml\u00edtett\u00fck, az Inf\u00f3 men\u00fc elind\u00edtja az AboutActivity-t. Els\u0151k\u00e9nt k\u00e9sz\u00edts\u00fck el az AboutActivity fel\u00fclet\u00e9t, melyet a res/layout/activity_about.xml \u00edr le. Mint kor\u00e1bban, itt is lehet ConstraintLayout-ot k\u00e9sz\u00edteni a seg\u00edts\u00e9ggel, vagy al\u00e1bb megtal\u00e1lhat\u00f3 az XML:

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".AboutActivity\">\n\n<TextView\nandroid:id=\"@+id/textView\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_margin=\"8dp\"\nandroid:text=\"@string/txt_about\"\nandroid:textSize=\"32sp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n
    "},{"location":"laborok/tictactoe/#navigacio-megvalositasa-activityk-kozt","title":"Navig\u00e1ci\u00f3 megval\u00f3s\u00edt\u00e1sa Activityk k\u00f6zt","text":"

    A k\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt val\u00f3s\u00edtsuk meg a navig\u00e1ci\u00f3t (v\u00e1lt\u00e1st) az Activity-k k\u00f6z\u00f6tt. Az \u00daj j\u00e1t\u00e9k men\u00fcpont hat\u00e1s\u00e1ra a GameActivity-re, az Inf\u00f3 men\u00fcpont hat\u00e1s\u00e1ra pedig az AboutActivity-re kell \u00e1tv\u00e1ltanunk. Activity-k k\u00f6zti v\u00e1lt\u00e1st egy Intent seg\u00edts\u00e9g\u00e9vel tudunk implement\u00e1lni - besz\u00e9lj\u00fck meg a laborvezet\u0151vel az Intent-ek alapjait. Ezt a t\u00e9m\u00e1t el\u0151ad\u00e1son k\u00e9s\u0151bb m\u00e9lyebben fogjuk m\u00e9g \u00e9rinteni.

    Val\u00f3s\u00edtsuk meg ezen k\u00e9t gomb esem\u00e9nykezel\u0151j\u00e9t szint\u00e9n a MainActivity onCreate() f\u00fcggv\u00e9ny\u00e9ben!

    binding.btnStart.setOnClickListener {\nstartActivity(Intent(this@MainActivity, GameActivity::class.java))\n}\n\nbinding.btnAbout.setOnClickListener {\nstartActivity(Intent(this@MainActivity, AboutActivity::class.java))\n}\n

    setContentView

    Gyakori hiba, hogy a setContentView f\u00fcggv\u00e9nynek a gy\u00f6k\u00e9r n\u00e9zet helyett v\u00e9letlen\u00fcl az ID-val hivatkozott layout-ot adjuk oda. (R.layout.activity_main.xml). Ilyenkor k\u00e9tszer is p\u00e9ld\u00e1nyosodik a fel\u00fclet, r\u00e1ad\u00e1sul a k\u00e9perny\u0151n az egyik jelenik meg, m\u00edg a binding-ok a m\u00e1sikra lesznek be\u00e1ll\u00edtva.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a az AboutActivity (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), egy ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a TextView sz\u00f6vegek\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/tictactoe/#jatek-logika","title":"J\u00e1t\u00e9k logika","text":"

    A 3x3-as TicTacToe t\u00e1blaj\u00e1t\u00e9k logik\u00e1j\u00e1t k\u00fcl\u00f6n oszt\u00e1lyban val\u00f3s\u00edtjuk meg egy Singleton form\u00e1j\u00e1ban, \u00edgy k\u00f6nnyen hozz\u00e1f\u00e9rhet\u00fcnk majd.

    Amennyiben nem ismeri ezt a tervez\u00e9si mint\u00e1t, \u00e9rdemes ut\u00e1nan\u00e9zni, illetve r\u00e1k\u00e9rdezni a laborvezet\u0151n\u00e9l.

    K\u00e9sz\u00edts\u00fcnk a tictactoe package-en bel\u00fcl egy model package-et, majd abban egy TicTacToeModel oszt\u00e1lyt (a package-en jobb eg\u00e9rgomb, majd New -> Kotlin File/Class). Az oszt\u00e1ly egy 3x3-as m\u00e1trixban t\u00e1rolja a j\u00e1t\u00e9kt\u00e9r mez\u0151inek tartalm\u00e1t \u00e9s k\u00fcl\u00f6nf\u00e9le publikus f\u00fcggv\u00e9nyeket biztos\u00edt a j\u00e1t\u00e9kt\u00e9r lek\u00e9rdez\u00e9s\u00e9hez \u00e9s m\u00f3dos\u00edt\u00e1s\u00e1hoz.

    package hu.bme.aut.android.tictactoe.model\n\nobject TicTacToeModel {\n\nconst val EMPTY: Byte = 0\nconst val CIRCLE: Byte = 1\nconst val CROSS: Byte = 2\n\nvar nextPlayer: Byte = CIRCLE\n\nprivate var model: Array<ByteArray> = arrayOf(\nbyteArrayOf(EMPTY, EMPTY, EMPTY),\nbyteArrayOf(EMPTY, EMPTY, EMPTY),\nbyteArrayOf(EMPTY, EMPTY, EMPTY))\n\nfun resetModel() {\nfor (i in 0 until 3) {\nfor (j in 0 until 3) {\nmodel[i][j] = EMPTY\n}\n}\n}\n\nfun getFieldContent(x: Int, y: Int): Byte {\nreturn model[x][y]\n}\n\nfun changeNextPlayer() {\nif (nextPlayer == CIRCLE) {\nnextPlayer = CROSS\n} else {\nnextPlayer = CIRCLE\n}\n}\n\nfun setFieldContent(x: Int, y: Int, content: Byte): Byte {\nchangeNextPlayer()\nmodel[x][y] = content\nreturn content\n}\n\n}\n

    Singleton

    Kotlinban nyelvi szint\u0171 t\u00e1mogat\u00e1s van a singletonok l\u00e9trehoz\u00e1s\u00e1ra. Ahelyett, hogy nek\u00fcnk k\u00e9ne egyetlen statikus p\u00e9ld\u00e1nyt felvenn\u00fcnk, el\u00e9g csak a class kulcssz\u00f3 helyett az object kulcssz\u00f3val l\u00e9trehoznunk az oszt\u00e1lyt hogy egy singletont kapjunk.

    const

    A ford\u00edt\u00e1s id\u0151ben konstans \u00e9rt\u00e9keket \u00e9rdemes a const kulcssz\u00f3val megjel\u00f6ln\u00fcnk (erre a fejleszt\u0151k\u00f6rnyezet is figyelmeztet, ha nem tenn\u00e9nk), ezzel teljes\u00edtm\u00e9ny optimaliz\u00e1ci\u00f3kat \u00e9rhet\u00fcnk el, illetve a sz\u00e1nd\u00e9kainkat is tiszt\u00e1bban jelezz\u00fck.

    collection

    A Kotlin standard library sz\u00e1mos f\u00fcggv\u00e9nyt ny\u00fajt k\u00fcl\u00f6nb\u00f6z\u0151 collection-\u00f6k egyszer\u0171 l\u00e9trehoz\u00e1s\u00e1ra. Figyelj\u00fck meg a k\u00f3dban az arrayOf \u00e9s a byteArrayOf haszn\u00e1lat\u00e1t, amelyek megh\u00edv\u00e1s\u00e1val l\u00e9trehozunk t\u00f6mb\u00f6ket, \u00e9s azonnal fel is t\u00f6ltj\u00fck \u0151ket elemekkel.

    "},{"location":"laborok/tictactoe/#jatekter-kirajzolasa","title":"J\u00e1t\u00e9kt\u00e9r kirajzol\u00e1sa","text":"

    A k\u00f6vetkez\u0151 l\u00e9p\u00e9s a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1sa \u00e9s annak hozz\u00e1rendel\u00e9se a GameActivity-hez.

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt a megl\u00e9v\u0151 tictactoe package-ben hozzunk l\u00e9tre egy view package-et , majd abban egy TicTacToeView oszt\u00e1lyt, mely a View \u0151soszt\u00e1lyb\u00f3l sz\u00e1rmazik:

    package hu.bme.aut.android.tictactoe.view\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.view.View\nimport kotlin.math.min\n\nclass TicTacToeView : View {\n\nprivate val paintBg = Paint()\nprivate val paintLine = Paint()\n\nconstructor(context: Context?) : super(context)\nconstructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n\ninit {\npaintBg.color = Color.BLACK\npaintBg.style = Paint.Style.FILL\n\npaintLine.color = Color.WHITE\npaintLine.style = Paint.Style.STROKE\npaintLine.strokeWidth = 5F\n}\n\noverride fun onDraw(canvas: Canvas) {\ncanvas.drawRect(0F, 0F, width.toFloat(), height.toFloat(), paintBg)\n\ndrawGameArea(canvas)\ndrawPlayers(canvas)\n}\n\nprivate fun drawGameArea(canvas: Canvas) {\n//TODO\n}\n\nprivate fun drawPlayers(canvas: Canvas) {\n//TODO\n}\n\noverride fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\nval w = MeasureSpec.getSize(widthMeasureSpec)\nval h = MeasureSpec.getSize(heightMeasureSpec)\nval d: Int\n\nwhen {\nw == 0 -> { d = h }\nh == 0 -> { d = w }\nelse -> { d = min(w, h) }\n}\n\nsetMeasuredDimension(d, d)\n}\n\noverride fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\n// TODO\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n\n}\n

    L\u00e1that\u00f3, hogy az oszt\u00e1ly egy n\u00e9zet rajzol\u00e1s\u00e1\u00e9rt felel\u0151s. L\u00e9trehozunk k\u00e9t Paint objektumot, melyek a h\u00e1tt\u00e9r, illetve a p\u00e1lyaelemek rajzol\u00e1s\u00e1hoz lesznek haszn\u00e1lva. A konstruktorok, mint l\u00e1tjuk csak egy super() h\u00edv\u00e1st val\u00f3s\u00edtanak meg, mivel ebben a megval\u00f3s\u00edt\u00e1sban az init blokk v\u00e9gzi az oszt\u00e1ly inicializ\u00e1l\u00e1s\u00e1t. Fontos, hogy az onDraw()-ban ne hozzunk l\u00e9tre objektumokat, hiszen az onDraw() minden k\u00e9pkocka kirajzol\u00e1sakor megh\u00edv\u00f3dik \u00e9s sokszor hozn\u00e1 l\u00e9tre feleslegesen \u0151ket, lass\u00edtva ezzel a m\u0171k\u00f6d\u00e9st \u00e9s megnehez\u00edtve a garbage collector dolg\u00e1t.

    Az oszt\u00e1ly egyik legl\u00e9nyegesebb f\u00fcggv\u00e9nye az onDraw, mely a kapott canvas objektumra rajzolja ki a n\u00e9zet tartalm\u00e1t. A jelenlegi implement\u00e1ci\u00f3 feket\u00e9re festi a ter\u00fcletet \u00e9s megh\u00edvja a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1s\u00e9rt (n\u00e9gyzetr\u00e1cs) \u00e9s a j\u00e1t\u00e9kosok (X \u00e9s O) kirajzol\u00e1s\u00e1\u00e9rt felel\u0151s \u2013 egyel\u0151re m\u00e9g \u00fcres \u2013 f\u00fcggv\u00e9nyeket.

    Az onMeasure f\u00fcggv\u00e9ny fel\u00fcldefini\u00e1l\u00e1s\u00e1val biztos\u00edthat\u00f3, hogy a n\u00e9zet mindig n\u00e9gyzetes form\u00e1ban jelenjen meg, azaz ugyanakkora legyen a sz\u00e9less\u00e9ge, mint a magass\u00e1ga.

    V\u00e9g\u00fcl az onTouchEvent f\u00fcggv\u00e9nyben tudjuk kezelni az \u00e9rint\u00e9s esem\u00e9nyeket. Jelenleg az ACTION_DOWN esem\u00e9nyt vizsg\u00e1ljuk, de m\u00e1s \u00e9rint\u00e9s esem\u00e9nyek is hasonl\u00f3an kezelhet\u0151k itt.

    init

    Az init blokkban v\u00e9gezhetj\u00fck el az oszt\u00e1lyunk olyan inicializ\u00e1l\u00e1si feladatait, amelyekre b\u00e1rmilyen konstruktor megh\u00edv\u00e1sakor sz\u00fcks\u00e9g\u00fcnk van.

    when

    Figyelj\u00fck meg a when k\u00e9tf\u00e9le haszn\u00e1lat\u00e1t. Az onTouchEvent f\u00fcggv\u00e9nyben egy Java-s switch-hez hasonl\u00f3an futtat k\u00f3dot a param\u00e9terk\u00e9nt megkapott kifejez\u00e9s \u00e9rt\u00e9k\u00e9t\u0151l f\u00fcgg\u0151en, m\u00edg az onMeasure f\u00fcggv\u00e9nyben egy kev\u00e9sb\u00e9 olvashat\u00f3 if-else l\u00e1nc helyett haszn\u00e1ljuk, param\u00e9ter n\u00e9lk\u00fcl.

    kasztol\u00e1s

    Kotlinban a (float) x \u00e9s (int) y st\u00edlus\u00fa castol\u00e1sok helyett a numerikus t\u00edpusok k\u00f6z\u00f6tt a toInt(), toFloat(), \u00e9s hasonl\u00f3 f\u00fcggv\u00e9nyekkel v\u00e9gezhet\u00fcnk konverzi\u00f3t.

    Ahhoz, hogy a GameActivity ezt a j\u00e1t\u00e9kteret megjelen\u00edtse, m\u00f3dos\u00edtsuk a hozz\u00e1 tartoz\u00f3 res/layout/activity_game.xml f\u00e1jlt. A fel\u00fclet egy Fragment kont\u00e9nert tartalmaz, amibe majd a j\u00e1t\u00e9kt\u00e9r ker\u00fcl:

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".GameActivity\">\n\n<androidx.fragment.app.FragmentContainerView\nandroid:id=\"@+id/fragmentGameArea\"\nandroid:name=\"hu.bme.aut.android.tictactoe.fragments.GameFragment\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\ntools:layout=\"@layout/fragment_game\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck el a j\u00e1t\u00e9kteret tartalmaz\u00f3 Fragmentet. Ezt k\u00e9sz\u00edthetj\u00fck az Activity-hez hasonl\u00f3an var\u00e1zsl\u00f3val is, (jobb klinn -> New -> Fragment...) azonban ez t\u00fal sok olyan k\u00f3dot gener\u00e1lna, amire nek\u00fcnk most nincs sz\u00fcks\u00e9g\u00fcnk. Csin\u00e1ljunk teh\u00e1t egy \u00faj layout f\u00e1jlt, aminek a neve legyen fragment_game. Ez egy sz\u00fcrk\u00e9s h\u00e1tter\u0171 ConstraintLayout k\u00f6zep\u00e9n jelen\u00edtse meg a TicTacToeView n\u00e9zetet:

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\nandroid:background=\"#888888\">\n\n<hu.bme.aut.android.tictactoe.view.TicTacToeView\nandroid:id=\"@+id/ticTacToeView\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:layout_marginBottom=\"8dp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:layout_constraintVertical_bias=\"0.495\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

    package

    Fontos, hogy az itt szerepl\u0151 package n\u00e9v a saj\u00e1t TicTacToeView oszt\u00e1lyunk neve el\u0151tt azonos legyen a n\u00e9zet forr\u00e1s\u00e1nak tetej\u00e9n szerepl\u0151 package n\u00e9vvel, egy\u00e9bk\u00e9nt hib\u00e1t fogunk kapni, amikor megpr\u00f3b\u00e1ljuk megnyitni ezt a k\u00e9perny\u0151t. - De szerencs\u00e9re a k\u00f3dkieg\u00e9sz\u00edt\u0151 ebben is seg\u00edt.

    A fel\u00fclet ut\u00e1n k\u00e9sz\u00edts\u00fck el egy k\u00fcl\u00f6n fragments package-be mag\u00e1t a GameFragment-et is, aminek egyetlen feladata, hogy megjelen\u00edtse a fel\u00fclet\u00fcnket:

    package hu.bme.aut.android.tictactoe.fragments\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.tictactoe.databinding.FragmentGameBinding\n\nclass GameFragment : Fragment() {\n\nprivate lateinit var binding: FragmentGameBinding\n\noverride fun onCreateView(\ninflater: LayoutInflater,\ncontainer: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\nbinding = FragmentGameBinding.inflate(layoutInflater, container, false)\nreturn binding.root\n}\n}\n
    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Most m\u00e1r az \u00daj j\u00e1t\u00e9k gombra nyomva meg kell, hogy jelenjen a (m\u00e9g er\u0151sen hi\u00e1nyos) j\u00e1t\u00e9kter\u00fcnk.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9kt\u00e9r (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a GameFragmenthez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt val\u00f3s\u00edtsuk meg a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1s\u00e1t a TicTacToeView drawGameArea f\u00fcggv\u00e9ny\u00e9ben, azaz rajzoljuk meg a v\u00edzszintes \u00e9s f\u00fcgg\u0151leges vonalakat:

    private fun drawGameArea(canvas: Canvas) {\nval widthFloat: Float = width.toFloat()\nval heightFloat: Float = height.toFloat()\n\n// border\ncanvas.drawRect(0F, 0F, widthFloat, heightFloat, paintLine)\n\n// two horizontal lines\ncanvas.drawLine(0F, heightFloat / 3, widthFloat, widthFloat / 3, paintLine)\ncanvas.drawLine(0F, 2 * heightFloat / 3, widthFloat, 2 * heightFloat / 3, paintLine)\n\n// two vertical lines\ncanvas.drawLine(widthFloat / 3, 0F, widthFloat / 3, heightFloat, paintLine)\ncanvas.drawLine(2 * widthFloat / 3, 0F, 2 * widthFloat / 3, heightFloat, paintLine)\n}\n

    Ezt k\u00f6vet\u0151en val\u00f3s\u00edtsuk meg a modell alapj\u00e1n a j\u00e1t\u00e9kt\u00e9rben az X-ek \u00e9s O-k kirajzol\u00e1s\u00e1t az drawPlayers f\u00fcggv\u00e9nyben. A megval\u00f3s\u00edt\u00e1s sor\u00e1n v\u00e9gigmegy\u00fcnk a j\u00e1t\u00e9kt\u00e9r m\u00e1trix elemein \u00e9s a benne tal\u00e1lhat\u00f3 \u00e9rt\u00e9kek szerint O-t vagy X-et rajzolunk az adott mez\u0151be:

    private fun drawPlayers(canvas: Canvas) {\n// draw a circle at the center of the field\n// X coordinate: left side of the square + half width of the square\nfor (i in 0 until 3) {\nfor (j in 0 until 3) {\nwhen (TicTacToeModel.getFieldContent(i, j)) {\nTicTacToeModel.CIRCLE -> {\nval centerX = i * width / 3 + width / 6\nval centerY = j * height / 3 + height / 6\nval radius = height / 6 - 2\ncanvas.drawCircle(centerX.toFloat(), centerY.toFloat(), radius.toFloat(), paintLine)\n}\nTicTacToeModel.CROSS -> {\ncanvas.drawLine(\n(i * width / 3).toFloat(),\n(j * height / 3).toFloat(),\n((i + 1) * width / 3).toFloat(),\n((j + 1) * height / 3).toFloat(),\npaintLine\n)\ncanvas.drawLine(\n((i + 1) * width / 3).toFloat(),\n(j * height / 3).toFloat(),\n(i * width / 3).toFloat(),\n((j + 1) * height / 3).toFloat(),\npaintLine\n)\n}\n}\n}\n}\n}\n

    for ciklus

    A Kotlin for ciklus\u00e1nak nincs h\u00e1rom r\u00e9szre bontott, ;-vel elv\u00e1lasztott verzi\u00f3ja. Csak a fenti k\u00f3dban is l\u00e1that\u00f3 for each st\u00edlus\u00fa for ciklust t\u00e1mogatja a nyelv, amellyel azonban b\u00e1rmilyen iter\u00e1lhat\u00f3 objektumon ugyan\u00fagy tudunk iter\u00e1lni. Ha egyszer\u0171en sz\u00e1mokon szeretn\u00e9nk ezt megtenni, l\u00e9trehozhatunk egy iter\u00e1lhat\u00f3 Range-et p\u00e9ld\u00e1ul a 0..3 szintaxissal amivel egy z\u00e1rt intervallumot kapunk, vagy a fent haszn\u00e1lt 0 until 3 szintaxissal, ami egy jobbr\u00f3l ny\u00edlt intervallumot hoz l\u00e9tre, teh\u00e1t a 3 \u00e9rt\u00e9ket m\u00e1r nem fogja felvenni a ciklus v\u00e1ltoz\u00f3.

    V\u00e9g\u00fcl val\u00f3s\u00edtsuk meg az \u00e9rint\u00e9s esem\u00e9nyre val\u00f3 reag\u00e1l\u00e1st \u00fagy, hogy a megfelel\u0151 mez\u0151be \u2013 ha az \u00fcres \u2013 elhelyezz\u00fck az aktu\u00e1lis j\u00e1t\u00e9kost, melyet a modell nextPlayer v\u00e1ltoz\u00f3ja reprezent\u00e1l.

    A modell friss\u00edt\u00e9se ut\u00e1n az \u00fajrarajzol\u00e1st az invalidate() f\u00fcggv\u00e9ny megh\u00edv\u00e1s\u00e1val tudjuk el\u00e9rni.

    override fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\nval tX: Int = (event.x / (width / 3)).toInt()\nval tY: Int = (event.y / (height / 3)).toInt()\nif (tX < 3 && tY < 3 && TicTacToeModel.getFieldContent(tX, tY) == TicTacToeModel.EMPTY) {\nTicTacToeModel.setFieldContent(tX, tY, TicTacToeModel.nextPlayer)\ninvalidate()\n}\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n
    "},{"location":"laborok/tictactoe/#alkalmazas-ikon-lecserelese-onallo-feladat","title":"Alkalmaz\u00e1s ikon lecser\u00e9l\u00e9se - \u00d6n\u00e1ll\u00f3 feladat","text":"

    Az alkalmaz\u00e1s ikonj\u00e1t jelenleg a res/mipmap[-ldpi/mdpi/hdpi/xhdpi/...] mapp\u00e1kban tal\u00e1lhat\u00f3 ic_launcher.png jelk\u00e9pezi. Keress\u00fcnk egy \u00faj ikont \u00e9s cser\u00e9lj\u00fck le. Nem musz\u00e1j az ikont minden felbont\u00e1sban elk\u00e9sz\u00edteni, egyszer\u0171en elhelyezhet\u00f3nk egy m\u00e9retet a mipmap mapp\u00e1ban is (melyet l\u00e9tre kell hozni), ekkor term\u00e9szetesen k\u00fcl\u00f6nb\u00f6z\u0151 felbont\u00e1s\u00fa eszk\u00f6z\u00f6k\u00f6n torzulhat az ikon k\u00e9pe. (Ha marad id\u0151, a be\u00e9p\u00edtett Asset Studio-val elk\u00e9sz\u00edthetj\u00fck az \u00f6sszes sz\u00fcks\u00e9ges v\u00e1ltozatot.)

    Ikon gener\u00e1l\u00e1sa

    Ikon gener\u00e1l\u00e1s\u00e1ra haszn\u00e1lhatjuk p\u00e9ld\u00e1ul a k\u00f6vetkez\u0151 oldalt: https://icon.kitchen/

    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st!

    \u00c9szrevehetj\u00fck, hogy ha a j\u00e1t\u00e9kt\u00e9rr\u0151l visszal\u00e9p\u00fcnk \u00e9s megint \u00faj j\u00e1t\u00e9kot kezd\u00fcnk, a j\u00e1t\u00e9kt\u00e9r nem t\u00f6rl\u0151dik. Ez\u00e9rt a GameActivity-re val\u00f3 navig\u00e1ci\u00f3 el\u0151tt a TicTacToeModel-t alap\u00e1llapotba kell \u00e1ll\u00edtanunk, hogy \u00faj j\u00e1t\u00e9k kezd\u0151dj\u00f6n (MainActivity.kt):

    binding.btnStart.setOnClickListener {\nTicTacToeModel.resetModel()\nstartActivity(Intent(this@MainActivity, GameActivity::class.java))\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9kt\u00e9r j\u00e1t\u00e9k k\u00f6zben (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a TicTacToeView k\u00f3dj\u00e1nak egy r\u00e9szlete, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/tictactoe/#jateklogika-ellenorzese-onallo-feladat","title":"J\u00e1t\u00e9klogika ellen\u0151rz\u00e9se - \u00d6n\u00e1ll\u00f3 feladat","text":"

    Val\u00f3s\u00edtson meg egy f\u00fcggv\u00e9nyt, mely minden l\u00e9p\u00e9s ut\u00e1n leellen\u0151rzi, hogy gy\u0151z\u00f6tt-e valamelyik j\u00e1t\u00e9kos, vagy nincs-e d\u00f6ntetlen. Amennyiben v\u00e9ge a j\u00e1t\u00e9knak, egy Toast \u00fczenettel jelezze ezt a felhaszn\u00e1l\u00f3nak \u00e9s l\u00e9pjen vissza a f\u0151men\u00fcbe. A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel vizsg\u00e1lja meg, hogy a View oszt\u00e1lyb\u00f3l hogyan \u00e9rhet\u0151 el az \u0151t tartalmaz\u00f3 \"host\" Activity, aminek \u00edgy p\u00e9ld\u00e1ul egy gameOver() f\u00fcggv\u00e9nye megh\u00edvhat\u00f3, ami megval\u00f3s\u00edtja a fent le\u00edrt j\u00e1t\u00e9k befejez\u00e9st.

    J\u00f3 munk\u00e1t k\u00edv\u00e1nunk!

    Seg\u00edts\u00e9g

    A j\u00e1t\u00e9k\u00e1llapot ellen\u0151rz\u00e9se a TicTacToeModel feladata, \u00edgy oda k\u00e9sz\u00edts\u00fcnk egy f\u00fcggv\u00e9nyt, ami ezt teszi meg:

    fun checkGameState(): Byte { ///TODO 4 \u00e1llapottal t\u00e9rhet vissza: \n// k\u00f6r nyert\n// kereszt nyert\n// d\u00f6ntetlen\n// m\u00e9g nincs v\u00e9ge\nreturn CIRCLE\n}\n

    J\u00e1t\u00e9k \u00e1llapotot ellen\u0151rizni \u00faj jel lehelyez\u00e9se ut\u00e1n \u00e9rdemes, teh\u00e1t pl. a TicTacToeView onTouchEvent() f\u00fcggv\u00e9ny\u00e9ben:

    override fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\nval tX: Int = (event.x / (width / 3)).toInt()\nval tY: Int = (event.y / (height / 3)).toInt()\nif (tX < 3 && tY < 3 && TicTacToeModel.getFieldContent(tX, tY) == TicTacToeModel.EMPTY) {\nTicTacToeModel.setFieldContent(tX, tY, TicTacToeModel.nextPlayer)\ninvalidate()\nval result = TicTacToeModel.checkGameState()\n///v\u00e9ge van, teh\u00e1t tov\u00e1bb h\u00edvunk\nif (result != TicTacToeModel.EMPTY) {\n(context as GameActivity).gameOver(result)\n}\n}\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n

    A context ilyen kasztol\u00e1sa nem sz\u00e9p, \u00e9s vesz\u00e9lyes is. Itt csak az egyszer\u0171s\u00e9ge miatt haszn\u00e1ljuk. A f\u00e9l\u00e9v k\u00e9s\u0151bbi r\u00e9sz\u00e9ben tanulunk szebb megold\u00e1st erre a probl\u00e9m\u00e1ra.

    A Toast-ot pedig a GameActivity-b\u0151l dobjuk az eredm\u00e9ny alapj\u00e1n, majd bez\u00e1rjuk az Activity-t:

    fun gameOver(result: Byte) {\nwhen (result) {\n///TODO t\u00f6bb eset\nTicTacToeModel.CIRCLE -> {\nToast.makeText(this@GameActivity, \"A k\u00f6r nyert\", Toast.LENGTH_LONG).show()\n}\n}\nfinish()\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9k v\u00e9g\u00e9t jelz\u0151 Toast \u00fczenet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a j\u00e1t\u00e9k\u00e1llapot ellen\u0151rz\u00e9s\u00e9hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/timetable/","title":"LaborExtra - \u00d3rarend","text":""},{"location":"laborok/timetable/#bevezeto","title":"Bevezet\u0151","text":"

    A labor sor\u00e1n a feladat egy \u00f3rarend alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9se, ahol a felhaszn\u00e1l\u00f3 grafikus fel\u00fcleten szerkesztheti \u00e9s l\u00e1thatja a napi, valamint heti beoszt\u00e1s\u00e1t.

    Az Play Store-ban sz\u00e1mos ilyen szoftver tal\u00e1lhat\u00f3, melyeket \u00e9rdemes megvizsg\u00e1lni \u00e9s \u00f6tleteket mer\u00edteni a megval\u00f3s\u00edt\u00e1shoz.

    "},{"location":"laborok/timetable/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/timetable/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Checkout

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    FILE PATH

    A projekt mindenk\u00e9ppen egy repository-ban l\u00e9v\u0151 k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    "},{"location":"laborok/timetable/#kovetelmenyek","title":"K\u00f6vetelm\u00e9nyek","text":"

    Az alkalmaz\u00e1s megtervez\u00e9se \u00e9s megval\u00f3s\u00edt\u00e1sa teljes m\u00e9rt\u00e9kben k\u00f6tetlen, az elv\u00e1rt minim\u00e1lis funkcionalit\u00e1s:

    • \u00d3ra (esem\u00e9ny) l\u00e9trehoz\u00e1sa, szerkeszt\u00e9se, t\u00f6rl\u00e9se. Attrib\u00fatumai (legal\u00e1bb):
      • Kezd\u00e9si, befejez\u00e9si id\u0151 (pl. 8:15-11:45),
      • T\u00e1rgy (pl. Android alap\u00fa szoftverfejleszt\u00e9s),
      • Helysz\u00edn (pl. QBF08),
      • Sz\u00edn, amellyel megjelenik a napt\u00e1rban (pl. #234567)
    • \u00d3r\u00e1k \u00e1tl\u00e1that\u00f3 megjelen\u00edt\u00e9se napi \u00e9s heti bont\u00e1sban teljes k\u00e9perny\u0151n
    • Egy \u00f3ra r\u00e9szletes adatlapja
    • Adatok perzisztens t\u00e1rol\u00e1sa

    A fenti specifik\u00e1ci\u00f3 megval\u00f3s\u00edt\u00e1sa 4-es oszt\u00e1lyzatot jelent, jobb min\u0151s\u00edt\u00e9shez tov\u00e1bbi kreat\u00edv funkci\u00f3k be\u00e9p\u00edt\u00e9se \u00e9s ig\u00e9nyes felhaszn\u00e1l\u00f3i fel\u00fclet sz\u00fcks\u00e9ges.

    N\u00e9h\u00e1ny \u00f6tlet:

    • Widget
    • Eml\u00e9keztet\u0151 be\u00e1ll\u00edt\u00e1sa az \u00f3r\u00e1khoz
    • A-h\u00e9t, B-h\u00e9t t\u00e1mogat\u00e1sa
    • Vizsg\u00e1k, ZH-k be\u00e9p\u00edt\u00e9se
    • telefon \u00e9s tablet, \u00e1ll\u00f3-fekv\u0151 felhaszn\u00e1l\u00f3i fel\u00fclet
    • nem default UI elemek haszn\u00e1lata (mint a School Helper-ben)
    • Esem\u00e9nyek import\u00e1l\u00e1sa napt\u00e1rb\u00f3l, vagy iCal f\u00e1jlb\u00f3l
    • Teljes \u00f3rarend export\u00e1l\u00e1sa-import\u00e1l\u00e1sa saj\u00e1t tervez\u00e9s\u0171 f\u00e1jlba
    • T\u00f6bb skin t\u00e1mogat\u00e1sa

    BEADAND\u00d3 (5 pont)

    A rep\u00f3ba felt\u00f6ltend\u0151 az alkalmaz\u00e1s projekt k\u00f6nyvt\u00e1r\u00e1n fel\u00fcl egy felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv is (README.md), ami le\u00edrja az elk\u00e9sz\u00edtett szoftver funkci\u00f3it, \u00e9s k\u00e9peket is tartalmaz minden relev\u00e1ns k\u00e9perny\u0151r\u0151l.

    "},{"location":"laborok/todo_compose_basics/","title":"Labor05 - Todo Alkalmaz\u00e1s","text":"

    A labor c\u00e9lja, hogy bemutassa, hogyan lehet egy egyszer\u0171 ToDo alkalmaz\u00e1st megval\u00f3s\u00edtani a Compose keretrendszerben.

    "},{"location":"laborok/todo_compose_basics/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/todo_compose_basics/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    "},{"location":"laborok/todo_compose_basics/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

    Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
    2. A projekt neve legyen Todo, a kezd\u0151 package pedig hu.bme.aut.android.todo.
    3. A projektet a repository-n bel\u00fcl egy k\u00fcl\u00f6n mapp\u00e1ban hozzuk l\u00e9tre.
    4. A minimum API szint legyen 26 (Android 8.0).
    5. A Build configuration language-n\u00e9l v\u00e1lasszuk a Kotlin DSL-t.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

    "},{"location":"laborok/todo_compose_basics/#verziok-frissitese","title":"Verzi\u00f3k friss\u00edt\u00e9se","text":"

    Vegy\u00fck fel a sz\u00fcks\u00e9ges k\u00f6nyvt\u00e1rakat a libs.versions.toml f\u00e1jlban:

    [versions]\n...\ncomposeBom = \"2024.09.03\"\nkotlinxDatetime = \"0.4.1\"\nlifecycleVersion = \"2.8.6\"\nnavigationCompose = \"2.8.2\"\n\n[libraries]\n...\nandroidx-lifecycle-runtime-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-runtime-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-lifecycle-viewmodel-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-viewmodel-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\" }\nandroidx-navigation-compose = { group = \"androidx.navigation\", name=\"navigation-compose\", version.ref = \"navigationCompose\" }\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\n

    Majd pedig haszn\u00e1ljuk is ezeket a modul szint\u0171 build.gradle.kts f\u00e1jlban:

    dependencies {\n    ...\n\n    //Compose Bill of Materials\n    implementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n\n    //ViewModel Lifecycle\n    implementation(libs.androidx.lifecycle.runtime.compose)\n    implementation(libs.androidx.lifecycle.viewmodel.compose)\n\n    //Kotlin Extensions DateTime - LocalDate\n    implementation(libs.kotlinx.datetime)\n\n    //Compose Navigation\n    implementation(libs.androidx.navigation.compose)\n\n    //Material Icons\n    implementation(libs.androidx.material.icons.extended)\n}\n

    F\u00fcgg\u0151s\u00e9gek

    Az itt tal\u00e1lhat\u00f3 k\u00f3dban minden f\u00fcgg\u0151s\u00e9g szerepel, a labor sor\u00e1n \u00fajat hozz\u00e1adni nem kell. Azonban az egy\u00e9rtelm\u0171s\u00e9g kedv\u00e9\u00e9rt a k\u00e9s\u0151bbiekben mindenhol felt\u00fcntetj\u00fck az adott ter\u00fclethez sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket.

    A fenti f\u00fcgg\u0151s\u00e9gekhez 34-es SDK-val kell ford\u00edtanunk a projektet, ha a legener\u00e1lt alkalmaz\u00e1sban kor\u00e1bbi lenne megadva, akkor friss\u00edts\u00fck ezt is a modul szint\u0171 build.gradle.kts f\u00e1jlunkban:

        compileSdk = 34\n
    "},{"location":"laborok/todo_compose_basics/#szoveges-eroforrasok-definialasa","title":"Sz\u00f6veges er\u0151forr\u00e1sok defini\u00e1l\u00e1sa","text":"

    A strings.xml f\u00e1jl m\u0171k\u00f6d\u00e9s\u00e9t m\u00e1r ismerj\u00fck, t\u00f6lts\u00fck fel ezt el\u0151re a k\u00e9s\u0151bb sz\u00fcks\u00e9ges sz\u00f6veges c\u00edmk\u00e9kkel, hogy k\u00e9s\u0151bb a l\u00e9nyeges elemekre tudjunk koncentr\u00e1lni:

    <resources>\n<string name=\"app_name\">Todo</string>\n<string name=\"some_error_message\">Error</string>\n<string name=\"priority_title_none\">none</string>\n<string name=\"priority_title_low\">low</string>\n<string name=\"priority_title_medium\">medium</string>\n<string name=\"priority_title_high\">high</string>\n<string name=\"text_empty_todo_list\">\"You haven\\\\'t added any todos yet. \"</string>\n<string name=\"text_your_todo_list\">Your todos</string>\n<string name=\"list_item_supporting_text\">The due date is: %1$s</string>\n<string name=\"textfield_label_description\">Description</string>\n<string name=\"textfield_label_title\">Title</string>\n<string name=\"app_bar_title_create_todo\">Create todo</string>\n<string name=\"dialog_ok_button_text\">OK</string>\n<string name=\"dialog_dismiss_button_text\">Close</string>\n</resources>\n
    "},{"location":"laborok/todo_compose_basics/#adatosztalyok-letrehozasa","title":"Adatoszt\u00e1lyok l\u00e9trehoz\u00e1sa","text":"

    Miel\u0151tt nekil\u00e1tn\u00e1nk az alkalmaz\u00e1s fel\u00fcleteinek, illetve logik\u00e1j\u00e1nak kialak\u00edt\u00e1s\u00e1ba, \u00e9rdemes l\u00e9trehozni azokat a modelloszt\u00e1lyokat, amiket az alkalmaz\u00e1son bel\u00fcl haszn\u00e1lni fogunk. Az alkalmaz\u00e1sunkban feladatokat akarunk t\u00e1rolni, melyek a k\u00f6vetkez\u0151 tulajdons\u00e1gokkal fognak rendelkezni:

    • N\u00e9v
    • Le\u00edr\u00e1s
    • Feladat hat\u00e1rideje
    • Fontoss\u00e1g
    • Azonos\u00edt\u00f3

    Hozzunk l\u00e9tre egy \u00faj domain package-t l\u00e9tre a projekt\u00fcnk gy\u00f6ker\u00e9ben, mely az alkalmaz\u00e1sunk adatr\u00e9teg\u00e9nek r\u00e9szeit fogja tartalmazni, majd ezen bel\u00fcl hozzunk l\u00e9tre egy model package-et, mely az adatmodellek oszt\u00e1ly megfelel\u0151it fogja tartalmazni. Ebben hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt: Todo.kt:

    package hu.bme.aut.android.todo.domain.model\n\nimport kotlinx.datetime.LocalDate\n\ndata class Todo(\nval id: Int,\nval title: String,\nval priority: Priority,\nval dueDate: LocalDate,\nval description: String  )\n

    Priority.kt:

    package hu.bme.aut.android.todo.domain\n\nenum class Priority {  NONE,  LOW,  MEDIUM,  HIGH,  }\n

    LocalDate

    A LocalDate egy \u00e1ltal\u00e1nos implement\u00e1ci\u00f3ja az id\u0151 kezel\u00e9s\u00e9nek, mely multiplatform k\u00f6rnyezetben is haszn\u00e1lhat\u00f3, ehhez a k\u00f6vetkez\u0151 f\u00fcgg\u0151s\u00e9get kell hozz\u00e1adnunk a modul szint\u0171 build.gradle f\u00e1jlhoz:

    [versions]\nkotlinxDatetime = \"0.4.1\"\n\n[libraries]\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\n

    Id\u0151 oszt\u00e1lyok kezel\u00e9se

    A labor sor\u00e1n a LocalDate mindig a kotlinx, mig a LocalDateTime mindig a java k\u00f6nyvt\u00e1rb\u00f3l legyen import\u00e1lva.

    Az adat t\u00edpus\u00fa oszt\u00e1lyok eset\u00e9ben a Kotlin automatikusan deklar\u00e1l gyakran haszn\u00e1lt f\u00fcggv\u00e9nyeket, mint p\u00e9ld\u00e1ul az equals() \u00e9s hashCode() f\u00fcggv\u00e9nyeket k\u00fcl\u00f6nb\u00f6z\u0151 objektumok \u00f6sszehasonl\u00edt\u00e1s\u00e1hoz, illetve egy toString() f\u00fcggv\u00e9nyt, mely visszaadja a t\u00e1rolt v\u00e1ltoz\u00f3k \u00e9rt\u00e9k\u00e9t.

    A felhaszn\u00e1l\u00f3i fel\u00fclet k\u00f3dj\u00e1nak egyszer\u0171s\u00edt\u00e9se \u00e9rd\u00e9k\u00e9ben \u00e9rdemes olyan seg\u00e9doszt\u00e1lyokat is defini\u00e1lni, melyek m\u00e1r k\u00f6zvetlen\u00fcl a fel\u00fcleten haszn\u00e1lt \u00e9rt\u00e9keket fogj\u00e1k haszn\u00e1lni. Ehhez deifini\u00e1ljuk a ui package-en bel\u00fcl a model package-et, \u00e9s vegy\u00fck fel a k\u00f6vetkez\u0151 oszt\u00e1lyokat: UiText.kt:

    package hu.bme.aut.android.todo.ui.model\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport hu.bme.aut.android.todo.R\n\nsealed class UiText {\ndata class DynamicString(val value: String): UiText()\ndata class StringResource(@StringRes val id: Int): UiText()\n\nfun asString(context: Context): String {\nreturn when(this) {\nis DynamicString -> this.value\nis StringResource -> context.getString(this.id)\n}\n}\n}\n\nfun Throwable.toUiText(): UiText {\nval message = this.message.orEmpty()\nreturn if (message.isBlank()) {\nUiText.StringResource(R.string.some_error_message)\n} else {\nUiText.DynamicString(message)\n}\n}\n

    Vizsg\u00e1ljuk meg, hogy tudjuk a sealed class seg\u00edts\u00e9g\u00e9vel \u00e1ltal\u00e1nosan defini\u00e1lni a sz\u00f6vegeket, melyek \u00edgy j\u00f6hetnek a be\u00e9getett er\u0151forr\u00e1sb\u00f3l, vagy \u00e9rkezhetnek a szerveren kereszt\u00fcl egy k\u00fcls\u0151 forr\u00e1sb\u00f3l.

    PriorityUi.kt:

    package hu.bme.aut.android.todo.ui.model\n\nimport androidx.compose.ui.graphics.Color\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.domain.model.Priority\n\nenum class PriorityUi(\nval title: Int,\nval color: Color\n) {\nNone(\ntitle =  R.string.priority_title_none,\ncolor = Color(0xFFE6E4E4)\n),\nLow(\ntitle = R.string.priority_title_low,\ncolor = Color(0xFF8BC34A)\n),\nMedium(\ntitle = R.string.priority_title_medium,\ncolor = Color(0xFFFFC107)\n),\nHigh(\ntitle = R.string.priority_title_high,\ncolor = Color(0xFFF44336)\n),\n}\n\nfun PriorityUi.asPriority(): Priority {\nreturn when(this) {\nPriorityUi.None -> Priority.NONE\nPriorityUi.Low -> Priority.LOW\nPriorityUi.Medium -> Priority.MEDIUM\nPriorityUi.High -> Priority.HIGH\n}\n}\n\nfun Priority.asPriorityUi(): PriorityUi {\nreturn when(this) {\nPriority.NONE -> PriorityUi.None\nPriority.LOW -> PriorityUi.Low\nPriority.MEDIUM -> PriorityUi.Medium\nPriority.HIGH -> PriorityUi.High\n}\n}\n

    TodoUi.kt

    package hu.bme.aut.android.todo.ui.model\n\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.toLocalDate\nimport java.time.LocalDateTime\n\ndata class TodoUi(\nval id: Int = 0,\nval title: String = \"\",\nval priority: PriorityUi = PriorityUi.None,\nval dueDate: String = LocalDate(  LocalDateTime.now().year,  LocalDateTime.now().monthValue,  LocalDateTime.now().dayOfMonth  ).toString(),\nval description: String = \"\"  )  fun Todo.asTodoUi(): TodoUi = TodoUi(  id = id,  title = title,  priority = priority.asPriorityUi(),  dueDate = dueDate.toString(),  description = description  )  fun TodoUi.asTodo(): Todo = Todo(  id = id,  title = title,  priority = priority.asPriority(),  dueDate = dueDate.toLocalDate(),  description = description  )\n
    "},{"location":"laborok/todo_compose_basics/#navigacio-kialakitasa","title":"Navig\u00e1ci\u00f3 kialak\u00edt\u00e1sa","text":"

    Az el\u0151z\u0151 laborhoz hasonl\u00f3an alak\u00edtsuk ki a projektben a navig\u00e1ci\u00f3n\u00e1l haszn\u00e1lt oszt\u00e1lyokat!

    Navig\u00e1ci\u00f3

    Itt is a Compose Navigation k\u00f6nyvt\u00e1rat fogjuk haszn\u00e1lni, ez\u00e9rt adjuk ezt hozz\u00e1 a modul szint\u0171 build.gradle f\u00e1jlunkhoz.

    [versions]\nnavigationCompose = \"2.8.1\"\n\n[libraries]\nandroidx-navigation-compose = { group = \"androidx.navigation\", name=\"navigation-compose\", version.ref = \"navigationCompose\" }\n

    Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban egy \u00faj package-et navigation n\u00e9ven, majd hozzuk l\u00e9tre benne az \u00fatvonalakat reprezent\u00e1l\u00f3 Screen oszt\u00e1lyt:

    package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {  }\n

    Illetve hozzuk l\u00e9tre a navig\u00e1ci\u00f3t v\u00e9gz\u0151 Composable f\u00fcggv\u00e9nyt is a NavGraph.kt f\u00e1jlban:

    package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.rememberNavController\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = \"\"\n) {\n\n}\n}\n

    A NavGraph Composable szerepe, hogy karban tartsa az \u00fatvonalakat, itt fogjuk a navgi\u00e1ci\u00f3s esem\u00e9nyeket feldolgozni.

    V\u00e9g\u00fcl friss\u00edts\u00fck a MainActivity tartalm\u00e1t \u00fagy, hogy a NavGraph Composable-t haszn\u00e1lja:

    package hu.bme.aut.android.todo\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport hu.bme.aut.android.todo.navigation.NavGraph\nimport hu.bme.aut.android.todo.ui.theme.TodoTheme\n\nclass MainActivity : ComponentActivity() {\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nTodoTheme {\nNavGraph()\n}\n}\n}\n}\n
    "},{"location":"laborok/todo_compose_basics/#lista-oldal-kialakitasa","title":"Lista oldal kialak\u00edt\u00e1sa","text":"

    Ahhoz, hogy az alkalmaz\u00e1sunk m\u0171k\u00f6dj\u00f6n, sz\u00fcks\u00e9g\u00fcnk lesz egy oldalra, amit indul\u00e1skor meg tudunk jelen\u00edteni. Az els\u0151 oldal, melyet l\u00e9trehozunk, a feladatokat megjelen\u00edt\u0151 lista oldal lesz. Gondoljuk v\u00e9gig, milyen feladatokat kell elv\u00e9gezni, illetve milyen interakci\u00f3k t\u00f6rt\u00e9nnek ezen a fel\u00fcleten:

    • Az oldalra val\u00f3 navig\u00e1l\u00e1skor be kell t\u00f6lteni az \u00f6sszes feladatot.
    • Egy feladatra val\u00f3 kattint\u00e1s ut\u00e1n el kell navig\u00e1lni egy r\u00e9szletez\u0151 oldalra.
    • El\u00e9rhet\u0151v\u00e9 kell tenni egy \u00faj feladat l\u00e9trehoz\u00e1s\u00e1t, melynek hat\u00e1s\u00e1ra \u00faj oldalra kell navig\u00e1lnunk.

    Az \u00faj oldalakra val\u00f3 navig\u00e1l\u00e1shoz sz\u00fcks\u00e9g\u00fcnk van a navig\u00e1ci\u00f3t vez\u00e9rl\u0151 kontrollerre, melyet a NavGraph Composable kezel, ez\u00e9rt ezekn\u00e9l az esem\u00e9nyekn\u00e9l az oldal olyan f\u00fcggv\u00e9ny callback objektumokat fog megh\u00edvni, melyeket a konstruktor\u00e1n kereszt\u00fcl kap meg, \u00edgy a NavGraph k\u00f6nnyen tud \u00e9rtes\u00fclni r\u00f3luk.

    Az adatok kezel\u00e9s\u00e9hez tipikusan a ViewModel oszt\u00e1lyt haszn\u00e1ljuk. A ViewModel seg\u00edts\u00e9g\u00e9vel biztos\u00edtjuk azt, hogy elk\u00fcl\u00f6n\u00fcljenek az alkalmaz\u00e1sunk megjelen\u00edt\u00e9s\u00e9rt szolg\u00e1l\u00f3 k\u00f3djai az alkalmaz\u00e1s logik\u00e1j\u00e1t biztos\u00edt\u00f3 k\u00f3djait\u00f3l. M\u00edg az el\u0151bbiek a fel\u00fclet megjelen\u00e9s\u00e9\u00e9rt felelnek, a ViewModel t\u00e1rolja \u00e9s dolgozza fel a UI-nak sz\u00fcks\u00e9ges adatokat.

    ViewModel Lifecycle

    Vegy\u00fck fel a sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket:

    [versions]\nlifecycleVersion = \"2.8.6\"\n[libraries]\nandroidx-lifecycle-runtime-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-runtime-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-lifecycle-viewmodel-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-viewmodel-compose\", version.ref = \"lifecycleVersion\" }\n

    Hozzuk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl a feature package-et, mely az egyes oldalak Composable \u00e9s ViewModel oszt\u00e1lyait fogja tartalmazni funkci\u00f3nk\u00e9nt k\u00fcl\u00f6n package-ben, majd hozzuk l\u00e9tre ebben a todo_list package-t.

    El\u0151sz\u00f6r foglalkozzunk az oldalhoz tartoz\u00f3 ViewModel oszt\u00e1llyal. Hozzuk l\u00e9tre a TodoListViewModel.kt f\u00e1jlt, majd m\u00e1soljuk be az al\u00e1bbi k\u00f3dr\u00e9szletet:

    package hu.bme.aut.android.todo.feature.todo_list\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nsealed class TodoListState {\nobject Loading : TodoListState()\ndata class Error(val error: Throwable) : TodoListState()\ndata class Result(val todoList : List<TodoUi>) : TodoListState()\n}\n\nclass TodoListViewModel() : ViewModel() {\nprivate val _state = MutableStateFlow<TodoListState>(TodoListState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\ndelay(2000)\n//TODO: Add todo loading logic\n_state.value = TodoListState.Result(\ntodoList = listOf(\nTodoUi(\nid = 1,\ntitle = \"Teszt feladat 1\",\npriority = PriorityUi.Low,\ndescription = \"Feladat le\u00edr\u00e1s 1\",\n),\nTodoUi(\nid = 2,\ntitle = \"Teszt feladat 2\",\npriority = PriorityUi.Medium,\ndescription = \"Feladat le\u00edr\u00e1s 2\",\n),\nTodoUi(\nid = 3,\ntitle = \"Teszt feladat 3\",\npriority = PriorityUi.High,\ndescription = \"Feladat le\u00edr\u00e1s 3\",\n),\n),\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoListViewModel()\n}\n}\n}\n}\n

    A fel\u00fcletet le\u00edr\u00f3 \u00e1llapot oszt\u00e1lyt sealed class-k\u00e9nt deklar\u00e1ljuk, \u00e9s j\u00f3l elk\u00fcl\u00f6n\u00edtett \u00e1llapot oszt\u00e1lyokat vesz\u00fcnk fel, \u00edgy is jelezve, hogy az egyes \u00e1llapotokban az oldalunkon mit kell megjelen\u00edteni. Ezeket egy MutableStateFlow seg\u00edts\u00e9g\u00e9vel kezelj\u00fck, melyet egy csak olvashat\u00f3 v\u00e1ltozat\u00e1ban osztunk meg az oldalt reprezent\u00e1l\u00f3 Composable-el. Az adatok bet\u00f6lt\u00e9s\u00e9t egyel\u0151re a ViewModel mag\u00e1ban v\u00e9gzi el, ez azonban hamarosan ki lesz b\u0151v\u00edtve k\u00fcls\u0151 adatbet\u00f6lt\u00e9s t\u00e1mogat\u00e1s\u00e1val.

    Mivel a ViewModel k\u00e9pes t\u00fal\u00e9lni az \u0151t l\u00e9trehoz\u00f3 komponenst, ez\u00e9rt a k\u00f3db\u00f3l mi nem a konstruktor h\u00edv\u00e1s\u00e1val fogjuk l\u00e9trehozni a p\u00e9ld\u00e1nyt, hanem a keretrendszernek tudunk \u00e1tadni egy speci\u00e1lis factory met\u00f3dust, amit a rendszer az els\u0151 alkalommal meg fog h\u00edvni. Ezt a met\u00f3dust szervezt\u00fck ki a companion object r\u00e9szbe, ami jelenleg csak l\u00e9trehoz egy p\u00e9ld\u00e1nyt, a k\u00e9s\u0151bbiekben azonban hasznos lesz k\u00fcl\u00f6nb\u00f6z\u0151 k\u00fcls\u0151 \u00e9rt\u00e9kek inicializ\u00e1l\u00e1s\u00e1ra.

    Hozzuk l\u00e9tre a fel\u00fcletet megval\u00f3s\u00edt\u00f3 TodoListScreen.kt f\u00e1jlt is ugyanebben a packageben:

    package hu.bme.aut.android.todo.feature.todo_list\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.ui.model.toUiText\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Composable\nfun TodoListScreen(\nonListItemClick: (Int) -> Unit,\nonFabClick: () -> Unit,\nviewModel: TodoListViewModel = viewModel(factory = TodoListViewModel.Factory),\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\nval context = LocalContext.current\n\nScaffold(\nmodifier = Modifier.fillMaxSize(),\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = onFabClick,\ncontainerColor = MaterialTheme.colorScheme.primary,\ncontentColor = MaterialTheme.colorScheme.onPrimary\n) {\nIcon(imageVector = Icons.Default.Add, contentDescription = null)\n}\n}\n) { innerPadding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(innerPadding)\n.padding(8.dp)\n.background(\ncolor = if (state is TodoListState.Loading || state is TodoListState.Error) {\nMaterialTheme.colorScheme.secondaryContainer\n} else {\nMaterialTheme.colorScheme.background\n}\n),\ncontentAlignment = Alignment.Center\n) {\nwhen (state) {\nis TodoListState.Loading -> CircularProgressIndicator(\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\n\nis TodoListState.Error -> Text(\ntext = state.error.toUiText().asString(context)\n)\n\nis TodoListState.Result -> {\nif (state.todoList.isEmpty()) {\nText(text = stringResource(id = R.string.text_empty_todo_list))\n} else {\n///TODO: handle list\n}\n}\n}\n}\n}\n}\n

    Mint a legt\u00f6bb esetben, itt is egy Scaffold-ot haszn\u00e1lunk az oldalunk kezel\u00e9s\u00e9re, melyhez most egy LargeFloatingActionButton-t is adunk, amellyel majd \u00faj feladatokat lehet l\u00e9trehozni. Ne felejts\u00fck el a Scaffold f\u0151 tartalm\u00e1ban innerPadding n\u00e9vvel megkapott PaddingValues \u00e9rt\u00e9keket a megfelel\u0151 helyre besz\u00farni (ez ebben az esetben a f\u0151 Box k\u00f6r\u00e9 ker\u00fcl. Ezek mellett l\u00e1that\u00f3, hogyan tudunk az aktu\u00e1lis \u00e1llapot k\u00fcl\u00f6nb\u00f6z\u0151 \u00e9rt\u00e9keinek f\u00fcggv\u00e9ny\u00e9ben el\u00e1gazni, \u00e9s k\u00fcl\u00f6nb\u00f6z\u0151 elemeket megjelen\u00edteni.

    Vizsg\u00e1ljuk meg, hogyan t\u00f6rt\u00e9nik az oldal friss\u00edt\u00e9se! A collectAsStateWithLifecycle() f\u00fcggv\u00e9nyh\u00edv\u00e1ssal automatikusan feliratkozunk a ViewModel-ben t\u00e1rolt \u00e1llapotra. Ha v\u00e1ltoz\u00e1s t\u00f6rt\u00e9nik ebben, \u00fajra le fog futni a Composable, mely \u00edgy m\u00e1r a frisebb \u00e1llapotot fogja megjelen\u00edteni.

    Val\u00f3s\u00edtsuk meg a lista megjelen\u00edt\u00e9s\u00e9t is! M\u00e1soljuk be az al\u00e1bbi k\u00f3dot a megfelel\u0151 else \u00e1gba:

    Column {\nText(\ntext = stringResource(id = R.string.text_your_todo_list),\nfontSize = 24.sp\n)\nLazyColumn(\nmodifier = Modifier\n.fillMaxSize()\n) {\nitems(state.todoList, key = { todo -> todo.id }) { todo ->\nListItem(\nleadingContent = {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = todo.priority.color,\nmodifier = Modifier\n.size(64.dp)\n)\n},\nheadlineContent = {\nText(text = todo.title)\n},\nsupportingContent = {\nText(\ntext = stringResource(\nid = R.string.list_item_supporting_text,\ntodo.dueDate\n)\n)\n},\nmodifier = Modifier.clickable(onClick = {\nonListItemClick(\ntodo.id\n)\n})\n)\nif (state.todoList.last() != todo) {\nHorizontalDivider(\nthickness = 2.dp,\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\n}\n}\n}\n}\n
    Material Icon

    A Circle ikon csak a kieg\u00e9sz\u00edt\u0151 Material Icon k\u00f6nyvt\u00e1rban tal\u00e1lhat\u00f3 meg, melyet az al\u00e1bbi f\u00fcgg\u0151s\u00e9ggel tudunk hozz\u00e1adni a projekthez:

    [libraries]\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\" }\n

    items

    Ha hib\u00e1t dobna az items-re, \u00e9s nem tal\u00e1lja az importot, adjuk hozz\u00e1 az al\u00e1bbi importot a f\u00e1jl tetej\u00e9hez:

    import androidx.compose.foundation.lazy.items\n

    L\u00e1that\u00f3, hogy a lista megjelen\u00edt\u00e9s\u00e9re a LazyColumn Composable-t haszn\u00e1ljuk, mely k\u00e9pes nagy elemsz\u00e1m\u00fa list\u00e1t hat\u00e9konyan megjelen\u00edteni. Ahhoz, hogy j\u00f3l m\u0171k\u00f6dj\u00f6n a lista m\u00f3dos\u00edt\u00e1sa eset\u00e9n is (pl. hozz\u00e1ad\u00e1s, t\u00f6rl\u00e9s, \u00e1trendez\u00e9s), mindenk\u00e9pp \u00e9rdemes a key param\u00e9tert \u00fagy defini\u00e1lni, hogy az adott listaelemet egy\u00e9rtelm\u0171en beazonos\u00edtsa.

    Lista elemnek most egy egyszer\u0171 ListItem-et haszn\u00e1lunk, ami eset\u00fcnkben t\u00f6k\u00e9letesen el\u00e9g. A leadingContent hely\u00e9re a priority ikon, a headlineContent hely\u00e9re a todo c\u00edme, a supportingContent hely\u00e9re a hat\u00e1rid\u0151 ker\u00fcl. Term\u00e9szetesen komplexebb listaelemek eset\u00e9n k\u00fcl\u00f6n Composable k\u00e9sz\u00edt\u00e9se aj\u00e1nlott.

    Az oldal elk\u00e9sz\u00fclt, m\u00e1r csak a navig\u00e1ci\u00f3t kell friss\u00edteni az oldalhoz. Vegy\u00fck fel az \u00fatvonalat a Screen oszt\u00e1lyba:

    package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\n}\n

    Illetve a NavGraph Composable-t:

    package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\n//TODO: Navigate to detailed screen\n},\nonFabClick = {\n//TODO: Navigate to create screen\n}\n)\n}\n}\n}\n

    Futtassuk az alkalmaz\u00e1s!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l!

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/todo_compose_basics/#adatreteg-kialakitasa","title":"Adatr\u00e9teg kialak\u00edt\u00e1sa","text":"

    Ezen a laboron egy egyszer\u0171s\u00edtett megold\u00e1st mutatunk be a feladatok t\u00e1rol\u00e1s\u00e1ra, mely csak a mem\u00f3ri\u00e1ban menti el az \u00e9rt\u00e9keket. Hozzunk l\u00e9tre egy data package-et a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl, majd hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt:

    TodoRepository.kt:

    package hu.bme.aut.android.todo.data\n\nimport hu.bme.aut.android.todo.domain.model.Todo\n\ninterface ITodoRepository {\nsuspend fun insertTodo(todo: Todo)\nsuspend fun deleteTodo(todo: Todo)\nsuspend fun getTodoById(id: Int): Todo\nsuspend fun getAllTodos(): List<Todo>\nsuspend fun updateTodo(updatedTodo: Todo)\n}\n

    MemoryTodoRepository.kt :

    package hu.bme.aut.android.todo.data\n\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.coroutines.delay\nimport kotlinx.datetime.toKotlinLocalDateTime\nimport java.time.LocalDateTime\n\nobject MemoryTodoRepository : ITodoRepository {\nprivate val todos = mutableListOf(\nTodo(\nid = 1,\ntitle = \"Teszt feladat 1\",\npriority = Priority.LOW,\ndescription = \"Feladat le\u00edr\u00e1s 1\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 2,\ntitle = \"Teszt feladat 2\",\npriority = Priority.MEDIUM,\ndescription = \"Feladat le\u00edr\u00e1s 2\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 3,\ntitle = \"Teszt feladat 3\",\npriority = Priority.HIGH,\ndescription = \"Feladat le\u00edr\u00e1s 3\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 4,\ntitle = \"Teszt feladat 4 hossz\u0171 sz\u00f6veg, hogy t\u00f6bb sorba kelljen \u00edrni\",\npriority = Priority.HIGH,\ndescription = \"Feladat le\u00edr\u00e1s 4\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 5,\ntitle = \"Teszt feladat 5\",\npriority = Priority.LOW,\ndescription = \"Feladat le\u00edr\u00e1s 5\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 6,\ntitle = \"Teszt feladat 6\",\npriority = Priority.MEDIUM,\ndescription = \"Feladat le\u00edr\u00e1s 6\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n)\n)\n\noverride suspend fun insertTodo(todo: Todo) {\ndelay(1000)\ntodos.add(todo)\n}\n\noverride suspend fun deleteTodo(todo: Todo) {\ndelay(1000)\ntodos.remove(todo)\n}\n\noverride suspend fun getTodoById(id: Int): Todo {\ndelay(1000)\nfor (todo in todos) {\nif (todo.id == id) return todo\n}\nreturn todos.first()\n}\n\noverride suspend fun getAllTodos(): List<Todo> {\ndelay(1000)\nreturn todos.toList()\n}\n\noverride suspend fun updateTodo(updatedTodo: Todo) {\ndelay(1000)\nfor (todo in todos) {\nif (todo.id == updatedTodo.id)\ntodos[todos.indexOf(todo)] = updatedTodo\n}\n}\n}\n

    Az ITodoRepository egy \u00e1ltal\u00e1nos interf\u00e9szt \u00edr le, mellyel el\u00e9rhet\u0151v\u00e9 v\u00e1lnak a feladatok az alkalmaz\u00e1s sz\u00e1m\u00e1ra, m\u00edg a MemoryTodoRepository egy mem\u00f3ria alap\u00fa megval\u00f3s\u00edt\u00e1s\u00e1t mutatja be. B\u00e1r itt most nem lenne sz\u00fcks\u00e9g a suspend kulcssz\u00f3 haszn\u00e1lat\u00e1ra, ezzel tudjuk biztos\u00edtani, hogy a k\u00e9s\u0151bbiekben egy adatb\u00e1zis vagy h\u00e1l\u00f3zati TodoRepository elk\u00e9sz\u00edt\u00e9se ut\u00e1n k\u00f6nnyed\u00e9n tudjuk migr\u00e1lni a projektet, ezt a k\u00e9sleltet\u00e9st imit\u00e1ljuk a delay() f\u00fcggv\u00e9ny h\u00edv\u00e1s\u00e1val is. Az object kulcssz\u00f3val a Singleton mint\u00e1t tudjuk egyszer\u0171en megval\u00f3s\u00edtani.

    Friss\u00edts\u00fck a TodoListViewModel oszt\u00e1lyt, hogy ezt a mem\u00f3ria alap\u00fa megval\u00f3s\u00edt\u00e1st haszn\u00e1lja:

    class TodoListViewModel(private val repository: ITodoRepository) : ViewModel() {\nprivate val _state = MutableStateFlow<TodoListState>(TodoListState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\ndelay(2000)\nval list = repository.getAllTodos()\n_state.value = TodoListState.Result(\ntodoList = list.map { it.asTodoUi() }\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoListViewModel(\nMemoryTodoRepository\n)\n}\n}\n}\n}\n

    Futassuk az alkalmaz\u00e1st, \u00e9s ellen\u0151rizz\u00fck, hogy tov\u00e1bbra is megjelennek a feladatok a list\u00e1ban!

    "},{"location":"laborok/todo_compose_basics/#reszletes-feladat-felulet","title":"R\u00e9szletes feladat fel\u00fclet","text":"

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck fel a r\u00e9szletez\u0151 fel\u00fcletet, melyen a feladat le\u00edr\u00e1s\u00e1t tudjuk megn\u00e9zni. K\u00e9sz\u00edts\u00fck el az oldalt a lista oldal mint\u00e1j\u00e1ra.

    Kezdj\u00fck a navig\u00e1ci\u00f3 implement\u00e1l\u00e1s\u00e1val. Ebben az esetben az \u00fatvonal fogja tartalmazni az azonos\u00edt\u00f3j\u00e1t a feladatnak az al\u00e1bbi m\u00f3don: Screen.kt:

    package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\nobject TodoDetail : Screen(\"todo_detail/{id}\"){\nfun passId(id: Int) = \"todo_detail/$id\"\n}\n}\n
    A feladat azonos\u00edt\u00f3j\u00e1t egy / jellel elv\u00e1lasztva tessz\u00fck be az \u00fatvonalba.

    NavGraph.kt:

    package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navArgument\nimport hu.bme.aut.android.todo.feature.todo_detail.TodoDetailScreen\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\nnavController.navigate(Screen.TodoDetail.passId(it))\n},\nonFabClick = {\n//TODO: Navigate to create screen\n}\n)\n}\ncomposable(\nroute = Screen.TodoDetail.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.IntType\n}\n)\n) {\nTodoDetailScreen(onNavigateBack = { navController.popBackStack() })\n}\n}\n}\n

    Az azonos\u00edt\u00f3t a composable-ben is fel kell t\u00fcntetn\u00fcnk az arguments param\u00e9terben. Itt tudjuk megadni, hogy milyen t\u00edpus\u00fa lesz az \u00e9rt\u00e9k, amit \u00e1tadunk a param\u00e9terben, \u00edgy a keretrendszer automatikusan \u00e1t tudja alak\u00edtani a megfelel\u0151 t\u00edpuss\u00e1. Hozzunk l\u00e9tre egy \u00faj package-et a feature package-en bel\u00fcl todo_detail n\u00e9ven.

    TodoDetailViewModel.kt:

    package hu.bme.aut.android.todo.feature.todo_detail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.createSavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.data.ITodoRepository\nimport hu.bme.aut.android.todo.data.MemoryTodoRepository\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport hu.bme.aut.android.todo.ui.model.asTodoUi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nsealed class TodoDetailState {\nobject Loading : TodoDetailState()\ndata class Error(val error: Throwable) : TodoDetailState()\ndata class Result(val todo: TodoUi) : TodoDetailState()\n}\n\nclass TodoDetailViewModel(private val repository: ITodoRepository, private val savedStateHandle: SavedStateHandle) : ViewModel() {\n\nprivate val _state = MutableStateFlow<TodoDetailState>(TodoDetailState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nval id = checkNotNull<Int>(savedStateHandle[\"id\"])\nviewModelScope.launch {\ntry {\n_state.value = TodoDetailState.Loading\ndelay(2000)\nval todo = repository.getTodoById(id)\n_state.value = TodoDetailState.Result(\ntodo.asTodoUi()\n)\n} catch (e: Exception) {\n_state.value = TodoDetailState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval savedStateHandle = createSavedStateHandle()\nTodoDetailViewModel(\nMemoryTodoRepository,\nsavedStateHandle\n)\n}\n}\n}\n}\n
    Az \u00fatvonalban \u00e1tadott param\u00e9ter kiolvas\u00e1s\u00e1hoz a SavedStateHandle oszt\u00e1lyt haszn\u00e1ljuk. Ennek az oszt\u00e1lynak a szerepe az olyan adatok ment\u00e9se, melyet az alkalmaz\u00e1s h\u00e1tt\u00e9rben t\u00f6rt\u00e9n\u0151 megsemmis\u00edt\u00e9se \u00e9s \u00fajraind\u00edt\u00e1sa ut\u00e1n is ki akarunk olvasni. Ezt a funkci\u00f3j\u00e1t most nem haszn\u00e1ljuk ki, viszont a keretrendszer ebbe t\u00f6lti be az \u00fatvonal param\u00e9tereket is, melyekhez \u00edgy k\u00f6nnyen hozz\u00e1f\u00e9r\u00fcnk, amikor az \u00faj feladatot kell bet\u00f6lteni.

    TodoDetailScreen.kt:

    package hu.bme.aut.android.todo.feature.todo_detail\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.ui.model.toUiText\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TodoDetailScreen(\nonNavigateBack: () -> Unit,\nviewModel: TodoDetailViewModel = viewModel(factory = TodoDetailViewModel.Factory)\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\n\nval context = LocalContext.current\n\nScaffold(\ntopBar = {\nif (state is TodoDetailState.Result) {\nTopAppBar(\ntitle = { Text(state.todo.title) },\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nIcon(imageVector = Icons.Default.ArrowBack, contentDescription = null)\n}\n},\ncolors = TopAppBarDefaults.topAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary\n),\n)\n}\n},\n) {\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(it),\ncontentAlignment = Alignment.Center\n) {\nwhen (state) {\nis TodoDetailState.Loading -> CircularProgressIndicator(\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\nis TodoDetailState.Error -> Text(\ntext = state.error.toUiText().asString(context)\n)\nis TodoDetailState.Result -> {\nval todo = state.todo\nColumn(\nmodifier = Modifier.fillMaxSize().padding(all = 8.dp)\n) {\nText(\ntodo.dueDate,\nstyle = MaterialTheme.typography.titleMedium\n)\nRow(\nmodifier = Modifier\n.height(TextFieldDefaults.MinHeight)\n.fillMaxWidth()\n.clip(shape = RoundedCornerShape(size = 5.dp)),\nverticalAlignment = Alignment.CenterVertically\n) {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = todo.priority.color,\nmodifier = Modifier\n.size(24.dp)\n)\nSpacer(modifier = Modifier.width(8.dp))\nText(\nmodifier = Modifier\n.weight(weight = 8f),\ntext = stringResource(id = todo.priority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\n}\nText(\ntodo.description\n)\n}\n}\n}\n}\n}\n}\n

    V\u00e9g\u00fcl a lista oldalhoz hasonl\u00f3an kiolvassuk a ViewModel-ben t\u00e1rolt \u00e1llapotot \u00e9s megjelen\u00edtj\u00fck a megfelel\u0151 fel\u00fcleti elemeket.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a r\u00e9szletes n\u00e9zet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l!

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/todo_compose_basics/#feladat-letrehozasa-felulet-komponensek","title":"Feladat l\u00e9trehoz\u00e1sa fel\u00fclet komponensek","text":"

    Az utols\u00f3 fel\u00fclet, melyet elk\u00e9sz\u00edt\u00fcnk az alkalmaz\u00e1shoz, a feladat l\u00e9trehoz\u00e1sa fel\u00fclet lesz. Ehhez t\u00f6bb \u00f6n\u00e1ll\u00f3 fel\u00fcleti elemre lesz sz\u00fcks\u00e9g\u00fcnk, melyeket az oldal el\u0151tt l\u00e9trehozunk. Hozzuk l\u00e9tre a ui package-en bel\u00fcl a common package-et, mely az olyan Composable elemeket tartalmazza, melyeket ak\u00e1r t\u00f6bb oldalon is fel tudn\u00e1nk haszn\u00e1lni. Ezen bel\u00fcl hozzuk l\u00e9tre az al\u00e1bbi elemeket:

    DateSelector.kt:

    package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.EditCalendar\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport kotlinx.datetime.LocalDate\nimport java.time.LocalDateTime\n\n@Composable\nfun DateSelector(\npickedDate: LocalDate,\nonClick: () -> Unit,\nmodifier: Modifier = Modifier,\nenabled: Boolean = true\n) {\nval shape = RoundedCornerShape(5.dp)\n\nSurface(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.background(MaterialTheme.colorScheme.background)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape)\n.clickable(enabled = enabled, onClick = onClick),\nshape = shape\n) {\nRow(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape),\nverticalAlignment = Alignment.CenterVertically\n) {\nText(\nmodifier = Modifier\n.weight(weight = 8f)\n.padding(start = 20.dp),\ntext = pickedDate.toString(),\nstyle = MaterialTheme.typography.labelMedium\n)\nIconButton(\nmodifier = Modifier\n.weight(weight = 1.5f),\nonClick = onClick\n) {\nIcon(\nimageVector = Icons.Default.EditCalendar,\ncontentDescription = null,\ntint = MaterialTheme.colorScheme.primary\n)\n}\n}\n}\n}\n\n@Preview\n@Composable\nfun DateSelectorPreview() {\nval d = LocalDateTime.now()\nDateSelector(\npickedDate = LocalDate(d.year, d.month, d.dayOfMonth),\nonClick = { }\n)\n}\n

    NormalTextField.kt:

    package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActionScope\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun NormalTextField(\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\nleadingIcon: @Composable (() -> Unit)? = null,\ntrailingIcon: @Composable (() -> Unit)? = null,\nsingleLine: Boolean = false,\nenabled: Boolean = true,\nonDone: (KeyboardActionScope.() -> Unit)?\n) {\nval shape = RoundedCornerShape(5.dp)\n\nTextField(\nvalue = value,\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = trailingIcon,\nmodifier = modifier.clip(shape),\nsingleLine = singleLine,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Text,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n),\nshape = shape\n)\n}\n\n@Preview\n@Composable\nfun NormalTextFieldPreview() {\nNormalTextField(\nvalue = \"name\",\nlabel = \"P\u00e9ter\",\nonValueChange = {}) {\n\n}\n}\n

    PriorityDropdown.kt:

    package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\n\n@Composable\nfun PriorityDropDown(\npriorities: List<PriorityUi>,\nselectedPriority: PriorityUi,\nonPrioritySelected: (PriorityUi) -> Unit,\nmodifier: Modifier = Modifier,\nenabled: Boolean = true\n) {\nvar expanded by remember { mutableStateOf(false) }\nval angle: Float by animateFloatAsState(\ntargetValue = if (expanded) 180f else 0f,\nlabel = \"Priority arrow angle animation\"\n)\n\nval shape = RoundedCornerShape(5.dp)\n\nSurface(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape)\n.background(MaterialTheme.colorScheme.background)\n.clickable(enabled = enabled) { expanded = true },\nshape = shape\n) {\nRow(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape),\nverticalAlignment = Alignment.CenterVertically\n) {\nSpacer(modifier = Modifier.width(20.dp))\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = selectedPriority.color,\nmodifier = Modifier\n.size(20.dp)\n)\nSpacer(modifier = Modifier.width(5.dp))\nText(\nmodifier = Modifier\n.weight(weight = 8f),\ntext = stringResource(id = selectedPriority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\nIconButton(\nmodifier = Modifier\n.weight(weight = 1.5f)\n.rotate(degrees = angle),\nonClick = { expanded = true }\n) {\nIcon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n)\n}\nDropdownMenu(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nexpanded = expanded,\nonDismissRequest = { expanded = false }\n) {\npriorities.forEach { priority ->\nDropdownMenuItem(\ntext = {\nText(\ntext = stringResource(id = priority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\n},\nonClick = {\nexpanded = false\nonPrioritySelected(priority)\n},\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = priority.color,\nmodifier = Modifier.size(22.dp)\n)\n}\n)\n}\n}\n}\n}\n}\n\n@Composable\n@Preview\nfun PriorityDropdown_Preview() {\nval priorities = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High)\nvar selectedPriority by remember { mutableStateOf(priorities[0]) }\n\nColumn(\nmodifier = Modifier.fillMaxSize(),\nverticalArrangement = Arrangement.Center,\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nPriorityDropDown(\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = {\nselectedPriority = it\n}\n)\n}\n}\n

    Ezt a h\u00e1rom elemet fogjuk \u00f6ssze a TodoEditor komponenssel, melyet ugyanitt hozzunk l\u00e9tre:

    package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport kotlinx.datetime.LocalDate\nimport java.time.LocalDateTime\n\n@Composable\nfun TodoEditor(\ntitleValue: String,\ntitleOnValueChange: (String) -> Unit,\ndescriptionValue: String,\ndescriptionOnValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\npriorities: List<PriorityUi> = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High),\nselectedPriority: PriorityUi,\nonPrioritySelected: (PriorityUi) -> Unit,\npickedDate: LocalDate,\nonDateSelectorClicked: () -> Unit,\nenabled: Boolean = true,\n) {\nval fraction = 0.95f\n\nval keyboardController = LocalSoftwareKeyboardController.current\n\nColumn(\nmodifier = modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.secondaryContainer),\nhorizontalAlignment = Alignment.CenterHorizontally,\nverticalArrangement = Arrangement.SpaceAround,\n) {\nif (enabled) {\nNormalTextField(\nvalue = titleValue,\nlabel = stringResource(id = R.string.textfield_label_title),\nonValueChange = titleOnValueChange,\nsingleLine = true,\nonDone = { keyboardController?.hide() },\nmodifier = Modifier\n.fillMaxWidth(fraction)\n.padding(top = 5.dp)\n)\n}\nSpacer(modifier = Modifier.height(5.dp))\nPriorityDropDown(\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = onPrioritySelected,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction),\nenabled = enabled\n)\nSpacer(modifier = Modifier.height(5.dp))\nDateSelector(\npickedDate = pickedDate,\nonClick = onDateSelectorClicked,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction),\nenabled = enabled\n)\nSpacer(modifier = Modifier.height(5.dp))\nNormalTextField(\nvalue = descriptionValue,\nlabel = stringResource(id = R.string.textfield_label_description),\nonValueChange = descriptionOnValueChange,\nsingleLine = false,\nonDone = { keyboardController?.hide() },\nmodifier = Modifier\n.weight(10f)\n.fillMaxWidth(fraction)\n.padding(bottom = 5.dp),\nenabled = enabled\n)\n}\n}\n\n@Composable\n@Preview(showBackground = true)\nfun TodoEditor_Preview() {\nvar title by remember { mutableStateOf(\"\") }\nvar description by remember { mutableStateOf(\"\") }\n\nval priorities = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High)\nvar selectedPriority by remember { mutableStateOf(priorities[0]) }\n\nval c = LocalDateTime.now()\nval pickedDate by remember { mutableStateOf(LocalDate(c.year, c.month, c.dayOfMonth)) }\n\nBox(Modifier.fillMaxSize()) {\nTodoEditor(\ntitleValue = title,\ntitleOnValueChange = { title = it },\ndescriptionValue = description,\ndescriptionOnValueChange = { description = it },\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = { selectedPriority = it },\npickedDate = pickedDate,\nonDateSelectorClicked = {\n\n},\n)\n}\n}\n

    Ezek mellett a l\u00e9trehoz\u00e1s oldalon sz\u00fcks\u00e9g\u00fcnk lesz egy TopAppBar elemre is. Egy ilyet m\u00e1r l\u00e9trehoztunk a r\u00e9szletes n\u00e9zeten, ezt kiemelve \u00e9s \u00e1ltal\u00e1nos\u00edtva hozzuk l\u00e9tre az \u00faj TodoAppBar elemet ugyanebbe a package-be:

    package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TodoAppBar(\nmodifier: Modifier = Modifier,\ntitle: String,\nactions: @Composable() RowScope.() -> Unit = {},\nonNavigateBack: () -> Unit\n) {\nTopAppBar(\nmodifier = modifier,\ntitle = { Text(text = title) },\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nIcon(imageVector = Icons.Default.ArrowBack, contentDescription = null)\n\n}\n},\nactions = actions,\ncolors = TopAppBarDefaults.topAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n}\n\n@Composable\n@Preview\nfun TodoAppBar_Preview() {\nTodoAppBar(\ntitle = \"Title\",\nactions = {},\nonNavigateBack = {}\n)\n}\n

    Ezzel a TodoDetailScreen TopAppBar r\u00e9sze az al\u00e1bbi egyszer\u0171bb deklar\u00e1ci\u00f3ra cser\u00e9lhet\u0151:

    TodoAppBar(\ntitle = state.todo.title,\nonNavigateBack = onNavigateBack,\n)\n
    "},{"location":"laborok/todo_compose_basics/#feladat-keszitese-oldal","title":"Feladat k\u00e9sz\u00edt\u00e9se oldal","text":"

    Hozzuk l\u00e9tre a feature package-en bel\u00fcl a todo_create package-et. Ezen bel\u00fcl k\u00e9sz\u00edts\u00fck el az oldal logik\u00e1j\u00e1t megval\u00f3s\u00edt\u00f3 TodoCreateViewModel oszt\u00e1lyt:

    package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.data.ITodoRepository\nimport hu.bme.aut.android.todo.data.MemoryTodoRepository\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport hu.bme.aut.android.todo.ui.model.UiText\nimport hu.bme.aut.android.todo.ui.model.asTodo\nimport hu.bme.aut.android.todo.ui.model.toUiText\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.LocalDate\n\ndata class TodoCreateState(\nval todo: TodoUi = TodoUi()\n)\n\nsealed class TodoCreateUiEvent {\nobject Success : TodoCreateUiEvent()\ndata class Failure(val error: UiText) : TodoCreateUiEvent()\n}\n\nsealed class TodoCreateEvent {\ndata class ChangeTitle(val text: String) : TodoCreateEvent()\ndata class ChangeDescription(val text: String) : TodoCreateEvent()\ndata class SelectPriority(val priority: PriorityUi) : TodoCreateEvent()\ndata class SelectDate(val date: LocalDate) : TodoCreateEvent()\nobject SaveTodo : TodoCreateEvent()\n}\n\nclass TodoCreateViewModel(\nprivate val todoRepository: ITodoRepository\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(TodoCreateState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<TodoCreateUiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: TodoCreateEvent) {\nwhen (event) {\nis TodoCreateEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo.copy(title = newValue)\n)\n}\n}\n\nis TodoCreateEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo.copy(description = newValue)\n)\n}\n}\n\nis TodoCreateEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo.copy(priority = newValue)\n)\n}\n}\n\nis TodoCreateEvent.SelectDate -> {\nval newValue = event.date\n_state.update {\nit.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n)\n}\n}\n\nTodoCreateEvent.SaveTodo -> {\n_state.update {\nit.copy(\ntodo = it.todo.copy(id = (Math.random()*Int.MAX_VALUE).toInt())\n)\n}\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoRepository.insertTodo(state.value.todo.asTodo())\n_uiEvent.send(TodoCreateUiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(TodoCreateUiEvent.Failure(e.toUiText()))\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoCreateViewModel(\ntodoRepository = MemoryTodoRepository\n)\n}\n}\n}\n}\n

    Ebben a ViewModel oszt\u00e1lyban k\u00e9t \u00faj architekt\u00fara mint\u00e1t is megfigyelhet\u00fcnk:

    • A felhaszn\u00e1l\u00f3i fel\u00fcletr\u0151l \u00e9rkez\u0151 esem\u00e9nyeknek egy \u00faj oszt\u00e1lyt defini\u00e1ltunk TodoCreateEvent n\u00e9ven. Ezeket az esem\u00e9nyeket egy \u00e1ltal\u00e1nos, onEvent() met\u00f3dusban kezelj\u00fck le, \u00edgy k\u00f6nnyebben k\u00f6vethet\u0151, milyen interakci\u00f3kra sz\u00e1m\u00edthatunk a ViewModel oldal\u00e1r\u00f3l. Sz\u00fcks\u00e9g eset\u00e9n az egyes esem\u00e9nyek kezel\u00e9s\u00e9re l\u00e9trehozhat\u00f3 egyedi priv\u00e1t met\u00f3dus, de a UI csak az onEvent()-et h\u00edvja meg.
    • Vannak olyan esem\u00e9nyek, melyekre a UI r\u00e9tegnek reag\u00e1lnia kell a megjelen\u00edt\u00e9s helyett. P\u00e9ld\u00e1ul egy feladat sikeres l\u00e9trehoz\u00e1sa ut\u00e1n azt szeretn\u00e9nk, hogy az alkalmaz\u00e1s navig\u00e1ljon vissza az el\u0151z\u0151 oldalra. Ilyenkor azt akarjuk, hogy ez az esem\u00e9ny csak \u00e9s kiz\u00e1r\u00f3lag egyszer ker\u00fclj\u00f6n feldolgoz\u00e1sra. Ezeket egy Channel seg\u00edts\u00e9g\u00e9vel osztjuk meg.

    Az ehhez tartoz\u00f3 oldalhoz hozzuk l\u00e9tre a TodoCreateScreen.kt f\u00e1jlt ebbe a package-be:

    package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.ui.common.TodoAppBar\nimport hu.bme.aut.android.todo.ui.common.TodoEditor\nimport hu.bme.aut.android.todo.ui.model.asPriorityUi\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.toLocalDate\n\n@Composable\nfun TodoCreateScreen(\nonNavigateBack: () -> Unit,\nviewModel: TodoCreateViewModel = viewModel(factory = TodoCreateViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval hostState = remember { SnackbarHostState() }\n\nval scope = rememberCoroutineScope()\n\nval context = LocalContext.current\n\nLaunchedEffect(key1 = true) {\nviewModel.uiEvent.collect { uiEvent ->\nwhen(uiEvent) {\nis TodoCreateUiEvent.Success -> { onNavigateBack() }\nis TodoCreateUiEvent.Failure -> {\nscope.launch {\nhostState.showSnackbar(uiEvent.error.asString(context))\n}\n}\n}\n}\n}\n\nScaffold(\nsnackbarHost = { SnackbarHost(hostState) },\ntopBar = {\nTodoAppBar(\ntitle = stringResource(id = R.string.app_bar_title_create_todo),\nonNavigateBack = onNavigateBack,\nactions = { }\n)\n},\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = { viewModel.onEvent(TodoCreateEvent.SaveTodo) },\ncontainerColor = MaterialTheme.colorScheme.primary,\ncontentColor = MaterialTheme.colorScheme.onPrimary\n) {\nIcon(imageVector = Icons.Default.Save, contentDescription = null)\n}\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nTodoEditor(\ntitleValue = state.todo.title,\ntitleOnValueChange = { viewModel.onEvent(TodoCreateEvent.ChangeTitle(it)) },\ndescriptionValue = state.todo.description,\ndescriptionOnValueChange = { viewModel.onEvent(TodoCreateEvent.ChangeDescription(it)) },\npriorities = Priority.entries.map { it.asPriorityUi() },\nselectedPriority = state.todo.priority,\nonPrioritySelected = { viewModel.onEvent(TodoCreateEvent.SelectPriority(it)) },\npickedDate = state.todo.dueDate.toLocalDate(),\nonDateSelectorClicked = {\n//TODO: Open date picker dialog\n},\nmodifier = Modifier\n)\n}\n}\n}\n

    Utols\u00f3 l\u00e9p\u00e9sk\u00e9nt k\u00f6ss\u00fck be a navig\u00e1ci\u00f3t is ehhez az oldalhoz. Friss\u00edts\u00fck a Screen.kt f\u00e1jlt az al\u00e1bbi k\u00f3ddal:

    package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\nobject TodoDetail : Screen(\"todo_detail/{id}\"){\nfun passId(id: Int) = \"todo_detail/$id\"\n}\nobject TodoCreate : Screen(\"todo_create\")\n}\n

    Val\u00f3s\u00edtsuk meg a navig\u00e1ci\u00f3t is a NavGraph.kt f\u00e1jlban:

    package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navArgument\nimport hu.bme.aut.android.todo.feature.todo_create.TodoCreateScreen\nimport hu.bme.aut.android.todo.feature.todo_detail.TodoDetailScreen\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\nnavController.navigate(Screen.TodoDetail.passId(it))\n},\nonFabClick = {\nnavController.navigate(Screen.TodoCreate.route)\n}\n)\n}\ncomposable(\nroute = Screen.TodoDetail.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.IntType\n}\n)\n) {\nTodoDetailScreen(onNavigateBack = { navController.popBackStack() })\n}\ncomposable(Screen.TodoCreate.route) {\nTodoCreateScreen(\nonNavigateBack = {\nnavController.popBackStack()\n}\n)\n}\n}\n}\n

    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Mit tapasztalunk egy feladat l\u00e9trehoz\u00e1s\u00e1n\u00e1l?

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a feladat l\u00e9trehoz\u00e1sa n\u00e9zet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l!

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/todo_compose_basics/#kiegeszito-feladat-1-feladat-lista-frissitese","title":"Kieg\u00e9sz\u00edt\u0151 feladat 1 - Feladat lista friss\u00edt\u00e9se","text":"

    \u00c9szrevehetj\u00fck, hogy ha l\u00e9trehozunk egy \u00faj feladatot, az nem jelenik meg a list\u00e1ban. Ez az\u00e9rt t\u00f6rt\u00e9nik, mert a lista oldal tartalm\u00e1t csak az oldal l\u00e9trej\u00f6ttekor friss\u00edtj\u00fck be, a feladat l\u00e9trehoz\u00e1sa ut\u00e1n t\u00f6rt\u00e9n\u0151 visszal\u00e9p\u00e9s viszont a megl\u00e9v\u0151 oldalra l\u00e9p vissza, nem hoz l\u00e9tre egy \u00fajat. Ezt t\u00f6bb m\u00f3don is meg tudjuk oldani.

    • A visszal\u00e9p\u00e9s sor\u00e1n megh\u00edvunk egy met\u00f3dust, mely befriss\u00edti a lista oldal tartalm\u00e1t. Ez t\u00f6rt\u00e9nhet egy Channel-en kereszt\u00fcl az oldal fel\u00e9, vagy kiemelhetj\u00fck a lista n\u00e9zet ViewModel oszt\u00e1ly\u00e1t a navig\u00e1ci\u00f3s komponensbe, amin \u00edgy k\u00f6zvetlen\u00fcl meg tudjuk h\u00edvni a lista friss\u00edt\u00e9st.
    • A feladatokat t\u00e1rol\u00f3 Repository egy reakt\u00edv Flow-al t\u00e9rne vissza az egyszeri lista helyett, \u00edgy a lista oldal ezen kereszt\u00fcl tud \u00e9rtes\u00fclni a v\u00e1ltoz\u00e1sokr\u00f3l (pl. Firebase hasonl\u00f3 elven is tud m\u0171k\u00f6dni)
    • Az oldal akt\u00edvv\u00e1 v\u00e1l\u00e1sakor automatikusan friss\u00edtj\u00fck a lista tartalm\u00e1t is.

    Mi most a harmadik megold\u00e1st fogjuk alkalmazni. Ehhez \u00e9rtes\u00fcln\u00fcnk kell arr\u00f3l, amikor az adott oldal akt\u00edvv\u00e1 v\u00e1l\u00edk (hasonl\u00f3an az Activity onResume() \u00e9letciklus\u00e1hoz). Ezt az itt l\u00e1that\u00f3 le\u00edr\u00e1s alapj\u00e1n tudjuk megval\u00f3s\u00edtani.

    TodoListScreen.kt:

    @Composable\nfun TodoListScreen(\nonListItemClick: (Int) -> Unit,\nonFabClick: () -> Unit,\nviewModel: TodoListViewModel = viewModel(factory = TodoListViewModel.Factory),\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\nval context = LocalContext.current\n\nval lifecycleOwner = LocalLifecycleOwner.current\nDisposableEffect(lifecycleOwner) {\nval observer = LifecycleEventObserver { _, event ->\nif (event == Lifecycle.Event.ON_RESUME) {\nviewModel.loadTodos()\n}\n}\nlifecycleOwner.lifecycle.addObserver(observer)\nonDispose {\nlifecycleOwner.lifecycle.removeObserver(observer)\n}\n}\n\nScaffold(\n...\n}\n

    Tegy\u00fck a ViewModel loadTodos() met\u00f3dus\u00e1t publikuss\u00e1, \u00e9s t\u00f6r\u00f6lj\u00fck az inicializ\u00e1l\u00f3 k\u00f3dblokkban t\u00f6rt\u00e9n\u0151 megh\u00edv\u00e1s\u00e1t. Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Ha zavar a t\u00f6lt\u00e9s miatti k\u00e9perny\u0151 bevillan\u00e1sa, akkor ak\u00e1r ki is vehetj\u00fck a loadTodos() met\u00f3dusb\u00f3l a Loading \u00e1llapot be\u00e1ll\u00edt\u00e1s\u00e1t.

    "},{"location":"laborok/todo_compose_basics/#kiegeszito-feladat-2-animacio-optimalizalas","title":"Kieg\u00e9sz\u00edt\u0151 feladat 2 - Anim\u00e1ci\u00f3 optimaliz\u00e1l\u00e1s","text":"

    Vizsg\u00e1ljuk meg, hogyan t\u00f6rt\u00e9nik a fontoss\u00e1got kiv\u00e1laszt\u00f3 fel\u00fcleti elemen a lenyit\u00e1st jelz\u0151 elem anim\u00e1ci\u00f3ja:

    PriorityDropdown.kt:

    @Composable\nfun PriorityDropDown(\n...\n) {\nvar expanded by remember { mutableStateOf(false) }\nval angle: Float by animateFloatAsState(\ntargetValue = if (expanded) 180f else 0f,\nlabel = \"Priority arrow angle animation\"\n)\n\nSurface(\n...\n) {\nRow(\n...\n) {\n...\nIconButton(\nmodifier = Modifier\n.rotate(degrees = angle),\nonClick = { expanded = true }\n) {\nIcon(\n...\n)\n}\n...\n}\n}\n}\n
    Az animateFloatAsState() egy nagyon hasznos State objektumot tesz el\u00e9rhet\u0151v\u00e9 az objektumonkun bel\u00fcl. A targetValue \u00e9rt\u00e9k\u00e9t \u00e1ll\u00edtva a kiolvasott \u00e9rt\u00e9k nem egyb\u0151l, hanem egy \u00e1tmenettel fogja megk\u00f6zel\u00edteni a c\u00e9l\u00e9rt\u00e9ket, mely \u00edgy k\u00f6nnyen felhaszn\u00e1lhat\u00f3 anim\u00e1ci\u00f3k k\u00e9sz\u00edt\u00e9s\u00e9re. R\u00e1ad\u00e1sul mivel az \u00f6sszes Composable egy Kotlin f\u00fcggv\u00e9nynek felel meg, tetsz\u0151leges el\u00e1gaz\u00e1st vagy fel\u00fcletet l\u00e9tre tudok hozni egy ilyen anim\u00e1ci\u00f3 seg\u00edts\u00e9g\u00e9vel. Arra viszont \u00e9rdemes \u00fcgyelni, hogy ezek az anim\u00e1ci\u00f3k min\u00e9l hat\u00e9konyabbak legyenek, mindig csak a sz\u00fcks\u00e9ges elemeket rajzolj\u00e1k \u00fajra.

    Egy State objektum \u00e9rt\u00e9knek a v\u00e1ltoz\u00e1sa sor\u00e1n minden kontextust, mely kiolvasta az \u00e9rt\u00e9k\u00e9t, \u00fajra fogja futtatni. Ebben a helyzetben az IconButton l\u00e9trehoz\u00e1sakor olvassuk ki az aktu\u00e1lis \u00e9rt\u00e9k\u00e9t, mely a Row elem f\u00fcggv\u00e9ny callbackj\u00e9ben t\u00f6rt\u00e9nik meg, teh\u00e1t a sz\u00f6g m\u00f3dos\u00edt\u00e1sa hat\u00e1s\u00e1ra ezt a k\u00f3dblokkot mindenk\u00e9pp \u00fajra kell futtatnia a Composenak.

    Els\u0151 optimaliz\u00e1ci\u00f3s l\u00e9p\u00e9sk\u00e9nt beljebb vihetj\u00fck a forgat\u00e1st elv\u00e9gz\u00f3 k\u00f3dr\u00e9szletet az IconButton belsej\u00e9ben tal\u00e1lhat\u00f3 Icon elemre:

    IconButton(\nmodifier = Modifier\n.weight(weight = 1.5f),\nonClick = { expanded = true }\n) {\nIcon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n.rotate(degrees = angle),\n)\n}\n
    \u00cdgy a sz\u00f6g kiolvas\u00e1sa az Icon l\u00e9trehoz\u00e1sakor t\u00f6rt\u00e9nik, mely az IconButton callbackj\u00e9ben t\u00f6rt\u00e9nik, melyben csak az Icon l\u00e9trehoz\u00e1sa t\u00f6rt\u00e9nik, \u00edgy kevesebb elemet kell l\u00e9trehozni ennek a m\u00f3dos\u00edt\u00e1s\u00e1ra.

    A kiolvas\u00e1s hely\u00e9nek m\u00f3dos\u00edt\u00e1sa mellett egy m\u00e1sik szempontra is \u00e9rdemes figyeln\u00fcnk a State objektumok haszn\u00e1lat\u00e1n\u00e1l: a Compose melyik f\u00e1zis\u00e1ban t\u00f6rt\u00e9nik a kiolvas\u00e1s. Ennek meg\u00e9rt\u00e9s\u00e9re n\u00e9zz\u00fck \u00e1t az al\u00e1bbi \u00e1br\u00e1t:

    Az aktu\u00e1lis helyzetben a Composition r\u00e9tegben olvassuk ki az \u00e9rt\u00e9k\u00e9t a sz\u00f6gnek, pedig val\u00f3j\u00e1ban csak a kirajzol\u00e1skor kellene egy forgat\u00e1si transzform\u00e1ci\u00f3t haszn\u00e1lni. A Compose sok esetben k\u00e9t megold\u00e1st biztos\u00edt egy param\u00e9ter megad\u00e1s\u00e1ra: a k\u00f6zvetlen \u00e9rt\u00e9kad\u00e1s, illetve a callbacken kereszt\u00fcli visszat\u00e9r\u00e9s.

    Jelenleg a k\u00f6zvetlen \u00e9rt\u00e9kad\u00e1st haszn\u00e1ljuk, mert akkor megadjuk a forgat\u00e1s \u00e9rt\u00e9k\u00e9t, amikor l\u00e9trehozzuk az adott Modifier objektumot. Ezt \u00e1ltal\u00e1ban egyszer\u0171bb, viszont cser\u00e9be kor\u00e1bban kiolvas\u00e1sra ker\u00fcl az \u00e9rt\u00e9k, mint sz\u00fcks\u00e9g lenne. A callbacken kereszt\u00fcli visszat\u00e9r\u00e9s eset\u00e9n a Compose garant\u00e1lja, hogy csak abban a f\u00e1zisban olvassa ki az adott \u00e9rt\u00e9ket, amikor m\u00e1r mindenk\u00e9pp sz\u00fcks\u00e9ges. Ezt forgat\u00e1sn\u00e1l is lehet haszn\u00e1lni a k\u00f6vetkez\u0151 m\u00f3don:

    Icon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n.graphicsLayer {\nrotationZ = angle\n}\n)\n

    \u00cdgy ebben az esetben a sz\u00f6g kiolvas\u00e1sa m\u00e1r csak a Drawing f\u00e1zisban t\u00f6rt\u00e9nik, nem kell a teljes cikluson v\u00e9gigfutni az anim\u00e1ci\u00f3 sor\u00e1n.

    "},{"location":"laborok/todo_compose_basics/#onallo-feladat","title":"\u00d6n\u00e1ll\u00f3 feladat","text":""},{"location":"laborok/todo_compose_basics/#datumvalaszto-elkeszitese","title":"D\u00e1tumv\u00e1laszt\u00f3 elk\u00e9sz\u00edt\u00e9se","text":"

    Val\u00f3s\u00edtsuk meg a felugr\u00f3 d\u00e1tumv\u00e1laszt\u00f3 ablakot a TodoCreateScreen-en! Kor\u00e1bban ezt k\u00fcls\u0151 k\u00f6nyvt\u00e1rral kellett megoldanunk, azonban szerencs\u00e9re a Material3 m\u00e1r tartalmaz DatePickert \u00e9s DatePickerDialog-ot. A megval\u00f3s\u00edt\u00e1shoz el\u0151sz\u00f6r fel kell venn\u00fcnk egy showDialog v\u00e1ltoz\u00f3t az oldalon bel\u00fcl, melyet a TodoEditor megfelel\u0151 callbackj\u00e9ben be kell \u00e1ll\u00edtanunk. Ha pedig a showDialog true \u00e9rt\u00e9ket tartalmaz, akkor az oldalhoz tartoz\u00f3 Scaffold v\u00e9g\u00e9n jelen\u00edts\u00fck meg a dial\u00f3gust a megfelel\u0151 param\u00e9terez\u00e9s\u00e9vel. Ne felejts\u00fck el \u00e1tadni az aktu\u00e1lis d\u00e1tumot, illetve a k\u00e9t esem\u00e9nyt kezelj\u00fck le megfelel\u0151en:

    DatePickerDialog
    if (showDialog) {\nBox() {\n\nval datePickerState = rememberDatePickerState()\n\nDatePickerDialog(\nonDismissRequest = {\n// Dismiss the dialog when the user clicks outside the dialog or on the back\n// button. If you want to disable that functionality, simply use an empty\n// onDismissRequest.\nshowDialog = false\n},\nconfirmButton = {\nTextButton(\nonClick = {\nshowDialog = false\nval date =\nInstant.ofEpochMilli(datePickerState.selectedDateMillis!!)\n.atZone(ZoneId.systemDefault()).toLocalDateTime()\n\nviewModel.onEvent(\nTodoCreateEvent.SelectDate(\nLocalDate(date.year, date.month, date.dayOfMonth)\n)\n)\n}\n) {\nText(stringResource(R.string.dialog_ok_button_text))\n}\n},\ndismissButton = {\nTextButton(\nonClick = {\nshowDialog = false\n}\n) {\nText(stringResource(R.string.dialog_dismiss_button_text))\n}\n}\n) {\nDatePicker(state = datePickerState)\n}\n}\n}   

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a d\u00e1tumv\u00e1laszt\u00f3 dial\u00f3gus (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l!

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/todo_compose_basics/#lista-osszekeverese","title":"Lista \u00f6sszekever\u00e9se","text":"

    Adjunk hozz\u00e1 egy f\u00fcggv\u00e9nyt a TodoListViewModel-hez, mely megkeveri a lista elemeit! Haszn\u00e1ljuk ehhez a shuffled() f\u00fcggv\u00e9nyt. H\u00edvjuk meg ezt a f\u00fcggv\u00e9nyt egy \u00faj floating action button megnyom\u00e1s\u00e1ra (tegy\u00fck egy Column-be a l\u00e9trehoz\u00e1s gombot, \u00e9s f\u00f6l\u00e9 tegy\u00fcnk egy \u00faj gombot). Vegy\u00fck fel a list\u00e1n bel\u00fcl a ListItem modifier l\u00e1nc\u00e1hoz az animateItemPlacement() h\u00edv\u00e1st. Mit tapasztalunk, ha \u00edgy megkeverj\u00fck a lista tartalm\u00e1t? Mi t\u00f6rt\u00e9nik, ha kivessz\u00fck a LazyColumn items blokkj\u00e1b\u00f3l a key param\u00e9tert?

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lista megkevert \u00e1llapot\u00e1ban (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az anim\u00e1ci\u00f3t tartalmaz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l!

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"tudnivalok/github/GitHub-Actions/","title":"GitHub Actions ismertet\u0151","text":"

    A laborfeladatok ki\u00e9rt\u00e9kel\u00e9s\u00e9ben a GitHub Actions-re t\u00e1maszkodunk. Seg\u00edts\u00e9g\u00e9vel a git repository-kon m\u0171veleteket \u00e9s programokat tudunk futtatni. Ilyen m\u0171velet p\u00e9ld\u00e1ul a C# k\u00f3d leford\u00edt\u00e1sa, vagy a beadott k\u00f3d tesztel\u00e9se.

    A lefutott ki\u00e9rt\u00e9kel\u00e9sr\u0151l a pull request-ben fogsz \u00e9rtes\u00edt\u00e9st kapni. Ha meg szeretn\u00e9d n\u00e9zni r\u00e9szletesebben a h\u00e1tt\u00e9rben t\u00f6rt\u00e9nteket, vagy p\u00e9ld\u00e1ul az alkalmaz\u00e1s napl\u00f3kat, a GitHub fel\u00fclet\u00e9n az Actions alatt indulhatsz el.

    Az Actions fel\u00fclet\u00e9n un. Workflow-kat l\u00e1tsz; minden egyes ki\u00e9rt\u00e9kel\u00e9s futtat\u00e1s egy-egy elem lesz itt (teh\u00e1t historikusan is visszakereshet\u0151ek).

    Ezek k\u00f6z\u00fcl egyet kiv\u00e1lasztva (pl. a legfels\u0151 mindig a legutols\u00f3) l\u00e1thatod a workflow fut\u00e1s\u00e1nak r\u00e9szleteit. A fut\u00e1s napl\u00f3j\u00e1hoz a bal oldali list\u00e1ban m\u00e9g kattintani kell egyet. Jobb oldalon l\u00e1that\u00f3 a folyamat teljes napl\u00f3ja.

    Minden z\u00f6ld pipa egy-egy sikeres l\u00e9p\u00e9st jelent. Ezen l\u00e9p\u00e9sek nem azonosak a feladatokokkal, hanem a ki\u00e9rt\u00e9kel\u00e9s folyamat\u00e1nak l\u00e9p\u00e9sei lesznek. Ilyen l\u00e9p\u00e9s p\u00e9ld\u00e1ul a k\u00f6rnyezet el\u0151k\u00e9sz\u00edt\u00e9se, pl. a .NET SDK telep\u00edt\u00e9se (minden ki\u00e9rt\u00e9kel\u00e9s egy vadi\u00faj k\u00f6rnyezetben indul, \u00edgy mindent el\u0151 kell k\u00e9sz\u00edteni).

    Alapvet\u0151en a l\u00e9p\u00e9sek mindig sikeresek, akkor is, ha a megold\u00e1sodban hiba van, mert a ki\u00e9rt\u00e9kel\u00e9s erre fel van k\u00e9sz\u00edtve. Kiv\u00e9telt ez al\u00f3l csak a neptun.txt hi\u00e1nya ill. a C# k\u00f3d leford\u00edt\u00e1sa jelent. El\u0151bbi felt\u00e9tlen\u00fcl sz\u00fcks\u00e9ges, ez\u00e9rt semmilyen folyamatot nem hajtunk v\u00e9gre n\u00e9lk\u00fcle. Ut\u00f3bbi eset\u00e9ben a C# k\u00f3d ford\u00edt\u00e1sa szint\u00e9n sz\u00fcks\u00e9ges a tov\u00e1bbl\u00e9p\u00e9shez, ez\u00e9rt sikertelens\u00e9g eset\u00e9n le\u00e1ll a folyamat.

    N\u00e9ha el\u0151fordulhat azonban tranziens, id\u0151szakos hiba is. P\u00e9ld\u00e1ul a .NET k\u00f6rnyezet let\u00f6lt\u00e9se nem siker\u00fcl h\u00e1l\u00f3zati hiba miatt. Ilyen esetben a futtat\u00e1st k\u00e9zzel meg lehet ism\u00e9telni. Ez persze csak akkor seg\u00edt, ha t\u00e9nyleg \u00e1tmeneti hib\u00e1r\u00f3l van sz\u00f3, teh\u00e1t pl. egy C# ford\u00edt\u00e1si hib\u00e1n nem fog seg\u00edteni. (Ezt a hiba\u00fczenetb\u0151l illetve a l\u00e9p\u00e9s nev\u00e9b\u0151l tudod kider\u00edteni, vagy legal\u00e1bb is megtippelni kell\u0151 bizonyoss\u00e1ggal.)

    A feladat f\u00fcggv\u00e9ny\u00e9ben ak\u00e1r az alkalmaz\u00e1s napl\u00f3kat is meg tudod n\u00e9zni itt. Pl. amikor .NET alkalmaz\u00e1st k\u00e9sz\u00edtesz, az alkalmaz\u00e1st elind\u00edtjuk, \u00e9s minden, amit napl\u00f3z, itt megtekinthet\u0151.

    Az al\u00e1bbi p\u00e9ld\u00e1ul egy Entity Framework-\u00f6t haszn\u00e1l\u00f3 alkalmaz\u00e1s inicializ\u00e1s\u00e1t mutatja, k\u00f6zt\u00fck p\u00e9ld\u00e1ul a kiadott SQL parancsokat is. Debuggol\u00e1s k\u00f6zben a Visual Studio Output ablak\u00e1ban is hasonl\u00f3kat l\u00e1thatsz. Ez term\u00e9szetesen nagyban f\u00fcgg a konkr\u00e9t feladatt\u00f3l.

    "},{"location":"tudnivalok/github/GitHub-credentials/","title":"Egyetemi laborokban: GitHub bel\u00e9p\u00e9s","text":"

    Az egyetemi laborokban a g\u00e9pek megjegyzik a GitHub bel\u00e9p\u00e9si adatokat. Ezt a munka v\u00e9gezt\u00e9vel k\u00e9zzel kell t\u00f6r\u00f6lni.

    1. Nyisd meg a Credential Manager-t a Start men\u00fcb\u0151l.
    2. A Windows Credentials oldalon keresd meg a GitHubra mutat\u00f3 bejegyz\u00e9seket, \u00e9s t\u00f6r\u00f6ld \u0151ket.
    "},{"location":"tudnivalok/github/GitHub/","title":"Feladatok bead\u00e1sa (GitHub)","text":"

    A feladatok bead\u00e1s\u00e1hoz a GitHub platformot haszn\u00e1ljuk. Minden labor bead\u00e1sa egy-egy GitHub repository-ban t\u00f6rt\u00e9nik, melyet a feladatle\u00edr\u00e1sban tal\u00e1lhat\u00f3 linken kereszt\u00fcl kapsz meg. A labor feladatainak megold\u00e1s\u00e1t ezen repository-ban kell elk\u00e9sz\u00edtened, \u00e9s ide kell felt\u00f6ltened. A k\u00e9sz megold\u00e1s bead\u00e1sa a repository-ba val\u00f3 felt\u00f6lt\u00e9s ut\u00e1n egy un. pull request form\u00e1j\u00e1ban t\u00f6rt\u00e9nik, amelyet a laborvezet\u0151dh\u00f6z rendelsz.

    FONTOS

    Az itt le\u00edrt formai el\u0151\u00edr\u00e1sok betart\u00e1sa elv\u00e1r\u00e1s. A nem ilyen form\u00e1ban beadott megold\u00e1sokat nem \u00e9rt\u00e9kelj\u00fck.

    "},{"location":"tudnivalok/github/GitHub/#roviditett-verzio","title":"R\u00f6vid\u00edtett verzi\u00f3","text":"

    Al\u00e1bb r\u00e9szletesen bemutatjuk a bead\u00e1s menet\u00e9t. Itt egy r\u00f6vid \u00f6sszefoglal\u00f3 az \u00e1ttekint\u00e9shez, illetve a helyes bead\u00e1s ellen\u0151rz\u00e9s\u00e9hez.

    1. A munk\u00e1dat Moodle-ben tal\u00e1lhat\u00f3 GitHub Classroom megh\u00edv\u00f3 linken kereszt\u00fcl l\u00e9trehozott GitHub repository-ban kell elk\u00e9sz\u00edtsd.

    2. A megold\u00e1shoz k\u00e9sz\u00edts egy k\u00fcl\u00f6n \u00e1gat, ne a master-en dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. Mindenk\u00e9ppen pushold a megold\u00e1st.

    3. A bead\u00e1st egy pull request jelzi, amely pull request-et a laborvezet\u0151dh\u00f6z kell rendelned.

    4. Ha az eredm\u00e9nnyel vagy \u00e9rt\u00e9kel\u00e9ssel kapcsolatban k\u00e9rd\u00e9sed van, pull request kommentben k\u00e9rdezhetsz. A laborvezet\u0151 \u00e9rtes\u00edt\u00e9s\u00e9hez haszn\u00e1ld a @n\u00e9v c\u00edmz\u00e9st a komment sz\u00f6veg\u00e9ben.

    "},{"location":"tudnivalok/github/GitHub/#a-munka-elkezdese-git-checkout","title":"A munka elkezd\u00e9se: git checkout","text":"
    1. Regisztr\u00e1lj egy GitHub accountot, ha m\u00e9g nincs.

    2. Moodle-ben a kurzus oldal\u00e1n keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-t. Ez minden laborhoz m\u00e1s lesz, \u00fcgyelj r\u00e1, hogy a megfelel\u0151 linket haszn\u00e1ld.

    3. Ha k\u00e9ri, adj enged\u00e9lyt a GitHub Classroom alkalmaz\u00e1snak, hogy haszn\u00e1lja az account adataidat.

    4. L\u00e1tni fogsz egy oldalt, ahol elfogadhatod a feladatot (\"Accept the ... assignment\"). Kattints a gombra.

    5. V\u00e1rd meg, am\u00edg elk\u00e9sz\u00fcl a repository. A repository linkj\u00e9t itt kapod meg.

      Megjegyz\u00e9s

      A repository priv\u00e1t lesz, azaz az senki nem l\u00e1tja, csak te, \u00e9s az oktat\u00f3k.

    6. Nyisd meg a repository-t a webes fel\u00fcleten a linkre kattintva. Ezt az URL-t \u00edrd fel, vagy mentsd el.

    7. Kl\u00f3nozd le a repository-t. Ehhez sz\u00fcks\u00e9ges lesz a repository c\u00edm\u00e9re, amit a repository webes fel\u00fclet\u00e9n a Clone or download alatt tal\u00e1lsz.

      A git repository kezel\u00e9s\u00e9hez tetsz\u0151leges klienst haszn\u00e1lhatsz. Ha nincs kedvenced m\u00e9g, akkor legegyszer\u0171bb a GitHub Desktop. Ebben az alkalmaz\u00e1sban k\u00f6zvetlen\u00fcl tudod list\u00e1zni a repository-kat GitHub-r\u00f3l, vagy haszn\u00e1lhatod az URL-t is a kl\u00f3noz\u00e1shoz.

      Ha konzolt haszn\u00e1ln\u00e1l, az al\u00e1bbi parancs kl\u00f3nozza a repository-t (ha a git parancs el\u00e9rhet\u0151): git clone <repository link>

      Sikertelen kl\u00f3noz\u00e1s

      Amennyiben a bejelentkez\u00e9s sikertelen felhaszn\u00e1l\u00f3n\u00e9v/jelsz\u00f3 p\u00e1rossal a \"Clone with HTTPS\" eset\u00e9n, (r\u00e9gebb \u00f3ta haszn\u00e1lt felhaszn\u00e1l\u00f3n\u00e1l) \u00e9rdemes ellen\u0151rizni a git-en tal\u00e1lhat\u00f3 Personal Access token lej\u00e1rati d\u00e1tum\u00e1t.

      Jobb fels\u0151 sarokban a profilk\u00e9p melletti lefel\u00e9 mutat\u00f3 nyil > Settings > bal oldalon (legals\u00f3) Developer settings > ugyanitt Personal access tokens.

      Alternat\u00edv m\u00f3dszerk\u00e9nt: HTTP kl\u00f3noz\u00e1s helyett, SSH kulcs haszn\u00e1lat\u00e1hoz, angol nyelv\u0171 instrukci\u00f3k itt tal\u00e1lhat\u00f3ak.

    8. Ha siker\u00fclt a kl\u00f3noz\u00e1s, M\u00c9G NE KEZDJ EL DOLGOZNI! A megold\u00e1st ne a repository master/main \u00e1g\u00e1n k\u00e9sz\u00edtsd el. Hozz l\u00e9tre egy \u00faj \u00e1gat (branch) megoldas n\u00e9ven.

      GitHub Desktop-ban a Branch men\u00fcben teheted ezt meg.

      Ha konzolt haszn\u00e1lsz, az \u00faj \u00e1g elk\u00e9sz\u00edthet\u0151 ezzel a paranccsal: git checkout -b megoldas

    9. Ezen a megold\u00e1s \u00e1gon dolgozva k\u00e9sz\u00edtsd el a beadand\u00f3kat. Ak\u00e1rh\u00e1nyszor kommitolhatsz \u00e9s pusholhatsz. A megold\u00e1s r\u00e9sze a forr\u00e1sk\u00f3d \u00e9s a feladatokban elv\u00e1rt k\u00e9perny\u0151k\u00e9pek. Ha a feladat k\u00e9perny\u0151k\u00e9pet v\u00e1r el, akkor azt a repository gy\u00f6ker\u00e9be commitold az elv\u00e1rt n\u00e9ven.

      Egyetemi laborban

      Laborg\u00e9peken mindig ellen\u0151r\u00edzd, hogy a megfelel\u0151 n\u00e9vvel \u00e9s email c\u00edmmel kommitolsz-e. Ezt a k\u00f6vetkez\u0151 command line paranccsal tudod megtenni.

      git config user.name\ngit config user.email\n

      Ha ez nem megfelel\u0151 lenne, akkor add ki az al\u00e1bbi parancsokat a git repository mapp\u00e1j\u00e1ban. Ezzel az adott repository-ra fogod be\u00e1ll\u00edtani a k\u00edv\u00e1nt nevet \u00e9s email c\u00edmet. (\u00c9rdemes olyan email c\u00edmet, megadni ami a github useretekhez van rendelve)

      git config user.name \"John Doe\"\ngit config user.email \"john@doe.org\"\n

      Otthon

      Otthon a fentieket \u00e9rdemes lehet a glob\u00e1lisan vizsg\u00e1lni \u00e9s fel\u00fcl\u00edrni a --global kapcsol\u00f3val.

      GitHub Desktop-ban \u00edgy tudsz kommitolni. Mindig ellen\u0151rizd, hogy j\u00f3 \u00e1gon vagy-e. Els\u0151 alkalommal a megoldas \u00e1g csak helyben l\u00e9tezik, ez\u00e9rt publik\u00e1lni kell: Publish this branch.

      A tov\u00e1bbi kommitokn\u00e1l is mindig ellen\u0151rizd a megfelel\u0151 \u00e1gat. Ha egy kommit m\u00e9g nincs fel\u00f6ltve, azt a Push origin gombbal teheted meg. A kis sz\u00e1m a gombon jelzi, hogy h\u00e1ny, m\u00e9g nem pusholt kommit van.

      Ha konzolt haszn\u00e1lsz, akkor az al\u00e1bbi parancsokat haszn\u00e1ld (felt\u00e9ve, hogy a j\u00f3 \u00e1gon vagy):

      # Ellen\u0151rizd az \u00e1gat, \u00e9s hogy milyen f\u00e1jlok m\u00f3dosultak\ngit status\n\n# Minden v\u00e1ltoztat\u00e1st el\u0151k\u00e9sz\u00edt kommitol\u00e1sra\ngit add .\n\n# Kommit\ngit commit -m \"f1\"\n\n# Push els\u0151 alkalommal az \u00faj \u00e1g publik\u00e1l\u00e1s\u00e1hoz\ngit push --set-upstream origin megoldas\n\n# Push a tov\u00e1bbiakban, amikor az \u00e1g m\u00e1r nem \u00faj\ngit push\n
    "},{"location":"tudnivalok/github/GitHub/#a-megoldas-beadasa","title":"A megold\u00e1s bead\u00e1sa","text":"
    1. Ha v\u00e9gezt\u00e9l a megold\u00e1ssal, ellen\u0151rizd a GitHub webes fel\u00fclet\u00e9n, hogy mindent felt\u00f6lt\u00f6tt\u00e9l-e. Ehhez a webes fel\u00fcleten v\u00e1ltanod kell az \u00e1gak k\u00f6z\u00f6tt.

      Felt\u00f6lt\u00e9s a webes fel\u00fcleten

      Azt javasoljuk, hogy ne haszn\u00e1ld a GitHub f\u00e1jl felt\u00f6lt\u00e9s funkci\u00f3j\u00e1t. Ha valami hi\u00e1nyzik, a helyi git repository-ban p\u00f3told, \u00e9s kommitold majd pushold.

    2. Ha t\u00e9nyleg k\u00e9sz vagy, akkor nyiss egy pull request-et.

      Minek a pull request?

      Ez a pull request fogja \u00f6ssze a megold\u00e1sodat, \u00e9s annak \"v\u00e9geredm\u00e9ny\u00e9t\" mutatja. \u00cdgy a laborvezet\u0151nek nem az egyes kommitjaidat vagy f\u00e1jljaidat kell n\u00e9znie, hanem csak a relev\u00e1ns, v\u00e1ltozott r\u00e9szeket l\u00e1tja egyben. A pull request jelenti a feladatod bead\u00e1s\u00e1t is, \u00edgy ez a l\u00e9p\u00e9s nem hagyhat\u00f3 ki.

      A pull request nyit\u00e1s\u00e1hoz a GitHub webes fel\u00fclet\u00e9re kell menj. Itt, ha nem r\u00e9g pusholt\u00e1l, a GitHub fel is aj\u00e1nlja a pull request l\u00e9trehoz\u00e1s\u00e1t.

      A pull request-et a fenti men\u00fcben is l\u00e9trehozhatod. Fontos, hogy a megfelel\u0151 brancheket v\u00e1laszd ki: master-be megy a megoldas \u00e1g.

      Ha minden rendben siker\u00fclt, a men\u00fcben fent l\u00e1tod a kis \"1\" sz\u00e1mot a Pull request elem mellett, jelezve, hogy van egy nyitott pull request. DE M\u00c9G NEM V\u00c9GEZT\u00c9L!

    3. A pull request hat\u00e1s\u00e1ra le fog futni egy \u00e9rt\u00e9kel\u00e9s. Ennek eredm\u00e9ny\u00e9t a pull request alatt kommentben fogod l\u00e1tni.

      Ez az \u00e9rt\u00e9kel\u00e9s minden labor eset\u00e9ben m\u00e1s lesz. Egyes laborokn\u00e1l a programodat lefuttatjuk, \u00e9s el\u0151zetes pontsz\u00e1mot is kapsz. M\u00e1s laborokn\u00e1l csak \"szintaktikai ellen\u0151rz\u00e9st\" v\u00e9gz\u00fcnk.

      Ha a ki\u00e9rt\u00e9kel\u00e9s eredm\u00e9ny\u00e9vel kapcsolatban t\u00f6bb inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ged, mint amit itt l\u00e1tsz, a GitHub Actions webes fel\u00fclete seg\u00edts\u00e9g\u00fcl szolg\u00e1lhat. Err\u0151l itt tal\u00e1lsz egy r\u00f6vid ismertet\u0151t.

    4. Ha nem vagy megel\u00e9gedve a munk\u00e1ddal, akkor m\u00e9g jav\u00edthatsz rajta. Ehhez kommitolj \u00e9s pusholj \u00fajra. Ha tov\u00e1bbra is a megfelel\u0151 \u00e1gon dolgozol, akkor a pull request \u00fajb\u00f3l le fogja futtatni a ki\u00e9rt\u00e9kel\u00e9st. Arra k\u00e9r\u00fcnk, hogy MAXIMUM 5 alkalommal futtasd le a ki\u00e9rt\u00e9kel\u00e9st!

      Megold\u00e1s jav\u00edt\u00e1sa ki\u00e9rt\u00e9kel\u00e9s n\u00e9lk\u00fcl

      Ha \u00fagy l\u00e1tod, hogy a megold\u00e1sodat m\u00e9g jav\u00edtani akarod, \u00e9s nem szeretn\u00e9d, hogy mindig lefusson az \u00e9rt\u00e9kel\u00e9s, akkor \u00e1ll\u00edtsd \u00e1t a pull request-et a webes fel\u00fcleten draft \u00e1llapotra.

      Ezzel az \u00e1llapottal jelzed, hogy m\u00e9g dolgozol. Kommitolj \u00e9s pusholj. Ilyenkor nem fog futni ki\u00e9rt\u00e9kel\u00e9s. Ha v\u00e9gezt\u00e9l, akkor vissza kell \u00e1ll\u00edtanod a pull request-et: menj a PR alj\u00e1ra \u00e9s kattints a \"Ready for review\" gombra. Ennek hat\u00e1s\u00e1ra vissza\u00e1ll a PR \u00e9s le fog futni az automata \u00e9rt\u00e9kel\u00e9s.

      Maximum 5

      A maximum 5 alkalomba nem sz\u00e1moljuk bele az esetlegesen megszakadt, vagy tranziens hiba miatt sikertelen futtat\u00e1sokat. Ha viszont figyelmetlens\u00e9gb\u0151l, vagy sz\u00e1nd\u00e9kosan t\u00fall\u00e9ped az \u00f6t\u00f6t, akkor pontlevon\u00e1ssal szankcion\u00e1lunk. Arra k\u00e9r\u00fcnk, hogy bead\u00e1s el\u0151tt teszteld a megold\u00e1sod, ne a GitHub platformot \"dolgoztasd\" magad helyett!

    5. V\u00c9GEZET\u00dcL, ha k\u00e9sz vagy, a pull request-et rendeld a laborvezet\u0151dh\u00f6z. Ez a l\u00e9p\u00e9s felt\u00e9tlen\u00fcl fontos, ez jelzi a bead\u00e1st.

      Pull request n\u00e9lk\u00fcl

      Ha nincs pull request-ed, vagy nincs a laborvezet\u0151h\u00f6z rendelve, akkor \u00fagy tekintj\u00fck, hogy m\u00e9g nem vagy k\u00e9szen, \u00e9s nem adtad be a megold\u00e1st.

      V\u00e9gezt\u00e9l

      Miut\u00e1n a laborvezet\u0151h\u00f6z rendelted a pull request-et, m\u00e1r ne m\u00f3dos\u00edts semmin. A laborvezet\u0151 \u00e9rt\u00e9kelni fogja a munk\u00e1dat, \u00e9s a pull request lez\u00e1r\u00e1s\u00e1val kommentben jelzi a v\u00e9geredm\u00e9nyt.

    "},{"location":"tudnivalok/github/GitHub/#kapott-eredmennyel-kapcsolatban-kerdes-vagy-reklamacio","title":"Kapott eredm\u00e9nnyel kapcsolatban k\u00e9rd\u00e9s vagy reklam\u00e1ci\u00f3","text":"

    Ha a feladatok \u00e9rt\u00e9kel\u00e9s\u00e9vel vagy az eredm\u00e9nnyel kapcsolatban k\u00e9rd\u00e9st tenn\u00e9l fel, vagy reklam\u00e1ln\u00e1l, haszn\u00e1ld a Pull Request kommentel\u00e9si lehet\u0151s\u00e9g\u00e9t erre. Annak \u00e9rdek\u00e9ben, hogy a laborvezet\u0151 biztosan \u00e9rtes\u00fclj\u00f6n a k\u00e9rd\u00e9sr\u0151l haszn\u00e1ld a @n\u00e9v mention funkci\u00f3t a laborvezet\u0151d megnevez\u00e9s\u00e9hez. Err\u0151l automatikusan kapni fog egy email \u00e9rtes\u00edt\u00e9st.

    Reklam\u00e1ci\u00f3 csak indokl\u00e1ssal

    Ha nem \u00e9rtesz egyet az \u00e9rt\u00e9kel\u00e9ssel, a bizony\u00edt\u00e1s t\u00e9ged terhel, azaz al\u00e1 kell t\u00e1masztanod a reklam\u00e1ci\u00f3d (pl. annak le\u00edr\u00e1s\u00e1val, hogyan tesztelted a megold\u00e1sod, \u00e9s mi bizony\u00edtja a helyess\u00e9g\u00e9t).

    "},{"location":"tudnivalok/github/contributing/","title":"Hozz\u00e1j\u00e1rul\u00e1s az anyaghoz","text":"

    Az anyag terjedelm\u00e9b\u0151l adand\u00f3an apr\u00f3bb hib\u00e1k esetenk\u00e9nt hi\u00e1nyoss\u00e1gok jelentkezhetnek a laborokban. Ha egy ilyennel tal\u00e1lkozol \u00e9s \u00fagy d\u00f6ntesz szeretn\u00e9l seg\u00edteni hallgat\u00f3t\u00e1rsaidnak, azt a k\u00f6vetkez\u0151kben le\u00edrtak alapj\u00e1n tudod megtenni.

    Plusz pont jegyzet jav\u00edt\u00e1s\u00e9rt

    M\u00e1s tant\u00e1rgyak mint\u00e1j\u00e1ra itt is szeretn\u00e9nk plusz pontot adni a jegyzet open-source hozz\u00e1j\u00e1rul\u00e1sai\u00e9rt. Akik a t\u00e1rgyat jelenleg hallgatj\u00e1k, pontokat kaphatnak hozz\u00e1j\u00e1rul\u00e1saik\u00e9rrt.

    A f\u00e9l\u00e9v sor\u00e1n max 3 db plusz pontot lehet szerezni fejenk\u00e9nt olyan jav\u00edt\u00e1sok\u00e9rt, amik a trivi\u00e1lis 1-2 bet\u0171 elg\u00e9pel\u00e9sen t\u00fal \u00e9rdemben jav\u00edtanak a githubon tal\u00e1lhat\u00f3 labor jegyzetek min\u0151s\u00e9g\u00e9n. Pl.: jelent\u0151s mennyis\u00e9g\u0171 elg\u00e9pel\u00e9s jav\u00edt\u00e1sa, egy\u00e9rtelm\u0171s\u00edt\u00e9sek, illusztr\u00e1ci\u00f3k kieg\u00e9sz\u00edt\u00e9sek k\u00e9sz\u00edt\u00e9se vagy ak\u00e1r egy teljes kieg\u00e9sz\u00edt\u0151 jegyzet \u00edr\u00e1sa (term\u00e9szetesen nem azonos pont\u00e9rt\u00e9kkel).

    Persze a pont n\u00e9lk\u00fcl az 1-1 bet\u0171s elg\u00e9pel\u00e9seket is sz\u00edvesen fogadjuk, ami bemeleg\u00edt\u00e9snek is t\u00f6k\u00e9letes.

    "},{"location":"tudnivalok/github/contributing/#hibak-jelzese","title":"Hib\u00e1k jelz\u00e9se","text":"

    Amennyiben hib\u00e1t tal\u00e1lsz az anyagban, vagy szeretn\u00e9d b\u0151v\u00edteni, de nem \u00e1ll m\u00f3dodban jav\u00edtani, nyithatsz egy issue-t amiben le\u00edrod a hib\u00e1t.

    1. N\u00e9zd meg, hogy valaki nem jelezte-e, amit szeretn\u00e9l. Gyakran m\u00e1r l\u00e9tez\u0151 probl\u00e9m\u00e1kat tal\u00e1lnak, amire m\u00e1r van pull request, \u00edgy miel\u0151tt b\u00e1rmit tenn\u00e9l n\u00e9zd meg valaki nem el\u0151z\u00f6tt-e meg
    2. Az issues tabon a new issue gombbal hozz l\u00e9tre egy \u00faj issue-t.
    3. L\u00e1sd el a megfelel\u0151 c\u00edmk\u00e9kkel
      1. A labor t\u00edpusa (android az androidos laborokn\u00e1l)
      2. A hiba t\u00edpusa (clarification, typo, illustration vagy notes)
    4. \u00cdrd le, hogy mit k\u00e9ne tartalmaznia a jav\u00edt\u00e1snak

    Tip

    Az c\u00edme legyen r\u00f6vid \u00e9s l\u00e9nyegret\u00f6r\u0151, pl.: Megfogalmaz\u00e1s pontos\u00edt\u00e1sa a 4. laborban vagy A 6. laborban a le\u00edrt k\u00f3d hib\u00e1san m\u0171k\u00f6dik Android 12-n

    A issue descriptionj\u00e9ben pedig fejtsd ki, hol tal\u00e1lhat\u00f3 a hi\u00e1nyoss\u00e1g, illetve ha van r\u00e1 \u00f6tleted, hogy lehetne orvosolni ezt. Ha ezeken t\u00fal m\u00e9g screenshotot is tudsz mell\u00e9kelni, az nagyban megseg\u00edti a probl\u00e9ma mihamarabbi jav\u00edt\u00e1s\u00e1t.

    Warning

    A github issues nem a laborfeladatok megold\u00e1s\u00e1val kapcsolatos probl\u00e9m\u00e1k helye, \u00edgy a \"Nem tudom megoldani hogy az \u00e9rtes\u00edt\u00e9s meg\u00e9rkezzen\" jelleg\u0171 probl\u00e9m\u00e1kat ne itt jelezz\u00e9tek, erre vannak a laboralkalmak.

    "},{"location":"tudnivalok/github/contributing/#valtoztatasok-javaslasa","title":"V\u00e1ltoztat\u00e1sok javasl\u00e1sa","text":"

    Amennyiben a hozz\u00e1j\u00e1rul\u00e1sod meg tudod val\u00f3s\u00edtani ind\u00edts pull requestet

    1. Forkold a repository-t a Githubon jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 gombbal

    2. V\u00e9gezd el a v\u00e1ltoztat\u00e1sokat.

      Tip

      Ez nagyon hasonl\u00f3an m\u0171k\u00f6dik a laborok beada\u00e1s\u00e1hoz

      1. Hozz l\u00e9tre egy branchet a saj\u00e1t forkodon, amin a v\u00e1ltoztat\u00e1sokat el fogod v\u00e9gezni.

      2. Ezen a branchen k\u00e9sz\u00edtsd el a jav\u00edt\u00e1sokat

      3. Ellen\u0151rizd, hogy ne ker\u00fclj\u00f6n bele a commitba olyan file, amit az editor gener\u00e1lt (pl.: .idea mappa) illetve olyan file aminek nem k\u00e9ne kiker\u00fclnie, pl.: Github Private Access Token

      4. Ha k\u00e9sz vagy a laborok bead\u00e1s\u00e1hoz hasonl\u00f3an ind\u00edts egy pull requestet a VIAUAC00/laborok master branch\u00e9re.

      5. L\u00e1sd el a megfelel\u0151 c\u00edmk\u00e9kkel

        1. A labor t\u00edpusa (android az androidos laborokn\u00e1l \u00e9s web a webes laborokn\u00e1l)
        2. A hiba t\u00edpusa (clarification, typo, illustration vagy notes)
      6. A le\u00edr\u00e1sban r\u00e9szletezd v\u00e1ltoztat\u00e1sok ok\u00e1t. Ne felejtsd el bele\u00edrni a NEPTUN k\u00f3dod a le\u00edr\u00e1sba, mert \u00edgy fogjuk tudni megadni a pontokat.
    3. Valaki, akinek hozz\u00e1f\u00e9r\u00e9se van a repositoryhoz, ellen\u0151rzi a v\u00e1ltoztat\u00e1sok sz\u00fcks\u00e9gess\u00e9g\u00e9t, \u00e9s elb\u00edr\u00e1lja, hogy val\u00f3ban beker\u00fclhet az anyagba.

    4. A v\u00e1ltoztat\u00e1sokra review-t ind\u00edtunk \u00e9s ha kell m\u00f3dos\u00edt\u00e1sokat fogunk k\u00e9rni.
    5. Ha minden k\u00e9rt v\u00e1ltoztat\u00e1s megt\u00f6rt\u00e9nt, a hozz\u00e1j\u00e1rul\u00e1sod beleker\u00fcl az anyagba.
    "},{"location":"tudnivalok/github/contributing/#code-style","title":"Code style","text":"
    • Kotlin: a hivatalos style guide alapj\u00e1n
    • Markdown: Mivel az alap spec nem mindig a legtiszt\u00e1bban \u00e9rthet\u0151, a markdownlint szab\u00e1lyai alapj\u00e1n, az n\u00e9h\u00e1ny kiv\u00e9tel\u00e9vel. Ezeket a .markdownlint.yaml-ben tal\u00e1lod, ha VSCode-ot haszn\u00e1lsz automatikusan alkalmazza \u0151ket az editor \u00e9s jelzi ha nem megfelel\u0151 amit \u00edrsz.

    Ezek a st\u00edlusok a t\u00e1rgyban aj\u00e1nlott editorokban k\u00f6nnyen be\u00e1ll\u00edthat\u00f3ak.

    "},{"location":"tudnivalok/github/contributing/#vscode","title":"VSCode","text":"

    Aj\u00e1nlott extension\u00f6k:

    • yzhang.markdown-all-in-one: MD szinkroniz\u00e1lt live preview
    • DavidAnson.vscode-markdownlint: MD form\u00e1z\u00e1s, szab\u00e1lyok stb.
    • Prettier: HTML+CSS form\u00e1z\u00f3
    • Error Lens: Kiemeli a hib\u00e1kat hogy gyorsabben megtal\u00e1ljuk \u0151ket

    Az editor be\u00e1ll\u00edt\u00e1s\u00e1hoz nyisd meg a repo-t a gy\u00f6ker\u00e9ben VSCode-al. A VSCode fel fogja aj\u00e1nlani a k\u00e9t markdown extension-t.

    Ha ez megt\u00f6rt\u00e9nt, nyiss meg egy markdown dokumentumot, \u00e9s haszn\u00e1ld a Ctrl+Shift+P shortcutot, a command palette megnyit\u00e1s\u00e1hoz.

    Tip

    A command palette a VSCode parancsaihoz ny\u00fajt hozz\u00e1f\u00e9r\u00e9st, autocompleteeli a parancsokat \u00e9s egy minim\u00e1lis GUI-t is biztos\u00edt.

    A command palette-be keress\u00fck meg a Format Document With... men\u00fcpontot \u00e9s v\u00e1lasszuk ki. Ekkor egy almen\u00fcbe dob az editor \u00e9s kiv\u00e1laszthatjuk hogy melyik form\u00e1z\u00f3val form\u00e1zzuk a MD dokumentumokat. Legalul lesz egy Configure Default Formatter, v\u00e1lasszuk ezt. Ezut\u00e1n v\u00e1lasszuk a markdownlint extensiont, \u00e9s k\u00e9szen vagyunk.

    Megfelel\u0151 formatter kiv\u00e1laszt\u00e1sa

    Ne v\u00e1laszd ki a prettiert formatterk\u00e9nt, mert elt\u00f6ri a sz\u00f6vegbubor\u00e9kokat.

    Ezen fel\u00fcl \u00e9rdemes lehet bekapcsolni a ment\u00e9s el\u0151tti form\u00e1z\u00e1st.

    A Ctrl+, shortcuttal megnyitjuk a be\u00e1ll\u00edt\u00e1sokat, \u00e9s r\u00e1keres\u00fcnk arra, hogy format on save. Itt kipip\u00e1ljuk a checkboxot \u00e9s k\u00e9szen vagyunk.

    Ha ehhez nem lenne t\u00fcrelmed, itt a json amit a settings.json-ba illesztve be\u00e1ll\u00edt\u00f3dik minden.

    {\n\"[markdown]\": {\n\"editor.defaultFormatter\": \"DavidAnson.vscode-markdownlint\",\n\"editor.formatOnSave\": true\n}\n}\n
    "},{"location":"tudnivalok/github/contributing/#ajanlasok","title":"Aj\u00e1nl\u00e1sok","text":""},{"location":"tudnivalok/github/contributing/#android","title":"Android","text":"
    • Az androidos Kotlin \u00e9s XML fileokat illetve k\u00f3dr\u00e9szleteket Android Studioban form\u00e1zva \u00e9rdemes hozz\u00e1adni az anyaghoz
    • Ahhoz hogy biztosan form\u00e1zva legyenek a fileok haszn\u00e1ld a Ctrl+Alt+L shortcutot
    "},{"location":"tudnivalok/github/contributing/#markdown-fileok","title":"Markdown Fileok","text":"
    • A markdown fileokat se az Android Studio se a Visual Studio Code nem rendereli alaphelyzetben. Erre a feladatra a k\u00f6vetkez\u0151 extension\u00f6ket/pluginokat tudom aj\u00e1nlani:
    • VSCode: yzhang.markdown-all-in-one
    • Android Studio: Markdown Editor
    "}]} \ No newline at end of file +{"config":{"lang":["hu"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"T\u00e1rgy ismertet\u0151","text":"

    A t\u00e1rgyk\u00f6vetelm\u00e9nyeket l\u00e1sd a hivatalos tant\u00e1rgyi adatlapon.

    A laborok sorrendj\u00e9t \u00e9s a bead\u00e1sok hat\u00e1ridej\u00e9t Moodle-ben tal\u00e1lod.

    Jav\u00edt\u00e1s az anyagban

    A t\u00e1rgy hallgat\u00f3inak az anyagban t\u00f6rt\u00e9n\u0151 jav\u00edt\u00e1s\u00e9rt, kieg\u00e9sz\u00edt\u00e9s\u00e9rt plusz pontot adunk! Ha hib\u00e1t tal\u00e1lsz, vagy kieg\u00e9sz\u00edten\u00e9d/pontos\u00edtan\u00e1d a feladatle\u00edr\u00e1sokat, nyiss egy pull request-et! A repository linkj\u00e9t a jobb fels\u0151 sarokban tal\u00e1lod.

    A jav\u00edt\u00e1s menet\u00e9r\u0151l \u00e9s form\u00e1j\u00e1r\u00f3l b\u0151vebben a \"Hozz\u00e1j\u00e1rul\u00e1s az anyaghoz\" dokumentumban olvashatsz b\u0151vebben.

    Felhaszn\u00e1l\u00e1si felt\u00e9telek

    Az itt tal\u00e1lhat\u00f3 oktat\u00e1si seg\u00e9danyagok a BMEVIAUAV21 t\u00e1rgy hallgat\u00f3inak k\u00e9sz\u00fcltek. Az anyagok oly m\u00f3d\u00fa felhaszn\u00e1l\u00e1sa, amely a t\u00e1rgy oktat\u00e1s\u00e1hoz nem szorosan kapcsol\u00f3dik, csak a szerz\u0151(k) \u00e9s a forr\u00e1s megjel\u00f6l\u00e9s\u00e9vel t\u00f6rt\u00e9nhet.

    Az anyagok a t\u00e1rgy keret\u00e9ben oktatott kontextusban \u00e9rtelmezhet\u0151ek. Az anyagok\u00e9rt egy\u00e9b felhaszn\u00e1l\u00e1s eset\u00e9n a szerz\u0151(k) felel\u0151ss\u00e9get nem v\u00e1llalnak.

    "},{"location":"#altalanos-tudnivalok","title":"\u00c1ltal\u00e1nos tudnival\u00f3k","text":""},{"location":"#laborok-megoldasainak-beadasa","title":"Laborok megold\u00e1sainak bead\u00e1sa","text":"

    A laborok megold\u00e1s\u00e1t egy szem\u00e9lyre sz\u00f3l\u00f3 git repository-ban kell beadni. Ennek pontos folyamat\u00e1t l\u00e1sd itt. K\u00e9r\u00fcnk, hogy alaposan olvasd v\u00e9gig a le\u00edr\u00e1st!

    FONTOS

    A laborok elk\u00e9sz\u00edt\u00e9se \u00e9s bead\u00e1sa sor\u00e1n az itt le\u00edrtak szerint kell elj\u00e1rnod. A nem ilyen form\u00e1ban beadott megold\u00e1sokat nem \u00e9rt\u00e9kelj\u00fck.

    A bead\u00e1s sor\u00e1n a munkafolyamati hib\u00e1k\u00e9rt (pl. nem megfelel\u0151 emberhez hozz\u00e1rendel\u00e9se, hozz\u00e1rendel\u00e9s elfelejt\u00e9se) pontot vonunk le.

    "},{"location":"#laborok-ertekelese","title":"Laborok \u00e9rt\u00e9kel\u00e9se","text":"

    Minden labort k\u00fcl\u00f6n jeggyel \u00e9rt\u00e9kel\u00fcnk. A teljes\u00edt\u00e9s felt\u00e9tele a hat\u00e1rid\u0151ig t\u00f6rt\u00e9n\u0151 bead\u00e1s. A jegy (1-5 sk\u00e1l\u00e1n) a labor feladatokon megszerezhet\u0151 5 pont alapj\u00e1n t\u00f6rt\u00e9nik. A feladatok bead\u00e1s\u00e1hoz minden esetben a GitHub platformot haszn\u00e1ljuk.

    A feladatok ki\u00e9rt\u00e9kel\u00e9se egyes laborok eset\u00e9n r\u00e9szben automatikusan t\u00f6rt\u00e9nik. A futtathat\u00f3 k\u00f3dokat val\u00f3ban le fogjuk futtatni, ez\u00e9rt minden esetben fontos a feladatle\u00edr\u00e1sok pontos k\u00f6vet\u00e9se (kiindul\u00f3 k\u00f3d v\u00e1z haszn\u00e1lata, csak a megengedett f\u00e1jlok v\u00e1ltoztat\u00e1sa, stb.)!

    A ki\u00e9rt\u00e9kel\u00e9s eredm\u00e9ny\u00e9r\u0151l a GitHub-on kapsz sz\u00f6veges visszajelz\u00e9st (l\u00e1sd itt). Ha enn\u00e9l t\u00f6bb inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ged, a GitHub Actions webes fel\u00fclete seg\u00edts\u00e9g\u00fcl szolg\u00e1lhat. Err\u0151l itt tal\u00e1lsz egy r\u00f6vid ismertet\u0151t.

    "},{"location":"#kepernyokepek","title":"K\u00e9perny\u0151k\u00e9pek","text":"

    A laborok k\u00e9rik, hogy k\u00e9sz\u00edts k\u00e9perny\u0151k\u00e9pet a megold\u00e1s egy-egy r\u00e9sz\u00e9r\u0151l. Ez k\u00fcl\u00f6n\u00f6sen akkor fontos, ha a feladatot otthon k\u00e9sz\u00edted el, mert ezzel bizony\u00edtod, hogy a megold\u00e1sod saj\u00e1t magad k\u00e9sz\u00edtetted. A k\u00e9perny\u0151k\u00e9pek elv\u00e1rt tartalm\u00e1t a feladat minden esetben pontosan megnevezi. A k\u00e9perny\u0151k\u00e9p k\u00e9sz\u00fclhet a teljes desktopr\u00f3l is, de lehet csak a k\u00e9rt alkalmaz\u00e1sr\u00f3l k\u00e9sz\u00edteni.

    A k\u00e9perny\u0151k\u00e9peket a megold\u00e1s r\u00e9szek\u00e9nt kell beadni, \u00edgy felker\u00fclnek a git repository tartalm\u00e1val egy\u00fctt. Mivel a repository priv\u00e1t, azt az oktat\u00f3kon k\u00edv\u00fcl m\u00e1s nem l\u00e1tja. Amennyiben olyan tartalom ker\u00fcl a k\u00e9perny\u0151k\u00e9pre, amit nem szeretn\u00e9l felt\u00f6lteni, kitakarhatod a k\u00e9pr\u0151l.

    "},{"location":"#elvarasaink-a-munkaval-kapcsolatban","title":"Elv\u00e1r\u00e1saink a munk\u00e1val kapcsolatban","text":"

    Hova kell felt\u00f6lteni a megold\u00e1st? Fentebb megtal\u00e1lod a le\u00edr\u00e1st.

    Egy\u00e9ni munka? Otthoni munka? Mivel a laborokra jegyet kapsz, elv\u00e1r\u00e1s, hogy mindenki saj\u00e1t megold\u00e1st k\u00e9sz\u00edtsen el \u00e9s adjon be. Ez nem z\u00e1rja ki az egym\u00e1snak ny\u00fajtott seg\u00edts\u00e9get. Kiz\u00e1rja viszont m\u00e1s megold\u00e1s\u00e1nak lem\u00e1sol\u00e1s\u00e1t. Ez\u00e9rt k\u00e9rj\u00fck a k\u00e9perny\u0151k\u00e9peket, mert \u00edgy a munka folyamat\u00e1val bizony\u00edtod a megold\u00e1s saj\u00e1t elk\u00e9sz\u00edt\u00e9s\u00e9t.

    M\u00e1s munk\u00e1j\u00e1nak lem\u00e1sol\u00e1sa: A BME etikai k\u00f3dexe \u00e9s a TVSZ szab\u00e1lyozza. Komolyan vessz\u00fck.

    Egy labor csak 2 \u00f3ra, nem? Nem. A t\u00e1rgy 4 kredit, amely a f\u00e9l\u00e9v sor\u00e1n megk\u00f6zel\u00edt\u0151leg 120 munka\u00f3ra befektet\u00e9s\u00e9t ig\u00e9nyli. A labor teh\u00e1t nem csak a teremben elt\u00f6lt\u00f6tt 2 \u00f3ra, hanem az el\u0151zetes felk\u00e9sz\u00fcl\u00e9s \u00e9s a feladat befejez\u00e9se / otthoni elv\u00e9gz\u00e9se is.

    Egy apr\u00f3 el\u00edr\u00e1s miatt nem m\u0171k\u00f6d\u00f6tt a k\u00f3dom, \u00e9s nem \u00e9rt\u00e9kelt\u00e9tek. A laborok sor\u00e1n m\u0171k\u00f6d\u0151 programot, k\u00f3dot, k\u00f3dr\u00e9szletet kell k\u00e9sz\u00edteni. Az\u00e9rt sz\u00e1m\u00edt\u00f3g\u00e9p laborban vagy otthon k\u00e9sz\u00edtj\u00fck a feladatot, mert \u00edgy tudod magad ellen\u0151rizni. Minimum elv\u00e1r\u00e1s, hogy a beadott k\u00f3d leforduljon, lefusson. Ha a viselked\u00e9s nem teljesen helyes, azt \u00e9rt\u00e9kelj\u00fck. De ha egy\u00e1ltal\u00e1n nem m\u0171k\u00f6dik, nem \u00e9rt\u00e9kelj\u00fck a megold\u00e1st.

    Az\u00e9rt \u00edgy tesz\u00fcnk, mert m\u00e9rn\u00f6kk\u00e9nt a feladatod a probl\u00e9m\u00e1k megold\u00e1sa lesz, \u00e9s nem csak egy k\u00eds\u00e9rlet a megold\u00e1sra. Mit gondolsz, ha a munkahelyeden a f\u0151n\u00f6k\u00f6dnek \u00e1tadsz egy nem fordul\u00f3 k\u00f3dot, mit fog tenni?

    Ha otthonr\u00f3l k\u00e9sz\u00edtem el a megold\u00e1st, hogyan kapok seg\u00edts\u00e9get? Ak\u00e1r otthonr\u00f3l dolgozol, ak\u00e1r egyetemi laborban, egy laborvezet\u0151h\u00f6z tartozol. \u0150 felel nem csak a kontakt\u00f3ra megtart\u00e1s\u00e1\u00e9rt, hanem az\u00e9rt is, hogy a f\u00e9l\u00e9v k\u00f6zben a feladatok bead\u00e1sa \u00e9s ellen\u0151rz\u00e9se rendben t\u00f6rt\u00e9njen.

    Nem seg\u00edt a laborvezet\u0151. Mi\u00e9rt? Dehogynem seg\u00edt. Viszont ha egyb\u0151l megmondan\u00e1 a megold\u00e1st, csak azt tanuln\u00e1d meg, hogy legk\u00f6zelebb is meg kell k\u00e9rdezni. Pr\u00f3b\u00e1ld magad megoldani, mutass alternat\u00edv\u00e1kat, k\u00e9rdezz konkr\u00e9tan. Mutasd meg, hogy professzion\u00e1lis a hozz\u00e1\u00e1ll\u00e1sod.

    Akkor mit k\u00e9rdezhetek meg a laborvezet\u0151t\u0151l? R\u00f6viden: https://stackoverflow.com/help/how-to-ask. Hosszabban: Ha valamivel elakadsz, \u00e9rtsd meg a probl\u00e9m\u00e1t. A probl\u00e9ma nem az, hogy \"nem m\u0171k\u00f6dik\" vagy \"nem tudom, hogyan csin\u00e1ljam\". Akkor tudsz j\u00f3l k\u00e9rdezni, ha m\u00e1r k\u00f6r\u00fclj\u00e1rtad a probl\u00e9m\u00e1t, \u00e9s azt is meg tudod mutatni, mivel pr\u00f3b\u00e1lkozt\u00e1l m\u00e1r.

    Sz\u00f3val Google \u00e9s StackOverflow a megold\u00e1s? Nem. Minden tud\u00e1s, amire sz\u00fcks\u00e9ged van, m\u00e1r el\u0151fordult egyetemi tanulm\u00e1nyaid sor\u00e1n. A Google j\u00f3, a StackOverflow m\u00e9g jobb.... De! A v\u00e1laszt is meg kell \u00e9rteni. Lehet, hogy a megtal\u00e1lt v\u00e1lasz megold\u00e1s, csak \u00e9pp nem a te probl\u00e9m\u00e1dra.

    Sok a hat\u00e1rid\u0151, meg az el\u0151\u00edr\u00e1s. Ez n\u00e9z\u0151pont k\u00e9rd\u00e9se. A m\u00e9rn\u00f6k nem csak programozni tud, hanem meghat\u00e1rozott keretek k\u00f6z\u00f6tt dolgozni. Mert a vil\u00e1g bonyolult, \u00e9s a bonyolults\u00e1got szab\u00e1lyokkal lehet kord\u00e1ban tartani. Ha id\u0151d engedi, \u00e9rdemes megn\u00e9zni, mit mond Robert C. Martin (Bob Martin, \"Uncle Bob\") arr\u00f3l, honnan sz\u00e1rmazik a szoftverfejleszt\u0151i szakmai: https://www.youtube.com/watch?v=ecIWPzGEbFc

    "},{"location":"hf/","title":"H\u00e1zi feladat inform\u00e1ci\u00f3k","text":"

    A t\u00e1rgyb\u00f3l h\u00e1zi feladat k\u00e9sz\u00edt\u00e9se nem k\u00f6telez\u0151, de aj\u00e1nlott. H\u00e1zi feladat k\u00e9sz\u00edt\u00e9s\u00e9vel a vizsg\u00e1t kiv\u00e1ltva megaj\u00e1nlott 4-es vagy 5-\u00f6s szerezhet\u0151. A h\u00e1zi feladatra maximum 40 pont kaphat\u00f3, amib\u0151l a megaj\u00e1nlott jegyhez minimum 25 pontot el kell \u00e9rni. Akinek nem siker\u00fcl ezt a pontsz\u00e1mot megszerezni, az a vizsg\u00e1ra vihet maximum 20 pontot.

    "},{"location":"hf/#kovetelmenyek","title":"K\u00f6vetelm\u00e9nyek","text":"
    • Technolo\u00f3gi\u00e1k:
      • K\u00f6telez\u0151en:
        • Compose UI
        • MVVM vagy ezzel egyen\u00e9rt\u00e9k\u0171 egy\u00e9b architekt\u00fara
        • Dependency Injection
      • Legal\u00e1bb 3 komplexebb technol\u00f3gia haszn\u00e1lata, melyet minden esetben a laborvezet\u0151 d\u00f6nt el, hogy el\u00e9gs\u00e9ges-e, pl.:
        • perzisztencia,
        • h\u00e1l\u00f3zat,
        • Firebase,
        • poz\u00edci\u00f3meghat\u00e1roz\u00e1s,
        • komplex anim\u00e1ci\u00f3,
        • st\u00edlusok/t\u00e9m\u00e1k (komplex, teljes alkalmaz\u00e1sra kiterjed\u0151 kin\u00e9zet),
        • Service,
        • BroadcastReceiver,
        • Content Provider,
        • stb.
    • Az alkalmaz\u00e1s felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9hez Jetpack Compose-t kell haszn\u00e1lni.
    • Kotlin nyelven kell k\u00e9sz\u00fclnie.
    • \u00d6n\u00e1ll\u00f3 alkalmaz\u00e1s legal\u00e1bb 3-4 k\u00e9perny\u0151vel/n\u00e9zettel.
    • B\u00e1rmilyen k\u00fcls\u0151 k\u00f6nyvt\u00e1r haszn\u00e1lhat\u00f3 a fejleszt\u00e9shez, hogy m\u00e9g l\u00e1tv\u00e1nyosabb alkalmaz\u00e1sok k\u00e9sz\u00fcljenek:
      • https://github.com/wasabeef/awesome-android-ui
      • https://github.com/nisrulz/android-tips-tricks
      • https://foso.github.io/Jetpack-Compose-Playground
      • https://www.jetpackcompose.app/compose-catalog

    N\u00e9h\u00e1ny p\u00e9lda alkalmaz\u00e1s

    • Kiad\u00e1s/bev\u00e9tel nyomk\u00f6vet\u0151 figyelmeztet\u0151 funkci\u00f3val \u00e9s grafikonokkal
    • Turisztikai l\u00e1tv\u00e1nyoss\u00e1gokat gy\u0171jt\u0151 alkalmaz\u00e1s
    • Rakt\u00e1r kezel\u0151 alkalmaz\u00e1s
    • Sz\u00e1mla kezel\u0151 megold\u00e1s
    • Recept kezel\u0151 alkalmaz\u00e1s
    • Napl\u00f3 k\u00e9sz\u00edt\u0151 alkalmaz\u00e1s f\u00e9nyk\u00e9pekkel
    • Sport tracker alkalmaz\u00e1s
    • K\u00e9sz\u00fcl\u00e9k esem\u00e9ny napl\u00f3z\u00f3 alkalmaz\u00e1s
    • Apr\u00f3hirdet\u00e9s alkalmaz\u00e1s
    • Tal\u00e1lkoz\u00f3 szervez\u0151 alkalmaz\u00e1s
    • Sportfogad\u00f3 megold\u00e1s
    • Szaki keres\u0151 alkalmaz\u00e1s
    • J\u00e1t\u00e9k alkalmaz\u00e1s, pl. aknakeres\u0151, shooter, stb.
    • Valamilyen REST API-t haszn\u00e1l\u00f3 alkalmaz\u00e1s, p\u00e9ld\u00e1ul valuta v\u00e1lt\u00e1s, t\u0151zsdei inf\u00f3k, stb:
      • https://github.com/toddmotto/public-apis
      • https://github.com/Kikobeats/awesome-api
      • https://github.com/abhishekbanthia/Public-APIs
    • A h\u00e1zi feladat haszn\u00e1lhat felh\u0151 megold\u00e1st is, pl. Firebase, Amazon, stb.
    "},{"location":"hf/#beadas-modja","title":"Bead\u00e1s m\u00f3dja","text":"

    A h\u00e1zi feladat bead\u00e1s\u00e1nak platformja a laborokhoz hasonl\u00f3an a Github Classroom. A megh\u00edv\u00f3 a Moodle oldalon tal\u00e1lhat\u00f3.

    neptun.txt

    Az els\u0151 \u00e9s legfontosabb, hogy az eddigiekhez hasonl\u00f3an t\u00f6ltsd ki a neptun.txt f\u00e1jlt, hogy a rendszer azonos\u00edtani tudjon.

    "},{"location":"hf/#specifikacio","title":"Specifik\u00e1ci\u00f3","text":"

    A specifik\u00e1ci\u00f3 bead\u00e1s hat\u00e1rideje a 9. h\u00e9t v\u00e9ge (2024. november 3. 23:59). A specifik\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se k\u00f6zben a \"spec\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. Sablont a README.md f\u00e1jl tartalmaz, azt kell kieg\u00e9sz\u00edteni, \u00e9s felt\u00f6lteni a rep\u00f3ba a megadott hat\u00e1rid\u0151ig. A bead\u00e1s akkor teljes, ha a \"spec\" branch-en megtal\u00e1lhat\u00f3 a README.md f\u00e1jlban a specifik\u00e1ci\u00f3. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A specifik\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se el\u0151felt\u00e9tele a h\u00e1zi feladat elfogad\u00e1s\u00e1nak.

    "},{"location":"hf/#hazi-feladat","title":"H\u00e1zi feladat","text":"

    A h\u00e1zi feladat bead\u00e1s hat\u00e1rideje a 13. h\u00e9t v\u00e9ge (2024. december 1. 23:59). A h\u00e1zi feladat elk\u00e9sz\u00edt\u00e9se k\u00f6zben a \"hf\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. A projektet mindenk\u00e9ppen ebbe a repository-ba hozd l\u00e9tre, a fejleszt\u00e9st v\u00e9gig itt v\u00e9gezd. A bead\u00e1s akkor teljes, ha a \"hf\" branch-en megtal\u00e1lhat\u00f3 a projekted teljes forr\u00e1sk\u00f3dja. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A h\u00e1zi feladathoz mindenk\u00e9ppen tartozik h\u00e1zi feladat v\u00e9d\u00e9s is. Ennek ideje a bead\u00e1st k\u00f6vet\u0151en 14. heti laboron van.

    "},{"location":"hf/#hazi-feladat-potlas-fizeteskoteles","title":"H\u00e1zi feladat p\u00f3tl\u00e1s - fizet\u00e9sk\u00f6teles!","text":"

    A p\u00f3tbead\u00e1s hat\u00e1rideje a p\u00f3tl\u00e1si h\u00e9ten, laborvezet\u0151vel egyeztetve. A h\u00e1zi feladat p\u00f3tl\u00e1sa k\u00f6zben a \"pothf\" branchen dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. A bead\u00e1s akkor teljes, ha a \"pothf\" branch-en megtal\u00e1lhat\u00f3 a projekted teljes forr\u00e1sk\u00f3dja. A bead\u00e1st egy pull request jelzi, amely pull requestet a laborvezet\u0151dh\u00f6z kell rendelned. A h\u00e1zi feladat p\u00f3tl\u00e1s\u00e1hoz mindenk\u00e9ppen tartozik p\u00f3t h\u00e1zi feladat v\u00e9d\u00e9s is. Ennek m\u00f3dj\u00e1r\u00f3l \u00e9s idej\u00e9r\u0151l egyeztess a laborvezet\u0151ddel.

    "},{"location":"hf/#dokumentacio","title":"Dokument\u00e1ci\u00f3","text":"

    A h\u00e1zi feladatot a specifik\u00e1ci\u00f3n t\u00fal dokument\u00e1lni is kell a README.md f\u00e1jlba. (A specifik\u00e1ci\u00f3 ut\u00e1n.) Ebben r\u00f6viden ismertetni kell az elk\u00e9sz\u00fclt alkalmaz\u00e1s funkcionalit\u00e1s\u00e1t \u00e9s az \u00e9rdekesebb megold\u00e1sokat.

    Androidalap\u00fa szoftverfejleszt\u00e9s + Mobil- \u00e9s webes szoftverek k\u00f6z\u00f6s h\u00e1zi feladat

    Ha valaki mind a k\u00e9t t\u00e1rgyat hallgatja a f\u00e9l\u00e9vben, van lehet\u0151s\u00e9g k\u00f6z\u00f6s h\u00e1zi feladat \u00edr\u00e1s\u00e1ra, DE: - Ezt mindenk\u00e9ppen egyeztetni kell mindk\u00e9t laborvezet\u0151vel. - Ugyanaz a h\u00e1zi csak \u00fagy adhat\u00f3 le mindk\u00e9t t\u00e1rgyon, ha a nehezebb k\u00f6vetelm\u00e9nyeket (vagyis az Androidalap\u00fa szoftverfejleszt\u00e9s\u00e9t) fel\u00fclteljes\u00edti. Teh\u00e1t az Androidalap\u00fa szoftverfejleszt\u00e9s k\u00f6vetelm\u00e9nyei szerint nem 5, hanem 6-7 technol\u00f3gi\u00e1t kell haszn\u00e1lni. Ennek mennyis\u00e9g\u00e9r\u0151l \u00e9s a feladat komplexit\u00e1s\u00e1r\u00f3l a laborvezet\u0151k d\u00f6ntenek.

    "},{"location":"laborok/alarm/","title":"Labor10 - Id\u0151z\u00edt\u00e9s \u00e9s \u00e9rtes\u00edt\u00e9sek (Alarm)","text":""},{"location":"laborok/alarm/#bevezetes","title":"Bevezet\u00e9s","text":"

    Ebben a laborban egy id\u0151z\u00edt\u0151 alkalmaz\u00e1st k\u00e9sz\u00edt\u00fcnk, amely a be\u00e1ll\u00edtott id\u0151intervallum eltelte ut\u00e1n \u00e9rtes\u00edt\u00e9st k\u00fcld, akkor is, ha az alkalamz\u00e1s felhaszn\u00e1l\u00f3i fel\u00fclete nincs az el\u0151t\u00e9rben, mert k\u00f6zben a felhaszn\u00e1l\u00f3 m\u00e1sik alkalmaz\u00e1st ind\u00edtott.

    A laborban \u00e9rintett f\u0151bb t\u00e9mak\u00f6r\u00f6k:

    • H\u00e1tt\u00e9rfeladat futtat\u00e1sa Service seg\u00edts\u00e9g\u00e9vel
    • Id\u0151z\u00edtett feladatok
    • \u00c9rtes\u00edt\u00e9sek k\u00fcld\u00e9se
    "},{"location":"laborok/alarm/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/alarm/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    "},{"location":"laborok/alarm/#a-projekt-megnyitasa","title":"A projekt megnyit\u00e1sa","text":"

    Nyissuk meg a template-ben lev\u0151 projektet, \u00e9s a laborvezet\u0151vel tekints\u00fck \u00e1t a tartalm\u00e1t. A projektben a UI \u00e9p\u00edt\u0151elemei \u00e9s a drawable er\u0151forr\u00e1sok m\u00e1r megtal\u00e1lhat\u00f3k. Ezekhez fogjuk elk\u00e9sz\u00edteni az id\u0151z\u00edt\u0151 \u00fczleti logik\u00e1j\u00e1t, amit \u00f6sszek\u00f6t\u00fcnk az el\u0151k\u00e9sz\u00edtett felhaszn\u00e1l\u00f3i fel\u00fclettel.

    "},{"location":"laborok/alarm/#a-fuggosegek-beallitasa","title":"A f\u00fcgg\u0151s\u00e9gek be\u00e1ll\u00edt\u00e1sa","text":"

    Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket a modul szint\u0171 build.gradle.kts f\u00e1jlba. Ezekre az id\u0151 megad\u00e1s\u00e1hoz lesz majd sz\u00fcks\u00e9g\u00fcnk.

    implementation(\"com.maxkeppeler.sheets-compose-dialogs:core:1.0.3\")\nimplementation(\"com.maxkeppeler.sheets-compose-dialogs:clock:1.0.3\")\nimplementation(\"com.maxkeppeler.sheets-compose-dialogs:duration:1.0.3\")\n
    "},{"location":"laborok/alarm/#a-domenmodellek-elkeszitese","title":"A dom\u00e9nmodellek elk\u00e9sz\u00edt\u00e9se","text":"

    El\u0151sz\u00f6r n\u00e9h\u00e1ny olyan oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, amelyekkel az alkalmaz\u00e1s aktu\u00e1lis \u00e1llapota, illetve az \u00e1llapotv\u00e1ltoz\u00e1st el\u0151id\u00e9z\u0151 esem\u00e9nyek reprezent\u00e1lhat\u00f3k. Ehhez k\u00e9sz\u00edts\u00fcnk egy util package-et. Ebbe hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt:

    sealed class AlarmEvent {\ndata class SetAlarmDuration(val duration: Duration): AlarmEvent()\ndata class SetAlarm(val context: Context): AlarmEvent()\ndata class PauseAlarm(val context: Context): AlarmEvent()\ndata class ResumeAlarm(val context: Context): AlarmEvent()\ndata class StopAlarm(val context: Context): AlarmEvent()\n}\n

    Az oszt\u00e1ly tartalma k\u00f6nnyen meg\u00e9rthet\u0151, az egyes felhaszn\u00e1l\u00f3i interakci\u00f3kat mint lehets\u00e9ges esem\u00e9nyeket jelk\u00e9pezi.

    Most hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyokat egy f\u00e1jlban:

    enum class AlarmServiceState {\nIDLE, SET, PAUSE, CANCELED\n}\n\ndata class AlarmState(\nval currentAlarmDuration: Duration = Duration.ZERO,\nval alarmDuration: Duration = Duration.ZERO,\nval alarmState: AlarmServiceState = AlarmServiceState.IDLE,\n) {\ncompanion object {\nval state = MutableStateFlow(AlarmState())\n\nfun isAlarmSet(): Boolean = state.value.alarmState == AlarmServiceState.SET\n\nfun isAlarmPaused(): Boolean = state.value.alarmState == AlarmServiceState.PAUSE\n\nfun isAlarmIdle(): Boolean = state.value.alarmState == AlarmServiceState.IDLE || state.value.alarmState == AlarmServiceState.CANCELED\n\nfun isStopEnabled(): Boolean = state.value.alarmState == AlarmServiceState.SET\n\n}\n}\n

    Az AlarmServiceState lehets\u00e9ges \u00e9rt\u00e9kei \u00edrj\u00e1k le, hogy az id\u0151z\u00edt\u0151 \u00e9pp v\u00e1rakozik, fut, pauz\u00e1lt vagy le\u00e1ll\u00edtott. Az AlarmState ezt az aktu\u00e1lis \u00e1llapotot t\u00e1rolja, \u00e9s mell\u00e9 m\u00e9g egys\u00e9gbe z\u00e1rja, hogy mennyi az id\u0151z\u00edt\u0151 c\u00e9lideje, \u00e9s mennyi telt el eddig.

    "},{"location":"laborok/alarm/#a-felhasznaloi-felulet-es-a-domenmodell-osszekotese","title":"A felhaszn\u00e1l\u00f3i fel\u00fclet \u00e9s a dom\u00e9nmodell \u00f6sszek\u00f6t\u00e9se","text":"

    A felhaszn\u00e1l\u00f3i fel\u00fclet\u00fcnk a template-ben m\u00e1r rendelkez\u00e9sre \u00e1ll, de a viewmodelleket m\u00e9g el kell k\u00e9sz\u00edten\u00fcnk, hogy az im\u00e9nt l\u00e9trehozott dom\u00e9nmodellel a UI-t \u00f6ssze tudjuk kapcsolni. A ui.alarm package-be hozzuk l\u00e9tre a ViewModel\u00fcnket:

    @HiltViewModel\nclass AlarmViewModel @Inject constructor(): ViewModel() {\n\nprivate val _state = AlarmState.state\nval state = _state.asStateFlow()\n\nfun onEvent(event: AlarmEvent) {\nwhen(event) {\nis AlarmEvent.SetAlarmDuration -> {\n_state.update { it.copy(\ncurrentAlarmDuration = event.duration,\nalarmDuration = event.duration\n) }\n}\nis AlarmEvent.SetAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.PauseAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.PAUSE,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.ResumeAlarm -> {\nval context = event.context\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\nis AlarmEvent.StopAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\n\n// TODO: Service kommunik\u00e1ci\u00f3\n}\n}\n}\n}\n

    L\u00e1that\u00f3, hogy a ViewModel kezeli az esem\u00e9nyeket, \u00e9s az alkalmaz\u00e1s \u00e1llapot\u00e1t ennek megfelel\u0151en friss\u00edti, azonban a h\u00e1tt\u00e9rben fut\u00f3, t\u00e9nyleges id\u0151z\u00edt\u00e9st v\u00e9gz\u0151 AlarmService m\u00e9g nincs k\u00e9sz, \u00edgy annak h\u00edv\u00e1sai ide nincsenek bek\u00f6tve.

    Most ugyanebbe a package-ben hozzuk l\u00e9tre a teljes k\u00e9perny\u0151t is:

    import androidx.compose.animation.ExperimentalAnimationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.maxkeppeker.sheets.core.models.base.rememberSheetState\nimport com.maxkeppeler.sheets.duration.DurationDialog\nimport com.maxkeppeler.sheets.duration.models.DurationConfig\nimport com.maxkeppeler.sheets.duration.models.DurationFormat\nimport com.maxkeppeler.sheets.duration.models.DurationSelection\nimport hu.bme.aut.android.alarm.R\nimport hu.bme.aut.android.alarm.ui.common.AlarmStateButton\nimport hu.bme.aut.android.alarm.ui.common.DurationCounter\nimport hu.bme.aut.android.alarm.ui.theme.pauseColor\nimport hu.bme.aut.android.alarm.ui.theme.startColor\nimport hu.bme.aut.android.alarm.ui.theme.stopColor\nimport hu.bme.aut.android.alarm.util.AlarmEvent\nimport hu.bme.aut.android.alarm.util.AlarmState\nimport kotlin.time.DurationUnit\nimport kotlin.time.toDuration\n\n@OptIn(ExperimentalMaterial3Api::class)\n@ExperimentalPermissionsApi\n@ExperimentalAnimationApi\n@Composable\nfun AlarmScreen(\nviewModel: AlarmViewModel = hiltViewModel()\n) {\n\nval state by viewModel.state.collectAsState()\n\nval context = LocalContext.current\n\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\nhorizontalAlignment = Alignment.CenterHorizontally,\nverticalArrangement = Arrangement.Center\n) {\n\nvar isDialogShown by remember { mutableStateOf(false) }\nDurationCounter(\nduration = state.currentAlarmDuration,\nclickEnabled = AlarmState.isAlarmIdle(),\nonClick = { isDialogShown = !isDialogShown }\n)\n\nif (isDialogShown) {\nPopup(\nalignment = Alignment.Center,\nonDismissRequest = {\nisDialogShown = !isDialogShown\n}\n) {\nBox(\nmodifier = Modifier.wrapContentSize()\n) {\nSurface(\nshape = MaterialTheme.shapes.medium,\ncolor = MaterialTheme.colorScheme.surface,\n) {\nDurationDialog(\nstate = rememberSheetState(\nvisible = true,\nonCloseRequest = { isDialogShown = !isDialogShown }\n),\nselection = DurationSelection {\nval duration = it.toDuration(DurationUnit.SECONDS)\nviewModel.onEvent(AlarmEvent.SetAlarmDuration(duration))\n},\nconfig = DurationConfig(\ntimeFormat = DurationFormat.HH_MM_SS,\ncurrentTime = 0L,\n),\n)\n}\n}\n}\n}\n\nRow(\nmodifier = Modifier\n.padding(vertical = 10.dp)\n.wrapContentSize(Alignment.Center)\n) {\nAlarmStateButton(\niconId = if (AlarmState.isAlarmSet() && !AlarmState.isAlarmPaused()) {\nR.drawable.pause_24\n} else R.drawable.play_24,\nsurfaceColor = if (AlarmState.isAlarmSet() && !AlarmState.isAlarmPaused()) {\nMaterialTheme.colorScheme.pauseColor\n} else MaterialTheme.colorScheme.startColor,\n) {\nif (!AlarmState.isAlarmSet()) {\nviewModel.onEvent(AlarmEvent.SetAlarm(context))\n} else {\nif (AlarmState.isAlarmPaused()) {\nviewModel.onEvent(AlarmEvent.ResumeAlarm(context))\n} else {\nviewModel.onEvent(AlarmEvent.PauseAlarm(context))\n}\n}\n}\nSpacer(modifier = Modifier.width(5.dp))\nAlarmStateButton(\niconId = R.drawable.stop_24,\nsurfaceColor = MaterialTheme.colorScheme.stopColor,\nenabled = AlarmState.isStopEnabled()\n) {\nviewModel.onEvent(AlarmEvent.StopAlarm(context))\n}\n}\n\n}\n}\n

    M\u00e9g l\u00e9tre kell hoznunk az AlarmApplication oszt\u00e1lyt puszt\u00e1n a Hilt inicializ\u00e1l\u00e1s\u00e1hoz:

    @HiltAndroidApp\nclass AlarmApplication : Application()\n

    \u00c9s az oszt\u00e1lyt regisztr\u00e1ljuk is be a Manifest f\u00e1jlban:

        <application\nandroid:name=\".AlarmApplication\"\n\n...\n

    B\u00e1r m\u00e9g az alkalmaz\u00e1slogik\u00e1nk nem m\u0171k\u00f6dik t\u00e9nylegesen, vegy\u00fck fel ide a sz\u00fcks\u00e9ges enged\u00e9lyeket is. Az alkalmaz\u00e1sunk egy el\u0151t\u00e9rben fut\u00f3 (foreground) Service-t fog haszn\u00e1lni ahhoz, hogy az id\u0151z\u00edt\u00e9s akkor is m\u0171k\u00f6dj\u00f6n, ha az alkalmaz\u00e1s Activity-je m\u00e1r nem l\u00e1that\u00f3. Az ilyen Service-ekhez k\u00f6telez\u0151, hogy legyen \u00e9rtes\u00edt\u00e9s az \u00e9rtes\u00edt\u00e9si s\u00e1von, hogy a felhaszn\u00e1l\u00f3 mindig l\u00e1ssa, hogy milyen alkalmaz\u00e1sok futnak a h\u00e1tt\u00e9rben. (Ennek egy tipikus p\u00e9ld\u00e1ja a zenelej\u00e1tsz\u00f3 alkalmaz\u00e1sok esete is. Ez\u00e9rt az \u00e9rtes\u00edt\u00e9sekhez sz\u00fcks\u00e9ges enged\u00e9lyt is fel kell venni. Sz\u00fcks\u00e9ges m\u00e9g a pontos riaszt\u00e1s, \u00e9s a pontos riaszt\u00e1s id\u0151z\u00edt\u00e9s\u00e9nek enged\u00e9lye:

    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_SPECIAL_USE\" />\n\n<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n<uses-permission android:name=\"android.permission.SCHEDULE_EXACT_ALARM\" />\n<uses-permission android:name=\"android.permission.USE_EXACT_ALARM\" />\n

    Android 14 (API level 34) \u00f3ta az el\u0151t\u00e9rben fut\u00f3 service-ek t\u00edpus\u00e1t is sz\u00fcks\u00e9ges megadni a Manifestben az enged\u00e9lyn\u00e9l/Service komponensn\u00e9l, illetve a Service ind\u00edt\u00e1sakor. L\u00e1sd: https://developer.android.com/develop/background-work/services/fg-service-types

    M\u00e9g a MainActivity elk\u00e9sz\u00edt\u00e9se maradt h\u00e1tra az alapvet\u0151 feladatok k\u00f6z\u00fcl. Ebben elk\u00e9rj\u00fck az enged\u00e9lyeket, \u00e9s megjelen\u00edtj\u00fck a l\u00e9trehozott k\u00e9perny\u0151t:

    @ExperimentalPermissionsApi\n@ExperimentalAnimationApi\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\n\nsetContent {\nAlarmTheme {\nAlarmScreen()\n}\n}\n\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\nrequestPermissions(\nManifest.permission.POST_NOTIFICATIONS\n)\n}\n}\n\nprivate fun requestPermissions(vararg permissions: String) {\nval requestPermissionLauncher = registerForActivityResult(\nActivityResultContracts.RequestMultiplePermissions()\n) { result ->\nresult.entries.forEach {\nLog.d(\"MainActivity\", \"${it.key} = ${it.value}\")\n}\n}\nrequestPermissionLauncher.launch(permissions.asList().toTypedArray())\n}\n}\n

    Ha a POST_NOTIFICATIONS konstansot nem tal\u00e1lja a ford\u00edt\u00f3, akkor az import android.Manifest sort vegy\u00fck fel a f\u00e1jl elej\u00e9re.

    Most m\u00e1r ind\u00edthat\u00f3 az alkalmaz\u00e1s, \u00e9s megjelenik a felhaszn\u00e1l\u00f3i fel\u00fclet, de m\u00e9g nem m\u0171k\u00f6dik \u00e9rdemi m\u00f3don, hiszen a az \u00fczleti logik\u00e1t v\u00e9gz\u0151 Service nincs k\u00e9sz.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/alarm/#az-idozites-elkeszitese","title":"Az id\u0151z\u00edt\u00e9s elk\u00e9sz\u00edt\u00e9se","text":"

    A Service komponens\u00fcnk a fentiek alapj\u00e1n id\u0151z\u00edt\u00e9st \u00e9s \u00e9rtes\u00edt\u00e9seket is fog haszn\u00e1lni, ez\u00e9rt el\u0151sz\u00f6r ezekhez k\u00e9sz\u00edt\u00fcnk n\u00e9mi seg\u00e9dlogik\u00e1t, hogy a Service ut\u00e1na k\u00f6nnyebben elk\u00e9sz\u00edthet\u0151 legyen.

    Az id\u0151z\u00edt\u00e9st kiszervezz\u00fck egy seg\u00e9doszt\u00e1lyba, hogy a Service k\u00f3dja \u00e1tl\u00e1that\u00f3bb maradjon. El\u0151sz\u00f6r egy AlarmItem oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, ez egy id\u0151z\u00edtend\u0151 esem\u00e9nyt jel\u00f6l, tulajdonk\u00e9ppen csak az id\u0151z\u00edt\u00e9s idej\u00e9t tartalmazza. Ezt egy \u00faj, scheduler nev\u0171 package-be tegy\u00fck:

    data class AlarmItem(\nval time: LocalDateTime\n)\n

    Szint\u00e9n ebbe a package-be k\u00e9sz\u00edt\u00fcnk egy AlarmReceiver nev\u0171 BroadcastReceivert, ezt fogjuk beregisztr\u00e1lni majd az id\u0151z\u00edt\u00e9s letelt\u00e9re, \u00e9s az Android id\u0151z\u00edt\u0151 rendszere az id\u0151 leteltekor ennek fog \u00e9rtes\u00edt\u00e9st k\u00fcldeni. Ebben egy Intent seg\u00edts\u00e9g\u00e9vel a Service komponenst fogjuk megh\u00edvni, de mivel ez m\u00e9g nincs k\u00e9sz, \u00edgy a k\u00f3d jelenleg nem fordul le, majd a Service elk\u00e9sz\u00fcltekor v\u00e1lik az eg\u00e9sz m\u0171k\u00f6d\u0151k\u00e9pess\u00e9. Ezt az oszt\u00e1lyt is a scheduler package-be tegy\u00fck:

    class AlarmReceiver: BroadcastReceiver() {\n\noverride fun onReceive(context: Context?, intent: Intent?) {\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_PLAY\ncontext?.startService(this)\n}\n}\n}\n

    Mivel a BroadcastReceiver is egy f\u0151 komponenst\u00edpus Androidon, ez\u00e9rt ezt a Manifest f\u00e1jlban is sz\u00fcks\u00e9ges regisztr\u00e1lni:

    <receiver android:name=\".scheduler.AlarmReceiver\" />\n

    \u00c9s v\u00e9g\u00fcl egy AlarmScheduler oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk:

    class AlarmScheduler @Inject constructor(\n@ApplicationContext private val context: Context,\n) {\nprivate val alarmManager = context.getSystemService(AlarmManager::class.java)\n\nfun schedule(alarmItem: AlarmItem) {\nval intent = Intent(context, AlarmReceiver::class.java).apply {\nputExtra(\"ALARM_TIME\", alarmItem.time)\n}\nalarmManager.setExactAndAllowWhileIdle(\nAlarmManager.RTC_WAKEUP,\nalarmItem.time.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000,\nPendingIntent.getBroadcast(\ncontext,\nalarmItem.hashCode(),\nintent,\nPendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n)\n)\n}\n\nfun cancel(item: AlarmItem) {\nalarmManager.cancel(\nPendingIntent.getBroadcast(\ncontext,\nitem.hashCode(),\nIntent(context, AlarmReceiver::class.java),\nPendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n)\n)\n}\n\n}\n

    Ebben az oszt\u00e1lyban a rendszert\u0151l k\u00e9r\u00fcnk referenci\u00e1t az AlarmManagerre, amelynek seg\u00edts\u00e9g\u00e9vel a rendszerben lev\u0151 id\u0151z\u00edt\u0151 szolg\u00e1ltat\u00e1sok el\u00e9rhet\u0151ek. Az oszt\u00e1ly k\u00e9t met\u00f3dust defini\u00e1l, az egyikkel esem\u00e9nyt id\u0151z\u00edthet\u00fcnk, a m\u00e1sikkal pedig megl\u00e9v\u0151 id\u0151z\u00edt\u00e9st t\u00f6r\u00f6lhet\u00fcnk.

    "},{"location":"laborok/alarm/#az-ertesitesek-kuldese","title":"Az \u00e9rtes\u00edt\u00e9sek k\u00fcld\u00e9se","text":"

    Az \u00e9rtes\u00edt\u00e9sek k\u00fcld\u00e9s\u00e9t is kiszervezz\u00fck, hogy a Service oszt\u00e1lyunk egyszer\u0171bb legyen. El\u0151sz\u00f6r is a notification package-et hozzuk l\u00e9tre, \u00e9s ebben k\u00e9sz\u00edts\u00fck el a NotificationHelper oszt\u00e1lyt:

    class NotificationHelper @Inject constructor(\nval notificationManager: NotificationManager,\nval notificationBuilder: NotificationCompat.Builder\n) {\nfun updateNotification(\nhours: Int,\nminutes:Int,\nseconds:Int,\ndurationInSeconds: Int,\n) {\nval progress = durationInSeconds - hours * 60 * 60 - minutes * 60 - seconds\nnotificationManager.notify(\nNOTIFICATION_ID,\nnotificationBuilder\n.setProgress(durationInSeconds,progress,false)\n.setContentText(\nString.format(\"%02d:%02d:%02d\",hours,minutes,seconds)\n).build()\n)\n}\n\nfun cancelNotification() {\nnotificationManager.cancel(NOTIFICATION_ID)\n}\n\n@SuppressLint(\"RestrictedApi\")\nfun setNotificationButton(vararg actions: NotificationCompat.Action) {\nnotificationBuilder.mActions.clear()\nactions.forEachIndexed { index, action ->\nnotificationBuilder.mActions.add(index, action)\n}\n}\n\nfun createNotificationChannel() {\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\nval alarmChannel = NotificationChannel(\nNOTIFICATION_CHANNEL_ID,\nNOTIFICATION_CHANNEL_NAME,\nNotificationManager.IMPORTANCE_LOW\n)\nnotificationManager.createNotificationChannel(alarmChannel)\n}\n}\n\ncompanion object {\nconst val NOTIFICATION_ID = 101\nconst val NOTIFICATION_CHANNEL_NAME = \"ALARM_NOTIFICATION\"\nconst val NOTIFICATION_CHANNEL_ID = \"ALARM_NOTIFICATION_ID\"\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    Tekints\u00fck \u00e1t, hogyan \u00e9p\u00fcl fel ez az oszt\u00e1ly!

    • K\u00e9t f\u00fcgg\u0151s\u00e9ge van: a NotificationManager seg\u00edts\u00e9g\u00e9vel kezelhet\u0151k az \u00e9rtes\u00edt\u00e9sek a rendszeren, viszont az ennek \u00e1tadott \u00e9rtes\u00edt\u00e9sek ak\u00e1r el\u00e9g \u00f6sszetettek is lehetnek, ez\u00e9rt a builder tervez\u00e9si minta szerint a NotificationBuilder seg\u00edts\u00e9g\u00e9vel kell \u0151ket fel\u00e9p\u00edten\u00fcnk.
    • Az updateNotification l\u00e9trehozza vagy friss\u00edti az \u00e9rtes\u00edt\u00e9st, \u00e9s ezen megjelenik a h\u00e1tralev\u0151 id\u0151 \u00f3ra, perc, m\u00e1sodperc bont\u00e1sban, valamint egy folyamatjelz\u0151 s\u00e1v, \"progress bar\" is, amelyet a h\u00e1tralev\u0151 id\u0151 alapj\u00e1n sz\u00e1m\u00edtunk. L\u00e1that\u00f3, hogy egy numerikus azonos\u00edt\u00f3t is megadunk az \u00e9rtes\u00edt\u00e9shez, ez teszi lehet\u0151v\u00e9, hogy ut\u00e1na majd t\u00f6r\u00f6lni tudjuk.
    • A cancelNotification t\u00f6rli a m\u00e1r l\u00e9trehozott \u00e9rtes\u00edt\u00e9st.
    • A setNotificationButton seg\u00edts\u00e9g\u00e9vel gombokat adhatunk az \u00e9rtes\u00edt\u00e9shez. Hogy milyen \u00e9s h\u00e1ny gombot szeretn\u00e9nk hozz\u00e1adni, ez az id\u0151z\u00edt\u0151 \u00e1llapot\u00e1t\u00f3l f\u00fcgg, ez\u00e9rt hasznos, hogy k\u00fcl\u00f6n met\u00f3dussal adhassuk meg. P\u00e9ld\u00e1ul sz\u00fcneteltet\u00e9st v\u00e9gz\u0151 gomb hozz\u00e1ad\u00e1s\u00e1nak akkor van \u00e9rtelme, ha az id\u0151z\u00edt\u0151 \u00e9pp fut.

    • A createNotificationChannel \u00e9rtes\u00edt\u00e9si csatorn\u00e1t hoz l\u00e9tre. Az Android O \u00f3ta az \u00e9rtes\u00edt\u00e9seket csatorn\u00e1khoz kell rendelni. Ez az\u00e9rt hasznos, mert a felhaszn\u00e1l\u00f3 az alkalmaz\u00e1son bel\u00fcl csatorn\u00e1nk\u00e9nt tudja tiltani vagy enged\u00e9lyezni a csatorn\u00e1kat. Pl. a fontos \u00e9rtes\u00edt\u00e9sek k\u00fcl\u00f6n csatorn\u00e1ra szervezhet\u0151k, \u00e9s az elhanyagolhat\u00f3bb jelent\u0151s\u00e9g\u0171eket a felhaszn\u00e1l\u00f3 k\u00f6nnyed\u00e9n letilthatja. Jelen alkalmaz\u00e1sban ezt a lehet\u0151s\u00e9get nem haszn\u00e1ljuk ki, csup\u00e1n egy csatorn\u00e1nk lesz, de azt akkor is l\u00e9tre kell hozzuk. A csatorna t\u00f6bbsz\u00f6ri l\u00e9trehoz\u00e1sa nem okoz probl\u00e9m\u00e1t, ez\u00e9rt el\u00e9g minden \u00e9rtes\u00edt\u00e9s el\u0151tt megh\u00edvni, hogy biztosan l\u00e9trej\u00f6jj\u00f6n, nem sz\u00fcks\u00e9ges tesztelni, hogy l\u00e9tezik-e.

    L\u00e1that\u00f3, hogy a NotificationManager \u00e9s a NotificationBuilder a NotificationHelper f\u00fcgg\u0151s\u00e9gei, amelyeket a konstruktoron kereszt\u00fcl kap meg a Dagger/Hilt seg\u00edts\u00e9g\u00e9vel. Viszont ehhez m\u00e9g konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges, hogy ezek a komponensek val\u00f3ban l\u00e9trej\u00f6jjenek. Hozzuk l\u00e9tre a notification.di package-et, majd ebben az al\u00e1bbi modult:

    @ExperimentalAnimationApi\n@Module\n@InstallIn(ServiceComponent::class)\nobject NotificationModule {\n\n@Provides\n@ServiceScoped\nfun provideNotificationBuilder(\n@ApplicationContext context: Context\n) = NotificationCompat.Builder(context,NOTIFICATION_CHANNEL_ID)\n.setSmallIcon(R.drawable.ic_round_alarm_24)\n.setContentTitle(context.getString(R.string.alarm_notification_title))\n.setContentText(\"00:00\")\n.setOngoing(true)\n.addAction(\nR.drawable.ic_round_stop_24,\ncontext.getString(R.string.alarm_notification_action_cancel),\nnull\n)\n.setContentIntent(null)\n\n@Provides\n@ServiceScoped\nfun provideNotificationManager(\n@ApplicationContext context: Context\n) = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n@Provides\n@ServiceScoped\nfun provideNotificationHelper(\nnotificationBuilder: NotificationCompat.Builder,\nnotificationManager: NotificationManager\n) = NotificationHelper(notificationManager, notificationBuilder)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/alarm/#a-service-elkeszitese","title":"A Service elk\u00e9sz\u00edt\u00e9se","text":"

    Hozzunk l\u00e9tre egy service package-et, majd ebbe k\u00e9sz\u00edts\u00fck el az AlarmService oszt\u00e1lyt:

    import android.app.PendingIntent\nimport android.app.Service\nimport android.content.Intent\nimport android.media.MediaPlayer\nimport android.media.RingtoneManager\nimport android.os.Build\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport dagger.hilt.android.AndroidEntryPoint\nimport hu.bme.aut.android.alarm.notification.NotificationHelper\nimport hu.bme.aut.android.alarm.notification.NotificationHelper.Companion.NOTIFICATION_ID\nimport hu.bme.aut.android.alarm.scheduler.AlarmItem\nimport hu.bme.aut.android.alarm.scheduler.AlarmScheduler\nimport hu.bme.aut.android.alarm.util.AlarmServiceState\nimport hu.bme.aut.android.alarm.util.AlarmState\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport java.time.LocalDateTime\nimport java.util.Timer\nimport javax.inject.Inject\nimport kotlin.concurrent.fixedRateTimer\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n@AndroidEntryPoint\nclass AlarmService : Service(), MediaPlayer.OnPreparedListener {\n\n@Inject\nlateinit var notificationHelper: NotificationHelper\n\n@Inject\nlateinit var alarmScheduler: AlarmScheduler\n\nprivate val _state = AlarmState.state\nprivate val state = _state.asStateFlow()\n\nprivate lateinit var timer: Timer\n\nprivate var alarmItem: AlarmItem? = null\n\nprivate var mMediaPlayer: MediaPlayer? = null\n\noverride fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\nintent?.action.let { action ->\nwhen (action) {\nACTION_SET -> {\nintent?.getStringExtra(ALARM_TIME)?.let { durationString ->\nDuration.parse(durationString).let { duration ->\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\ncurrentAlarmDuration = duration\n) }\n}\n}\n\nalarmItem = AlarmItem(\nLocalDateTime.now().plusSeconds(\nstate.value.currentAlarmDuration.inWholeSeconds\n))\nalarmItem?.let(alarmScheduler::schedule)\n\nnotificationHelper.createNotificationChannel()\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Pause\",\npauseAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)\n{\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build(),\nFOREGROUND_SERVICE_TYPE_SPECIAL_USE\n)\n}\nelse\n{\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\n}\nsetAlarm { h, m, s ->\nnotificationHelper.updateNotification(\nh, m, s,\nstate.value.alarmDuration.inWholeSeconds.toInt()\n)\n}\n}\nACTION_PAUSE -> {\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Resume\",\nresumeAlarm()\n)\n)\nnotificationHelper.notificationManager.notify(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\n_state.update { it.copy(alarmState = AlarmServiceState.PAUSE) }\n\nalarmItem?.let(alarmScheduler::cancel)\n}\nACTION_CANCEL -> {\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n_state.update { it.copy(alarmState = AlarmServiceState.CANCELED) }\nnotificationHelper.cancelNotification()\nif (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {\nstopForeground(STOP_FOREGROUND_REMOVE)\n} else stopForeground(STOP_FOREGROUND_DETACH)\nstopSelf()\nalarmItem?.let(alarmScheduler::cancel)\nmMediaPlayer?.stop()\n}\nACTION_PLAY -> {\nmMediaPlayer = MediaPlayer()\nmMediaPlayer?.apply {\nreset()\nsetDataSource(\nthis@AlarmService,\nRingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)\n)\nsetOnPreparedListener(this@AlarmService)\nprepareAsync()\n}\n}\nelse -> Unit\n}\n}\n\nwhen(intent?.getStringExtra(ALARM_STATE)) {\nAlarmServiceState.SET.name -> {\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\nalarmItem = AlarmItem(\nLocalDateTime.now().plusSeconds(\nstate.value.currentAlarmDuration.inWholeSeconds\n)\n)\n\nalarmItem?.let(alarmScheduler::schedule)\n\nnotificationHelper.createNotificationChannel()\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Pause\",\npauseAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nstartForeground(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\nsetAlarm { h, m, s ->\nnotificationHelper.updateNotification(\nh, m, s,\nstate.value.alarmDuration.inWholeSeconds.toInt()\n)\n}\n}\nAlarmServiceState.PAUSE.name -> {\n_state.update { it.copy(alarmState = AlarmServiceState.PAUSE) }\n\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n\nnotificationHelper.setNotificationButton(\nNotificationCompat.Action(\n0,\n\"Resume\",\nresumeAlarm()\n),\nNotificationCompat.Action(\n0,\n\"Cancel\",\ncancelAlarm()\n)\n)\nnotificationHelper.notificationManager.notify(\nNOTIFICATION_ID,\nnotificationHelper.notificationBuilder.build()\n)\nalarmItem?.let(alarmScheduler::cancel)\n}\nAlarmServiceState.CANCELED.name -> {\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.IDLE,\n) }\n\nif (this::timer.isInitialized) {\ntimer.cancel()\n}\n\nnotificationHelper.cancelNotification()\nif (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {\nstopForeground(STOP_FOREGROUND_REMOVE)\n} else stopForeground(STOP_FOREGROUND_DETACH)\nstopSelf()\nalarmItem?.let(alarmScheduler::cancel)\nmMediaPlayer?.stop()\n}\n}\nreturn super.onStartCommand(intent, flags, startId)\n}\n\nprivate fun setAlarm(onTick: (hours: Int, minutes: Int, seconds: Int) -> Unit) {\ntimer = fixedRateTimer(initialDelay = 1000L, period = 1000L) {\nif (state.value.currentAlarmDuration != Duration.ZERO) {\nval newDuration = state.value.currentAlarmDuration - 1.seconds\n_state.update { it.copy(currentAlarmDuration = newDuration) }\nstate.value.currentAlarmDuration.toComponents { hours, minutes, seconds, _ ->\nonTick(hours.toInt(),minutes,seconds)\n}\n}\n}\n}\n\nprivate fun cancelAlarm(): PendingIntent {\nval cancelIntent = Intent(this, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.CANCELED.name)\n}\nreturn PendingIntent.getService(\nthis, CANCEL_REQUEST_CODE, cancelIntent, flag\n)\n}\n\nprivate fun resumeAlarm(): PendingIntent {\nval resumeIntent = Intent(this, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.SET.name)\n}\nreturn PendingIntent.getService(\nthis, RESUME_REQUEST_CODE, resumeIntent, flag\n)\n}\n\nprivate fun pauseAlarm(): PendingIntent {\nval pauseIntent = Intent(applicationContext, AlarmService::class.java).apply {\nputExtra(ALARM_STATE, AlarmServiceState.PAUSE.name)\n}\nreturn PendingIntent.getService(\nthis, PAUSE_REQUEST_CODE, pauseIntent, flag\n)\n}\n\noverride fun onDestroy() {\nsuper.onDestroy()\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\nif (this::timer.isInitialized) timer.cancel()\nmMediaPlayer?.release()\nmMediaPlayer = null\n}\n\noverride fun onBind(p0: Intent?): IBinder? = null\n\ncompanion object {\nconst val ALARM_STATE = \"ALARM_STATE\"\n\nconst val ACTION_SET = \"ALARM_SET\"\nconst val ACTION_PAUSE = \"ALARM_PAUSE\"\nconst val ACTION_CANCEL = \"ALARM_CANCEL\"\nconst val ACTION_PLAY = \"ALARM_PLAY\"\n\nconst val ALARM_TIME = \"ALARM_TIME\"\n\nprivate const val flag = PendingIntent.FLAG_IMMUTABLE\n\nprivate const val CANCEL_REQUEST_CODE = 102\nprivate const val RESUME_REQUEST_CODE = 103\nprivate const val PAUSE_REQUEST_CODE = 104\n}\n\noverride fun onPrepared(mediaPlayer: MediaPlayer) {\nmediaPlayer.start()\n}\n}\n

    Tekints\u00fck \u00e1t, hogyan is m\u0171k\u00f6dik a fenti Service! Egy Android Service k\u00e9t m\u00f3don m\u0171k\u00f6dhet. Lehet \"Started Service\", ez azt jelenti, hogy indul\u00e1s ut\u00e1n a h\u00e1tt\u00e9rben (Background Service) teszi a dolg\u00e1t, am\u00edg az alkalmaz\u00e1s valamilyen komponense l\u00e1that\u00f3, vagy ak\u00e1r akkor is m\u0171k\u00f6dik, amikor az alkalmaz\u00e1s nem l\u00e1that\u00f3 (Foreground Service), viszont ilyenkor egy \u00e9rtes\u00edt\u00e9st k\u00f6telez\u0151 megjelen\u00edtenie, hogy a felhaszn\u00e1l\u00f3 sz\u00e1m\u00e1ra ne maradhasson \u00e9szrev\u00e9tlen (biztons\u00e1gi okokb\u00f3l). Az AlarmService az alkalmaz\u00e1sunkban egy Foreground Service-k\u00e9nt m\u0171k\u00f6dik, hogy az id\u0151z\u00edt\u0151 akkor se \u00e1lljon le, ha az alkalmaz\u00e1s fel\u00fclete m\u00e1r nincs el\u0151t\u00e9rben. A Service m\u00e1sik lehets\u00e9ges m\u0171k\u00f6d\u00e9si m\u00f3dja a \"Bound Service\". Ez azt jelenti, hogy m\u00e1s komponensek kapcsol\u00f3dhatnak hozz\u00e1, \u00e9s feladatokat v\u00e9geztethetnek vele. Addig m\u0171k\u00f6dik ilyen m\u00f3don, am\u00edg legal\u00e1bb egy kapcsol\u00f3d\u00f3 komponens van. Egy Service lehetne egyszerre \"Started Service\" \u00e9s \"Bound Service\" is, de most ut\u00f3bbi funkcionalit\u00e1sra nincs sz\u00fcks\u00e9g az alkalmaz\u00e1sban, mert \"Started Service\"-k\u00e9nt k\u00e9pes az \u00e1llapotot friss\u00edteni, aminek v\u00e1ltoz\u00e1sa ut\u00e1na a felhaszn\u00e1l\u00f3i fel\u00fcleten is megjelenik.

    N\u00e9zz\u00fck \u00e1t el\u0151bb az AlarmService seg\u00e9dmet\u00f3dusait:

    • setAlarm: ez elind\u00edtja az id\u0151z\u00edt\u00e9st, \u00e9s ehhez be\u00e1ll\u00edt egy m\u00e1sodpercenk\u00e9nti \u00fctemez\u0151t is, amellyel majd lehet friss\u00edteni a felhaszn\u00e1l\u00f3i fel\u00fcleten, illetve a megjelen\u00edtett \u00e9rtes\u00edt\u00e9sben kijelzett h\u00e1tralev\u0151 id\u0151t. Ez a met\u00f3dus callbackk\u00e9nt kapja meg, hogy mit kell futtatnia, amikor egy m\u00e1sodperc eltelt.
    • cancelAlarm: ez egy PendingIntentet gy\u00e1rt le, amellyel meg\u00e1ll\u00edthat\u00f3 az id\u0151z\u00edt\u0151. A PendingIntent mag\u00e1nak az AlarmService-nek k\u00fcldi a megfelel\u0151 Intentet, \u00e9s ez a PendingIntent majd az \u00e9rtes\u00edt\u00e9sben megjelen\u00edtett akci\u00f3hoz rendelhet\u0151.
    • resumeAlarm: az el\u0151z\u0151h\u00f6z hasonl\u00f3, de az id\u0151z\u00edt\u00e9s \u00fajraind\u00edt\u00e1s\u00e1hoz haszn\u00e1land\u00f3.
    • pauseAlarm: mint az el\u0151z\u0151ek, de pauz\u00e1l\u00e1sra.

    Az AlarmService l\u00e9nyegi logik\u00e1ja az onStartCommand met\u00f3dusban van megval\u00f3s\u00edtva. Ez az esem\u00e9nykezel\u0151 akkor fut le, amikor a Service-t \"Started Service\"-k\u00e9nt ind\u00edtj\u00e1k. Ez egy Intent k\u00fcld\u00e9s\u00e9vel t\u00f6rt\u00e9nik, amelyben az action \u00e9s az extras seg\u00edts\u00e9g\u00e9vel utaznak a param\u00e9terek, hogy a Service-t\u0151l mit is k\u00e9r\u00fcnk. N\u00e9zz\u00fck v\u00e9gig az egyes eseteket! Alapvet\u0151en mindig az \u00e1llapot megfelel\u0151 be\u00e1ll\u00edt\u00e1sa, az id\u0151z\u00edt\u0151 logika megh\u00edv\u00e1sa, illetve az \u00e9rtes\u00edt\u00e9s megjelen\u00edt\u00e9se/friss\u00edt\u00e9se t\u00f6rt\u00e9nik a kor\u00e1bban \u00e1ttekintett k\u00f3dr\u00e9szletek megh\u00edv\u00e1s\u00e1val. K\u00fcl\u00f6n\u00f6sen fontos r\u00e9szlet, hogy az ACTION_SET akci\u00f3, azaz az id\u0151z\u00edt\u0151 elind\u00edt\u00e1sa eset\u00e9n a Service a startForeground h\u00edv\u00e1ssal foreground m\u00f3dba ker\u00fcl, hogy akkor is m\u0171k\u00f6dj\u00f6n tov\u00e1bbra is az id\u0151z\u00edt\u00e9s, ha az alkalmaz\u00e1s nem l\u00e1that\u00f3. Megfigyelend\u0151 m\u00e9g az ACTION_PLAY eset\u00e9ben a MediaPlayer API haszn\u00e1lata az \u00e9breszt\u0151 dallam lej\u00e1tsz\u00e1s\u00e1hoz.

    Mivel a Service is egy f\u0151 komponenst\u00edpus Androidon, ez\u00e9rt ezt a Manifest f\u00e1jlban is sz\u00fcks\u00e9ges regisztr\u00e1lni:

    <service\nandroid:name=\".service.AlarmService\"\nandroid:enabled=\"true\"\nandroid:exported=\"false\"\nandroid:foregroundServiceType=\"specialUse\" />\n

    Most m\u00e1r \u00f6sszek\u00f6thet\u0151k a ViewModel \u00e9s a Service is az AlarmViewModel megfelel\u0151 kieg\u00e9sz\u00edt\u00e9s\u00e9vel:

    @HiltViewModel\nclass AlarmViewModel @Inject constructor(): ViewModel() {\n\nprivate val _state = AlarmState.state\nval state = _state.asStateFlow()\n\nfun onEvent(event: AlarmEvent) {\nwhen(event) {\nis AlarmEvent.SetAlarmDuration -> {\n_state.update { it.copy(\ncurrentAlarmDuration = event.duration,\nalarmDuration = event.duration\n) }\n}\nis AlarmEvent.SetAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.SET,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_SET\nstate.value.currentAlarmDuration.toString()\ncontext.startService(this)\n}\n}\nis AlarmEvent.PauseAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\nalarmState = AlarmServiceState.PAUSE,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_PAUSE\ncontext.startService(this)\n}\n}\nis AlarmEvent.ResumeAlarm -> {\nval context = event.context\n\n_state.update { it.copy(alarmState = AlarmServiceState.SET) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_SET\ncontext.startService(this)\n}\n}\nis AlarmEvent.StopAlarm -> {\nval context = event.context\n\n_state.update { it.copy(\ncurrentAlarmDuration = Duration.ZERO,\nalarmDuration = Duration.ZERO,\nalarmState = AlarmServiceState.CANCELED,\n) }\n\nIntent(context, AlarmService::class.java).apply {\nthis.action = AlarmService.ACTION_CANCEL\ncontext.startService(this)\n}\n}\n}\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/alarm/#onallo-feladat","title":"\u00d6n\u00e1ll\u00f3 feladat","text":"

    Val\u00f3s\u00edtsd meg az \u00e9breszt\u0151\u00f3r\u00e1kon megszokott \"szundi\" funkci\u00f3t! Amikor az id\u0151z\u00edt\u0151 letelik, jelen\u00edts meg az \u00e9rtes\u00edt\u00e9sen egy \"Snooze\" gombot is, amivel e riaszt\u00e1s abbamarad, \u00e9s egy \u00fajabb 5 perces id\u0151z\u00edt\u00e9s indul.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1ltalad \u00edrt k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/basics/","title":"Labor 01 - Alapok (HighLowGame)","text":"

    Az els\u0151 labor rendhagy\u00f3 a t\u00f6bbihez k\u00e9pest. Itt kev\u00e9s k\u00f3ddal fogunk tal\u00e1lkozni, ink\u00e1bb az alapok \u00e1tn\u00e9z\u00e9s\u00e9n van a hangs\u00faly.

    A labor c\u00e9lja, hogy bemutassa az Android fejleszt\u0151k\u00f6rnyezetet, az alkalmaz\u00e1sk\u00e9sz\u00edt\u00e9s, illetve a tesztel\u00e9s \u00e9s ford\u00edt\u00e1s folyamat\u00e1t, az alkalmaz\u00e1s fel\u00fcgyelet\u00e9t, valamint az emul\u00e1tor \u00e9s a fejleszt\u0151k\u00f6rnyezet funkci\u00f3it. Ismertetj\u00fck egy egyszer\u0171 barchoba alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9s\u00e9nek m\u00f3dj\u00e1t \u00e9s labor sor\u00e1n a laborvezet\u0151 r\u00e9szletesen bemutatja az eszk\u00f6z\u00f6ket.

    A labor v\u00e9g\u00e9n egy jegyz\u0151k\u00f6nyvet kell beadni a jegy megszerz\u00e9s\u00e9hez.

    A m\u00e9r\u00e9s az al\u00e1bbi t\u00e9m\u00e1kat \u00e9rinti:

    • Az Android platform alapfogalmainak ismerete
    • Android Studio fejleszt\u0151k\u00f6rnyezet alapok
    • Android Emul\u00e1tor tulajdons\u00e1gai
    • Android projekt l\u00e9trehoz\u00e1sa \u00e9s futtat\u00e1sa emul\u00e1toron
    • Manifest \u00e1llom\u00e1ny fel\u00e9p\u00edt\u00e9se
    • Android Profiler
    "},{"location":"laborok/basics/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/basics/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    "},{"location":"laborok/basics/#markdown-fajl-megnyitasa","title":"Markdown f\u00e1jl megnyit\u00e1sa","text":"

    A feladatok megold\u00e1sa sor\u00e1n a dokument\u00e1ci\u00f3t markdown form\u00e1tumban k\u00e9sz\u00edtsd. Az el\u0151bb let\u00f6lt\u00f6tt git repository-t nyisd meg egy markdown kompatibilis szerkeszt\u0151vel. Javasolt a Visual Studio Code haszn\u00e1lata:

    1. Ind\u00edtsd el a VS Code-ot.

    2. A File > Open Folder... men\u00fcvel nyisd meg a git repository k\u00f6nyvt\u00e1r\u00e1t.

    3. A bal oldali f\u00e1ban keresd meg a README.md f\u00e1jlt \u00e9s dupla kattint\u00e1ssal nyisd meg.

    4. Ezt a f\u00e1jlt szerkeszd.

    5. Ha k\u00e9pet k\u00e9sz\u00edtesz, azt is tedd a repository al\u00e1 a t\u00f6bbi f\u00e1jl mell\u00e9. \u00cdgy relat\u00edv el\u00e9r\u00e9si \u00fatvonallal (f\u00e1jln\u00e9v) fogod tudni hivatkozni.

      F\u00e1jln\u00e9v: csupa kisbet\u0171 \u00e9kezet n\u00e9lk\u00fcl

      A k\u00e9pek f\u00e1jlnev\u00e9ben ne haszn\u00e1lj \u00e9kezetes karaktereket, sz\u00f3k\u00f6z\u00f6ket, se kis- \u00e9s nagybet\u0171ket keverve. A k\u00fcl\u00f6nb\u00f6z\u0151 platformok \u00e9s a git elt\u00e9r\u0151en kezelik a f\u00e1jlneveket. A GitHub webes fel\u00fclet\u00e9n akkor fog minden rendben megjelenni, ha csak az angol \u00e1b\u00e9c\u00e9 kisbet\u0171it haszn\u00e1lod a f\u00e1jlnevekben.

    6. A k\u00e9nyelmes szerkeszt\u00e9shez nyisd meg az el\u0151n\u00e9zet funkci\u00f3t (Ctrl-K + V).

    M\u00e1s szerkeszt\u0151eszk\u00f6z

    Ha nem szimpatikus ez a szerkeszt\u0151, haszn\u00e1lhatod a GitHub webes fel\u00fclet\u00e9t is a dokument\u00e1ci\u00f3 szerkeszt\u00e9s\u00e9hez, itt is van el\u0151n\u00e9zet. Ekkor a f\u00e1jlok felt\u00f6lt\u00e9se kicsit k\u00f6r\u00fclm\u00e9nyesebb lesz.

    "},{"location":"laborok/basics/#android-alapok","title":"Android alapok","text":""},{"location":"laborok/basics/#forditas-menete-android-platformon","title":"Ford\u00edt\u00e1s menete Android platformon","text":"

    A projekt l\u00e9trehoz\u00e1sa ut\u00e1n a forr\u00e1sk\u00f3d az src k\u00f6nyvt\u00e1rban, m\u00edg a felhaszn\u00e1l\u00f3i fel\u00fclet le\u00edr\u00e1s\u00e1ra szolg\u00e1l\u00f3 XML \u00e1llom\u00e1nyok a res k\u00f6nyvt\u00e1rban tal\u00e1lhat\u00f3k. Az er\u0151forr\u00e1s \u00e1llom\u00e1nyokat egy R.java \u00e1llom\u00e1ny k\u00f6ti \u00f6ssze a forr\u00e1sk\u00f3ddal, \u00edgy k\u00f6nnyed\u00e9n el\u00e9rhetj\u00fck Java/Kotlin oldalr\u00f3l az XML-ben defini\u00e1lt fel\u00fcleti elemeket. Az Android projekt ford\u00edt\u00e1s\u00e1nak eredm\u00e9nye egy APK \u00e1llom\u00e1ny, melyet k\u00f6zvetlen\u00fcl telep\u00edthet\u00fcnk mobil eszk\u00f6zre.

    Ford\u00edt\u00e1s menete Android platformon

    1. A fejleszt\u0151 elk\u00e9sz\u00edti a Kotlin forr\u00e1sk\u00f3dot, valamint az XML alap\u00fa felhaszn\u00e1l\u00f3i fel\u00fclet le\u00edr\u00e1st a sz\u00fcks\u00e9ges er\u0151forr\u00e1s \u00e1llom\u00e1nyokkal.

    2. A fejleszt\u0151k\u00f6rnyezet az er\u0151forr\u00e1s \u00e1llom\u00e1nyokb\u00f3l folyamatosan naprak\u00e9szen tartja az R.java er\u0151forr\u00e1s f\u00e1jlt a fejleszt\u00e9shez \u00e9s a ford\u00edt\u00e1shoz.

      FONTOS

      Az R.java \u00e1llom\u00e1ny gener\u00e1lt, k\u00e9zzel SOHA ne m\u00f3dos\u00edtsuk! (Az Android Studio egy\u00e9bk\u00e9nt nem is hagyja.)

    3. A fejleszt\u0151 a Manifest \u00e1llom\u00e1nyban be\u00e1ll\u00edtja az alkalmaz\u00e1s hozz\u00e1f\u00e9r\u00e9si jogosults\u00e1gait (pl. Internet el\u00e9r\u00e9s, szenzorok haszn\u00e1lata, stb.), illetve ha fut\u00e1s idej\u0171 jogosults\u00e1gok sz\u00fcks\u00e9gesek, ezt kezeli.

    4. A ford\u00edt\u00f3 a forr\u00e1sk\u00f3db\u00f3l, az er\u0151forr\u00e1sokb\u00f3l \u00e9s a k\u00fcls\u0151 k\u00f6nyvt\u00e1rakb\u00f3l el\u0151\u00e1ll\u00edtja az ART virtu\u00e1lis g\u00e9p g\u00e9pi k\u00f3dj\u00e1t.

    5. A g\u00e9pi k\u00f3db\u00f3l \u00e9s az er\u0151forr\u00e1sokb\u00f3l el\u0151\u00e1ll a nem al\u00e1\u00edrt APK \u00e1llom\u00e1ny.

    6. V\u00e9g\u00fcl a rendszer v\u00e9grehajtja az al\u00e1\u00edr\u00e1st \u00e9s el\u0151\u00e1ll a k\u00e9sz\u00fcl\u00e9kekre telep\u00edthet\u0151, al\u00e1\u00edrt APK.

    Az Android Studio a Gradle build rendszert haszn\u00e1lja ezeknek a l\u00e9p\u00e9seknek az elv\u00e9g\u00e9z\u00e9s\u00e9hez.

    Megjegyz\u00e9sek

    • A teljes folyamat a fejleszt\u0151i g\u00e9pen megy v\u00e9gbe, a k\u00e9sz\u00fcl\u00e9kekre m\u00e1r csak bin\u00e1ris \u00e1llom\u00e1ny jut el.

    • A k\u00fcls\u0151 k\u00f6nyvt\u00e1rak \u00e1ltal\u00e1ban JAR \u00e1llom\u00e1nyk\u00e9nt, vagy egy m\u00e1sik projekt hozz\u00e1ad\u00e1s\u00e1val illeszthet\u0151k az aktu\u00e1lis projekthez (de ezt nem kell k\u00e9zzel megtenn\u00fcnk, a f\u00fcgg\u0151s\u00e9gek kezel\u00e9s\u00e9ben is a Gradle fog seg\u00edteni).

    • Az APK \u00e1llom\u00e1ny legink\u00e1bb a Java vil\u00e1gban ismert JAR \u00e1llom\u00e1nyokhoz hasonl\u00edthat\u00f3.

    • A Manifest \u00e1llom\u00e1nyban meg kell adni a t\u00e1mogatni k\u00edv\u00e1nt Android verzi\u00f3t, mely felfele kompatibilis az \u00fajabb verzi\u00f3kkal, enn\u00e9l r\u00e9gebbi verzi\u00f3ra azonban az alkalmaz\u00e1s m\u00e1r nem telep\u00edthet\u0151.

    • Az Android folyamatosan friss\u00fcl\u0151 verzi\u00f3ival folymatosan l\u00e9p\u00e9st kell tartaniuk a fejleszt\u0151knek.

    • Az Android alkalmaz\u00e1sokat tipikusan a Google Play Store-ban szokt\u00e1k publik\u00e1lni, \u00edgy az APK form\u00e1tumban val\u00f3 terjeszt\u00e9s nem annyira elterjedt.

    "},{"location":"laborok/basics/#sdk-es-konyvtarai","title":"SDK \u00e9s k\u00f6nyvt\u00e1rai","text":"

    A developer.android.com/studio oldalr\u00f3l let\u00f6lthet\u0151 az IDE \u00e9s az SDK. Ennek fontosabb mapp\u00e1it, eszk\u00f6zeit tekints\u00fck \u00e1t a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel!

    SDK szerkezet:

    • docs: Dokument\u00e1ci\u00f3
    • extras: K\u00fcl\u00f6nb\u00f6z\u0151 extra szoftverek helye. Maven repository, support libes anyagok, analytics SDK, Google Android USB driver (amennyiben SDK managerrel ezt is let\u00f6lt\u00f6tt\u00fck) stb.
    • platform-tools: Fastboot \u00e9s ADB bin\u00e1risok helye (legt\u00f6bbet haszn\u00e1lt eszk\u00f6z\u00f6k)
    • platforms, samples, sources, system-images: Minden API levelhez k\u00fcl\u00f6n almapp\u00e1ban a platform anyagok, forr\u00e1sok, p\u00e9ldaprojektek, OS image-ek
    • tools: Ford\u00edt\u00e1st \u00e9s tesztel\u00e9st seg\u00edt\u0151 eszk\u00f6z\u00f6k, SDK manager, 9Patch drawer, emul\u00e1tor bin\u00e1risok stb.
    "},{"location":"laborok/basics/#avd-es-sdk-manager","title":"AVD \u00e9s SDK manager","text":"

    Az SDK kezel\u00e9s\u00e9re az SDK managert haszn\u00e1ljuk, ezzel lehet let\u00f6lteni \u00e9s frissen tartani az eszk\u00f6zeinket. Ind\u00edt\u00e1sa az Android Studion kereszt\u00fcl lehets\u00e9ges.

    Az SDK Manager ikonja a fenti toolbaron (vagy Tools -> SDK Manager):

    vagy

    SDK manager fel\u00fclete:

    Megjegyz\u00e9s

    Kor\u00e1bban l\u00e9tezett egy standalone SDK manager de ennek haszn\u00e1lata m\u00e1ra deprecated lett. Ha online forr\u00e1sokban ilyet l\u00e1tunk ne lep\u0151dj\u00fcnk meg.

    Ind\u00edtsuk el az AVD managert, \u00e9s vizsg\u00e1ljuk meg a laborvezet\u0151vel, hogy rendelkez\u00e9sre \u00e1ll-e minden, ami az els\u0151 alkalmaz\u00e1sunkhoz kelleni fog.

    "},{"location":"laborok/basics/#avd","title":"AVD","text":"

    Az AVD az Android Virtual Device r\u00f6vid\u00edt\u00e9se. Ahogy arr\u00f3l m\u00e1r el\u0151ad\u00e1son is sz\u00f3 esett, nem csak val\u00f3di eszk\u00f6z\u00f6n futtathatjuk a k\u00f3dunkat, hanem emul\u00e1toron is. (Mi is a k\u00fcl\u00f6nbs\u00e9g szimul\u00e1tor \u00e9s emul\u00e1tor k\u00f6z\u00f6tt?) Az AVD ind\u00edt\u00e1sa a fejleszt\u0151i k\u00f6rnyezeten kereszt\u00fcl lehets\u00e9ges (illetve parancssorb\u00f3l is, de ennek a haszn\u00e1lat\u00e1ra csak speci\u00e1lis esetekben van sz\u00fcks\u00e9g).

    Az AVD Manager ikonja:

    vagy

    A fenti k\u00e9pen jobb oldalon, a kiny\u00edl\u00f3 panelben, a l\u00e9tez\u0151 virtu\u00e1lis eszk\u00f6z\u00f6k list\u00e1j\u00e1t tal\u00e1ljuk, bal oldalon pedig az \u00fan. eszk\u00f6z defin\u00edci\u00f3k\u00e9t. Itt n\u00e9h\u00e1ny el\u0151re elk\u00e9sz\u00edtett sablon \u00e1ll rendelkez\u00e9sre. Magunk is k\u00e9sz\u00edthet\u00fcnk ilyet, ha tipikusan egy adott eszk\u00f6zre szeretn\u00e9nk fejleszteni (pl. Galaxy S4). K\u00e9sz\u00edts\u00fcnk \u00faj emul\u00e1tort! \u00c9rtelemszer\u0171en csak olyan API szint\u0171 eszk\u00f6zt k\u00e9sz\u00edthet\u00fcnk, amilyenek rendelkez\u00e9sre \u00e1llnak az SDK manageren kereszt\u00fcl.

    1. A jobb oldali panelon kattintsunk a fent tal\u00e1lhat\u00f3 Create Virtual Device... gombra!
    2. V\u00e1lasszunk az el\u0151re defini\u00e1lt k\u00e9sz\u00fcl\u00e9k sablonokb\u00f3l (pl. Pixel 7 Pro), majd nyomjuk meg a Next gombot.
    3. D\u00f6nts\u00fck el, hogy milyen Android verzi\u00f3j\u00fa emul\u00e1tort k\u00edv\u00e1nunk haszn\u00e1lni. CPU/ABI alapvet\u0151en x86_64 legyen, mivel ezekhez kapunk hardveres gyors\u00edt\u00e1st is. Itt v\u00e1lasszunk a rendelkez\u00e9sre \u00e1ll\u00f3k k\u00f6z\u00fcl egyet, majd Next.
    4. Az eszk\u00f6z r\u00e9szletes konfigur\u00e1ci\u00f3ja.

      • A virtu\u00e1lis eszk\u00f6z neve legyen p\u00e9ld\u00e1ul Labor_1.
      • V\u00e1lasszuk ki az alap\u00e9rtelmezett orient\u00e1ci\u00f3t, tetsz\u00e9s szerint kapcsoljuk ki vagy be a k\u00e9sz\u00fcl\u00e9k keret\u00e9nek megjelen\u00edt\u00e9s\u00e9t.

      A Show Advanced Settings alatt tov\u00e1bbi opci\u00f3kat tal\u00e1lunk:

      • Kamera opci\u00f3k:
        • WebcamX, hardveres kamera, ami a sz\u00e1m\u00edt\u00f3g\u00e9pre van csatlakoztatva
        • Emulated, egy egyszer\u0171 szoftveres megold\u00e1s, most legal\u00e1bb az egyik kamera legyen ilyen.
        • VirtualScene, egy kifinomultabb szoftveres megold\u00e1s, amelyben egy 3D vil\u00e1gban mozgathatjuk a kamer\u00e1t.
      • H\u00e1l\u00f3zat: \u00c1ll\u00edthatjuk a sebess\u00e9g\u00e9t \u00e9s a k\u00e9sleltet\u00e9s\u00e9t is kommunik\u00e1ci\u00f3s technol\u00f3gi\u00e1k szerint.
      • Boot Option: Nemr\u00e9g jelent meg az Android emul\u00e1tor \u00e1llapot\u00e1r\u00f3l val\u00f3 pillanatk\u00e9p elment\u00e9s\u00e9nek lehet\u0151s\u00e9ge. Ez azt takarja, hogy a virtu\u00e1lis oper\u00e1ci\u00f3s rendszer csak felf\u00fcggeszt\u00e9sre ker\u00fcl az emul\u00e1tor bez\u00e1r\u00e1skor (p\u00e9ld\u00e1ul a megnyitott alkalmaz\u00e1s is megmarad, a teljes \u00e1llapot\u00e1val), \u00e9s Quick boot esetben a teljes OS ind\u00edt\u00e1sa helyett m\u00e1sodperceken bel\u00fcl elindul az emul\u00e1lt rendszer. Cold Boot esetben minden alkalommal le\u00e1ll\u00edtja \u00e9s \u00fajra ind\u00edtja a virt\u00e1lis eszk\u00f6z teljes oper\u00e1ci\u00f3s rendszer\u00e9t.
      • Mem\u00f3ria \u00e9s t\u00e1rhely:

        • RAM: Ha kev\u00e9s a rendszermem\u00f3ri\u00e1nk, nem \u00e9rdemes 768 MB-n\u00e1l t\u00f6bbet adni, mert k\u00f6nnyen futhatunk probl\u00e9m\u00e1kba. Ha az emul\u00e1tor lefagy, vagy az eg\u00e9sz OS meg\u00e1ll m\u0171k\u00f6d\u00e9s k\u00f6zben, akkor \u00e1ll\u00edtsuk alacsonyabbra ezt az \u00e9rt\u00e9ket. 8 GB vagy t\u00f6bb rendszermem\u00f3ria mellett nyugodtan \u00e1ll\u00edthatjuk az emul\u00e1tor mem\u00f3ri\u00e1j\u00e1t 1024, 1536, vagy 2048 MB-ra.
        • VM heap: az alkalmaz\u00e1sok virtu\u00e1lis g\u00e9p\u00e9nek sz\u00f3l, maradhat az alap\u00e9rt\u00e9k. Tudni kell, hogy k\u00e9sz\u00fcl\u00e9kek eset\u00e9ben gy\u00e1rt\u00f3nk\u00e9nt v\u00e1ltozik.
        • Bels\u0151 flash mem\u00f3ria \u00e9s SD k\u00e1rtya m\u00e9rete, alapvet\u0151en j\u00f3k az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1sai.
      • Ha mindent rendben tal\u00e1l az ablak, akkor Finish!

    Az Android Virtual Device Manager-ben megjelent az im\u00e9nt l\u00e9trehozott eszk\u00f6z\u00fcnk. Itt lehet\u0151s\u00e9g van a kor\u00e1bban megadott param\u00e9terek szerkeszt\u00e9s\u00e9re, a \"k\u00e9sz\u00fcl\u00e9kr\u0151l\" a felhaszn\u00e1l\u00f3i adatok t\u00f6rl\u00e9s\u00e9re (Wipe Data - Teljes vissza\u00e1ll\u00edt\u00e1s), illetve az emul\u00e1tor p\u00e9ld\u00e1ny duplik\u00e1l\u00e1s\u00e1ra vagy t\u00f6rl\u00e9s\u00e9re.

    A Play gombbal ind\u00edtsuk el az \u00faj emul\u00e1tort!

    Az elind\u00edtott emul\u00e1toron pr\u00f3b\u00e1ljuk ki az API Demos \u00e9s Dev Tools alkalmaz\u00e1sokat!

    Megjegyz\u00e9s

    A gy\u00e1ri emul\u00e1toron k\u00edv\u00fcl t\u00f6bb alternat\u00edva is l\u00e9tezik, mint pl. a Genymotion vagy a BigNox, viszont a Google f\u00e9le emul\u00e1tor a legelterjedtebb, \u00edgy amennyiben ezzel nem jelentkeznek probl\u00e9m\u00e1ink, maradjunk enn\u00e9l.

    Tesztel\u00e9s c\u00e9lj\u00e1b\u00f3l nagyon j\u00f3l haszn\u00e1lhat\u00f3 az emul\u00e1tor, amely az al\u00e1bbi k\u00e9pen l\u00e1that\u00f3 plusz funkci\u00f3kat is adja. Lehet\u0151s\u00e9g van t\u00f6bbek k\u00f6z\u00f6tt egyedi hely be\u00e1ll\u00edt\u00e1s\u00e1ra, bej\u00f6v\u0151 h\u00edv\u00e1s szimul\u00e1l\u00e1s\u00e1ra, stb. A panelt a fut\u00f3 emul\u00e1tor jobb oldal\u00e1n tal\u00e1lhat\u00f3 vez\u00e9rl\u0151 gombok k\u00f6z\u00fcl a ... gombbal lehet megnyitni:

    "},{"location":"laborok/basics/#fejlesztoi-kornyezet","title":"Fejleszt\u0151i k\u00f6rnyezet","text":"

    Android fejleszt\u00e9sre a labor sor\u00e1n a JetBrains IntelliJ alapjain nyugv\u00f3 Android Studio-t fogjuk haszn\u00e1lni. A Studio-val ismerked\u0151k sz\u00e1m\u00e1ra hasznos funkci\u00f3 a Tip of the day, \u00e9rdemes egyb\u0151l kipr\u00f3b\u00e1lni, megn\u00e9zni az adott funkci\u00f3t. Indul\u00e1skor alap\u00e9rtelmezetten a legut\u00f3bbi projekt ny\u00edlik meg, ha nincs ilyen, vagy ha minden projekt\u00fcnket bez\u00e1rtuk, akkor a nyit\u00f3 k\u00e9perny\u0151. (A legut\u00f3bbi projekt \u00fajranyit\u00e1s\u00e1t a Settings -> Appeareance & Behavior -> System Settings -> Reopen last project on startup opci\u00f3val ki is kapcsolhatjuk.)

    Az Android Studio Giraffe-ban meg\u00fajult a k\u00f6rnyezet felhaszn\u00e1l\u00f3i fel\u00fclete. Amint l\u00e1that\u00f3, j\u00f3val letisztultabb diz\u00e1jnt v\u00e1lasztottak, sokkal kevesebb a figyelmet elvon\u00f3 extra a k\u00e9perny\u0151n, sokkal ink\u00e1bb a k\u00f3don van a hangs\u00faly. Ezek k\u00f6z\u00f6tt a n\u00e9zetek k\u00f6z\u00f6tt egyszer\u0171en v\u00e1lthatunk a Be\u00e1ll\u00edt\u00e1sokban, a New UI men\u00fcpontban.

    "},{"location":"laborok/basics/#high-low-game","title":"High Low Game","text":"

    A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edtsenek egy \u00faj alkalmaz\u00e1st!

    "},{"location":"laborok/basics/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Views Activity lehet\u0151s\u00e9get.
    2. A projekt neve legyen HighLowGame, a kezd\u0151 package pedig hu.bme.aut.android.highlowgame.
    3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
    4. A minimum API szint legyen API24: Android 7.0.
    5. A Build configuration language Kotlin DSL legyen.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 HighLowGame k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n!

    A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel tekints\u00e9k \u00e1t a l\u00e9trej\u00f6tt projekt strukt\u00far\u00e1j\u00e1t!

    Miut\u00e1n \u00e1ttekintett\u00fck a projektet, val\u00f3s\u00edtsuk meg a barch\u00f3ba j\u00e1t\u00e9kot! El\u0151sz\u00f6r is kapcsoljuk be a modulunkra a ViewBinding-ot a fel\u00fcleti elemek el\u00e9r\u00e9hez. Az app modulhoz tartoz\u00f3 build.gradle.kts f\u00e1jlban az android tagen bel\u00fclre illessz\u00fck be az enged\u00e9lyez\u00e9st:

    android {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

    Az alkalmaz\u00e1sunk fel\u00fclete (activity_main.xml) a k\u00f6vetkez\u0151 lesz: - lesz k\u00e9t beviteli mez\u0151nk: egy a tippnek, egy a n\u00e9vnek - lesz egy gombunk a tipp lead\u00e1s\u00e1hoz - lesz egy eredm\u00e9ny mez\u0151 az eredm\u00e9ny megjelen\u00edt\u00e9s\u00e9hez.

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\nandroid:orientation=\"vertical\"\ntools:context=\".MainActivity\">\n\n<com.google.android.material.textfield.TextInputLayout\nstyle=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:hint=\"Enter number here\">\n\n<com.google.android.material.textfield.TextInputEditText\nandroid:id=\"@+id/etGuess\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\" />\n</com.google.android.material.textfield.TextInputLayout>\n\n<com.google.android.material.textfield.TextInputLayout\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\">\n\n<EditText\nandroid:id=\"@+id/etName\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:hint=\"Enter a name here\" />\n</com.google.android.material.textfield.TextInputLayout>\n\n<Button\nandroid:id=\"@+id/btnGuess\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:text=\"Guess\" />\n\n<TextView\nandroid:id=\"@+id/tvResult\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:text=\"have fun :)\"\nandroid:textSize=\"28sp\" />\n\n</LinearLayout>\n

    A j\u00e1t\u00e9k k\u00f3dja pedig a k\u00f6vetkez\u0151k\u00e9ppen alakul (MainActivity.kt):

    class MainActivity : AppCompatActivity() {\n\ncompanion object {\nconst val KEY_NUM = \"KEY_NUM\"\n}\n\nlateinit var binding: ActivityMainBinding\n\nvar generatedNum = 0\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\n\n\nbinding = ActivityMainBinding.inflate(layoutInflater)\nsetContentView(binding.root)\n\nif (savedInstanceState != null && savedInstanceState!!.containsKey(KEY_NUM)) {\ngeneratedNum = savedInstanceState.getInt(KEY_NUM)\n} else {\ngenerateNewNumber()\n}\n\nbinding.btnGuess.setOnClickListener {\ntry {\n\nif (binding.etGuess.text!!.isNotEmpty()) {\nval myNum = binding.etGuess.text.toString().toInt()\n\nif (myNum == generatedNum) {\nbinding.tvResult.text = \"${binding.etName.text.toString()}, You have won!\"\n\n} else if (myNum < generatedNum) {\nbinding.tvResult.text = \"The number is higher\"\n} else if (myNum > generatedNum) {\nbinding.tvResult.text = \"The number is lower\"\n}\n} else {\nbinding.etGuess.error = \"This value is not valid\"\n\n}\n\n} catch (e: Exception) {\nbinding.etGuess.error = e.message\n}\n}\n}\n\noverride fun onSaveInstanceState(outState: Bundle) {\noutState.putInt(KEY_NUM, generatedNum)\n\nsuper.onSaveInstanceState(outState)\n}\n\n\nfun generateNewNumber() {\nval rand = Random(System.currentTimeMillis())\ngeneratedNum = rand.nextInt(3) // 0..2\n}\n}\n

    Az Activty els\u0151 indul\u00e1sakor sorsol egy kital\u00e1land\u00f3 sz\u00e1mot. A tippel\u00e9s gombnyom\u00e1sra t\u00f6rt\u00e9nik, aminek hat\u00e1s\u00e1ra friss\u00fcl az eredm\u00e9ny mez\u0151.

    Figyelj\u00fck meg, hogy a j\u00e1t\u00e9k t\u00fal\u00e9li a forgat\u00e1sokat is! Ez annak k\u00f6sz\u00f6nhet\u0151, hogy az Activity-n bel\u00fcl elt\u00e1rolt c\u00e9lsz\u00e1mot konfigur\u00e1ci\u00f3 v\u00e1lt\u00e1skor elmentj\u00fck, majd \u00faj Activity ind\u00edt\u00e1sa eset\u00e9n bet\u00f6ltj\u00fck.

    "},{"location":"laborok/basics/#android-studio","title":"Android Studio","text":"

    Ez a r\u00e9sz azoknak sz\u00f3l, akik kor\u00e1bban m\u00e1r haszn\u00e1lt\u00e1k az Eclipse nev\u0171 IDE-t, \u00e9s szeretn\u00e9k megismerni a k\u00fcl\u00f6nbs\u00e9geket az Android Studio-hoz k\u00e9pest.

    • Import r\u00e9gi projektekb\u0151l: Android Studioban lehets\u00e9ges a projekt import\u00e1l\u00e1sa r\u00e9gebbi verzi\u00f3j\u00fa projektekb\u0151l \u00e9s a r\u00e9gi Eclipse projektekb\u0151l is.
    • Projektstrukt\u00fara: Az Android Studio Gradle-lel ford\u00edt, \u00e9s m\u00e1s fel\u00e9p\u00edt\u00e9st haszn\u00e1l. Projekten bel\u00fcl:

      • .idea: IDE f\u00e1jlok
      • app: forr\u00e1s
        • build: ford\u00edtott \u00e1llom\u00e1nyok
        • libs: libraryk
        • src: forr\u00e1sk\u00f3d, azon bel\u00fcl is k\u00fcl\u00f6n projekt a tesztnek, \u00e9s azon bel\u00fcl pedig res k\u00f6nyvt\u00e1r, illetve java. Ut\u00f3bbin bel\u00fcl m\u00e1r a csomagok vannak.
      • gradle: Gradle f\u00e1jlok
    • Hasznos funkci\u00f3k:

      • IntelliSense, fejlett refaktor\u00e1l\u00e1s t\u00e1mogat\u00e1s
      • Ha egy sorban sz\u00ednre, vagy k\u00e9pi er\u0151forr\u00e1sra hivatkozunk, a sor elej\u00e9re kitesz egy miniat\u0171r v\u00e1ltozatot.
      • Ha k\u00f6zvetve hivatkozott er\u0151forr\u00e1st (ak\u00e1r resources.get..., ak\u00e1r R...) adunk meg, \u00f6sszecsukja a hivatkoz\u00e1st \u00e9s a t\u00e9nyleges \u00e9rt\u00e9ket mutatja. Ha r\u00e1vissz\u00fck az egeret felfedi, ha kattintunk kibontja a hivatkoz\u00e1st.
      • N\u00e9vtelen bels\u0151 oszt\u00e1lyokkal is hasonl\u00f3t tud, jav\u00edtva a k\u00f3d olvashat\u00f3s\u00e1g\u00e1t.
      • K\u00f3dkieg\u00e9sz\u00edt\u00e9sn\u00e9l szabad a keres\u0151, a sz\u00f3t\u00f6red\u00e9ket keresi, nem pedig a sz\u00f3val kezd\u0151d\u0151 lehet\u0151s\u00e9geket (l\u00e1sd k\u00e9pen)
      • V\u00e1ltoz\u00f3n\u00e9v aj\u00e1nl\u00e1s: amikor v\u00e1ltoz\u00f3n\u00e9vre van sz\u00fcks\u00e9g\u00fcnk, nyomjunk Ctrl+Space-t. Ha adottak a k\u00f6r\u00fclm\u00e9nyek, a Studio eg\u00e9sz j\u00f3 neveket tud felaj\u00e1nlani.
      • Szigor\u00fa lint. A Studio megengedi a warningot. Ez\u00e9rt szigor\u00fabb a lint, t\u00f6bb mindenre figyelmeztet (olyan apr\u00f3s\u00e1gra is, hogy egy View egyik oldal\u00e1n van padding, a m\u00e1sikon nincs)
      • Layout szerkeszt\u00e9s. A grafikus layout \u00e9p\u00edt\u00e9s lehets\u00e9ges.
      • CTRL-t lenyomva navig\u00e1lhatunk a k\u00f3dban, pl. oszt\u00e1lyra, met\u00f3dush\u00edv\u00e1sra kattintva. Ezt a navig\u00e1ci\u00f3t (\u00e9s az egyszer\u0171 m\u00e1sik oszt\u00e1lyba kattint\u00e1st is) r\u00f6gz\u00edti, \u00e9s a historyban el\u0151re-h\u00e1tra gombokkal lehet l\u00e9pkedni. Ha van az eger\u00fcnk\u00f6n/billenty\u0171zet\u00fcnk\u00f6n ilyen gomb, \u00e9s netes b\u00f6ng\u00e9sz\u00e9s k\u00f6zben akt\u00edvan haszn\u00e1ljuk, ezt a funkci\u00f3t nagyon hasznosnak fogjuk tal\u00e1lni.

    Sz\u00edn ikonja a sor elej\u00e9n; kiemelve jobb oldalon, hogy melyik n\u00e9zeten vagyunk; szabadszavas kieg\u00e9sz\u00edt\u00e9s; a \"Hello world\" igaz\u00e1b\u00f3l @string/very_very_very_long_hello_world.

    "},{"location":"laborok/basics/#billentyukombinaciok","title":"Billenty\u0171kombin\u00e1ci\u00f3k","text":"
    • CTRL + ALT + L: K\u00f3dform\u00e1z\u00e1s
    • CTRL + SPACE: K\u00f3dkieg\u00e9sz\u00edt\u00e9s
    • SHIFT + F6 \u00c1tnevez\u00e9s (Mindenhol)
    • F2: A k\u00f6vetkez\u0151 error-ra ugrik. Ha nincs error, akkor warningra.
    • CTRL + Z illetve CTRL + SHIFT + Z: Visszavon\u00e1s \u00e9s M\u00e9gis
    • CTRL + P: Param\u00e9terek mutat\u00e1sa
    • ALT + INSERT: Met\u00f3dus gener\u00e1l\u00e1sa
    • CTRL + O: Met\u00f3dus fel\u00fcldefini\u00e1l\u00e1sa
    • CTRL + F9: Ford\u00edt\u00e1s
    • SHIFT + F10: Ford\u00edt\u00e1s \u00e9s futtat\u00e1s
    • SHIFT SHIFT: Keres\u00e9s mindenhol
    • CTRL + N: Keres\u00e9s oszt\u00e1lyokban
    • CTRL + SHIFT + N: Keres\u00e9s f\u00e1jlokban
    • CTRL + ALT + SHIFT + N: Keres\u00e9s szimb\u00f3lumokban (p\u00e9ld\u00e1ul f\u00fcggv\u00e9nyek, property-k)
    • CTRL + SHIFT + A: Keres\u00e9s a be\u00e1ll\u00edt\u00e1sokban, kiadhat\u00f3 parancsokban.
    "},{"location":"laborok/basics/#eszkozok-szerkesztok","title":"Eszk\u00f6z\u00f6k, szerkeszt\u0151k","text":"

    A View men\u00fc Tool Windows men\u00fcpontj\u00e1ban lehet\u0151s\u00e9g van k\u00fcl\u00f6nb\u00f6z\u0151 ablakok ki- \u00e9s bekapcsol\u00e1s\u00e1ra. Laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel tekints\u00e9k \u00e1t az al\u00e1bbi eszk\u00f6z\u00f6ket!

    • Project
    • Structure
    • TODO
    • Logcat
    • Terminal
    • Event Log
    • Gradle

    Lehet\u0151s\u00e9g van felosztani a szerkeszt\u0151ablakot, ehhez kattinsunk egy megnyitott f\u00e1jl tabf\u00fcl\u00e9re jobb gombbal, Split Vertically/Horizontally!

    "},{"location":"laborok/basics/#hasznos-beallitasok","title":"Hasznos be\u00e1ll\u00edt\u00e1sok","text":"

    A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel \u00e1ll\u00edts\u00e1k be a k\u00f6vetkez\u0151 hasznos funkci\u00f3kat:

    • kis- nagybet\u0171 \u00e9rz\u00e9kenys\u00e9g kikapcsol\u00e1sa a k\u00f3dkieg\u00e9sz\u00edt\u0151ben (settingsben keres\u00e9s: sensitive)
    • \"laptop m\u00f3d\" ki- \u00e9s bekapcsol\u00e1sa (File -> Power Save Mode)
    • sorsz\u00e1moz\u00e1s bekapcsol\u00e1sa (k\u00f3d melletti r\u00e9szen bal oldalt: jobb eg\u00e9rgomb, Show Line Numbers)
    "},{"location":"laborok/basics/#generalhato-elemek","title":"Gener\u00e1lhat\u00f3 elemek","text":"

    A Studio sok sablont tartalmaz, r\u00f6viden tekints\u00e9k \u00e1t a lehet\u0151s\u00e9geket:

    • Projektf\u00e1ban, projektre jobb gombbal kattintva -> new -> module
    • Projektf\u00e1ban, modulon bel\u00fcl, \"java\"-ra kattintva jobb gombbal -> new
    • Forr\u00e1sk\u00f3dban ALT+INSERT billenty\u0171kombin\u00e1ci\u00f3ra
    "},{"location":"laborok/basics/#android-profiler","title":"Android Profiler","text":"

    A k\u00e9sz\u00fcl\u00e9k er\u0151forr\u00e1shaszn\u00e1lata monitorozhat\u00f3 ezen a fel\u00fcleten, amelyet az eml\u00edtett View -> Tool Windows-b\u00f3l \u00e9rhet\u00fcnk el.

    P\u00e9ld\u00e1ul r\u00e9szletes inform\u00e1ci\u00f3t kaphatunk a h\u00e1l\u00f3zati forgalomr\u00f3l:

    "},{"location":"laborok/basics/#database-inspector","title":"Database Inspector","text":"

    A k\u00e9sz\u00fcl\u00e9ken debuggolt alkalmaz\u00e1sunknak az adatb\u00e1zis\u00e1t is meg tudjuk tekinteni.

    "},{"location":"laborok/basics/#device-file-explorer","title":"Device File Explorer","text":"

    A k\u00e9sz\u00fcl\u00e9ken l\u00e9v\u0151 f\u00e1jlrendszert is b\u00f6ng\u00e9szhetj\u00fck.

    "},{"location":"laborok/basics/#feladatok-10-x-05-pont","title":"Feladatok (10 x 0.5 pont)","text":"
    1. Az \u00faj alkalmaz\u00e1st futtass\u00e1k emul\u00e1toron (akinek saj\u00e1t k\u00e9sz\u00fcl\u00e9ke van, az is pr\u00f3b\u00e1lja ki)!
    2. Helyezzenek breakpointot a k\u00f3dba, \u00e9s debug m\u00f3dban ind\u00edts\u00e1k az alkalmaz\u00e1st! (\u00c9rdemes megyfigyelni, hogy most m\u00e1sik Gradle Task fut a k\u00e9perny\u0151 alj\u00e1n.)
    3. Ind\u00edtsanak h\u00edv\u00e1st \u00e9s k\u00fcldjenek SMS-t az emul\u00e1torra! Mit tapasztalnak?
    4. Ind\u00edtsanak h\u00edv\u00e1st \u00e9s k\u00fcldjenek SMS-t az emul\u00e1torr\u00f3l! Mit tapasztalnak?
    5. Tekintse \u00e1t az Android Profiler n\u00e9zet funkci\u00f3it a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel!
    6. V\u00e1ltoztassa meg a k\u00e9sz\u00fcl\u00e9k tart\u00f3zkod\u00e1si hely\u00e9t (GPS) az emul\u00e1tor megfelel\u0151 panelj\u00e9nek seg\u00edts\u00e9g\u00e9vel!
    7. Vizsg\u00e1lja meg az elind\u00edtott HighLowGame projekt nyitott sz\u00e1lait, mem\u00f3riafoglal\u00e1s\u00e1t!
    8. Vizsg\u00e1lja meg a Logcat panel tartalm\u00e1t!
    9. Vizsg\u00e1lja meg a Code -> Inspect code eredm\u00e9ny\u00e9t!
    10. Keresse ki a l\u00e9trehozott HighLowGame projekt mapp\u00e1j\u00e1t \u00e9s a build k\u00f6nyvt\u00e1ron bel\u00fcl vizsg\u00e1lja meg az .apk \u00e1llom\u00e1ny tartalm\u00e1t! Ezen bel\u00fcl hol tal\u00e1lhat\u00f3 a leford\u00edtott k\u00f3d?

    BEADAND\u00d3

    A labor teljes\u00edt\u00e9s\u00e9hez a fenti feladatokat kell v\u00e9grehajtani \u00e9s az eredm\u00e9nyeket dokument\u00e1lni. Ezt minden egyes feladatn\u00e1l egy k\u00e9perny\u0151k\u00e9ppel \u00e9s r\u00f6vid, n\u00e9h\u00e1ny mondatos magyar\u00e1zattal kell megtenni. A jegyz\u0151k\u00f6nyvet a repository-ban l\u00e9v\u0151 README.md f\u00e1jlban kell elk\u00e9sz\u00edteni.

    A dokument\u00e1ci\u00f3nak a k\u00e9pekkel egy\u00fctt helyesen kell megjelenni\u00fck a GitHub webes fel\u00fclet\u00e9n is! Ezt ellen\u0151rizd a bead\u00e1s sor\u00e1n: nyisd meg a repository-d webes fel\u00fclet\u00e9t, v\u00e1ltsd \u00e1t a megfelel\u0151 \u00e1gra, \u00e9s a GitHub automatikusan renderelni fogja a README.md f\u00e1jlt a k\u00e9pekkel egy\u00fctt.

    "},{"location":"laborok/calculator/","title":"Labor03 - Sz\u00e1mol\u00f3g\u00e9p (Calculator)","text":""},{"location":"laborok/calculator/#a-labor-celja","title":"A labor c\u00e9lja","text":"

    A legfontosabb XML-alap\u00fa UI fejleszt\u00e9si komponensek haszn\u00e1lat\u00e1nak bemutat\u00e1sa egy sz\u00e1mol\u00f3g\u00e9p alkalmaz\u00e1son kereszt\u00fcl. A labor sor\u00e1n megismerked\u00fcnk a Jetpack Navigation k\u00f6nyvt\u00e1rral, a Fragment-ekkel \u00e9s a RecyclerView elk\u00e9sz\u00edt\u00e9si l\u00e9p\u00e9seivel.

    "},{"location":"laborok/calculator/#felhasznalt-technologiak","title":"Felhaszn\u00e1lt technol\u00f3gi\u00e1k:","text":"
    • Activity
    • Fragment
    • Jetpack Navigation
    • RecyclerView \u00e9s RecyclerViewAdapter
    • TableLayout, TextView, Button
    • View Binding
    "},{"location":"laborok/calculator/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/calculator/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    "},{"location":"laborok/calculator/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk ki a No Activity opci\u00f3t, majd kattintsunk a Next gombra.
    2. A projekt neve legyen Calculator, a kezd\u0151 package pedig hu.bme.aut.android.calculator.
    3. Nyelvnek tov\u00e1bbra is a Kotlin-t haszn\u00e1ljuk.
    4. A minimum API szint pedig legyen 24: Android 7.0 (Nougat).
    5. A Build configuration language legyen Kotlin DSL.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Calculator k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ha ezzel megvagyunk, akkor t\u00e9rj\u00fcnk r\u00e1 a Gradle f\u00e1jlokra, amik a projekt\u00fcnk buildel\u00e9si folyamat\u00e1nak konfigur\u00e1ci\u00f3j\u00e1\u00e9rt felelnek. Els\u0151re n\u00e9zz\u00fck a project szint\u0171 Gradle f\u00e1jlt:

    A Jetpack Navigation k\u00f6nyvt\u00e1r haszn\u00e1lata miatt vegy\u00fck fel a t\u00f6bbi plugin mell\u00e9 a androidx.navigation.safeargs-ot:

    buildscript {\nrepositories {\ngoogle()\n}\ndependencies {\nval nav_version = \"2.8.0\"\nclasspath(\"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version\")\n}\n}\n

    A libs.version.toml f\u00e1jlba vegy\u00fck fel a k\u00f6vetkez\u0151ket:

    [versions]\n...\nnavigation = \"2.8.0\"\n\n[libraries]\n...\nandroidx-navigation-fragment-ktx = { group = \"androidx.navigation\", name = \"navigation-fragment-ktx\", version.ref = \"navigation\" }\nandroidx-navigation-ui-ktx = { group = \"androidx.navigation\", name = \"navigation-ui-ktx\", version.ref = \"navigation\" }\n

    Nyissuk meg a module szint\u0171 Gradle f\u00e1jlunkat.

    Enged\u00e9lyezz\u00fck a View Binding-ot:

    ...\nandroid {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

    Vegy\u00fck fel azokat a tov\u00e1bbi f\u00fcgg\u0151s\u00e9geket, amikre m\u00e9g sz\u00fcks\u00e9g\u00fcnk lesz a projekt sor\u00e1n. Ehhez a pluginok k\u00f6z\u00e9 m\u00e9g vegy\u00fck fel a androidx.navigation.safeargs.kotlin-t.

    plugins {\n...\nid(\"androidx.navigation.safeargs.kotlin\")\n}\n\nandroid { ... }\n\ndependencies {\n...\nimplementation (libs.androidx.navigation.fragment.ktx)\nimplementation (libs.androidx.navigation.ui.ktx)\n}\n

    Ha v\u00e9gezt\u00fcnk szinkroniz\u00e1ljuk a Gradle f\u00e1jlokat. (Ha esetleg nem lenne, meg hogy ezt hol lehet, akkor a SHIFT gomb dupla lenyom\u00e1s\u00e1val megny\u00edlik egy keres\u0151, amiben a Sync project with Gradle files-t megadva ez elv\u00e9gezhet\u0151.)

    Sz\u00fcks\u00e9g\u00fcnk lesz m\u00e9g n\u00e9h\u00e1ny string \u00e9s drawable er\u0151forr\u00e1sra, amit most \u00e9rdemes el\u0151re felvenni. A string er\u0151forr\u00e1sok el\u00e9r\u00e9s\u00e9hez nyissuk meg a res/values/string.xml f\u00e1jlt \u00e9s vegy\u00fck fel az al\u00e1bbi er\u0151forr\u00e1sokat:

    <resources>\n<string name=\"app_name\">Calculator</string>\n<string name=\"text_calculator_console\">0</string>\n<string name=\"text_operation\">%1$.2f %2$s %3$.2f = %4$.2f</string>\n<string name=\"button_text_delete\">C</string>\n<string name=\"button_text_sign\">+/-</string>\n<string name=\"button_text_modulo\">%</string>\n<string name=\"button_text_division\">/</string>\n<string name=\"button_text_number_seven\">7</string>\n<string name=\"button_text_number_eight\">8</string>\n<string name=\"button_text_number_nine\">9</string>\n<string name=\"button_text_multiplication\">*</string>\n<string name=\"button_text_number_four\">4</string>\n<string name=\"button_text_number_five\">5</string>\n<string name=\"button_text_number_six\">6</string>\n<string name=\"button_text_subtraction\">-</string>\n<string name=\"button_text_number_one\">1</string>\n<string name=\"button_text_number_two\">2</string>\n<string name=\"button_text_number_three\">3</string>\n<string name=\"button_text_addition\">+</string>\n<string name=\"button_text_number_zero\">0</string>\n<string name=\"button_text_come\">,</string>\n<string name=\"button_text_equivalence\">=</string>\n<string name=\"menu_item_title_delete\">Delete</string>\n<string name=\"menu_item_content_description_delete\">Action to delete history</string>\n<string name=\"button_text_load\">Load</string>\n<string name=\"app_bar_title_history\">History</string>\n</resources>\n

    Majd nyissuk meg a ResourceManager-t (bal oldali men\u00fcs\u00e1v), v\u00e1lasszuk ki a drawable er\u0151forr\u00e1sokat, majd kattintsunk a + gombra, ahol v\u00e1lasszuk ki a Vector Asset lehet\u0151s\u00e9get.

    Ekkor megny\u00edlik az Asset Studio. Itt kattintsunk a Clip art mellett l\u00e9v\u0151 gombra, \u00e9s keress\u00fck meg az arrow back nev\u0171 ikont. Az Name attrib\u00fatumot \u00e1ll\u00edtsuk \u00e1t ic_arrow_back_24-re. Kattintsunk a Next majd a Finish gombra.

    V\u00e9gezet\u00fcl szeretn\u00e9nk a Material You keretrendszerre \u00e1tt\u00e9rni a UI kin\u00e9zete eset\u00e9ben. Ehhez nyissuk a res/values/ el\u00e9r\u00e9si \u00faton l\u00e9v\u0151 themes.xml f\u00e1jlt, \u00e9s \u00edrjuk \u00e1t a tartalm\u00e1t a k\u00f6vetkez\u0151re:

    <resources xmlns:tools=\"http://schemas.android.com/tools\">\n<!-- Base application theme. -->\n<style name=\"Base.Theme.Calculator\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n<!-- Customize your light theme here. -->\n<!-- <item name=\"colorPrimary\">@color/my_light_primary</item> -->\n</style>\n\n<style name=\"Theme.Calculator\" parent=\"Base.Theme.Calculator\" />\n</resources>\n

    Majd t\u00f6r\u00f6lj\u00fck ki a night er\u0151forr\u00e1smin\u0151s\u00edt\u0151vel ell\u00e1tott verzi\u00f3t.

    "},{"location":"laborok/calculator/#jetpack-navigation","title":"Jetpack Navigation","text":"

    A k\u00f6vetkez\u0151 r\u00e9szben a Jetpack Navigation k\u00f6nyvt\u00e1rral fogunk megismerkedni. Seg\u00edts\u00e9g\u00e9vel Activity-k \u00e9s Fragment-ek k\u00f6z\u00f6tti navig\u00e1ci\u00f3t lehet megval\u00f3s\u00edtani \u00fagy, hogy az egyes k\u00e9perny\u0151k k\u00f6zti \u00fatvonalakat egy gr\u00e1ffal modellezz\u00fck.

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt a ResourceManager seg\u00edts\u00e9g\u00e9vel, hozzunk l\u00e9tre egy navig\u00e1ci\u00f3s gr\u00e1fot. V\u00e1lasszuk ki a Navigation opci\u00f3t, majd kattintsunk a + gombra, ahol v\u00e1lasszuk a Navigation Resource File lehet\u0151s\u00e9get. Az er\u0151forr\u00e1s f\u00e1jl neve legyen nav_graph.

    Ezut\u00e1n hozzunk l\u00e9tre egy \u00faj Empty Views Activity-t (jobb klikk calculator package-n \u2192 New \u2192 Activity \u2192 Empty Views Activity). Itt pip\u00e1ljuk be a Launcher Activity opci\u00f3t, ugyanis szeretn\u00e9nk, hogy az Activity a futtat\u00f3 eszk\u00f6z men\u00fcj\u00e9b\u0151l ind\u00edthat\u00f3 legyen. Majd kattintsunk a Finish gombra.

    Keress\u00fck meg a MainActivity-hez tartoz\u00f3 activity_main.xml f\u00e1jlt (res/layout), \u00e9s vegy\u00fcnk fel benne egy FragmentContainerView komponenst:

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:id=\"@+id/main\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".MainActivity\">\n\n<androidx.fragment.app.FragmentContainerView\nandroid:id=\"@+id/nav_host_fragment\"\nandroid:name=\"androidx.navigation.fragment.NavHostFragment\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\napp:defaultNavHost=\"true\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintLeft_toLeftOf=\"parent\"\napp:layout_constraintRight_toRightOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:navGraph=\"@navigation/nav_graph\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

    FragmentContainerView

    Ez egy egyedi layout t\u00edpus, ami a Fragment-ek megjelen\u00edt\u00e9s\u00e9re haszn\u00e1latos.

    • Az android:name attrib\u00fatum tartalmazza a NavHost implement\u00e1ci\u00f3nk oszt\u00e1lynev\u00e9t.
    • Az app:navGraph attrib\u00fatum hivatkozik arra a navig\u00e1ci\u00f3s er\u0151forr\u00e1sra, amit kor\u00e1bban gener\u00e1ltunk.
    • Az app:defaultNavhost=\"true\" attrib\u00fatum biztos\u00edtja, hogy a NavHostFragment kezelni tudja a visszafel\u00e9 navig\u00e1l\u00e1st (amit egy dedik\u00e1lt fizikai gombbal vagy interakci\u00f3val v\u00e1lthatunk ki). Csak egyetlen NavHost lehet alap\u00e9rtelmezettnek (default) be\u00e1ll\u00edtva.

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt nyissuk meg a nav_graph.xml-t (res/navigation) Design m\u00f3dban. Kattintsunk a New Destination gombra, ott v\u00e1lasszuk ki a Create New Destination majd Fragment (Blank) opci\u00f3kat. A Fragment neve legyene CalculatorFragment. Ezut\u00e1n v\u00e9gleges\u00edts\u00fck a l\u00e9trehoz\u00e1st a Next \u00e9s Finish gombra val\u00f3 kattint\u00e1ssal.

    Hozzunk l\u00e9tre ugyanezzel a m\u00f3dszerrel egy \u00fajabb Fragment-t HistoryFragment n\u00e9ven. Ha ezzel megvagyunk, vigy\u00fck a kurzorunkat a CalculatorFragment f\u00f6l\u00e9, ekkor megjelenik egy karika a Fragment jobb oldal\u00e1n. Kattintsunk r\u00e1, majd a bal klikket lenyomva h\u00fazzuk a kurzort a m\u00e1sik Fragment f\u00f6l\u00e9 \u00e9s ott engedj\u00fck el. \u00cdgy l\u00e9trej\u00f6tt egy \u00fatvonal a CalculatorFragment \u00e9s a HistoryFragment k\u00f6z\u00f6tt. V\u00e9gezz\u00fck el ugyanezt visszafel\u00e9. Ha ezzel megvagyunk, akkor k\u00f6vetkez\u0151t kell l\u00e1tnunk:

    "},{"location":"laborok/calculator/#calculatoroperator","title":"CalculatorOperator","text":"

    A labor k\u00f6vetkez\u0151 szakasz\u00e1ban a CalculatorOperator nev\u0171 seg\u00e9doszt\u00e1lyt fogjuk implement\u00e1lni, aminek a feladata, hogy elt\u00e1rolja a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1t, \u00e9s kisz\u00e1m\u00edtsa a t\u00e1mogatott m\u0171veletek eredm\u00e9ny\u00e9t. Ezt a Kotlin Regex k\u00f6nyvt\u00e1r\u00e1nak seg\u00edts\u00e9g\u00e9vel v\u00e9gzi el.

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt hozzunk l\u00e9tre egy \u00faj util package-t (jobb klikk calculator package-en \u2192 New \u2192 package), benne Util nev\u0171 Kotlin objektummal. Ez az objektum fogja tartalmazni az olyan konstansokat \u00e9s seg\u00e9dv\u00e1ltoz\u00f3kat, amik a sz\u00e1mol\u00f3g\u00e9p m\u0171k\u00f6dtet\u00e9s\u00e9hez sz\u00fcks\u00e9gesek.

    object Util {\nconst val COMMA = \".\"\n\nprivate const val numberPattern = \"0[.][0-9]+|[1-9][0-9]*[.][0-9]+|[1-9][0-9]*|0|^[\\\\s]\"\nval numberRegex = Regex(numberPattern)\n\nprivate const val halfOperation = \"[/*%+-]($numberPattern)|^[\\\\s]\"\nval halfOperationRegex = Regex(halfOperation)\n\nprivate const val operationSymbolPattern = \"[/*%+-]|^[\\\\s]\"\nval operationSymbol = Regex(operationSymbolPattern)\n}\n

    Ha ezzel megvagyunk hozzunk l\u00e9tre egy \u00faj model package-t a calculator package-n bel\u00fcl, majd hozzunk l\u00e9tre benne egy \u00faj enum oszt\u00e1lyt OperationSymbol n\u00e9ven, ami a sz\u00e1mol\u00f3g\u00e9ppel elv\u00e9gezhet\u0151 m\u0171veleteket reprezent\u00e1lja. Az enum oszt\u00e1lyunk rendelkezzen egy konstruktorral, ami a m\u0171veletekhez a hozz\u00e1juk tartoz\u00f3 szimb\u00f3lumot rendeli String-k\u00e9nt.

    enum class OperationSymbol(val symbol: String) {\nDIVISION(\"/\"),   // 0\nMULTIPLICATION(\"*\"),   // 1\nSUBTRACTION(\"-\"),   // 2\nADDITION(\"+\"),   // 3\nMODULO(\"-\");   // 4\n}\n

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt eg\u00e9sz\u00edts\u00fck ki az oszt\u00e1lyt egy companion object-el benne egy olyan getByOrdinal() nev\u0171 seg\u00e9df\u00fcggv\u00e9nnyel, ami a sorrend szerinti index alapj\u00e1n visszaadja a megfelel\u0151 OperationSymbol-t. Teh\u00e1t 0 eset\u00e9n a DIVISION-t, 1 eset\u00e9n MULTIPLICATION-t \u00e9s \u00edgy tov\u00e1bb.

    companion object {\nfun getByOrdinal(ordinal: Int): OperationSymbol? = OperationSymbol.entries.getOrNull(ordinal)\n}\n

    Az enum oszt\u00e1lyhoz tartoz\u00f3 entries az oszt\u00e1lyban defini\u00e1lt enum objektumok t\u00f6mbj\u00e9t adja vissza.

    Ezut\u00e1n a util package-ben hozzunk l\u00e9tre egy CalculatorOperator nev\u0171 Kotlin object-t (Singleton oszt\u00e1ly). Ez a Singleton felel a sz\u00e1mol\u00f3g\u00e9p vez\u00e9rl\u00e9s\u00e9\u00e9rt.

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt a CalculatorOperator oszt\u00e1lyon bel\u00fcl vegy\u00fcnk fel egy CalculatorState nev\u0171 data class-t. Ez a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak t\u00e1rol\u00e1s\u00e1\u00e9rt felel, ami a CalculatorOperator met\u00f3dusai sz\u00e1m\u00e1ra \u00edrhat\u00f3 \u00e9s olvashat\u00f3, m\u00e1s oszt\u00e1lyok sz\u00e1m\u00e1ra pedig csak olvashat\u00f3.

    object CalculatorOperator {\n\nvar state: CalculatorState = CalculatorState()\nprivate set\n\ndata class CalculatorState(\nval input: String = \"\",\nval number1: Double = Double.NaN,\nval number2: Double = Double.NaN,\nval operation: OperationSymbol = OperationSymbol.ADDITION,\nval result: Double = Double.NaN\n)\n}\n

    data class

    A data class egy nagyon hasznos funkci\u00f3 a Kotlin nyelvben. Seg\u00edts\u00e9g\u00e9vel az els\u0151dleges konstruktorban (ami k\u00f6zvetlen\u00fcl az oszt\u00e1lyn\u00e9v ut\u00e1n \u00e1ll) deklar\u00e1lt v\u00e1ltoz\u00f3kra automatikusan gener\u00e1l\u00f3dnak a hashCode(), equals(), illetve toString() f\u00fcggv\u00e9nyek, melyek hasznosak a k\u00fcl\u00f6nb\u00f6z\u0151 adathalmazok kezel\u00e9s\u00e9re. B\u00e1r megengedett, az adat oszt\u00e1lyokn\u00e1l lehet\u0151leg ker\u00fclj\u00fck a v\u00e1ltoztathat\u00f3 v\u00e1ltoz\u00f3kat (var).

    Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat, majd implement\u00e1ljuk a sz\u00e1mok bevitel\u00e9\u00e9rt felel\u0151s onNumberPressed() met\u00f3dust:

    fun onNumberPressed(number: Int) {\nstate = state.copy(\ninput = state.input + \"$number\"\n)\n}\n

    A sz\u00e1mok bevitele \u00fagy t\u00f6rt\u00e9nik, hogy az \u00e1llapot input attrib\u00fatum\u00e1t mindig egy hozz\u00e1f\u0171z\u00f6tt sz\u00e1mmal megv\u00e1ltoztatott \u00e9rt\u00e9kkel \"friss\u00edtj\u00fck\".

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt implement\u00e1ljuk a m\u0171veletek elv\u00e9gz\u00e9s\u00e9\u00e9rt felel\u0151s met\u00f3dust:

    fun onOperationPressed(operation: Int) {\nval input = state.input\nif (Util.numberRegex.matches(input)) {\nstate = state.copy(\nnumber1 = Util.numberRegex.find(input)!!.value.toDouble(),\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\nstate = state.copy(\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.operationSymbol.matches(input)) {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n}\n}\n
    Ha \u00e9rdekel az onOperationPressed() m\u0171k\u00f6d\u00e9si elve, akkor ezt a f\u00fclet lenyitva tudod megismerni.

    A m\u0171veletek kezel\u00e9se sor\u00e1n h\u00e1rom esetet k\u00fcl\u00f6nb\u00f6ztet\u00fcnk meg egym\u00e1st\u00f3l:

    Az els\u0151 eset, amikor az input string m\u00e1r tartalmaz valamilyen sz\u00e1mot. Ekkor ezt a sz\u00e1mot elt\u00e1roljuk a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak (state) number1 attrib\u00fatum\u00e1ban, majd az input-ot fel\u00fcl\u00edrj\u00fck a bevitt m\u0171velet szimb\u00f3lum\u00e1val, illetve az operation attrib\u00fatumba is elmentj\u00fck a megfelel\u0151 OperationSymbol enum objektumot.

    A m\u00e1sodik eset akkor t\u00f6rt\u00e9nik, amikor az input-ban egy szimb\u00f3lumot valamilyen sz\u00e1m is k\u00f6vet (pl. \"*5\"). Ekkor a number1 attrib\u00fatum m\u00e1r tartalmaz egy sz\u00e1mot (az 1. eset m\u00e1r lezajlott), \u00edgy az els\u0151 esetben felvett m\u0171veleti szimb\u00f3lummal \u00e9s az azt k\u00f6vet\u0151 sz\u00e1mmal m\u00e1r el is v\u00e9gezhet\u0151 egy m\u0171velet. A m\u0171velet eredm\u00e9ny\u00e9vel fel\u00fcl\u00edrjuk a number1 \u00e9rt\u00e9k\u00e9t, majd a kor\u00e1bbiakhoz hasonl\u00f3an elt\u00e1roljuk az \u00faj m\u0171velet szimb\u00f3lum\u00e1t.

    A harmadik eset, akkor \u00e1ll fenn, amikor csak egy m\u0171veleti szimb\u00f3lum van az input-ban (pl. \"*\"), ami helyett m\u00e1st akarunk elv\u00e9gezni, ekkor csak sim\u00e1n lecser\u00e9lj\u00fck az operation-t \u00e9s az input-ot.

    V\u00e9gs\u0151 esetben, amikor az input teljesen \u00fcres, akkor inicializ\u00e1ljuk az \u00faj m\u0171veleti \u00e9rt\u00e9kekkel a state-et.

    M\u00e9g sz\u00fcks\u00e9g\u00fcnk van a countResult() met\u00f3dusra, aminek seg\u00edts\u00e9g\u00e9vel kisz\u00e1m\u00edthat\u00f3ak a m\u0171veletek eredm\u00e9nyei:

    private fun countResult(number2: Double): Double {\nreturn when (state.operation) {\nOperationSymbol.DIVISION -> state.number1 / number2\nOperationSymbol.MULTIPLICATION -> state.number1 * number2\nOperationSymbol.MODULO -> state.number1 % number2\nOperationSymbol.ADDITION -> state.number1 + number2\nOperationSymbol.SUBTRACTION -> state.number1 - number2\n}\n}\n

    V\u00e9gezet\u00fcl vegy\u00fck fel az el\u0151jelv\u00e1lt\u00e1s\u00e9rt, a tizedesjegy\u00e9rt, a m\u0171velet elv\u00e9gz\u00e9s\u00e9\u00e9rt (=) \u00e9s a t\u00f6rtl\u00e9s\u00e9rt felel\u0151s met\u00f3dusokat. Ezekben a met\u00f3dusokban a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1nak friss\u00edt\u00e9se a fentiekhez hasonl\u00f3an t\u00f6rt\u00e9nik.

    fun onSignChange(): Double {\nval input = state.input\nreturn if (Util.numberRegex.matches(input)) {\nval number1 = -Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nresult = Double.NaN,\nnumber1 = number1,\ninput = \"\"\n)\nnumber1\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 =  -Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nnumber2 = number2,\ninput = \"\"\n)\nnumber2\n} else Double.NaN\n}\n\nfun addComa() {\nstate = state.copy(\ninput = state.input + Util.COMMA\n)\n}\n\nfun onEquivalence(): Double {\nval input = state.input\nreturn if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else if (!state.number2.isNaN()) {\nval result = countResult(state.number2)\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else Double.NaN\n}\n\nfun onDelete() {\nstate = state.copy(\nnumber1 = Double.NaN,\nnumber2 = Double.NaN,\nresult = Double.NaN,\ninput = \"\"\n)\n}\n
    "},{"location":"laborok/calculator/#calculatorfragment","title":"CalculatorFragment","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt keress\u00fck meg a res/layout alatt tal\u00e1lhat\u00f3 fragment_calculator.xml f\u00e1jlt, ahol CalculatorFragment View komponenseit \u00e9s azok elrendez\u00e9s\u00e9t fogjuk meghat\u00e1rozni.

    A laborra ford\u00edthat\u00f3 id\u0151 miatt csak ismerkedj\u00fcnk meg az XML fel\u00e9p\u00edt\u00e9si elv\u00e9vel \u00e9s m\u00e1soljuk \u00e1t a k\u00e9sz View hierarchi\u00e1t, ami az al\u00e1bbi r\u00e9szt k\u00f6vet\u0151en el\u00e9rhet\u0151 egyben, egy leny\u00edl\u00f3 r\u00e9szt kinyitva.

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TableLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_weight=\"1\">\n...\n    </TableRow>\n...\n</TableLayout>\n

    A CalculatorFragment-\u00fcnk t\u00e1bl\u00e1zatos elrendez\u00e9s\u00e9t egy TableLayout hat\u00e1rozza meg, aminek a sorait TableRow-k seg\u00edts\u00e9g\u00e9vel tudjuk megadni. A t\u00e1bl\u00e1zat mind a 6 sora ugyanolyan magass\u00e1g\u00fa, amit \u00fagy tudunk el\u00e9rni, hogy a TableRow-k eset\u00e9n az android:layout_weight=\"1\" attrib\u00fatumot felvessz\u00fck. Emellett minden gomb rendelkezik 5dp margin-nal, hogy ne legyenek t\u00fal k\u00f6zel egym\u00e1shoz.

    <TextView\nandroid:id=\"@+id/consoleTextView\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/text_calculator_console\" android:textSize=\"40sp\"\nandroid:gravity=\"center_vertical|end\"\nandroid:fontFamily=\"sans-serif-medium\"/>\n

    A t\u00e1bl\u00e1zat els\u0151 sor\u00e1ban a konzol szerep\u00e9t bet\u00f6lt\u0151 TextView szerepel. Az \u00e1ltala megjelen\u00edtett sz\u00f6veg a View-n bel\u00fcl jobb oldalt, f\u00fcgg\u0151legesen k\u00f6z\u00e9pre igaz\u00edtva l\u00e1that\u00f3 f\u00e9lk\u00f6v\u00e9r bet\u0171t\u00edpussal. Ez az android:gravity \u00e9s android:fontFamily attrib\u00fatumokkal \u00e9rhet\u0151 el.

     <Button\nandroid:id=\"@+id/deleteButton\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:textSize=\"22sp\"\nandroid:text=\"@string/button_text_delete\"\nstyle=\"@style/Widget.Material3.Button\"/>\n

    A konzol alatt l\u00e9v\u0151 sorokban a m\u0171velet \u00e9s sz\u00e1m gombok k\u00f6vetkeznek. A gombok egyenletesen t\u00f6ltik ki a sorokban elfoglalhat\u00f3 teret, ami ugyancsak az android:layout_weight=\"1\" attrib\u00fatum felv\u00e9tel\u00e9vel \u00e9rhet\u0151 el.

    CalculatorFragment teljes XML k\u00f3dja
    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TableLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_weight=\"1\">\n\n<TextView\nandroid:id=\"@+id/consoleTextView\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_weight=\"1\"\nandroid:clickable=\"true\"\nandroid:focusable=\"true\"\nandroid:fontFamily=\"sans-serif-medium\"\n\nandroid:gravity=\"center_vertical|end\"\nandroid:text=\"@string/text_calculator_console\"\nandroid:textSize=\"40sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/deleteButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_delete\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/signButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_sign\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/modulo_button\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_modulo\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationDivisionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_division\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number7Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_seven\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number8Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_eight\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number9Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_nine\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationMultiplicationButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_multiplication\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number4Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_four\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number5Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_five\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number6Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_six\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationSubtractionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_subtraction\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number1Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_one\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number2Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_two\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/number3Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_three\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/operationAdditionButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_addition\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n\n<TableRow\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_weight=\"1\">\n\n<Button\nandroid:id=\"@+id/number0Button\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_number_zero\"\nandroid:textSize=\"22sp\" />\n\n<Button\nandroid:id=\"@+id/commaButton\"\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_come\"\nandroid:textSize=\"22sp\" />\n\n<Button\nstyle=\"@style/Widget.Material3.Button.ElevatedButton.Icon\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:enabled=\"false\"\nandroid:visibility=\"invisible\" />\n\n<Button\nandroid:id=\"@+id/operationEquivalenceButton\"\nstyle=\"@style/Widget.Material3.Button\"\nandroid:layout_height=\"match_parent\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"1\"\nandroid:text=\"@string/button_text_equivalence\"\nandroid:textSize=\"22sp\" />\n</TableRow>\n</TableLayout>\n

    T\u00e9rj\u00fcnk \u00e1t a CalculatorFragment oszt\u00e1lyra, ahol a f\u00e1jl tartalm\u00e1t cser\u00e9lj\u00fck le a k\u00f6vetkez\u0151re:

    package hu.bme.aut.android.calculator\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.calculator.databinding.FragmentCalculatorBinding\n\nclass CalculatorFragment : Fragment() {\n\nprivate var _binding: FragmentCalculatorBinding? = null\nprivate val binding get() = _binding!!\n\noverride fun onCreateView(\ninflater: LayoutInflater, container: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\n_binding = FragmentCalculatorBinding.inflate(inflater, container, false)\nreturn binding.root\n}\n\noverride fun onDestroyView() {\nsuper.onDestroyView()\n_binding = null\n}\n}\n

    Miel\u0151tt r\u00e1t\u00e9rn\u00e9nk a Fragment \u00e1ltal kezelt View komponensek inicializ\u00e1l\u00e1s\u00e1ra, vegy\u00fcnk fel m\u00e9g k\u00e9t Set-et, amikben a sz\u00e1m \u00e9s m\u0171velet gombokhoz tartoz\u00f3 View-k referenci\u00e1it fogjuk t\u00e1rolni. Bevezet\u00e9s\u00fckkel az inicaliz\u00e1l\u00e1s k\u00f6nnyebben elv\u00e9gezhet\u0151. Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat.

    private lateinit var numberButtons: Set<Button>\n\nprivate lateinit var operationButtons: Set<Button>\n

    Ha ezzel megvagyunk t\u00e1roljunk el egy referenci\u00e1t a sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1r\u00f3l.

    private val calcState get() = CalculatorOperator.state\n

    Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1t. Majd t\u00e9rj\u00fcnk \u00e1t az onViewCreated() met\u00f3dusra, ahol megadhatjuk a View komponensek esem\u00e9nyekre adott viselked\u00e9s\u00e9t.

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt szervezz\u00fck ki a sz\u00e1mok megjelen\u00edt\u00e9s\u00e9nek elv\u00e9t egy setResult() nev\u0171 met\u00f3dusba. Ez seg\u00edti, hogy az eg\u00e9sz \u00e9s a val\u00f3s sz\u00e1mok k\u00fcl\u00f6nb\u00f6z\u0151 m\u00f3don jelenjenek meg a k\u00e9perny\u0151n.

    private fun setResult(value: Double) {\nif (value.isNaN()) {\nbinding.consoleTextView.text = \"\"\n} else if (value % 1.0 == 0.0) {\nbinding.consoleTextView.text = value.toInt().toString()\n} else {\nbinding.consoleTextView.text = String.format(\"%.2f\",value)\n}\n}\n
    Ezut\u00e1n implement\u00e1ljuk az initButtons() met\u00f3dust.

    private fun initButtons() {\n// Init number and operation button sets\nwith(binding) {\nnumberButtons = setOf(\nnumber0Button, number1Button, number2Button,\nnumber3Button, number4Button, number5Button,\nnumber6Button, number7Button, number8Button,\nnumber9Button\n)\n\noperationButtons = setOf(\noperationDivisionButton,\noperationMultiplicationButton,\noperationSubtractionButton,\noperationAdditionButton,\nmoduloButton\n)\n}\n\n// Init click listeners for number buttons\nnumberButtons.forEachIndexed { number, button ->\nbutton.setOnClickListener {\nCalculatorOperator.onNumberPressed(number)\nbinding.consoleTextView.text = numberRegex.find(calcState.input)?.value ?: \"\"\n}\n}\n\n// Init click listeners for number buttons\noperationButtons.forEachIndexed { operation, button ->\nbutton.setOnClickListener {\nCalculatorOperator.onOperationPressed(operation)\nsetResult(calcState.result)\n}\n}\n\n// Init click listener for sign button\nbinding.signButton.setOnClickListener {\nsetResult(CalculatorOperator.onSignChange())\n}\n\n// Init click listener for delete button\nbinding.deleteButton.setOnClickListener {\nCalculatorOperator.onDelete()\nsetResult(calcState.result)\n}\n\n// Init click listener for comma button\nbinding.commaButton.setOnClickListener {\nCalculatorOperator.addComa()\nbinding.consoleTextView.text = calcState.input\n}\n\n// Init click listener for equivalence button\nbinding.operationEquivalenceButton.setOnClickListener {\nsetResult(CalculatorOperator.onEquivalence())\n}\n}\n

    Itt el\u0151sz\u00f6r inicializ\u00e1ljuk Set-eket. Majd egy forEachIndexed ciklissal be\u00e1ll\u00edtjuk az esem\u00e9nykezel\u0151j\u00fcket. Ezut\u00e1n sorra elv\u00e9gezz\u00fck az esem\u00e9nykezel\u0151k be\u00e1ll\u00edt\u00e1s\u00e1t azokra a gombokra is, amikb\u0151l csak egy-egy p\u00e9ld\u00e1ny l\u00e9tezik.

    A with egy olyan scope f\u00fcggv\u00e9ny, aminek seg\u00edts\u00e9g\u00e9vel azt tudjuk kifejezni, hogy: ezzel az objektummal csin\u00e1ld a k\u00f6vetkez\u0151t. \u00cdgy sok esetben kicsit \u00e1tl\u00e1that\u00f3bb\u00e1 lehet tenni a k\u00f3dot, mivel a context-k\u00e9nt megadott binding objektumra this-k\u00e9nt hivatkozhatunk. Vannak m\u00e1s scope f\u00fcggv\u00e9nyek is k\u00fcl\u00f6nb\u00f6z\u0151 felhaszn\u00e1l\u00e1si esetekre. R\u00f3luk ezen a linken lehet olvasni.

    V\u00e9gezet\u00fcl h\u00edvjuk meg ezt az initButtons() met\u00f3dust az onViewCreated()-ben.

     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\nsuper.onViewCreated(view, savedInstanceState)\n\ninitButtons()   }\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a CalculatorFragment egy bele\u00edrt sz\u00e1mmal (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/calculator/#recyclerview","title":"RecyclerView","text":"

    A RecyclerView k\u00f6nyvt\u00e1r megk\u00f6nny\u00edti a nagy adathalmazok hat\u00e9kony megjelen\u00edt\u00e9s\u00e9t. Meg kell hat\u00e1rozni, hogy az egyes elemek hogyan n\u00e9zzenek ki. \u00cdgy az adathalmazt \u00e1tadva egy Adapter nev\u0171 komponensnek az dinamikusan l\u00e9trehozza az elemeket akkor, amikor \u00e9ppen sz\u00fcks\u00e9g van r\u00e1juk.

    Ahogy a n\u00e9v is sugallja, a RecyclerView \u00fajrahasznos\u00edtja ezeket a View elemeket. Amikor egy elem elt\u0171nik a k\u00e9perny\u0151r\u0151l, a RecyclerView nem szabad\u00edtja fel, hanem \u00fajra felhaszn\u00e1lja azt a k\u00e9perny\u0151n megjelen\u0151 \u00faj elemhez. Ennek k\u00f6sz\u00f6nhet\u0151en a RecyclerView bevezet\u00e9s\u00e9vel javul a teljes\u00edtm\u00e9ny \u00e9s az alkalmaz\u00e1s v\u00e1laszideje, tov\u00e1bb\u00e1 cs\u00f6kkenti az energiafogyaszt\u00e1st.

    A RecyclerView implement\u00e1l\u00e1sa \u00edgy h\u00e1rom l\u00e9p\u00e9sb\u0151l \u00e1ll:

    1. l\u00e9p\u00e9s: View elem layout-j\u00e1nak meghat\u00e1roz\u00e1sa
    2. l\u00e9p\u00e9s: Adapter oszt\u00e1ly implement\u00e1l\u00e1sa
    3. l\u00e9p\u00e9s: RecyclerView felv\u00e9tele a Fragment/Activity-ben \u00e9s inicializ\u00e1l\u00e1sa a megfelel\u0151 oszt\u00e1lyban

    Hozzunk l\u00e9re egy \u00faj XML f\u00e1jlt view_history_item.xml n\u00e9ven ares/layout mapp\u00e1ban. A lista elem LinearLayout-ot haszn\u00e1l a komponensei v\u00edzszintes sorba t\u00f6rt\u00e9n\u0151 elrendez\u00e9s\u00e9hez (android:orientation=\"horizontal\"). A LinearLayout eset\u00e9n is rendelhet\u00fcnk s\u00falyoz\u00e1st (ar\u00e1nyokat) az egyes View komponensekhez az android:weightSum \u00e9s az android:layout_weight attrib\u00fatumok seg\u00edts\u00e9g\u00e9vel.

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nandroid:id=\"@+id/historyView\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"wrap_content\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:orientation=\"horizontal\"\nandroid:gravity=\"center_vertical\"\nandroid:layout_margin=\"5dp\"\nandroid:weightSum=\"10\">\n\n<TextView\nandroid:id=\"@+id/operationTextView\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\ntools:text=\"1 + 1 = 2\"\nandroid:layout_margin=\"5dp\"\nandroid:layout_weight=\"7\"\nandroid:textSize=\"22sp\"\nandroid:layout_gravity=\"start\"\nandroid:fontFamily=\"sans-serif-medium\" />\n\n<Button\nandroid:id=\"@+id/loadButton\"\nstyle=\"?attr/materialButtonOutlinedStyle\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_margin=\"5dp\"\nandroid:text=\"@string/button_text_load\"\nandroid:layout_weight=\"3\"/>\n\n</LinearLayout>\n

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck el a HistoryAdapter oszt\u00e1lyunkat. Ehhez hozzunk l\u00e9tre egy \u00faj adapter package-et, majd benne egy \u00faj Kotlin oszt\u00e1lyt HistoryAdapter n\u00e9ven. Az oszt\u00e1lyunk belsej\u00e9ben vegy\u00fcnk fel egy ViewHolder nev\u0171 inner class-t, aminek konstruktor\u00e1ban egy ViewHistoryItemBinding szerepeljen, amit majd ez egyes View referenci\u00e1k inicializ\u00e1l\u00e1s\u00e1hoz haszn\u00e1lunk fel. Import\u00e1ljuk a hi\u00e1nyz\u00f3 ViewHistoryItemBinding referenci\u00e1t.

    package hu.bme.aut.android.calculator.adapter\n\nimport android.widget.Button\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport hu.bme.aut.android.calculator.databinding.ViewHistoryItemBinding\n\nclass HistoryAdapter {\n\ninner class ViewHolder(binding: ViewHistoryItemBinding) :\nRecyclerView.ViewHolder(binding.historyView) {\n\nval operationTextView: TextView\nval loadButton: Button\n\ninit {\noperationTextView = binding.operationTextView\nloadButton = binding.loadButton\n}\n}\n}\n

    A HistoryAdapter-t sz\u00e1rmaztassuk le a RecyclerView.Adapter absztrakt oszt\u00e1lyb\u00f3l, \u00e9s az els\u0151dleges konstruktor\u00e1ban vegy\u00fcnk fel egy history v\u00e1ltoz\u00f3t, ami a sz\u00e1mol\u00f3g\u00e9p \u00e1ltal elmentett kor\u00e1bbi m\u0171veleteket tartalmazza. Import\u00e1ljuk a hi\u00e1nyz\u00f3 referenci\u00e1kat.

    class HistoryAdapter(\nprivate val history: List<CalculatorOperator.CalculatorState>,\nprivate val context: Context\n) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>()\n

    Az AndroidStudio most hib\u00e1t jelez ez az\u00e9rt van, mert implement\u00e1lnunk kell az onCreateViewHolder(), onBindViewHolder() \u00e9s a getItemCount() met\u00f3dusokat.

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\nval binding = ViewHistoryItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)\nreturn ViewHolder(binding)\n}\n\noverride fun onBindViewHolder(holder: ViewHolder, position: Int) {\nval operation = history[position]\nholder.operationTextView.text = context.getString(\nR.string.text_operation,\noperation.number1,\noperation.operation.symbol,\noperation.number2,\noperation.result\n)\nholder.loadButton.setOnClickListener {\n// TODO: implement event handler\n}\n}\n\noverride fun getItemCount(): Int = history.size\n

    getString()

    Ha a String er\u0151forr\u00e1sunkban valamilyen v\u00e1ltoz\u00f3 \u00e9rt\u00e9k\u00e9t szeretn\u00e9nk megjelen\u00edteni (form\u00e1z\u00e1s), akkor azt \u00fagy tehetj\u00fck meg, hogy az er\u0151forr\u00e1sunkban argumantumokat helyez\u00fcnk el. Eset\u00fcnkben egy ilyen argumentum p\u00e9ld\u00e1ul a %1$.2f, ami kett\u0151 tizedesjegyig jelen\u00edti meg a sz\u00e1mokat.

    A Context-b\u0151l el\u00e9rhet\u0151 getString() met\u00f3dus seg\u00edts\u00e9g\u00e9vel pedig az er\u0151forr\u00e1s id-j\u00e1nak megad\u00e1s\u00e1t k\u00f6vet\u0151en felsorolhatjuk az argumentumainkat, amik alapj\u00e1n a getString() elv\u00e9gzi azok besz\u00far\u00e1s\u00e1t.

    A lista elemre val\u00f3 kattint\u00e1s kezel\u00e9s\u00e9hez sz\u00fcks\u00e9g\u00fcnk lesz egy interface-re, amit majd a list\u00e1t megjelen\u00edt\u00f3 Fragment fog implement\u00e1lni. Vegy\u00fcnk fel egy bels\u0151 interface-t ClickListener n\u00e9ven, ami rendelkezzen egy onClick(loadedData: String) met\u00f3dussal. \u00c9s eg\u00e9sz\u00edts\u00fck ki az Adapter konstruktor\u00e1t egy onClickListener v\u00e1ltoz\u00f3val.

    class HistoryAdapter(\nprivate val onClickListener: ClickListener,\nprivate val history: List<CalculatorOperator.CalculatorState>,\nprivate val context: Context\n) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {\n...\n
    interface ClickListener {\nfun onClick(loadedData: String)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a HistoryAdapter oszt\u00e1ly k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/calculator/#historyfragment","title":"HistoryFragment","text":"

    T\u00e9rj\u00fcnk vissza a CalculatorOperator-ra, hogy kezelni tudja az elv\u00e9gzett m\u0171veletek ment\u00e9s\u00e9t. Eg\u00e9sz\u00edts\u00fck ki az oszt\u00e1lyt egy history v\u00e1ltoz\u00f3val, egy loadState() \u00e9s egy addStateToHsitory() met\u00f3dussal, illetve m\u00f3dos\u00edtsuk az onOperationPressed() \u00e9s onEquaivalence() met\u00f3dusokat.

    object CalculatorOperator {\n\nval history = mutableListOf<CalculatorState>()\n\nfun loadState(value: String) {\nstate = state.copy(\ninput = \"\",\nnumber1 = value.toDouble(),\nnumber2 = 0.0,\nresult = 0.0,\noperation = OperationSymbol.ADDITION\n)\n}\n\nprivate fun addStateToHistory() {\nhistory.add(state)\n}\n...\n
    fun onOperationPressed(operation: Int) {\nval input = state.input\nif (Util.numberRegex.matches(input)) {\nstate = state.copy(\nnumber1 = Util.numberRegex.find(input)!!.value.toDouble(),\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nstate = state.copy(\nnumber2 = number2,\nresult = countResult(number2),\n)\n\naddStateToHistory()\n\nstate = state.copy(\nnumber1 = state.result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput =  OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else if (Util.operationSymbol.matches(input)) {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n} else {\nstate = state.copy(\noperation = OperationSymbol.getByOrdinal(operation)!!,\ninput = OperationSymbol.getByOrdinal(operation)!!.symbol\n)\n}\n}\n
    fun onEquivalence(): Double {\nval input = state.input\nreturn if (Util.halfOperationRegex.matches(input)) {\nval number2 = Util.numberRegex.find(input)!!.value.toDouble()\nval result = countResult(number2)\n\nstate = state.copy(\nnumber2 = number2,\nresult = result\n)\n\naddStateToHistory()\n\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else if (!state.number2.isNaN()) {\nval result = countResult(state.number2)\naddStateToHistory()\nstate = state.copy(\ninput = \"\",\nnumber1 = result,\nnumber2 = Double.NaN,\noperation = OperationSymbol.ADDITION,\nresult = Double.NaN\n)\nresult\n} else Double.NaN\n}\n

    Ha ezzel megvagyunk keress\u00fck meg a res/layout mapp\u00e1ban l\u00e9v\u0151 fragment_history.xml f\u00e1jlt \u00e9s m\u00f3dos\u00edtsuk a k\u00f6vetkez\u0151 m\u00f3don.

    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\">\n\n<com.google.android.material.appbar.AppBarLayout\nandroid:id=\"@+id/historyAppBar\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\">\n\n<com.google.android.material.appbar.MaterialToolbar\nandroid:id=\"@+id/topAppBar\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"?attr/actionBarSize\"\napp:navigationIcon=\"@drawable/ic_arrow_back_24\"\napp:title=\"@string/app_bar_title_history\" />\n\n</com.google.android.material.appbar.AppBarLayout>\n\n<androidx.recyclerview.widget.RecyclerView\nandroid:id=\"@+id/historyRecyclerView\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\nandroid:clipToPadding=\"false\"\napp:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@id/historyAppBar\"\ntools:listitem=\"@layout/view_history_item\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

    A HistoryFragment egy AppBar-t \u00e9s egy RecyclerView jelen\u00edt meg. Az AppBar rendelkezik egy vissza gombbal, amihez az ikont a labor elej\u00e9n m\u00e1r l\u00e9trehoztuk. A RecyclerView pedig egy f\u00fcgg\u0151legesen g\u00f6rgethet\u0151 list\u00e1t jelen\u00edt meg.

    T\u00e9rj\u00fcnk \u00e1t a HistoryFragment oszt\u00e1lyra. Az oszt\u00e1lynak most a Fragment-b\u0151l val\u00f3 sz\u00e1rmaz\u00e1s mellett a HistoryAdapter.ClickListener interf\u00e9sz\u00e9t is implement\u00e1lnia kell. Az Adapter inicializ\u00e1l\u00e1sa most is egy lateinit var v\u00e1ltoz\u00f3 \u00e9s az onViewCreated() met\u00f3dus seg\u00edts\u00e9g\u00e9vel t\u00f6rt\u00e9nik. El\u0151sz\u00f6r inicializ\u00e1ljuk, \u00e9s ut\u00e1na a binding seg\u00edts\u00e9s\u00e9g\u00e9vel \u00f6sszek\u00f6tj\u00fck a Fragment \u00e1ltal megjelen\u00edtett RecyclerView komponenssel. V\u00e9gs\u0151 soron pedig hozz\u00e1rendel\u00fcnk egy esem\u00e9nykezele\u0151t az AppBar-ban l\u00e9v\u0151 vissza gombhoz.

    package hu.bme.aut.android.calculator\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.calculator.adapter.HistoryAdapter\nimport hu.bme.aut.android.calculator.databinding.FragmentHistoryBinding\nimport hu.bme.aut.android.calculator.util.CalculatorOperator\n\nclass HistoryFragment: Fragment(), HistoryAdapter.ClickListener {\n\nprivate var _binding: FragmentHistoryBinding? = null\nprivate val binding get() = _binding!!\n\nprivate lateinit var adapter: HistoryAdapter\n\noverride fun onCreateView(\ninflater: LayoutInflater,\ncontainer: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\n_binding = FragmentHistoryBinding.inflate(inflater, container, false)\nreturn binding.root\n}\n\noverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {\nsuper.onViewCreated(view, savedInstanceState)\n\n// Init RecyclerView\nadapter = HistoryAdapter(this@HistoryFragment, CalculatorOperator.history, requireContext())\nbinding.historyRecyclerView.adapter = adapter\n\nbinding.topAppBar.setNavigationOnClickListener {\n// TODO: navigate back to CalculatorFragment\n}\n}\n\noverride fun onClick(loadedData: String) {\n// TODO: implement method\n}\n\noverride fun onDestroyView() {\nsuper.onDestroyView()\n_binding = null\n}\n}\n

    Az\u00e9rt, hogy a visszat\u00f6lt\u00f6tt eredm\u00e9ny a felhaszn\u00e1l\u00f3 sz\u00e1m\u00e1ra is l\u00e1that\u00f3v\u00e1 v\u00e1ljon, eg\u00e9sz\u00edts\u00fck ki a CalculatorFragment onViewCreated f\u00fcggv\u00e9ny\u00e9t:

    setResult(CalculatorOperator.state.number1)\n
    "},{"location":"laborok/calculator/#navigacio","title":"Navig\u00e1ci\u00f3","text":"

    Most m\u00e1r csak a Fragment-ek k\u00f6zti navig\u00e1ci\u00f3 v\u00e9gleges\u00edt\u00e9se van h\u00e1tra. A CalculatorFragment-r\u0151l a konzol\u00e9rt felel\u0151s TextView-ra kattintva t\u00e9rhet\u00fcnk \u00e1t a HistoryFragment-re. Ehhez t\u00e9rj\u00fcnk vissza a CalculatorFragment onViewCreated() met\u00f3dus\u00e1ra, ahol a View komponensek inicializ\u00e1l\u00e1st v\u00e9gezt\u00fck el. Vegy\u00fck fel a k\u00f6vetkez\u0151 esem\u00e9nykezel\u0151t \u00e9s enged\u00e9lyezz\u00fck, hogy a View kattinthat\u00f3 legyen:

    with(binding.consoleTextView) {\nisClickable = true\nsetOnClickListener {\nval action = CalculatorFragmentDirections.actionCalculatorFragmentToHistoryFragment()\nfindNavController().navigate(action)\n}\n}\n

    A navig\u00e1ci\u00f3 kezel\u00e9s\u00e9\u00e9rt az \u00fan. NavController felel, amit a findNavController() met\u00f3dussal \u00e9rhet\u00fcnk el, rajta pedig a navigate(action) met\u00f3dush\u00edv\u00e1ssal ki tudjuk v\u00e1ltani a navig\u00e1ci\u00f3t. Az action-ben most a navig\u00e1ci\u00f3s \"\u00fatvonal\" szerepel, de ha sz\u00fcks\u00e9ges, akkor ez kieg\u00e9sz\u00edthet\u0151 tov\u00e1bbi argumentumokkal (l\u00e1sd a dokument\u00e1ci\u00f3ban). A findNavController()-hez androidx.navigation.fragment.findNavController import\u00e1l\u00e1s\u00e1ra lesz sz\u00fcks\u00e9g\u00fcnk.

    Ha ezzel megvagyunk t\u00e9rj\u00fcnk \u00e1t a HistoryFragment-re, ahol a vissza gombra fogjuk be\u00e1ll\u00edtani, hogy megnyom\u00e1sra l\u00e9pjen vissza a CalculatorFragment-re. Itt is az onViewCreated() met\u00f3dusban vagy\u00fck fel k\u00f6vetkez\u0151 esem\u00e9nykezel\u0151t:

    binding.topAppBar.setNavigationOnClickListener {\nfindNavController().popBackStack()\n}\n

    V\u00e9gezet\u00fcl implement\u00e1ljuk a HistoryFragment onClick() met\u00f3dus\u00e1t.

    override fun onClick(loadedData: String) {\nval action = HistoryFragmentDirections.actionHistoryFragmentToCalculatorFragment()\nCalculatorOperator.loadState(loadedData)\nfindNavController().navigate(action)\n}\n

    Ebben az esetben is a findNavController()-hez androidx.navigation.fragment.findNavController import\u00e1l\u00e1s\u00e1ra lesz sz\u00fcks\u00e9g\u00fcnk.

    Ahhoz, hogy az adatok bet\u00f6lt\u00e9se megfelel\u0151en megt\u00f6rt\u00e9nnyen t\u00e9rj\u00fcnk vissza a HistoryAdapter onBindViewHolder() met\u00f3dus\u00e1ra, \u00e9s \u00e1ll\u00edtsuk be, hogy a Load gombra val\u00f3 kattint\u00e1s sor\u00e1n a HistoryFragment \u00e1ltal implement\u00e1lt onClick() met\u00f3dust h\u00edvja meg:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\nval operation = history[position]\nholder.operationTextView.text = context.getString(\nR.string.text_operation,\noperation.number1,\noperation.operation.symbol,\noperation.number2,\noperation.result\n)\nholder.loadButton.setOnClickListener {\nif (operation.result % 1.0 == 0.0) {\nonClickListener.onClick(operation.result.toInt().toString())\n} else {\nonClickListener.onClick(String.format(\"%.10f\", operation.result))\n}\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a HistoryFragment n\u00e9h\u00e1ny bejegyz\u00e9ssel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a HistoryFragment oszt\u00e1ly onClick() met\u00f3dus\u00e1nak k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/calculator/#onallo-resz-elozmenyek-torlese","title":"\u00d6n\u00e1ll\u00f3 r\u00e9sz - El\u0151zm\u00e9nyek t\u00f6rl\u00e9se","text":"
    1. Vegy\u00fcnk fel egy \u00faj t\u00f6rl\u00e9s Vector Asset-et a vissza gombhoz hasonl\u00f3an.
    2. A ResourceManager seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edts\u00fcnk egy menu er\u0151forr\u00e1st menu_top_app_bar n\u00e9ven, ami t\u00f6rl\u00e9s men\u00fc elemet tartalmazza. Hint
    3. Implement\u00e1ljunk egy a t\u00f6rl\u00e9s\u00e9rt felel\u0151s clearHistory() met\u00f3dust a CalculatorOperator-ban.
    4. A FragmentHistory-hoz tartoz\u00f3 XML layout f\u00e1jlban vagy\u00fck fel a Toolbar-hoz tartoz\u00f3 app:menu=\"@menu/menu_top_app_bar\" attrib\u00fatumot.
    5. A FragmentHistory-ban vegy\u00fck fel a menu esem\u00e9nykezel\u0151j\u00e9t.
    binding.topAppBar.setOnMenuItemClickListener { menuItem ->\nwhen(menuItem.itemId) {\nR.id.delete -> {\nval size = CalculatorOperator.history.size\nCalculatorOperator.clearHistory()\nadapter.notifyItemRangeRemoved(0,size)\ntrue\n}\nelse -> false\n}\n}\n

    Itt fontos megjegyezni, hogy az adapter-t \u00e9rtes\u00edten\u00fcnk kell arr\u00f3l, hogy milyen intervallumot \u00e9rintett a v\u00e1ltoztat\u00e1s. Ezt az adapter.notifyItemRangeRemoved()-al tudjuk elv\u00e9gezni. Ha esetleg \u00faj elemet venn\u00e9nk fel vagy valamilyen egy\u00e9b v\u00e1ltoztat\u00e1st csin\u00e1ln\u00e1nk az adapter \u00e1ltal megjelen\u00edtett adathalmazon, arr\u00f3l ugyan\u00edgy \u00e9rtes\u00edteni kell az adapter-t.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik az \u00fcres History k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a HistoryFragment oszt\u00e1ly setOnMenuItemClickListener met\u00f3dus\u00e1nak k\u00f3dja, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/calculator/#onallo-feladat-kontextus-fuggo-mezotorles","title":"\u00d6n\u00e1ll\u00f3 feladat - Kontextus-f\u00fcgg\u0151 mez\u0151t\u00f6rl\u00e9s","text":"

    A jelenlegi alkalmaz\u00e1s a t\u00f6rl\u00e9sn\u00e9l a teljes sz\u00e1mol\u00f3g\u00e9p \u00e1llapot\u00e1t t\u00f6rli. Val\u00f3s\u00edtsuk meg, hogy ha m\u00e1r az els\u0151 sz\u00e1m \u00e9s jelet megadtuk, \u00e9s a m\u00e1sodik sz\u00e1mra is elkezdt\u00fcnk \u00edrni, akkor a C helyett CE felirat legyen a gombon, \u00e9s ennek megnyom\u00e1sa csak a m\u00e1sodik sz\u00e1mot t\u00f6rli (a gomb ekkor visszav\u00e1lt a C m\u0171k\u00f6d\u00e9sre).

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amin l\u00e1tsz\u00f3dik a sz\u00e1mol\u00f3g\u00e9p CE* gombja (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a CalculatorFragment oszt\u00e1ly ehhez tartoz\u00f3 r\u00e9sze, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/compose/","title":"Labor04 - Felhaszn\u00e1l\u00f3i fel\u00fcletek k\u00e9sz\u00edt\u00e9se a Jetpack Compose seg\u00edts\u00e9g\u00e9vel (ComposeBasics)","text":""},{"location":"laborok/compose/#bevezetes","title":"Bevezet\u00e9s","text":"

    A labor c\u00e9lja a Jetpack Compose haszn\u00e1lat\u00e1nak bemutat\u00e1sa: felhaszn\u00e1l\u00f3i fel\u00fcletek k\u00e9sz\u00edt\u00e9se egyszer\u0171, egym\u00e1sba \u00e1gyazhat\u00f3 composable met\u00f3dusok seg\u00edts\u00e9g\u00e9vel, XML le\u00edr\u00f3k haszn\u00e1lata n\u00e9lk\u00fcl. A labor sor\u00e1n egy egyszer\u0171 alkalmaz\u00e1st fogunk k\u00e9sz\u00edteni, amelyben bejelentkez\u00e9si \u00e9s f\u0151k\u00e9perny\u0151k tal\u00e1lhat\u00f3k.

    Az alkalmaz\u00e1sban a t\u00e9nyleges bejelentkeztet\u00e9si logika most nem kap helyet, puszt\u00e1n a felhaszn\u00e1l\u00f3i fel\u00fclet l\u00e9trehoz\u00e1s\u00e1nak m\u00f3dj\u00e1ra koncentr\u00e1lunk.

    A megval\u00f3s\u00edtand\u00f3 felhaszn\u00e1l\u00f3i fel\u00fcletet az al\u00e1bbi k\u00e9perny\u0151k\u00e9pek szeml\u00e9ltetik:

    "},{"location":"laborok/compose/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/compose/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Checkout

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    "},{"location":"laborok/compose/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

    1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
    2. A projekt neve legyen ComposeBasics, a kezd\u0151 package pedig hu.bme.aut.android.composebasics.
    3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
    4. A minimum API szint legyen API24: Android 7.0.
    5. A Build configuration language Kotlin DSL legyen.

    FILE PATH

    A projekt mindenk\u00e9ppen a repository-ban l\u00e9v\u0151 ComposeBasics k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n a laborvezet\u0151 vezet\u00e9s\u00e9vel vizsg\u00e1ljuk meg a forr\u00e1s fel\u00e9p\u00edt\u00e9s\u00e9t:

    • Tekints\u00fck \u00e1t, hogyan m\u0171k\u00f6dnek a fel\u00fcletet le\u00edr\u00f3 composable function\u00f6k.
    • Buildelj\u00fck le a projektet, \u00e9s pr\u00f3b\u00e1ljuk ki az el\u0151n\u00e9zetet.
    • N\u00e9zz\u00fck meg, hogyan friss\u00fcl az el\u0151n\u00e9zet, ahogyan m\u00f3dos\u00edtjuk a k\u00f3dunkat.
    "},{"location":"laborok/compose/#szoveges-eroforrasok-definialasa","title":"Sz\u00f6veges er\u0151forr\u00e1sok defini\u00e1l\u00e1sa","text":"

    A strings.xml f\u00e1jl m\u0171k\u00f6d\u00e9s\u00e9t m\u00e1r ismerj\u00fck, t\u00f6lts\u00fck fel ezt el\u0151re a k\u00e9s\u0151bb sz\u00fcks\u00e9ges sz\u00f6veges c\u00edmk\u00e9kkel, hogy k\u00e9s\u0151bb a l\u00e9nyeges elemekre tudjunk koncentr\u00e1lni:

    <resources>\n<string name=\"app_name\">ComposeBasics</string>\n<string name=\"textfield_label_email\">email</string>\n<string name=\"textfield_label_password\">password</string>\n<string name=\"button_label_login\">Log in</string>\n<string name=\"textfield_label_username\">username</string>\n<string name=\"snackbar_message_this_is_a\">This is a Snackbar</string>\n<string name=\"top_app_bar_title_home\">Home</string>\n<string name=\"button_label_logout\">Log out</string>\n<string name=\"dropdown_menu_item_label_settings\">Settings</string>\n<string name=\"dropdown_menu_item_label_profile\">Profile</string>\n</resources>\n
    "},{"location":"laborok/compose/#fuggosegek","title":"F\u00fcgg\u0151s\u00e9gek","text":""},{"location":"laborok/compose/#fuggosegek-frissitese","title":"F\u00fcgg\u0151s\u00e9gek friss\u00edt\u00e9se","text":"

    Az Android Studio a projekt l\u00e9trehoz\u00e1sakor felveszi ugyan a Compose-t a f\u00fcgg\u00e9segek k\u00f6z\u00e9, de n\u00e9mileg elavult verzi\u00f3kat haszn\u00e1l. Friss\u00edts\u00fck a modul szint\u0171 build.gradle.kts f\u00e1jlban a f\u00fcgg\u0151s\u00e9geket az al\u00e1bbiakra, majd szinkroniz\u00e1ljuk is a projektet:

    Gradle Version Catalogs

    Az Android Studio Iguana-t\u00f3l vagy Gradle 8.3-t\u00f3l kezd\u0151d\u0151en a f\u00fcgg\u0151s\u00e9gek kezel\u00e9s\u00e9re a Gradle bevezette a Version Catalog-ot.

    A Gradle Version Catalogs lehet\u0151v\u00e9 teszi a f\u00fcgg\u0151s\u00e9gek \u00e9s b\u0151v\u00edtm\u00e9nyek sk\u00e1l\u00e1zhat\u00f3 m\u00f3don t\u00f6rt\u00e9n\u0151 hozz\u00e1ad\u00e1s\u00e1t \u00e9s karbantart\u00e1s\u00e1t a projekthez. Ahelyett, hogy a f\u00fcgg\u0151s\u00e9geket \u00e9s verzi\u00f3kat az egyes build f\u00e1jlokban be\u00e9getn\u00e9nk, egy k\u00f6zponti katal\u00f3gusban defini\u00e1ljuk \u0151ket, \u00e9s az egyes modulokban csak hivatkozunk r\u00e1juk. \u00cdgy friss\u00edt\u00e9s eset\u00e9n el\u00e9g egy helyen \u00e1t\u00edrnunk p\u00e9ld\u00e1ul a verzi\u00f3sz\u00e1mot.

    A f\u00fcgg\u0151s\u00e9geink a Version Catalogban (libs.version.toml):

    [versions]\nagp = \"8.5.2\"\nkotlin = \"1.9.0\"\ncoreKtx = \"1.13.1\"\njunit = \"4.13.2\"\njunitVersion = \"1.2.1\"\nespressoCore = \"3.6.1\"\nlifecycleRuntimeKtx = \"2.8.6\"\nactivityCompose = \"1.9.2\"\ncomposeBom = \"2024.09.02\"\n\n[libraries]\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"coreKtx\" }\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\nandroidx-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"junitVersion\" }\nandroidx-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espressoCore\" }\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycleRuntimeKtx\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"composeBom\" }\nandroidx-ui = { group = \"androidx.compose.ui\", name = \"ui\" }\nandroidx-ui-graphics = { group = \"androidx.compose.ui\", name = \"ui-graphics\" }\nandroidx-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\nandroidx-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-ui-test-junit4 = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\" }\nandroidx-material3 = { group = \"androidx.compose.material3\", name = \"material3\" }\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\njetbrains-kotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\n

    Illetve a modul szint\u0171 build.gradle.kts f\u00e1jlban:

    dependencies {\n\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.activity.compose)\n    implementation(platform(libs.androidx.compose.bom))\n    implementation(libs.androidx.ui)\n    implementation(libs.androidx.ui.graphics)\n    implementation(libs.androidx.ui.tooling.preview)\n    implementation(libs.androidx.material3)\n    testImplementation(libs.junit)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.espresso.core)\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(libs.androidx.ui.test.junit4)\n    debugImplementation(libs.androidx.ui.tooling)\n    debugImplementation(libs.androidx.ui.test.manifest)\n}\n

    A fenti f\u00fcgg\u0151s\u00e9gekhez 34-es SDK-val kell ford\u00edtanunk a projektet, ha a legener\u00e1lt alkalmaz\u00e1sban kor\u00e1bbi lenne megadva, akkor friss\u00edts\u00fck ezt is a modul szint\u0171 build.gradle.kts f\u00e1jlunkban:

        compileSdk = 34\n
    "},{"location":"laborok/compose/#fuggoseg-felvetele","title":"F\u00fcgg\u0151s\u00e9g felv\u00e9tele","text":"

    Az \u00e1ltalunk haszn\u00e1lni k\u00edv\u00e1nt ikonokhoz sz\u00fcks\u00e9g\u00fcnk van a Material Icons Extended modulra, valamint a navig\u00e1ci\u00f3hoz a Navigation Component-re.

    Vegy\u00fck fel a sz\u00fcks\u00e9ges referenci\u00e1kat a libs.versions.toml f\u00e1jlba:

    [versions]\nmaterialIconsExtended = \"1.7.2\"\nnavigationCompose = \"2.8.1\"\n...\n\n[libraries]\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\", version.ref=\"materialIconsExtended\"}\nandroidx-navigation-compose = { group = \"androidx.navigation\", name = \"navigation-compose\", version.ref = \"navigationCompose\" }\n...\n

    Majd a f\u00fcgg\u0151s\u00e9get a modul szint\u0171 build.gradle.kts f\u00e1jlba:

    implementation(libs.androidx.material.icons.extended)\nimplementation(libs.androidx.navigation.compose)\n...\n
    "},{"location":"laborok/compose/#elemi-ui-epitoelemek-elkeszitese","title":"Elemi UI \u00e9p\u00edt\u0151elemek elk\u00e9sz\u00edt\u00e9se","text":"

    A fenti k\u00e9peken l\u00e1that\u00f3, hogy a bejelentkeztet\u00e9si form egyedi kin\u00e9zet\u0171 sz\u00f6vegmez\u0151kb\u0151l \u00e9s c\u00edmk\u00e9kb\u0151l \u00e9p\u00fclnek fel. A Compose alapelve - ahogyan a neve is t\u00fckr\u00f6zi, - hogy a felhaszn\u00e1l\u00f3i fel\u00fclet\u00fcnket hierarchikusan \u00e9p\u00edthetj\u00fck fel, \u00e9s a kisebb \u00e9p\u00edt\u0151elemekb\u0151l \u00f6sszetettebbeket \u00e1ll\u00edthatunk \u00f6ssze. Ez egyr\u00e9szt seg\u00edti a fejleszt\u0151i gondolkod\u00e1st, hiszen k\u00f6nnyen tudunk a felhaszn\u00e1l\u00f3i fel\u00fclet adott r\u00e9sz\u00e9re koncentr\u00e1lni, ezeket f\u00fcggetlen\u00fcl elk\u00e9sz\u00edteni, \u00e9s \u00edgy id\u0151vel a r\u00e9szekb\u0151l m\u00e1r k\u00f6nnyen \u00f6sszerakhat\u00f3 lesz a teljes k\u00edv\u00e1nt UI is. M\u00e1sr\u00e9szt, ez a megk\u00f6zel\u00edt\u00e9s seg\u00edti az \u00fajrafelhaszn\u00e1l\u00e1st, hiszen a kisebb fel\u00fcleti elemek k\u00f6nnyen \u00fajrafelhaszn\u00e1lhat\u00f3k az alkalmaz\u00e1s k\u00fcl\u00f6nb\u00f6z\u0151 r\u00e9szeiben is.

    K\u00e9sz\u00edts\u00fcnk el\u0151sz\u00f6r egy igen \u00e1ltal\u00e1nos sz\u00f6vegmez\u0151t, amelyet majd az \u00e9ppen aktu\u00e1lis ig\u00e9nyeknek megfelel\u0151en gazdagon tudunk param\u00e9terezni. Tulajdonk\u00e9ppen a rendszer r\u00e9sz\u00e9t k\u00e9pez\u0151 TextField is sokr\u00e9t\u0171 funkcionalit\u00e1ssal rendelkezik, azonban szeretn\u00e9nk egy magasabb szint\u0171 komponenst, amely sz\u00e1munkra k\u00f6nnyebben haszn\u00e1lhat\u00f3, \u00e9s a hibajelz\u00e9s megjelen\u00edt\u00e9s\u00e9t is megoldja.

    El\u0151sz\u00f6r hozzunk l\u00e9tre ehhez egy hu.bme.aut.android.composebasics.ui.common package-et. Ebbe fognak ker\u00fclni az alapvet\u0151 fontoss\u00e1g\u00fa UI \u00e9p\u00edt\u0151elemeink.

    Ezen bel\u00fcl k\u00e9sz\u00edts\u00fcnk egy NormalTextField komponenst a k\u00f6vetkez\u0151 tartalommal:

    package hu.bme.aut.android.composebasics.ui.common\n\n@ExperimentalMaterial3Api\n@Composable\nfun NormalTextField(\nmodifier: Modifier = Modifier,\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nenabled: Boolean = true,\nreadOnly: Boolean = false,\nisError: Boolean = false,\nonDone: (KeyboardActionScope.() -> Unit)?,\nleadingIcon: @Composable (() -> Unit)?,\ntrailingIcon: @Composable (() -> Unit)?\n) {\nOutlinedTextField(\nvalue = value.trim(),\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = if (isError) {\n{\nIcon(\nimageVector = Icons.Default.ErrorOutline,\ncontentDescription = null\n)\n}\n} else {\n{\nif (trailingIcon != null) {\ntrailingIcon()\n}\n}\n},\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nsingleLine = true,\nreadOnly = readOnly,\nisError = isError,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Text,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n)\n)\n}\n

    A Kotlin nyelv megengedi, hogy a f\u00fcggv\u00e9nyparam\u00e9tereket f\u00fcggv\u00e9nyh\u00edv\u00e1skor neves\u00edtve adjuk meg, \u00edgy a param\u00e9terek sorrendje v\u00e1ltozhat, mivel a n\u00e9v alapj\u00e1n a ford\u00edt\u00f3 \u00f6ssze tudja kapcsolni a param\u00e9tereket a megadott \u00e9rt\u00e9kekkel. Egy m\u00e1sik hasznos tulajdons\u00e1ga a Kotlin nyelvnek, hogy a param\u00e9tereknek alap\u00e9rtelmezett (default) \u00e9rt\u00e9k adhat\u00f3 meg a f\u00fcggv\u00e9nydefin\u00edci\u00f3ban, \u00e9s ezzel elker\u00fclhetj\u00fck, hogy egy f\u00fcggv\u00e9nynek sok overloadolt v\u00e1ltozat\u00e1t kelljen elk\u00e9sz\u00edten\u00fcnk. A k\u00e9t funkci\u00f3t kombin\u00e1lva nagyon rugalmasan tudjuk az \u00edgy defini\u00e1lt f\u00fcggv\u00e9nyeket h\u00edvni, \u00e9s ezt a Compose technol\u00f3gia remek\u00fcl kihaszn\u00e1lja.

    Tekints\u00fck \u00e1t a fenti k\u00f3dot! A komponens a konstruktoron kereszt\u00fcl sz\u00e1mos param\u00e9tert \u00e1t tud venni:

    • value: a sz\u00f6vegmez\u0151 tartalma; ezt egyszer\u0171en tov\u00e1bbadjuk a felhaszn\u00e1lt OutlinedTextField komponensnek, de az eleji/v\u00e9gi whitespace karaktereket a trim() seg\u00edts\u00e9g\u00e9vel lev\u00e1gjuk
    • label: a sz\u00f6vegmez\u0151 c\u00edmk\u00e9je, amely magyar\u00e1zza annak tartalm\u00e1t; ezt egy Text composable-be csomagolva tov\u00e1bbadjuk
    • onValueChange: esem\u00e9nykezel\u0151, amely a tartalom megv\u00e1ltoztat\u00e1sakor h\u00edv\u00f3dik; egyszer\u0171en tov\u00e1bbadjuk
    • leadingIcon \u00e9s traliningIcon: a sz\u00f6vegmez\u0151 elej\u00e9n \u00e9s v\u00e9g\u00e9n megjelen\u00edtend\u0151 ikonok, amelyeket egy \u00fajabb composable f\u00fcggv\u00e9nyk\u00e9nt lehet megadni; a komponens\u00fcnk be\u00e9p\u00edtett hibajelz\u00e9st val\u00f3s\u00edt meg, ez\u00e9rt ha hiba van be\u00e1ll\u00edtva, akkor a sz\u00f6veg v\u00e9g\u00e9n nem a be\u00e1ll\u00edtott ikon, hanem hibajelz\u00e9s jelenik meg
    • modifier: a megjelen\u00e9st m\u00f3dos\u00edt\u00f3 param\u00e9terek; itt tov\u00e1bbadjuk a megadottakat, \u00e9s m\u00e9g hozz\u00e1adjuk, hogy a t\u00e9ma szerinti minim\u00e1lis sz\u00e9less\u00e9g l\u00e9pjen \u00e9rv\u00e9nyre
    • enabled: enged\u00e9lyezve van-e a sz\u00f6vegmez\u0151?
    • readOnly: csak olvashat\u00f3-e a sz\u00f6vegmez\u0151?
    • isError: ha a sz\u00f6vegmez\u0151 tartalma nem \u00e9rv\u00e9nyes, akkor be\u00e1ll\u00edthatjuk true \u00e9rt\u00e9kre, \u00e9s a sz\u00f6vegmez\u0151 v\u00e9g\u00e9n egy hibajelz\u0151 ikon fog megjelenni.
    • onDone: esem\u00e9nykezel\u0151, hogy mi t\u00f6rt\u00e9njen, ha a szerkeszt\u00e9st a felhaszn\u00e1l\u00f3 befejezte

    A modifier \u00e9rt\u00e9kek\u00e9nt a komponens felhaszn\u00e1l\u00e1sakor nagyon sok param\u00e9ter megadhat\u00f3. Erre sz\u00e1mos p\u00e9ld\u00e1t l\u00e1thatunk az Android hivatalos dokument\u00e1ci\u00f3j\u00e1ban: https://developer.android.com/jetpack/compose/modifiers

    A felhaszn\u00e1lt OutlinedTextField komponensen tov\u00e1bbi jellemz\u0151ket is be\u00e1ll\u00edtottunk, amelyeket egy\u00e9bk\u00e9nt a NormalTextField nem tud k\u00edv\u00fclr\u0151l fel\u00fclb\u00edr\u00e1lhat\u00f3v\u00e1 tenni. Ezek jelent\u00e9se:

    • singleLine: csak egy sort lehet beg\u00e9pelni a sz\u00f6vegmez\u0151be
    • keyboardOptions: ez \u00e1ll\u00edtja be, hogy milyen jelleg\u0171 billenty\u0171zet jelenjen meg a k\u00e9perny\u0151n, \u00e9s milyen IME gyorsgomb tartozzon a szerkeszt\u0151h\u00f6z. Itt mindig egyszer\u0171 sz\u00f6veges billenty\u0171zetet \u00e9s \"k\u00e9sz\" gombot v\u00e1lasztunk. Ha emailt vagy telefonsz\u00e1mot g\u00e9peltetn\u00e9nk be, akkor megjelen\u00edthet\u00fcnk ehhez alkalmasabb billenty\u0171zetet is.
    • keyboardActions: mi t\u00f6rt\u00e9njen az egyes IME akci\u00f3k kiv\u00e1lt\u00e1sakor. Itt csak a kor\u00e1bban megadott onDone esem\u00e9nykezel\u0151t h\u00edvjuk meg.

    Ezzel elk\u00e9sz\u00fclt az els\u0151 composable komponens\u00fcnk, de mivel m\u00e9g sok hi\u00e1nyzik a felhaszn\u00e1l\u00f3i fel\u00fcletb\u0151l, ez\u00e9rt ezt csak sok\u00e1 tudn\u00e1nk val\u00f3j\u00e1ban kipr\u00f3b\u00e1lni. Szerencs\u00e9re a Compose technol\u00f3gia lehet\u0151s\u00e9get ad r\u00e1, hogy fejleszt\u00e9s k\u00f6zben is pontos el\u0151n\u00e9zetet kapjunk a komponenseinkb\u0151l. Ezt c\u00e9lszer\u0171en \u00fagy tessz\u00fck meg, hogy defini\u00e1lunk egy el\u0151n\u00e9zeti f\u00fcggv\u00e9nyt, amely a k\u00edv\u00e1nt param\u00e9terez\u00e9ssel megh\u00edvja a composable f\u00fcggv\u00e9ny\u00fcnket, majd erre a f\u00fcggv\u00e9nyre is r\u00e1tessz\u00fck a @Composable \u00e9s az @ExperimentalMaterial3Api annot\u00e1ci\u00f3kat, illetve az el\u0151n\u00e9zet gener\u00e1l\u00e1s\u00e1\u00e9rt felel\u0151s @Preview annot\u00e1ci\u00f3t is. Pr\u00f3b\u00e1ljuk ki a komponens\u00fcnket az al\u00e1bbi tesztf\u00fcggv\u00e9nnyel, amit betehet\u00fcnk a NormalTextField f\u00e1jlj\u00e1ba:

    @ExperimentalMaterial3Api\n@Preview\n@Composable\nfun NormalTextViewPreview() {\nNormalTextField(\nvalue = \"Csetneki P\u00e9ter\",\nlabel = \"N\u00e9v\",\nonValueChange = {},\nleadingIcon = {},\ntrailingIcon = {},\nonDone = {}\n)\n}\n

    Ne feledj\u00fck, hogy a Preview csak egy build ut\u00e1n tekinthet\u0151 meg.

    El\u0151n\u00e9zeti f\u00fcggv\u00e9nyb\u0151l t\u00f6bbet is l\u00e9trehozhatunk, hogy l\u00e1ssuk, hogyan n\u00e9z ki a komponens\u00fcnk k\u00fcl\u00f6nb\u00f6z\u0151 param\u00e9terez\u00e9sek eset\u00e9n. Vizsg\u00e1ljuk meg a hibajelz\u00e9ssel ell\u00e1tott megjelen\u00e9st is:

    @ExperimentalMaterial3Api\n@Preview\n@Composable\nfun NormalTextViewErrorPreview() {\nNormalTextField(\nvalue = \"abc\",\nlabel = \"Mennyis\u00e9g (kg)\",\nonValueChange = {},\nleadingIcon = {},\ntrailingIcon = {},\nonDone = {},\nisError = true\n)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a k\u00e9t el\u0151n\u00e9zet a sz\u00f6vegmez\u0151 komponensr\u0151l \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet. A n\u00e9v mez\u0151be a saj\u00e1t neved ker\u00fclj\u00f6n.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A fentihez hasonl\u00f3an a ui.common package-be k\u00e9sz\u00edts\u00fcnk egy \u00fajabb komponenst PasswordTextField n\u00e9ven az al\u00e1bbi tartalommal:

    package hu.bme.aut.android.composebasics.ui.common\n\n@ExperimentalMaterial3Api\n@Composable\nfun PasswordTextField(\nmodifier: Modifier = Modifier,\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nenabled: Boolean = true,\nreadOnly: Boolean = false,\nisError: Boolean = false,\nonDone: (KeyboardActionScope.() -> Unit)?,\nleadingIcon: @Composable (() -> Unit)?,\nisVisible: Boolean = true,\nonVisibilityChanged: () -> Unit,\n) {\nval visibilityIcon = if (isVisible) {\nIcons.Rounded.VisibilityOff\n} else {\nIcons.Rounded.Visibility\n}\nOutlinedTextField(\nvalue = value.trim(),\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = if (isError) {\n{\nIcon(\nimageVector = Icons.Default.ErrorOutline,\ncontentDescription = null\n)\n}\n} else {\n{\nIconButton(onClick = onVisibilityChanged) {\nIcon(imageVector = visibilityIcon, contentDescription = null)\n}\n}\n},\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nsingleLine = true,\nreadOnly = readOnly,\nisError = isError,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Password,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n),\nvisualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(),\n)\n}\n

    Ez a komponens csak k\u00e9t apr\u00f3 dologban t\u00e9r el az el\u0151z\u0151t\u0151l:

    1. Mivel jelszavak beg\u00e9pel\u00e9s\u00e9hez haszn\u00e1ljuk, a jelsz\u00f3 kitakar\u00e1sa vagy mutat\u00e1sa is \u00e1ll\u00edthat\u00f3 a komponensben. Ezt \u00fagy val\u00f3s\u00edtjuk meg, hogy nem lehet k\u00fcl\u00f6n ikont megadni a sz\u00f6vegmez\u0151 v\u00e9g\u00e9hez, hanem ott egy csukott vagy nyitott szem jelenik meg, \u00e9s az erre t\u00f6rt\u00e9n\u0151 kattint\u00e1ssal lehet a l\u00e1that\u00f3s\u00e1got \u00e1ll\u00edtani. A l\u00e1that\u00f3s\u00e1g \u00e1llapota \u00e9s az esem\u00e9nykezel\u0151 param\u00e9terekk\u00e9nt vannak megadva, teh\u00e1t a l\u00e1that\u00f3s\u00e1g \u00e1llapot\u00e1t \u00e9s az esem\u00e9nykezel\u0151t a komponens bennfoglal\u00f3 komponens\u00e9ben kell megval\u00f3s\u00edtani.

    2. A komponensnek a l\u00e1that\u00f3s\u00e1g \u00e1llapot\u00e1t\u00f3l f\u00fcgg\u0151en egy vizu\u00e1lis transzform\u00e1ci\u00f3 is be van \u00e1ll\u00edtva, hogy a tartalm\u00e1t ne k\u00f6zvetlen, hanem kitakartan jelen\u00edtse meg.

    "},{"location":"laborok/compose/#az-alkalmazas-fo-kepernyoinek-elkeszitese","title":"Az alkalmaz\u00e1s f\u0151 k\u00e9perny\u0151inek elk\u00e9sz\u00edt\u00e9se","text":"

    Most, hogy a k\u00e9perny\u0151k minden fontos alkot\u00f3r\u00e9sze a rendelkez\u00e9s\u00fcnkre \u00e1ll, elkezdhetj\u00fck maguknak a k\u00e9perny\u0151knek az elk\u00e9sz\u00edt\u00e9s\u00e9t. Kezdj\u00fck a bejelentkez\u0151 k\u00e9perny\u0151vel!

    A k\u00e9perny\u0151knek \u00e9s a hozz\u00e1juk kapcsol\u00f3d\u00f3 k\u00f3doknak hozzunk l\u00e9tre egy k\u00f6z\u00f6s hu.bme.aut.android.composebasics.feature package-et, majd ezen bel\u00fcl a bejelentkez\u0151 k\u00e9perny\u0151 a login package-be ker\u00fclj\u00f6n! K\u00e9sz\u00edts\u00fck el a k\u00e9perny\u0151 k\u00f3dj\u00e1t LoginScreen n\u00e9ven, majd adjuk meg a k\u00f6vetkez\u0151 k\u00f3dot:

    package hu.bme.aut.android.composebasics.feature.login\n\n@ExperimentalMaterial3Api\n@Composable\nfun LoginScreen(\nmodifier: Modifier = Modifier,\nonLoginClick: (String) -> Unit\n) {\nvar usernameValue by remember { mutableStateOf(\"\") }\nvar isUsernameError by remember { mutableStateOf(false) }\n\nvar passwordValue by remember { mutableStateOf(\"\") }\nvar isPasswordVisible by remember { mutableStateOf(false) }\nvar isPasswordError by remember { mutableStateOf(false) }\n\nBox(\nmodifier = modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\ncontentAlignment = Alignment.Center\n) {\nColumn(horizontalAlignment = Alignment.CenterHorizontally) {\nNormalTextField(\nvalue = usernameValue,\nlabel = stringResource(id = R.string.textfield_label_username),\nonValueChange = { newValue ->\nusernameValue = newValue\nisUsernameError = false\n},\nisError = isUsernameError,\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Person,\ncontentDescription = null\n)\n},\ntrailingIcon = { },\nonDone = { }\n)\nSpacer(modifier = Modifier.height(16.dp))\nPasswordTextField(\nvalue = passwordValue,\nlabel = stringResource(id = R.string.textfield_label_password),\nonValueChange = { newValue ->\npasswordValue = newValue\nisPasswordError = false\n},\nisError = isPasswordError,\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Key,\ncontentDescription = null\n)\n},\nisVisible = isPasswordVisible,\nonVisibilityChanged = { isPasswordVisible = !isPasswordVisible },\nonDone = { }\n)\nSpacer(modifier = Modifier.height(16.dp))\nButton(\nonClick = {\nif (usernameValue.isEmpty()) {\nisUsernameError = true\n} else if (passwordValue.isEmpty()) {\nisPasswordError = true\n} else {\nonLoginClick(usernameValue)\n}\n},\nmodifier = Modifier.width(TextFieldDefaults.MinWidth),\nshape = RectangleShape\n) {\nText(text = stringResource(id = R.string.button_label_login))\n}\n}\n}\n}\n

    Egy fontos eddig nem l\u00e1tott elem, hogy a felhaszn\u00e1l\u00f3i fel\u00fclet elemeinek \u00e1llapott\u00e1rol\u00e1s\u00e1ra (pl. sz\u00f6vegmez\u0151 tartalma, l\u00e1that\u00f3-e valami, jel\u00f6l\u0151n\u00e9gyzet be van pip\u00e1lva stb.) MutableState t\u00edpus\u00fa t\u00e1rol\u00f3kat kell l\u00e9trehoznunk. Ezt a mutableStateOf() factory-met\u00f3dussal tudjuk megtenni, \u00e9s ennek meg kell adni a kezd\u0151\u00e1llapotot. Mindezt az inicializ\u00e1ci\u00f3t lazy bet\u00f6lt\u00e9ssel akarjuk v\u00e9gezni, hogy a fel\u00fclet fel\u00e9p\u00edt\u00e9se k\u00f6zben t\u00f6rt\u00e9njen. Ehhez haszn\u00e1ljuk a remember kulcssz\u00f3t.

    Felt\u0171nnek m\u00e9g k\u00fcl\u00f6nb\u00f6z\u0151 kont\u00e9nerelemek, amelyek seg\u00edts\u00e9g\u00e9vel a fel\u00fcleti elemek elrendez\u00e9s\u00e9t tudjuk meghat\u00e1rozni. Ilyen a kor\u00e1bban m\u00e1r \u00e9rintett Box. Ez alkalmas a teljes k\u00e9perny\u0151tartalmak befoglal\u00e1s\u00e1ra. Ezzel \u00e1ll\u00edtjuk be a h\u00e1tteret a Material t\u00e9m\u00e1nk szerintire, illetve hogy a k\u00e9perny\u0151 teljes eg\u00e9sz\u00e9t t\u00f6ltse ki a befoglalt tartalom. Ezen bel\u00fcl l\u00e1tunk egy Column elemet, amellyel egy oszlopba vannak rendezve egym\u00e1s al\u00e1 a sz\u00f6vegmez\u0151k \u00e9s a bejelentkez\u0151 gomb. A v\u00edzszintes igaz\u00edt\u00e1s az oszlopon k\u00f6z\u00e9pre van \u00e1ll\u00edtva. A norm\u00e1l \u00e9s a jelszavas saj\u00e1t sz\u00f6vegmez\u0151k, valamint a bejelentkeztet\u0151 gomb k\u00f6z\u00f6tt t\u00e9relv\u00e1laszt\u00f3 Spacer komponenseket tal\u00e1lunk.

    \u00d6sszess\u00e9g\u00e9ben azt figyelhetj\u00fck meg, hogy a logika egy r\u00e9sze m\u00e1r itt fel van oldva, hiszen az \u00e1llapot egyes r\u00e9szeit itt kezelj\u00fck, \u00e9s ehhez kapcsol\u00f3d\u00f3an esem\u00e9nykezel\u0151ket is adunk tov\u00e1bb az \u00e9p\u00edt\u0151elemk\u00e9nt szolg\u00e1l\u00f3 kisebb komponenseknek. Viszont vannak olyan dolgok, mint pl. a login gomb esem\u00e9nykezel\u0151je, amelyek m\u00e9g mindig fel\u00fclr\u0151l j\u00f6nnek. Alapvet\u0151en a Compose-ban \u00fagy kell gondolkodnunk, hogy az \u00e1llapotot, amire t\u00f6bb fel\u00fcleti elemnek sz\u00fcks\u00e9ge van, azt feljebb kell emeln\u00fcnk egy k\u00f6z\u00f6s \u0151sbe. Ezt az Android terminol\u00f3gia \u00fagy h\u00edvja, hogy state hoisting Pl. a beg\u00e9pelt felhaszn\u00e1l\u00f3nevet a sz\u00f6vegmez\u0151 is haszn\u00e1lja, illetve a befoglal\u00f3 bejelentkez\u0151 k\u00e9perny\u0151n\u00e9l is sz\u00fcks\u00e9g van r\u00e1. Maga a bejelentkez\u0151 k\u00e9perny\u0151 a legfels\u0151 komponens a hierarchi\u00e1ban, amelyik haszn\u00e1lja, ez\u00e9rt itt tudjuk ezt az \u00e1llapotot kezelni. A navig\u00e1ci\u00f3 viszont, hogy mi t\u00f6rt\u00e9njen a gombokra kattint\u00e1skor, az m\u00e1r m\u00e1s komponenseket is \u00e9rint, ez\u00e9rt azt fentebbi szinten kell kezelni, ez\u00e9rt ez m\u00e9g mindig param\u00e9terk\u00e9nt \u00e9rkezik a k\u00e9perny\u0151t megtestes\u00edt\u0151 komponenshez.

    Aki fejlesztett m\u00e1r a React webes keretrendszerben, annak ismer\u0151s lehet ez a koncepci\u00f3, mert nagyon hasonl\u00f3 a React komponensek m\u0171k\u00f6d\u00e9s\u00e9hez.

    N\u00e9zz\u00fck is meg az elk\u00e9sz\u00fclt komponenst:

    @ExperimentalMaterial3Api\n@Preview(showBackground = true)\n@Composable\nfun LoginScreenPreview() {\nLoginScreen(\nonLoginClick = { }\n)\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az el\u0151n\u00e9zet a bejelentkez\u0151 k\u00e9perny\u0151r\u0151l \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A m\u00e1sodik elk\u00e9sz\u00edtend\u0151 k\u00e9perny\u0151nk az alkalmaz\u00e1s \"f\u0151k\u00e9perny\u0151je\", amit sikeres bejelentkez\u00e9s ut\u00e1n l\u00e1t a felhaszn\u00e1l\u00f3. Viszont itt m\u00e1r r\u00e9szben \u00e9rinten\u00fcnk kell a k\u00e9perny\u0151k k\u00f6zti navig\u00e1ci\u00f3 k\u00e9rd\u00e9s\u00e9t is, hiszen a k\u00e9perny\u0151nek lesz egy men\u00fcje, ahonnan majd m\u00e1s k\u00e9perny\u0151kre lehet navig\u00e1lni. Ehhez egy navigation package-et hozzunk l\u00e9tre, \u00e9s ebbe ker\u00fclj\u00f6n az al\u00e1bbi Screen oszt\u00e1ly. Itt sealed class-t alkalmazunk a lehets\u00e9ges k\u00e9perny\u0151k le\u00edr\u00e1s\u00e1ra, mert csak el\u0151re megadott sz\u00e1m\u00fa k\u00e9perny\u0151nk van, \u00e9s a f\u0151k\u00e9perny\u0151 argumentumot is kaphat. A sealed class kicsit hasonl\u00edt az enumhoz, de t\u00e1mogatja ezt a fontos k\u00fcl\u00f6nbs\u00e9get is. Az oszt\u00e1ly el\u0151tt defini\u00e1lt konstansokat k\u00e9s\u0151bb fogjuk haszn\u00e1lni, amikor teljesen \u00f6sszerakjuk a navig\u00e1ci\u00f3s gr\u00e1fot.

    package hu.bme.aut.android.composebasics.navigation\n\nconst val ROOT_GRAPH_ROUTE = \"root\"\nconst val AUTH_GRAPH_ROUTE = \"auth\"\nconst val MAIN_GRAPH_ROUTE = \"main\"\n\nsealed class Screen(val route: String) {\nobject Login: Screen(route = \"login\")\nobject Home: Screen(route = \"home/{${Args.username}}\") {\nfun passUsername(username: String) = \"home/$username\"\nobject Args {\nconst val username = \"username\"\n}\n}\nobject Profile: Screen(route = \"profile\")\nobject Settings: Screen(route = \"settings\")\n}\n

    sealed class

    A Kotlin sealed class-ai olyan oszt\u00e1lyok, amelyekb\u0151l korl\u00e1tozott az \u00f6r\u00f6kl\u00e9s, \u00e9s ford\u00edt\u00e1si id\u0151ben minden lesz\u00e1rmazott oszt\u00e1lya ismert. Ezeket az oszt\u00e1lyokat az enumokhoz hasonl\u00f3 m\u00f3don tudjuk alkalmazni. Jelen esetben a Home val\u00f3j\u00e1ban nem a Screen k\u00f6zvetlen lesz\u00e1rmazottja, hanem anonim lesz\u00e1rmazott oszt\u00e1lya, mivel a felhaszn\u00e1l\u00f3n\u00e9v param\u00e9terk\u00e9nt t\u00f6rt\u00e9n\u0151 kezel\u00e9s\u00e9t is tartalmazza.

    Maga a f\u0151k\u00e9perny\u0151 egy feature.home subpackage-be ker\u00fclj\u00f6n. El\u0151sz\u00f6r itt is egy seg\u00e9doszt\u00e1lyt hozunk l\u00e9tre. Jelen esetben a men\u00fcpontokat fogjuk enumban modellezni. Minden men\u00fcpontra jellemz\u0151 a neve, az ikonja, illetve egy azonos\u00edt\u00f3, ahova navig\u00e1l:

    package hu.bme.aut.android.composebasics.feature.home\n\nenum class MenuItemUiModel(\nval text: @Composable () -> Unit,\nval icon: @Composable () -> Unit,\nval screenRoute: String\n) {\nPROFILE(\ntext = { Text(text = stringResource(id = R.string.dropdown_menu_item_label_profile))},\nicon = {\nIcon(imageVector = Icons.Default.Person, contentDescription = null)\n},\nscreenRoute = Screen.Profile.route\n),\nSETTINGS(\ntext = { Text(text = stringResource(id = R.string.dropdown_menu_item_label_settings))},\nicon = {\nIcon(imageVector = Icons.Default.Settings, contentDescription = null)\n},\nscreenRoute = Screen.Settings.route\n)\n}\n

    A men\u00fcben szerepelnek profil \u00e9s be\u00e1ll\u00edt\u00e1s lehet\u0151s\u00e9gek is, amelyekr\u0151l kor\u00e1bban nem volt sz\u00f3. Ezek nem lesznek igazi kidolgozott k\u00e9perny\u0151k, de p\u00e9ldak\u00e9pp szerepelnek itt, hogy bemutassuk, hogyan lehetne a f\u0151men\u00fcb\u0151l tov\u00e1bbi oldalakra is elnavig\u00e1lni. L\u00e1that\u00f3, hogy itt a men\u00fcpontokn\u00e1l meghivatkoztuk a kor\u00e1bban a Screen oszt\u00e1lyban defini\u00e1lt k\u00e9perny\u0151ket is. A le\u00edrt men\u00fcpontokb\u00f3l m\u00e9g fel kell \u00e9p\u00edten\u00fcnk a men\u00fct is. Elvileg ezt megtehetn\u00e9nk a teljes f\u0151k\u00e9perny\u0151 r\u00e9szek\u00e9nt, de \u00e1tl\u00e1that\u00f3bb strukt\u00far\u00e1t kapunk, ha ezt k\u00fcl\u00f6n composable komponensbe szervezz\u00fck. Ahogyan \u00e1ltal\u00e1ban v\u00e9ve a met\u00f3dusokn\u00e1l sem \u00e1tl\u00e1that\u00f3 a t\u00fal hossz\u00fa, \u00fagy a fel\u00fcleti komponenseinket is \u00e9rdemes kisebb, jobban kezelhet\u0151 egys\u00e9gekre osztani. K\u00e9sz\u00edts\u00fcnk teh\u00e1t egy Menu komponenst:

    package hu.bme.aut.android.composebasics.feature.home\n\n@Composable\nfun Menu(\nexpanded: Boolean,\nitems: Array<MenuItemUiModel>,\nonDismissRequest: () -> Unit,\nonClick: (String) -> Unit,\nmodifier: Modifier = Modifier\n) {\nDropdownMenu(\nmodifier = modifier.padding(5.dp),\nexpanded = expanded,\nonDismissRequest = onDismissRequest\n) {\nitems.forEachIndexed { index, item ->\nDropdownMenuItem(\ntext = item.text,\nleadingIcon = item.icon,\nonClick = { onClick(item.screenRoute) },\nmodifier = Modifier.clip(RoundedCornerShape(5.dp))\n)\nif (index != items.lastIndex) {\nHorizontalDivider(modifier = Modifier\n.height(10.dp)\n.padding(vertical = 5.dp))\n}\n}\n}\n}\n

    L\u00e1tjuk, hogy a men\u00fcelemek l\u00e1trehoz\u00e1sa is ciklussal t\u00f6rt\u00e9nik, \u00e9s a men\u00fcpontok igen k\u00f6nnyen b\u0151v\u00edthet\u0151ek. A bej\u00e1r\u00e1sn\u00e1l a men\u00fcpontok index\u00e9t is felhaszn\u00e1ljuk, hogy a men\u00fcpontok ut\u00e1n - az utols\u00f3 kiv\u00e9tel\u00e9vel - elv\u00e1laszt\u00f3t is gener\u00e1ljunk.

    Most r\u00e1t\u00e9rhet\u00fcnk a t\u00e9nyleges f\u0151k\u00e9perny\u0151 l\u00e9trehoz\u00e1s\u00e1ra:

    package hu.bme.aut.android.composebasics.feature.home\n\n@ExperimentalMaterial3Api\n@Composable\nfun HomeScreen(\nargument: String,\nmodifier: Modifier = Modifier,\nonLogout: () -> Unit,\nonMenuItemClick: (String) -> Unit\n) {\n\nval snackbarHostState = remember { SnackbarHostState() }\n\nvar expandedMenu by remember { mutableStateOf(false) }\n\nval scope = rememberCoroutineScope()\n\nval context = LocalContext.current\n\nScaffold(\nsnackbarHost = { SnackbarHost(snackbarHostState) },\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = stringResource(id = R.string.top_app_bar_title_home))\n},\nactions = {\nIconButton(onClick = onLogout) {\nIcon(\nimageVector = Icons.AutoMirrored.Filled.Logout,\ncontentDescription = null\n)\n}\nIconButton(onClick = { expandedMenu = !expandedMenu }) {\nIcon(imageVector = Icons.Default.MoreVert, contentDescription = null)\n}\nMenu(\nexpanded = expandedMenu,\nitems = MenuItemUiModel.entries.toTypedArray(),\nonDismissRequest = { expandedMenu = false },\nonClick = {\nonMenuItemClick(it)\nexpandedMenu = false\n},\n)\n}\n)\n},\nfloatingActionButton = {\nFloatingActionButton(onClick = {\nscope.launch {\nsnackbarHostState.showSnackbar(message = context.getString(R.string.snackbar_message_this_is_a))\n}\n}) {\nIcon(imageVector = Icons.Default.Add, contentDescription = null)\n}\n},\nmodifier = modifier\n) {\nBox(\nmodifier = Modifier\n.padding(it)\n.fillMaxSize(),\n) {\nText(\ntext = \"Hello, $argument!\",\ntextAlign = TextAlign.Center,\nmodifier = Modifier.align(Alignment.Center)\n)\n}\n}\n}\n

    A k\u00e9perny\u0151n t\u00f6bb \u00fajdons\u00e1got is felfedezhet\u00fcnk:

    1. A Scaffold elem szolg\u00e1l komplexebb Material st\u00edlus\u00fa k\u00e9perny\u0151k fel\u00e9p\u00edt\u00e9s\u00e9re. A param\u00e9terez\u00e9s\u00e9b\u0151l l\u00e1that\u00f3, hogy ez az elem be\u00e9p\u00edtetten t\u00e1mogat t\u00f6bb gyakran megszokott k\u00e9perny\u0151elemet, mint a SnackBar, TopBar vagy a FloatingActionButton. Ezeket a param\u00e9terez\u00e9ssel adjuk meg neki, \u00e9s gondoskodik a megfelel\u0151 elrendez\u00e9sr\u0151l.

    2. A k\u00e9perny\u0151n SnackBar is lesz, \u00e9s ennek az \u00e1llapot\u00e1t nem MutableState, hanem SnackbarHostState t\u00edpusk\u00e9nt tudjuk l\u00e9trehozni.

    3. A SnackBar \u00fczenetek megjelen\u00edt\u00e9s\u00e9t coroutine fogja v\u00e9gezni, \u00e9s ehhez scope-ot Compose k\u00f6rnyezetben a rememberCoroutineScope() f\u00fcggv\u00e9nnyel tudunk k\u00e9rni.

    4. A LocalContext.current kifejez\u00e9ssel kaphatunk egy kontextust Compose k\u00f6rnyezetben, amellyel a rendszerszint\u0171 er\u0151forr\u00e1sokhoz - pl. a sz\u00f6veges c\u00edmk\u00e9khez - hozz\u00e1f\u00e9rhet\u00fcnk.

    A k\u00e9perny\u0151 t\u00f6bbi r\u00e9sze a kor\u00e1bbi p\u00e9ld\u00e1k alapj\u00e1n m\u00e1r k\u00f6nnyen \u00e9rthet\u0151.

    N\u00e9zz\u00fck meg, hogyan fest az elk\u00e9sz\u00edtett f\u0151k\u00e9perny\u0151:

    @ExperimentalMaterial3Api\n@Preview(showBackground = true)\n@Composable\nfun HomeScreenPreview() {\nHomeScreen(\nargument = \"Felhaszn\u00e1l\u00f3\",\nonLogout = {},\nonMenuItemClick = {}\n)\n}\n
    "},{"location":"laborok/compose/#a-kepernyok-kozotti-navigacio-elkeszitese","title":"A k\u00e9perny\u0151k k\u00f6z\u00f6tti navig\u00e1ci\u00f3 elk\u00e9sz\u00edt\u00e9se","text":"

    Most m\u00e1r csak \u00f6ssze kell k\u00f6tn\u00fcnk a megl\u00e9v\u0151 k\u00e9perny\u0151ket a navig\u00e1ci\u00f3s szab\u00e1lyokkal. Ehhez navig\u00e1ci\u00f3s gr\u00e1fokat fogunk defini\u00e1lni. Egyr\u00e9szt defini\u00e1lunk egy gr\u00e1fot az authentik\u00e1ci\u00f3 el\u0151tti k\u00e9perny\u0151kre, itt most csak a bejelentkez\u00e9s lesz, de egy val\u00f3s alkalmaz\u00e1sban lehetne pl. egy regisztr\u00e1ci\u00f3s k\u00e9perny\u0151nk is. Ezeket a kor\u00e1bban l\u00e9trehozott navigation package-be tegy\u00fck. Az authentik\u00e1ci\u00f3 el\u0151tti gr\u00e1f a k\u00f6vetkez\u0151k\u00e9ppen n\u00e9z ki:

    package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\nfun NavGraphBuilder.authNavGraph(\nnavController: NavHostController\n) {\nnavigation(\nstartDestination = Screen.Login.route,\nroute = AUTH_GRAPH_ROUTE\n) {\ncomposable(\nroute = Screen.Login.route\n) {\nLoginScreen(\nonLoginClick = {\nnavController.navigate(Screen.Home.passUsername(it))\n}\n)\n}\n}\n}\n

    A k\u00f3db\u00f3l azt tudjuk meg\u00e1llap\u00edtani, hogy a navig\u00e1ci\u00f3s gr\u00e1f a bejelentkeztet\u00e9si k\u00e9perny\u0151n kezd\u0151dik, \u00e9s neki is van egy \u00fatvonalazonos\u00edt\u00f3ja, amelyet most a kor\u00e1bban defini\u00e1lt AUTH_GRAPH_ROUTE konstanssal adtunk meg. A navig\u00e1ci\u00f3ban composable fel\u00fcleti elemeket adhatunk meg: mindegyikhez tartozik egy-egy \u00fatvonal, ezekhez a Screen oszt\u00e1lyb\u00f3l hivatkozzuk meg a megfelel\u0151 \u00fatvonalat. L\u00e1that\u00f3, hogy a hierarchikusan \u00f6ssze\u00e1ll\u00edtott felhaszn\u00e1l\u00f3i fel\u00fcletek \"utols\u00f3\" param\u00e9terei itt kapnak konkr\u00e9t \u00e9rt\u00e9tet. Konkr\u00e9tan a bejelentkez\u00e9s gomb esem\u00e9nykezel\u0151je van itt lambda-kifejez\u00e9sk\u00e9nt megadva. Ez a lambda-kifejez\u00e9s val\u00f3j\u00e1ban a navig\u00e1ci\u00f3s kontrollert h\u00edvja meg, \u00e9s azzal navig\u00e1ltat a megfelel\u0151 \u00fatvonalra, amit a kontroller a navig\u00e1ci\u00f3s gr\u00e1f alapj\u00e1n felold. Figyelj\u00fck meg, hogy a bejelentkez\u00e9s ut\u00e1n a f\u0151k\u00e9perny\u0151 \u00fatvonal\u00e1ba a felhaszn\u00e1l\u00f3nevet mint param\u00e9tert is belek\u00f3doljuk. Azt is l\u00e1thatjuk, hogy t\u00e9nyleges bejelentkeztet\u0151 logika itt nem t\u00f6rt\u00e9nik, de ha erre lenne sz\u00fcks\u00e9g\u00fcnk, azt itt megtehetn\u00e9nk, hiszen itt van megadva a bejelentkez\u00e9s gomb esem\u00e9nykezel\u0151je.

    A m\u00e1sik navig\u00e1ci\u00f3s gr\u00e1f a bejelentkez\u00e9s ut\u00e1ni navig\u00e1ci\u00f3t \u00edrja le:

    package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\nfun NavGraphBuilder.mainNavGraph(\nnavController: NavHostController\n) {\nnavigation(\nstartDestination = Screen.Home.route,\nroute = MAIN_GRAPH_ROUTE\n) {\ncomposable(\nroute = Screen.Home.route,\narguments = listOf(\nnavArgument(Screen.Home.Args.username) {\ntype = NavType.StringType\n}\n)\n) {\nHomeScreen(\nargument = navController.currentBackStackEntry?.arguments\n?.getString(Screen.Home.Args.username) ?: \"\",\nonLogout = {\nnavController.popBackStack(route = Screen.Login.route, inclusive = false)\n},\nonMenuItemClick = { navController.navigate(it) }\n)\n}\ncomposable(route = Screen.Profile.route) {\nBox(\nmodifier = Modifier.fillMaxSize(),\ncontentAlignment = Alignment.Center\n) {\nText(text = \"Profile\")\n}\n}\ncomposable(route = Screen.Settings.route) {\nBox(\nmodifier = Modifier.fillMaxSize(),\ncontentAlignment = Alignment.Center\n) {\nText(text = \"Settings\")\n}\n}\n}\n}\n

    V\u00e9g\u00fcl a kett\u0151t egyes\u00edten\u00fcnk kell:

    package hu.bme.aut.android.composebasics.navigation\n\n@ExperimentalMaterial3Api\n@Composable\nfun NavGraph(\nnavController: NavHostController\n) {\nNavHost(\nnavController = navController,\nstartDestination = AUTH_GRAPH_ROUTE,\nroute = ROOT_GRAPH_ROUTE\n) {\nauthNavGraph(navController = navController)\nmainNavGraph(navController = navController)\n}\n}\n

    Figyelj\u00fck meg, hogy ebben a gr\u00e1fban a f\u0151k\u00e9perny\u0151re \u00e9rkezve hogyan lehet felhaszn\u00e1l\u00f3nevet kinyerni! Illetve azt is meg\u00e1llap\u00edthatjuk, hogy a f\u0151k\u00e9perny\u0151re \u00e9rkezve a backstackr\u0151l t\u00f6rl\u0151dik a bejelentkeztet\u0151 k\u00e9perny\u0151 \u00fatvonala. Ez \u00edgy logikus, hiszen ha m\u00e1r sikeresen bel\u00e9pt\u00fcnk, nem szeretn\u00e9nk, hogy a back gombra kattintva v\u00e9letlen kil\u00e9pj\u00fcnk az alkalmaz\u00e1sb\u00f3l. A gr\u00e1fban a profil \u00e9s be\u00e1ll\u00edt\u00e1s oldalak nincsenek kidolgozva, ez\u00e9rt ide csak egy-egy Box elemet vett\u00fcnk fel placeholder sz\u00f6veggel.

    M\u00e1r csak a MainActivity-be kell bek\u00f6tn\u00fcnk a navig\u00e1ci\u00f3 szerint feloldott felsz\u00edn megjelen\u00edt\u00e9s\u00e9t. Itt t\u00f6rt\u00e9nik az alkalmaz\u00e1s t\u00e9m\u00e1j\u00e1nak a megad\u00e1sa is:

    package hu.bme.aut.android.composebasics\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.safeDrawingPadding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.ui.Modifier\nimport androidx.navigation.compose.rememberNavController\nimport hu.bme.aut.android.composebasics.navigation.NavGraph\nimport hu.bme.aut.android.composebasics.ui.theme.ComposeBasicsTheme\n\nclass MainActivity : ComponentActivity() {\n@OptIn(ExperimentalMaterial3Api::class)\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nenableEdgeToEdge()\nsetContent {\nComposeBasicsTheme() {\nval navController = rememberNavController()\nBox(modifier = Modifier.safeDrawingPadding()) {\nNavGraph(navController = navController)\n}\n}\n}\n}\n}\n

    EdgeToEdge

    Android 15-t\u0151l (API 35) az alkalmaz\u00e1sunk k\u00e9pes a rendszer UI (StatusBar, NavigationBar, soft keyboard, stb.) al\u00e1 is rajzolni. Ezzel val\u00f3s\u00edtott\u00e1k meg azt, hogy a k\u00e9sz\u00fcl\u00e9k teljes k\u00e9perny\u0151j\u00e9t haszn\u00e1lni tudjuk a sz\u00e9l\u00e9t\u0151l a sz\u00e9l\u00e9ig. Ez hasznos lehet sz\u00e1mtalan esetben, amikor \"teljes k\u00e9perny\u0151s\" alkalmaz\u00e1st szeretn\u00e9nk \u00edrni, nem korl\u00e1toz minket az elfed\u0151 rendszer UI. A funkci\u00f3 term\u00e9szetesen alacsonyabb API szinteken is el\u00e9rhet\u0151, erre val\u00f3 a fent is l\u00e1that\u00f3 enableEdgeToEdge f\u00fcggv\u00e9nyh\u00edv\u00e1s.

    Ez viszont amennyire hasznos, annyi probl\u00e9m\u00e1t is tud okozni, ha e miatt valami vez\u00e9rl\u0151nk becs\u00faszik mondjuk a szoftveres billenty\u0171zet al\u00e1, amit \u00edgy nem tudunk el\u00e9rni. Ennek kik\u00fcsz\u00f6b\u00f6l\u00e9s\u00e9re tal\u00e1lt\u00e1k ki az inseteket. Ennek sz\u00e1mos be\u00e1ll\u00edt\u00e1sa van, amellyel nem kell nek\u00fcnk k\u00e9zzel megtippelni, hogy p\u00e9ld\u00e1ul a status bar h\u00e1ny dp magas, k\u00fcl\u00f6n\u00f6sen, hogy ezek az \u00e9rt\u00e9kek fut\u00e1sid\u0151ben v\u00e1ltozhatnak (l\u00e1sd szoftveres billenty\u0171zet). A sz\u00e1mos be\u00e1ll\u00edt\u00e1s k\u00f6z\u00fcl mi most a fent l\u00e1that\u00f3 safeDrawindPadding-et haszn\u00e1ljuk, ami mint neve is mutatja, pont akkora paddinget \u00e1ll\u00edt mindenhova, hogy semmit se takarjon ki a rendszer UI. (Term\u00e9szetesen ez nem csak az Activity-ben, hanem minden Screenen \u00e9s Composable-\u00f6n k\u00f6l\u00fcn is haszn\u00e1lhat\u00f3.)

    A funkci\u00f3 egyik j\u00f3 demonstr\u00e1ci\u00f3ja, hogy a LoginScreen vez\u00e9rl\u0151i, amik a teljes oldal k\u00f6zep\u00e9re vannak helyezve, a szoftveres billenty\u0171zet megjelen\u00e9sekor nem takar\u00f3dnak le, hanem a szabadon marad\u00f3 hely k\u00f6zep\u00e9re cs\u00fasznak.

    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az alkalmaz\u00e1s f\u0151k\u00e9perny\u0151je bel\u00e9p\u00e9s ut\u00e1n a saj\u00e1t neveddel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/compose/#onallo-feladat-sotet-mod","title":"\u00d6n\u00e1ll\u00f3 feladat - S\u00f6t\u00e9t m\u00f3d","text":"

    A Compose alkalmaz\u00e1s be\u00e9p\u00edtetten t\u00e1mogatja az \u00e9jszakai m\u00f3dot. Keresd meg az emul\u00e1lt k\u00e9sz\u00fcl\u00e9k be\u00e1ll\u00edt\u00e1sai k\u00f6zt a s\u00f6t\u00e9t t\u00e9ma haszn\u00e1lat\u00e1t, \u00e9s kapcsold be! (Settings -> Display -> Dark theme) Pr\u00f3b\u00e1ld ki \u00edgy az alkalmaz\u00e1st!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az alkalmaz\u00e1s dark mode-ban (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/compose/#onallo-feladat-regisztracio-gomb","title":"\u00d6n\u00e1ll\u00f3 feladat - Regisztr\u00e1ci\u00f3 gomb","text":"

    Adj hozz\u00e1 a login oldal alj\u00e1hoz egy teljes oldal sz\u00e9less\u00e9g\u0171 gombot, ahol az \u00faj felhaszn\u00e1l\u00f3 a regisztr\u00e1ci\u00f3 oldalra navig\u00e1lhatna. A gomb \u00fajrahaszn\u00e1lhat\u00f3 komponensk\u00e9nt legyen megval\u00f3s\u00edtva. Az al\u00e1bbi k\u00e9p mutatja az elk\u00e9sz\u00edtend\u0151 fel\u00fcletet:

    Seg\u00edts\u00e9g: a Surface \u00e9s a Text composable function\u00f6k a seg\u00edts\u00e9gedre lehetnek a megold\u00e1sban.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik az a login k\u00e9perny\u0151 a gombbal \u00e9s az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    "},{"location":"laborok/di/","title":"Labor08 - F\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1s a Dagger \u00e9s a Hilt seg\u00edts\u00e9g\u00e9vel, tesztel\u00e9s (Todo)","text":""},{"location":"laborok/di/#bevezetes","title":"Bevezet\u00e9s","text":"

    A kor\u00e1bbi laborokon m\u00e1r elsaj\u00e1t\u00edtottuk, hogyan lehet az Android alkalmaz\u00e1sunkat laz\u00e1n csatolt r\u00e9teges architekt\u00far\u00e1val megval\u00f3s\u00edtani. Ez egy\u00e9rtelm\u0171en seg\u00edti az alkalmaz\u00e1s rugalmas fejleszt\u00e9s\u00e9t, esetleg az egyes r\u00e9tegek is lecser\u00e9lhet\u0151k, de ha megn\u00e9zz\u00fck a k\u00f3dot, m\u00e9g mindig viszonylag jelent\u0151s f\u00fcgg\u00e9st tal\u00e1lunk, hiszen ahol egy m\u00e1sik r\u00e9tegbeli komponenst hozunk l\u00e9tre, ott a p\u00e9ld\u00e1nyos\u00edt\u00e1s a k\u00f3dba van \"\u00e9getve\", a m\u00e1sik r\u00e9teg lecser\u00e9l\u00e9s\u00e9hez itt is m\u00f3dos\u00edtanunk kellene a k\u00f3dot. Ezekre a probl\u00e9m\u00e1kra ny\u00fajt megold\u00e1st a f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1s (dependency injection). Ez egy \u00e1ltal\u00e1nos szoftverfejleszt\u00e9si technika, amelyet nemcsak Androidon, hanem m\u00e1s platformokon is haszn\u00e1lunk. Ebben a laborban az Androidon haszn\u00e1lhat\u00f3 Dagger \u00e9s Hilt k\u00f6nyvt\u00e1rakat ismerj\u00fck meg, amellyel Androidon tudunk f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1st v\u00e9gezni. A k\u00e9t k\u00f6nyvt\u00e1rat gyakran egy\u00fctt haszn\u00e1ljuk, a Dagger alapvet\u0151bb, alacsonyabb szint\u0171 funkci\u00f3kat ny\u00fajt, a Hilt pedig erre \u00e9p\u00fcl r\u00e1, hogy k\u00f6nnyebb\u00e9 tegye a fejleszt\u00e9st.

    A labornak egy m\u00e1sik fontos t\u00e9m\u00e1ja az Android alkalmaz\u00e1sok tesztel\u00e9se.

    "},{"location":"laborok/di/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/di/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    A Hilt megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektbe fogjuk integr\u00e1lni a k\u00fcl\u00f6nb\u00f6z\u0151 szolg\u00e1ltat\u00e1sokat, ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl. Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a projektet.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

    "},{"location":"laborok/di/#a-dagger-es-a-hilt-inicializalasa","title":"A Dagger \u00e9s a Hilt inicializ\u00e1l\u00e1sa","text":"

    A Dagger/Hilt feladata teh\u00e1t az lesz, hogy az alkalmaz\u00e1sunk egym\u00e1st\u00f3l f\u00fcgg\u0151 komponenseit laz\u00e1bban csatolt m\u00f3don k\u00f6ti \u00f6ssze. A gyakorlatban ez azt jelenti, hogy ha egy bizonyos t\u00edpus\u00fa f\u00fcgg\u0151s\u00e9gre van sz\u00fcks\u00e9g\u00fcnk, \u00e9s az adott f\u00fcgg\u0151s\u00e9g meg van jel\u00f6lve mint injekt\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9g, akkor a k\u00f6nyvt\u00e1rak el fogj\u00e1k v\u00e9gezni nek\u00fcnk az adott f\u00fcgg\u0151s\u00e9g megkeres\u00e9s\u00e9t \u00e9s be\u00e1ll\u00edt\u00e1s\u00e1t. Ez t\u00f6rt\u00e9nhet p\u00e9ld\u00e1ul egy konstruktornak t\u00f6rt\u00e9n\u0151 param\u00e9ter\u00e1tad\u00e1son kereszt\u00fcl. Ennek el\u0151nye, hogy ha egy komponenst lecser\u00e9l\u00fcnk - p\u00e9ld\u00e1ul ahogyan a Firebase laboron lecser\u00e9lt\u00fck a mem\u00f3riabeli implement\u00e1ci\u00f3kat \u00e9les, Firebase-ben m\u0171k\u00f6d\u0151kre - akkor nem sz\u00fcks\u00e9ges a k\u00f3dunkat m\u00f3dos\u00edtani, csup\u00e1n meg kell adni a Daggernek/Hiltnek, hogy az el\u00e9rhet\u0151 implement\u00e1ci\u00f3k k\u00f6z\u00fcl melyik legyen az, amelyiket sz\u00fcks\u00e9g eset\u00e9n alkalmazza.

    T\u00f6bbf\u00e9le f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00f3 keretrendszer l\u00e9tezik Androidon, \u00e9s az Android platformon k\u00edv\u00fcl is, ezek n\u00e9mileg m\u00e1s elveken m\u0171k\u00f6dnek. A Dagger a legjobb teljes\u00edtm\u00e9ny \u00e9rdek\u00e9ben \u00fagy m\u0171k\u00f6dik, hogy nem fut\u00e1s k\u00f6zben oldja fel a f\u00fcgg\u0151s\u00e9geket, hanem a ford\u00edt\u00e1si folyamatba avatkozik bele, \u00e9s m\u00e1r ak\u00f6zben felt\u00e9rk\u00e9pezi a f\u00fcgg\u0151s\u00e9gi viszonyok jel\u00f6l\u00e9s\u00e9re alkalmazott annot\u00e1ci\u00f3kat. Ez\u00e9rt a projekt inicializ\u00e1l\u00e1s\u00e1nak r\u00e9szek\u00e9nt sz\u00fcks\u00e9ges felvenn\u00fcnk egy gradle plugint is a folyamatba.

    El\u0151sz\u00f6r a libs.versions.toml f\u00e1jlunkba vegy\u00fck fel az \u00faj f\u00fcgg\u0151s\u00e9gekhez kapcsol\u00f3d\u00f3 bejegyz\u00e9seket:

    [versions]\nhilt = \"2.51.1\"\nhilt-navigation-compose = \"1.2.0\"\n\n[libraries]\nhilt-android = { module = \"com.google.dagger:hilt-android\", version.ref = \"hilt\" }\nhilt-compiler = { module = \"com.google.dagger:hilt-compiler\", version.ref = \"hilt\" }\nhilt-navigation-compose = { module = \"androidx.hilt:hilt-navigation-compose\", version.ref = \"hilt-navigation-compose\"}\n\n[plugins]\ngoogle-dagger-hilt-android = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\"}\n

    Majd a projekt szint\u0171 build.gradle.kts f\u00e1jlba vegy\u00fck fel a a k\u00f6vetkez\u0151 sort a pluginek k\u00f6z\u00e9:

    alias(libs.plugins.google.dagger.hilt.android) apply false\n

    Majd a modul szint\u0171 build.gradle f\u00e1jlban alkalmazzuk a plugint:

    plugins {\n...\n\nalias(libs.plugins.google.dagger.hilt.android)\n}\n

    \u00c9s vegy\u00fck m\u00e9g fel a sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket, majd szinkroniz\u00e1ljuk a projektet:

    // Hilt\nimplementation(libs.hilt.android)\nimplementation(libs.hilt.navigation.compose)\nksp(libs.hilt.compiler)\n

    Ezzel a build folyamat \u00e9s a f\u00fcgg\u0151s\u00e9gek rendben vannak. Most glob\u00e1lisan, az alkalmaz\u00e1s szintj\u00e9n inicializ\u00e1lnunk kell a Daggert, hogy l\u00e9trej\u00f6jj\u00f6n egy kontextus, amelyben a f\u00fcgg\u0151s\u00e9geket menedzseli. Ehhez a TodoApplication oszt\u00e1lyra tegy\u00fck r\u00e1 a @HiltAndroidApp annot\u00e1ci\u00f3t:

    @HiltAndroidApp\nclass TodoApplication : Application() {\n// ...\n}\n

    Majd nyissuk meg a MainActivity oszt\u00e1lyt is, ezen pedig az @AndroidEntryPoint annot\u00e1ci\u00f3t helyezz\u00fck el:

    @AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nTodoTheme {\nNavGraph()\n}\n}\n}\n}\n

    Ezzel a k\u00f6z\u00f6s inicializ\u00e1ci\u00f3s feladatok elk\u00e9sz\u00fcltek, de m\u00e9g t\u00e9nyleges injekt\u00e1lhat\u00f3 komponenseket \u00e9s injekt\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9geket nem hoztunk l\u00e9tre. Most elkezdj\u00fck a \"bedr\u00f3tozott\" f\u00fcgg\u0151s\u00e9gi viszonyokat f\u00fcgg\u0151s\u00e9ginjekt\u00e1l\u00e1sra cser\u00e9lni.

    "},{"location":"laborok/di/#az-adatbazismodul-elkeszitese","title":"Az adatb\u00e1zismodul elk\u00e9sz\u00edt\u00e9se","text":"

    A Dagger \u00e9s a Hilt \u00e1ltal kezelt komponensek a jobb \u00e1tl\u00e1that\u00f3s\u00e1g \u00e9rdek\u00e9ben modulokra oszthat\u00f3k. Minden modul komponenseket hoz l\u00e9tre, amelyeket a megjel\u00f6lt injekt\u00e1l\u00e1si pontokon a k\u00f6nyvt\u00e1rak fel fognak haszn\u00e1lni. Az els\u0151 modulunk a TodoDatabase \u00e9s a TodoDao l\u00e9trehoz\u00e1s\u00e1t fogja elv\u00e9gezni. Hozzunk l\u00e9tre ennek a modulnak egy data.di package-et, \u00e9s ebbe vegy\u00fck fel a modult megval\u00f3s\u00edt\u00f3 oszt\u00e1lyunkat:

    @Module\n@InstallIn(SingletonComponent::class)\nobject DatabaseModule {\n\n@Provides\n@Singleton\nfun provideDatabaseInstance(\n@ApplicationContext context: Context\n): TodoDatabase = Room.databaseBuilder(\ncontext,\nTodoDatabase::class.java,\n\"todo_database\"\n).fallbackToDestructiveMigration().build()\n\n@Provides\n@Singleton\nfun provideTodoDao(\ndb: TodoDatabase\n): TodoDao = db.dao\n}\n

    Ebben a @Module annot\u00e1ci\u00f3 azt jelenti, hogy az oszt\u00e1ly komponensekkel j\u00e1rul hozz\u00e1 a Dagger/Hilt \u00e1ltal kezelt objektumgr\u00e1fhoz. Az oszt\u00e1lyban legy\u00e1rtott komponensek teh\u00e1t f\u00fcgg\u0151s\u00e9gk\u00e9nt injekt\u00e1lhat\u00f3ak lesznek m\u00e1s komponensekbe, illetve maguk is hivatkozhatnak f\u00fcgg\u0151s\u00e9gekre. Az @InstallIn(SingletonComponent::class) a SingletonComponenthez k\u00f6ti a modult, aminek az lesz az eredm\u00e9nye, hogy a komponensek injekt\u00e1l\u00e1sa az eg\u00e9sz alkalmaz\u00e1son bel\u00fcl m\u0171k\u00f6dik majd.

    A met\u00f3dusokon lev\u0151 @Provides hat\u00e1rozza meg, hogy a met\u00f3dusok \"factory\" met\u00f3dusk\u00e9nt szolg\u00e1ljanak, \u00e9s az \u00e1ltaluk visszaadott objektumok a Dagger \u00e1ltal kezelt komponensekk\u00e9 v\u00e1ljanak. A @Singleton annot\u00e1ci\u00f3 azt adja meg, hogy ezekb\u0151l a komponensekb\u0151l egyetlen p\u00e9ld\u00e1ny k\u00e9sz\u00fclj\u00f6n, \u00e9s az eg\u00e9sz alkalmaz\u00e1son kereszt\u00fcl mindenhol ezt haszn\u00e1ljuk f\u00fcgg\u0151s\u00e9gk\u00e9nt. Legt\u00f6bbsz\u00f6r elegend\u0151 egy p\u00e9ld\u00e1ny, \u00e9s ez\u00e9rt ez a c\u00e9lravezet\u0151 megold\u00e1s.

    Most, hogy a komponenseket legy\u00e1rtottuk, arr\u00f3l kell gondoskodni, elt\u00e1vol\u00edtsuk ezeknek a komponenseknek a kor\u00e1bbi m\u00f3don t\u00f6rt\u00e9n\u0151 l\u00e9trehoz\u00e1s\u00e1t, \u00e9s megjel\u00f6lj\u00fck azokat a helyeket, ahova a Dagger \u00e9s Hilt k\u00f6nyvt\u00e1raknak ezeket a komponenseket f\u00fcgg\u0151s\u00e9gk\u00e9nt injekt\u00e1lniuk kell. Kor\u00e1bban a TodoDatabase l\u00e9trehoz\u00e1sa a TodoApplication oszt\u00e1lyban volt. Innen elt\u00e1vol\u00edtjuk a p\u00e9ld\u00e1nyos\u00edt\u00e1st, viszont a repository komponens\u00fcnket egyel\u0151re nem b\u00edztuk a Dagger/Hilt p\u00e1rosra, ez\u00e9rt ezt m\u00e9g mindig l\u00e9tre kell hozni, ehhez viszont sz\u00fcks\u00e9g van a TodoDao-ra, amit viszont m\u00e1r ide is injekt\u00e1lhatunk.

    A TodoApplication oszt\u00e1lyunk most \u00edgy fest:

    @HiltAndroidApp\nclass TodoApplication : Application() {\n\n@Inject\nlateinit var dao: TodoDao\n\ncompanion object {\nlateinit var repository: TodoRepositoryImpl\n}\n\noverride fun onCreate() {\nsuper.onCreate()\n\nrepository = TodoRepositoryImpl(dao)\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, a fenti l\u00e9p\u00e9sekhez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/di/#a-repository-modul-elkeszitese","title":"A repository modul elk\u00e9sz\u00edt\u00e9se","text":"

    A fentihez hasonl\u00f3an most a repository-t is a Dagger/Hilt kezel\u00e9s\u00e9re b\u00edzzuk, \u00e9s megsz\u00fcntetj\u00fck a k\u00e9zi l\u00e9trehoz\u00e1st. Ehhez el\u0151sz\u00f6r szint\u00e9n egy modult kell l\u00e9trehoznunk. Tegy\u00fck ezt szint\u00e9n a data.di package-be:

    @Module\n@InstallIn(SingletonComponent::class)\nabstract class RepositoryModule {\n\n@Binds\n@Singleton\nabstract fun bindTodoRepository(\ntodoRepositoryImpl: TodoRepositoryImpl\n): TodoRepository\n\n}\n

    Ez a modul n\u00e9mileg m\u00e1sk\u00e9pp m\u0171k\u00f6dik, mint a kor\u00e1bbi. Egyr\u00e9szt az oszt\u00e1ly absztrakt, illetve nem a @Provides annot\u00e1ci\u00f3t haszn\u00e1ltuk, hanem a @Binds-et, \u00e9s az ezzel megjel\u00f6lt met\u00f3dus azt hat\u00e1rozza meg, hogy amikor TodoRepository t\u00edpus\u00fa f\u00fcgg\u0151s\u00e9gre van sz\u00fcks\u00e9g\u00fcnk, akkor annak a konkr\u00e9t TodoRepositoryImpl t\u00edpus\u00fa implement\u00e1ci\u00f3j\u00e1t kell haszn\u00e1lni. Most m\u00e1r kivehetj\u00fck a TodoApplication oszt\u00e1lyb\u00f3l a repository kezel\u00e9s\u00e9t is, \u00edgy az oszt\u00e1lyunk \u00fcres lesz, de a Hilt inicializ\u00e1ci\u00f3ja miatt tov\u00e1bbra is sz\u00fcks\u00e9g van r\u00e1, hogy megtartsuk:

    @HiltAndroidApp\nclass TodoApplication : Application()\n

    Viszont a TodoRepositoryImpl p\u00e9ld\u00e1nyos\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9g van a TodoDao oszt\u00e1lyre, \u00e9s mivel innent\u0151l ezt a Dagger/Hilt v\u00e9gzi, ez\u00e9rt az @Inject annot\u00e1ci\u00f3 haszn\u00e1lat\u00e1val jelezni kell, hogy p\u00e9ld\u00e1nyos\u00edt\u00e1skor a TodoDao-t f\u00fcgg\u0151s\u00e9gk\u00e9nt szeretn\u00e9nk injekt\u00e1lni. \u00cdrjuk \u00e1t az oszt\u00e1ly fejl\u00e9c\u00e9t az al\u00e1bbira:

    class TodoRepositoryImpl @Inject constructor(\nprivate val dao: TodoDao\n) : TodoRepository {\n// ...\n}\n

    A repository viszont szorosan volt csatolva a viewmodelekhez, amelyeket a h\u00e1rom feature-h\u00f6z \u00edrtunk. M\u00e9g ezeket is \u00e1t kell alak\u00edtanunk hozz\u00e1, hogy az alkalmaz\u00e1s \u00fajra haszn\u00e1lhat\u00f3 legyen. Jelenleg mindh\u00e1rom viewmodel oszt\u00e1lyunkban az al\u00e1bbihoz hasonl\u00f3 factory-k vannak:

        companion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodosViewModel(\ntodoOperations = todoOperations\n)\n}\n}\n}\n

    Ezeket t\u00f6r\u00f6ln\u00fcnk kell, viszont gondoskodnunk kell r\u00f3la, hogy a TodoUseCases is inicializ\u00e1l\u00f3djon. Jelenlegi k\u00e9sz\u00fclts\u00e9gben ezt nem tudjuk m\u00e9g injekt\u00e1lni, csak a TodoRepository komponenst, ennek seg\u00edts\u00e9g\u00e9vel viszont p\u00e9ld\u00e1nyos\u00edthat\u00f3 a TodoUseCases. Illetve m\u00e9g el kell helyezni a viewmodel oszt\u00e1lyokon a @HiltViewModel annot\u00e1ci\u00f3t, hogy a Hilt \u00e1ltal menedzselt viewmodellekk\u00e9 v\u00e1ljanak. A TodosViewModel v\u00e9g\u00fcl \u00edgy alakul:

    @HiltViewModel\nclass TodosViewModel @Inject constructor(\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(TodosState())\nval state = _state.asStateFlow()\n\ninit {\ntodoOperations = TodoUseCases(repository)\nloadTodos()\n}\nprivate fun loadTodos() {\n\nviewModelScope.launch {\n_state.update { it.copy(isLoading = true) }\ntry {\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.update { it.copy(\nisLoading = false,\ntodos = todos\n) }\n}\n} catch (e: Exception) {\n_state.update {  it.copy(\nisLoading = false,\nerror = e\n) }\n}\n}\n}\n}\n

    Hasonl\u00f3 \u00e1talak\u00edt\u00e1sokat kell v\u00e9gezn\u00fcnk a CheckTodoViewModel oszt\u00e1lyon:

    @HiltViewModel\nclass CheckTodoViewModel @Inject constructor(\nprivate val savedState: SavedStateHandle,\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(CheckTodoState())\nval state: StateFlow<CheckTodoState> = _state\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CheckTodoEvent) {\nwhen (event) {\nCheckTodoEvent.EditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = true\n)\n}\n}\n\nCheckTodoEvent.StopEditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = false\n)\n}\n}\n\nis CheckTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(title = newValue)\n)\n}\n}\n\nis CheckTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(description = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo?.copy(priority = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectDate -> {\nval newValue = event.date.toString()\n_state.update {\nit.copy(\ntodo = it.todo?.copy(dueDate = newValue)\n)\n}\n}\n\nCheckTodoEvent.DeleteTodo -> {\nonDelete()\n}\n\nCheckTodoEvent.UpdateTodo -> {\nonUpdate()\n}\n}\n}\n\ninit {\ntodoOperations = TodoUseCases(repository)\nload()\n}\n\nprivate fun load() {\nval todoId = checkNotNull<Int>(savedState[\"id\"])\nviewModelScope.launch {\n_state.update { it.copy(isLoadingTodo = true) }\ntry {\nval todo = todoOperations.loadTodo(todoId)\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\n_state.update {\nit.copy(\nisLoadingTodo = false,\ntodo = todo.getOrThrow().asTodoUi()\n)\n}\n}\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onUpdate() {\nviewModelScope.launch(Dispatchers.IO) {\ntry {\ntodoOperations.updateTodo(\n_state.value.todo?.asTodo()!!\n)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onDelete() {\nviewModelScope.launch {\ntry {\ntodoOperations.deleteTodo(state.value.todo!!.id)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

    Majd a CreateTodoViewModel oszt\u00e1lyon is:

    @HiltViewModel\nclass CreateTodoViewModel @Inject constructor(\nprivate val repository: TodoRepository\n) : ViewModel() {\n\nval todoOperations: TodoUseCases\n\nprivate val _state = MutableStateFlow(CreateTodoState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\ninit {\ntodoOperations = TodoUseCases(repository)\n}\n\nfun onEvent(event: CreateTodoEvent) {\nwhen(event) {\nis CreateTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(title = newValue)\n) }\n}\nis CreateTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(description = newValue)\n) }\n}\nis CreateTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update { it.copy(\ntodo = it.todo.copy(priority = newValue)\n) }\n}\nis CreateTodoEvent.SelectDate -> {\nval newValue = event.date\n_state.update { it.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n) }\n}\nCreateTodoEvent.SaveTodo -> {\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

    Majd v\u00e9g\u00fcl az \u00f6sszes screen oszt\u00e1lyban v\u00e1ltoztassuk meg a viewmodel l\u00e9treoz\u00e1s\u00e1nak m\u00f3dj\u00e1t \u00fagy, hogy a hiltViewModel() h\u00edv\u00e1st haszn\u00e1ljuk. Pl. a CheckTodoScreen eddig \u00edgy n\u00e9zett ki:

    fun CheckTodoScreen(\nonNavigateBack: () -> Unit,\nviewModel: CheckTodoViewModel = viewModel(factory = CheckTodoViewModel.Factory)\n) {\n// ...\n}\n

    A k\u00f6vetkez\u0151k\u00e9ppen m\u00f3dos\u00edtsuk:

    fun CheckTodoScreen(\nonNavigateBack: () -> Unit,\nviewModel: CheckTodoViewModel = hiltViewModel()\n) {\n// ...\n}\n
    "},{"location":"laborok/di/#a-usecases-modul-elkeszitese","title":"A usecases modul elk\u00e9sz\u00edt\u00e9se","text":"

    M\u00e1r csak a usecases modult kell elk\u00e9sz\u00edten\u00fcnk. Ehhez k\u00e9sz\u00edts\u00fcnk el\u0151sz\u00f6r egy domain.di package-et, \u00e9s ebbe hozzuk l\u00e9tre az al\u00e1bbit:

    @Module\n@InstallIn(SingletonComponent::class)\nobject TodoUseCaseModule {\n\n@Provides\n@Singleton\nfun provideLoadTodosUseCase(\nrepository: TodoRepository\n): LoadTodosUseCase = LoadTodosUseCase(repository)\n\n@Provides\n@Singleton\nfun provideTodoUseCases(\nrepository: TodoRepository,\nloadTodos: LoadTodosUseCase\n): TodoUseCases = TodoUseCases(repository, loadTodos)\n\n}\n

    A TodoUseCases oszt\u00e1lyt pedig \u00edgy alak\u00edtsuk \u00e1t:

    class TodoUseCases(\nval repository: TodoRepository,\nval loadTodos: LoadTodosUseCase\n) {\n\nval loadTodo = LoadTodoUseCase(repository)\nval saveTodo = SaveTodoUseCase(repository)\nval updateTodo = UpdateTodoUseCase(repository)\nval deleteTodo = DeleteTodoUseCase(repository)\n}\n

    Most m\u00e1r a viewmodel oszt\u00e1lyokba nem sz\u00fcks\u00e9ges a TodoRepository injekt\u00e1l\u00e1sa \u00e9s a TodoUseCases k\u00e9zi l\u00e9trehoz\u00e1sa, hiszen a TodoUseCases k\u00f6zvetlen is injekt\u00e1lhat\u00f3v\u00e1 v\u00e1lt. M\u00f3dos\u00edtsuk ennek megfelel\u0151en a viewmodel oszt\u00e1lyokat!

    A CheckTodoViewModel k\u00f3dja:

    @HiltViewModel\nclass CheckTodoViewModel @Inject constructor(\nprivate val savedState: SavedStateHandle,\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(CheckTodoState())\nval state: StateFlow<CheckTodoState> = _state\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CheckTodoEvent) {\nwhen (event) {\nCheckTodoEvent.EditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = true\n)\n}\n}\n\nCheckTodoEvent.StopEditingTodo -> {\n_state.update {\nit.copy(\nisEditingTodo = false\n)\n}\n}\n\nis CheckTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(title = newValue)\n)\n}\n}\n\nis CheckTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo?.copy(description = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo?.copy(priority = newValue)\n)\n}\n}\n\nis CheckTodoEvent.SelectDate -> {\nval newValue = event.date.toString()\n_state.update {\nit.copy(\ntodo = it.todo?.copy(dueDate = newValue)\n)\n}\n}\n\nCheckTodoEvent.DeleteTodo -> {\nonDelete()\n}\n\nCheckTodoEvent.UpdateTodo -> {\nonUpdate()\n}\n}\n}\n\ninit {\nload()\n}\n\nprivate fun load() {\nval todoId = checkNotNull<Int>(savedState[\"id\"])\nviewModelScope.launch {\n_state.update { it.copy(isLoadingTodo = true) }\ntry {\nval todo = todoOperations.loadTodo(todoId)\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\n_state.update {\nit.copy(\nisLoadingTodo = false,\ntodo = todo.getOrThrow().asTodoUi()\n)\n}\n}\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onUpdate() {\nviewModelScope.launch(Dispatchers.IO) {\ntry {\ntodoOperations.updateTodo(\n_state.value.todo?.asTodo()!!\n)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n\nprivate fun onDelete() {\nviewModelScope.launch {\ntry {\ntodoOperations.deleteTodo(state.value.todo!!.id)\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

    A CreateTodoViewModel \u00edgy fest:

    @HiltViewModel\nclass CreateTodoViewModel @Inject constructor(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(CreateTodoState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<UiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: CreateTodoEvent) {\nwhen(event) {\nis CreateTodoEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(title = newValue)\n) }\n}\nis CreateTodoEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update { it.copy(\ntodo = it.todo.copy(description = newValue)\n) }\n}\nis CreateTodoEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update { it.copy(\ntodo = it.todo.copy(priority = newValue)\n) }\n}\nis CreateTodoEvent.SelectDate -> {\nval newValue = event.date\n_state.update { it.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n) }\n}\nCreateTodoEvent.SaveTodo -> {\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(UiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(UiEvent.Failure(e.toUiText()))\n}\n}\n}\n}\n

    A TodosViewModel \u00e1talak\u00edtott verzi\u00f3ja pedig az al\u00e1bbi:

    @HiltViewModel\nclass TodosViewModel @Inject constructor(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(TodosState())\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\nprivate fun loadTodos() {\n\nviewModelScope.launch {\n_state.update { it.copy(isLoading = true) }\ntry {\nCoroutineScope(coroutineContext).launch(Dispatchers.IO) {\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.update { it.copy(\nisLoading = false,\ntodos = todos\n) }\n}\n} catch (e: Exception) {\n_state.update {  it.copy(\nisLoading = false,\nerror = e\n) }\n}\n}\n}\n}\n

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, a fenti l\u00e9p\u00e9sekhez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/di/#az-alkalmazas-tesztelese","title":"Az alkalmaz\u00e1s tesztel\u00e9se","text":"

    Az elk\u00e9sz\u00fclt alkalmaz\u00e1snak most egy-egy r\u00e9sz\u00e9t automatiz\u00e1lt tesztekkel fogjuk ellen\u0151rizni. Az automatiz\u00e1lt teszteket k\u00e9t f\u0151 t\u00edpusra oszthatjuk Androidon:

    • Lok\u00e1lis tesztek: ezek olyan tesztek amelyek b\u00e1rmif\u00e9le virtualiz\u00e1lt k\u00f6rnyezet n\u00e9lk\u00fcl, puszt\u00e1n Java k\u00f3dnak a fejleszt\u0151i g\u00e9pen t\u00f6rt\u00e9n\u0151 futtat\u00e1s\u00e1val v\u00e9grehajthat\u00f3k.
    • Instrument\u00e1lt tesztek: ezek a tesztek az emul\u00e1toron futnak.
    "},{"location":"laborok/di/#lokalis-tesztek-futtatasa","title":"Lok\u00e1lis tesztek futtat\u00e1sa","text":"

    A lok\u00e1lis tesztek k\u00f6zt is megk\u00fcl\u00f6nb\u00f6ztethet\u00fcnk k\u00fcl\u00f6nb\u00f6z\u0151 t\u00edpusokat aszerint, hogy architektur\u00e1lisan mekkora r\u00e9sz\u00e9t \u00e9rintik a k\u00f3dnak. A k\u00f3d kis egys\u00e9geit, gyakorlatban met\u00f3dusait ellen\u0151rz\u0151 teszteket unitteszteknek nevezz\u00fck. Ha a teszt n\u00e9h\u00e1ny oszt\u00e1ly k\u00f3dj\u00e1n is \u00e1th\u00edv, akkor integr\u00e1ci\u00f3s tesztr\u0151l besz\u00e9l\u00fcnk, ha pedig valamilyen komplexebb, sok komponenst \u00e9rint\u0151 folyamatot tesztel, akkor rendszertesztnek nevezz\u00fck.

    A lok\u00e1lis tesztek k\u00f6z\u00fcl leggyakrabban unitteszteket k\u00e9sz\u00edt\u00fcnk, a nagyobb egys\u00e9geket pedig gyakrabban m\u00e1r instrument\u00e1lt tesztekkel ellen\u0151rizz\u00fck. Most a lok\u00e1lis tesztek k\u00f6z\u00fcl a unittesztekre koncentr\u00e1lunk. Ezek a legk\u00f6nnyebben elk\u00e9sz\u00edthet\u0151ek \u00e9s sz\u00e1mos el\u0151ny\u00fck van:

    • Szisztematikus m\u00f3dszert adnak a rendszer teljes letesztel\u00e9s\u00e9hez
    • Gyorsan lefutnak
    • Mivel minden teszt egy kis egys\u00e9gre vonatkozik, a meghi\u00fasul\u00f3 teszt j\u00f3l r\u00e1mutat a probl\u00e9ma hely\u00e9re is

    A unittesztek eset\u00e9n fontos kih\u00edv\u00e1s, hogy a f\u00fcgg\u0151s\u00e9geket izol\u00e1ljuk, lev\u00e1lasszuk, hiszen ha azok is megh\u00edv\u00f3dn\u00e1nak, akkor nem unittesztr\u0151l besz\u00e9ln\u00e9nk, hanem integr\u00e1ci\u00f3s tesztr\u0151l, \u00e9s a fenti el\u0151ny\u00f6k nem teljes\u00fcln\u00e9nek. K\u00fcl\u00f6n\u00f6sen az az el\u0151ny veszne el, hogy a teszt j\u00f3l mutatja a hiba hely\u00e9t. Ez\u00e9rt a tesztekben a f\u00fcgg\u0151s\u00e9geket valamilyen test double objektummal, tipikusan mock objektummal cser\u00e9lj\u00fck le. A mock objektum egy \"buta\", de \"felprogramozhat\u00f3\" komponens, ami \u00e9ppen csak annyit csin\u00e1l, amit a teszt idej\u00e9re elv\u00e1runk t\u0151le, azaz tipikusan valamilyen be\u00e9getett adatot ad vissza. Ezen k\u00edv\u00fcl a seg\u00edts\u00e9g\u00e9vel ellen\u0151rizhet\u0151 az is, hogy a lecser\u00e9lt f\u00fcgg\u0151s\u00e9gen a tesztelt k\u00f3dr\u00e9szlet t\u00e9nyleg elv\u00e9gezte a v\u00e1rt h\u00edv\u00e1st.

    Az alkalmaz\u00e1sban nincsenek t\u00fal bonyolult \u00fczleti logika r\u00e9szek, de a tesztel\u00e9s technik\u00e1j\u00e1t j\u00f3l meg tudjuk figyelni. Most a TodoRepositoryImpl oszt\u00e1lyt fogjuk tesztelni. Konvenci\u00f3 szerint oszt\u00e1lyokhoz k\u00e9sz\u00edt\u00fcnk tesztoszt\u00e1lyokat, \u00e9s a tesztoszt\u00e1lyokban minden tesztelt met\u00f3dus egy lehets\u00e9ges lefut\u00e1s\u00e1hoz k\u00e9sz\u00edt\u00fcnk egy tesztmet\u00f3dust. A tesztoszt\u00e1lyokat a tesztelt oszt\u00e1lyokkal azonos package-be tessz\u00fck, \u00e9s nev\u00fckben a Test ut\u00f3tagot haszn\u00e1ljuk.

    El\u0151sz\u00f6r fel kell venn\u00fcnk a tesztel\u00e9shez haszn\u00e1land\u00f3 f\u00fcgg\u0151s\u00e9geket a projektbe! Mivel a kapott v\u00e1zban eddig nem voltak tesztek, \u00edgy ezek a f\u00fcgg\u0151s\u00e9gek teljesen hi\u00e1nyoztak. A lok\u00e1lis tesztekhez a testImplementation scope-ot kell haszn\u00e1lnunk. Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket, el\u0151sz\u00f6r a libs.versions.toml f\u00e1jllal kezdve:

    [versions]\njunit = \"4.13.2\"\nmockitoCore = \"5.11.0\"\nmockitoInline = \"5.2.0\"\nmockitoKotlin = \"5.2.1\"\n\n[libraries]\njunit = { module = \"junit:junit\", version.ref = \"junit\" }\nmockito-inline = { module = \"org.mockito:mockito-inline\", version.ref = \"mockitoInline\" }\nmockito-core = { module = \"org.mockito:mockito-core\", version.ref = \"mockitoCore\" }\nmockito-kotlin = { module = \"org.mockito.kotlin:mockito-kotlin\", version.ref = \"mockitoKotlin\" }\n

    Majd folytassuk a modulszint\u0171 build.gradle.kts f\u00e1jllal:

    // Testing\ntestImplementation(libs.junit)\ntestImplementation(libs.mockito.core)\ntestImplementation(libs.mockito.inline)\ntestImplementation(libs.mockito.kotlin)\n

    Hozzuk l\u00e9tre a data.datasource package-et ez\u00e9rt a test k\u00f6nyvt\u00e1rban is! A lok\u00e1lis tesztek a test k\u00f6nyvt\u00e1rban vannak, az androidTest k\u00f6nyvt\u00e1r pedig az instrument\u00e1lt tesztek helye.

    A l\u00e9trehozott package-ben hozzunk l\u00e9tre egy TodoRepositoryImplTest oszt\u00e1lyt az al\u00e1bbi m\u00f3don:

    package hu.bme.aut.android.todo.data.datasource\n\nimport hu.bme.aut.android.todo.data.dao.TodoDao\nimport hu.bme.aut.android.todo.data.entities.TodoEntity\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.datetime.LocalDate\nimport org.junit.Assert\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mock\nimport org.mockito.junit.MockitoJUnitRunner\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\n\n@RunWith(MockitoJUnitRunner::class)\nclass TodoRepositoryImplTest {\n\n@Mock\nlateinit var todoDao: TodoDao\n\n@Test\nfun testGetAllTodos() {\nval sampleTodo = TodoEntity(\n1,\n\"Test app\",\nPriority.HIGH,\nLocalDate(2024, 4, 30),\n\"Write unit tests for our Todo app\"\n)\nval mockDao = mock<TodoDao> {\non { getAllTodos() } doReturn (flowOf(listOf(sampleTodo)))\n}\nval todoRepositoryImpl = TodoRepositoryImpl(mockDao)\nval result = todoRepositoryImpl.getAllTodos()\nrunBlocking {\nAssert.assertTrue(result.first().contains(sampleTodo))\nverify(mockDao, times(1)).getAllTodos()\n}\n}\n}\n

    N\u00e9zz\u00fck \u00e1t a laborvezet\u0151vel egy\u00fctt, hogyan m\u0171k\u00f6dik a teszt!

    Alapvet\u0151en minden teszt h\u00e1rom l\u00e9p\u00e9sb\u0151l \u00e1ll:

    1. A test fixture, azaz a tesztelni k\u00edv\u00e1nt k\u00f3dr\u00e9szlethez sz\u00fcks\u00e9ges kezdeti \u00e1llapot fel\u00e1ll\u00edt\u00e1sa. Ha egy \"sikeres\" lefut\u00e1st szeretn\u00e9nk tesztelni, akkor ennek megfelel\u0151en k\u00e9sz\u00edtj\u00fck el\u0151 a k\u00f6rnyezetet. Ha pedig egy hiba ut\u00e1na elv\u00e1rt hat\u00e1st, pl. exception dob\u00f3dik, hiba\u00fczenet \u00edr\u00f3dik ki stb. szeretn\u00e9nk tesztelni, akkor ennek megfelel\u0151en. Idetartozik a f\u00fcgg\u0151s\u00e9gek kiv\u00e1lt\u00e1sa is.
    2. A tesztelni k\u00edv\u00e1nt k\u00f3dr\u00e9szlet futtat\u00e1sa.
    3. Az elv\u00e1rt eredm\u00e9nyek megfogalmaz\u00e1sa, annak ellen\u0151rz\u00e9se, hogy teljes\u00fcltek-e (assertions). Ha mock objektumokat haszn\u00e1ltunk, akkor idetartozik annak ellen\u0151rz\u00e9se is, hogy rajtuk megh\u00edv\u00f3dtak-e azok a met\u00f3dusok, amelyek megh\u00edv\u00f3d\u00e1s\u00e1ra sz\u00e1m\u00edtottunk.

    A p\u00e9ld\u00e1nkban a TodoRepositoryImpl oszt\u00e1ly getAllTodos met\u00f3dusa csup\u00e1n annyit tesz, hogy tov\u00e1bbh\u00edvja a TodoDao oszt\u00e1ly getAllTodos met\u00f3dus\u00e1t, majd ennek eredm\u00e9ny\u00e9t visszaadja. A teszt\u00fcnk ez\u00e9rt nem lesz t\u00fal bonyolult. Alapvet\u0151en abb\u00f3l \u00e1ll, hogy k\u00e9sz\u00edten\u00fcnk kell egy TodoDao mock objektumot, amely be\u00e9getett TodoEntity list\u00e1t fog visszaadni. Ezt a mockot kell odaadnunk a TodoRepositoryImp f\u00fcgg\u0151s\u00e9g\u00e9nek, majd meg kell h\u00edvnunk a tesztelni k\u00edv\u00e1nt met\u00f3dust, \u00e9s meg kell vizsg\u00e1lni, hogy a be\u00e9getett list\u00e1t adta-e vissza, valamint a mockunknak az azonos nev\u0171 met\u00f3dusa szint\u00e9n h\u00edv\u00f3dott-e.

    Futtassuk le a tesztet!

    "},{"location":"laborok/di/#instrumentalt-tesztek-futtatasa","title":"Instrument\u00e1lt tesztek futtat\u00e1sa","text":"

    Bonyolultabb teszteket sem lehetetlen lok\u00e1lis tesztk\u00e9nt futtatni, de a f\u00fcgg\u0151s\u00e9gek sz\u00f6vev\u00e9nyess\u00e9ge miatt ez egy j\u00f3val bonyolultabb feladat lenne. Praktikusabb ez\u00e9rt ha \u00f6sszetettebb folyamatok tesztel\u00e9s\u00e9hez az emul\u00e1tort is seg\u00edts\u00e9g\u00fcl h\u00edvjuk. Ilyen p\u00e9ld\u00e1ul a Compose seg\u00edts\u00e9g\u00e9vel k\u00e9sz\u00edtett UI tesztel\u00e9se. Az Android \u00e1ltal biztos\u00edtott eszk\u00f6z\u00f6kkel l\u00e9tre tudjuk hozni a komponenseinket egy emul\u00e1lt k\u00f6rnyezetben, \u00e9s a teszt k\u00f3dj\u00e1b\u00f3l interakci\u00f3kat is ki tudunk v\u00e1ltani (pl. \u00edrjunk be sz\u00f6veget egy mez\u0151be, kattintsunk egy gombon stb.). Ez a fajta tesztel\u00e9s l\u00e1that\u00f3an j\u00f3val k\u00f6zelebb \u00e1ll ahhoz a m\u00f3dhoz, ahogyan az alkalmaz\u00e1s majd t\u00e9nylegesen futni fog. Logikusan bel\u00e9that\u00f3 ugyanakkor az is, hogy ezek a tesztek j\u00f3val bonyolultabbak, \u00e9s lassabban is fognak futni.

    Az instrument\u00e1lt teszteket az androidTest k\u00f6nyvt\u00e1rban lehet l\u00e9trehozni. Mivel ezek nagyobb l\u00e9pt\u00e9k\u0171 tesztek is lehetnek, nem felt\u00e9tlen tartoznak logikailag egy komponenshez. Amennyiben azonban odatartoznak, javasolt ezeket is azonos package-be tenni \u00e9s a lok\u00e1lis tesztek\u00e9hez hasonl\u00f3 elnevez\u00e9si konvenci\u00f3 szerint elnevezni.

    El\u0151sz\u00f6r itt is a f\u00fcgg\u0151s\u00e9gek felv\u00e9tel\u00e9vel kezd\u00fcnk, a libs.versions.toml f\u00e1jllal:

    [versions]\nespressoCore = \"3.6.1\"\nhiltAndroidTesting = \"2.51.1\"\nhiltAndroidCompiler = \"2.51.1\"\njunitVersion = \"1.2.1\"\n\n\n[libraries]\nandroidx-espresso-core = { module = \"androidx.test.espresso:espresso-core\", version.ref = \"espressoCore\" }\nandroidx-junit = { module = \"androidx.test.ext:junit\", version.ref = \"junitVersion\" }\nandroidx-ui-test-junit4 = { module = \"androidx.compose.ui:ui-test-junit4\" }\nandroidx-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\" }\nandroidx-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\" }\nhilt-android-testing = { module = \"com.google.dagger:hilt-android-testing\", version.ref = \"hiltAndroidTesting\" }\nhilt-android-compiler = { module = \"com.google.dagger:hilt-android-compiler\", version.ref = \"hiltAndroidCompiler\" }\n

    Majd hivatkozzuk is meg ezeket a modulszint\u0171 build.gradle.kts f\u00e1jlban:

    // Instrumented testing\nandroidTestImplementation(libs.androidx.junit)\nandroidTestImplementation(libs.androidx.espresso.core)\nandroidTestImplementation(libs.androidx.ui.test.junit4)\ndebugImplementation(libs.androidx.ui.tooling)\ndebugImplementation(libs.androidx.ui.test.manifest)\n\n// Hilt for testing\nandroidTestImplementation(libs.hilt.android.testing)\nkspAndroidTest(libs.hilt.android.compiler)\n

    A p\u00e9ld\u00e1nkban azt fogjuk tesztelni, hogy ha \u00faj teend\u0151 l\u00e9trehoz\u00e1s\u00e1n\u00e1l a d\u00e1tumv\u00e1laszt\u00f3 ikonj\u00e1ra kattintunk, akkor val\u00f3ban el\u0151ugrik a d\u00e1tumv\u00e1laszt\u00f3 komponens. Miel\u0151tt a t\u00e9nyleges tesztet meg\u00edrjuk, gondoskodnunk kell r\u00f3la, hogy a tesztb\u0151l majd a felhaszn\u00e1l\u00f3i fel\u00fcleten a d\u00e1tumv\u00e1laszt\u00f3 ikonj\u00e1t meg tudjuk hivatkozni. Ha lenne rajta megjelen\u00edtett sz\u00f6veg, a tesztb\u0151l ez alapj\u00e1n is lehetne hivatkozni, de jelen esetben csak egy ikonr\u00f3l van sz\u00f3. \u00dagy tudjuk azonos\u00edthat\u00f3v\u00e1 tenni, hogy a Modifier\u00e9n kereszt\u00fcl egy test taggel l\u00e1tjuk el. M\u00f3dos\u00edtsuk eszerint a TodoEditor oszt\u00e1lyban a DatePicker komponens h\u00edv\u00e1s\u00e1t:

    DatePicker(\npickedDate = pickedDate,\nonClick = onDatePickerClicked,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction)\n.testTag(\"datePickerIcon\"),\nenabled = enabled\n)\n

    Most m\u00e1r elk\u00e9sz\u00edthetj\u00fck a tesztet! Mivel a teszt a CreateTodoScreen oszt\u00e1lyhoz k\u00f6thet\u0151, ez pedig a feature.todo_create package-ben van, el\u0151sz\u00f6r hozzuk l\u00e9tre ezt a package-et az androidTest mapp\u00e1ban is.

    Majd k\u00e9sz\u00edts\u00fck el a CreateTodoScreenTest oszt\u00e1lyunkat:

    package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.hasTestTag\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport hu.bme.aut.android.todo.MainActivity\nimport org.junit.Rule\nimport org.junit.Test\n\nclass CreateTodoScreenTest {\n@get:Rule\nval composeTestRule = createAndroidComposeRule<MainActivity>()\n\n@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)\n@Test\nfun testDatePickerDialogIsShownWhenClickedOnDatePickerIcon() {\ncomposeTestRule.activity.setContent {\nCreateTodoScreen(\nonNavigateBack = { })\n}\n\ncomposeTestRule.onNodeWithText(\"Select date\").assertIsNotDisplayed()\n\ncomposeTestRule.onNode(hasTestTag(\"datePickerIcon\")).performClick()\n\ncomposeTestRule.onNodeWithText(\"Select date\").assertIsDisplayed()\n}\n}\n

    A teszt f\u0151 eleme a createAndroidComposeRule h\u00edv\u00e1s, amely egy teszt rule-t ad vissza. Ezen kereszt\u00fcl renderelhetj\u00fck a k\u00edv\u00e1nt Compose tartalmat, \u00e9s ezen kereszt\u00fcl t\u00f6rt\u00e9nik a k\u00edv\u00e1nt akci\u00f3k kiv\u00e1lt\u00e1sa \u00e9s az elv\u00e1rt eredm\u00e9ny ellen\u0151rz\u00e9se is. Ha valamilyen elemnek a megjelen\u00e9s\u00e9t akarjuk tesztelni, \u00e9rdemes azt is megfogalmazni, hogy a kiv\u00e1ltott interakci\u00f3 el\u0151tt m\u00e9g nincs megjelen\u00edtve.

    Futtassuk le a tesztet! Figyelj\u00fck meg, hogy v\u00e9gigk\u00f6vethet\u0151 az emul\u00e1toron is a teszt fut\u00e1sa.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lefutott teszt, a hozz\u00e1 tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/di/#onallo-feladat-1-dependency-injection-befejezese","title":"\u00d6n\u00e1ll\u00f3 feladat 1. - Dependency Injection befejez\u00e9se","text":"

    A TodoUseCases oszt\u00e1lyban egyel\u0151re csak a LoadTodosUseCase f\u00fcgg\u0151s\u00e9get hozzuk l\u00e9tre a modulban, \u00e9s injekt\u00e1ljuk a Dagger/Hilt seg\u00edts\u00e9g\u00e9vel, a t\u00f6bbi usecase most is manu\u00e1lisan p\u00e9ld\u00e1nyosodik a repository \u00e1tad\u00e1s\u00e1val. Folytasd az \u00e1talak\u00edt\u00e1st, \u00e9s hozd l\u00e9tre az \u00f6sszes usecase-t a usecase modulban, hogy ut\u00e1na m\u00e1r a Dagger/Hilt kezelje \u0151ket!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s, az \u00e1talak\u00edtott k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/di/#onallo-feladat-2-ujabb-teszt-keszitese","title":"\u00d6n\u00e1ll\u00f3 feladat 2. - \u00dajabb teszt k\u00e9sz\u00edt\u00e9se","text":"

    K\u00e9sz\u00edts\u00fcnk egy tesztet arra vonatkoz\u00f3an, hogy ha a priorit\u00e1sv\u00e1laszt\u00f3ra kattintunk, akkor az \u00f6sszes lehets\u00e9ges v\u00e1laszthat\u00f3 priorit\u00e1s megjelenik a k\u00e9perny\u0151n!

    Seg\u00edts\u00e9g az implement\u00e1ci\u00f3hoz:

    • Most el\u00e9g a TodoEditor komponensb\u0151l kiindulni, nem sz\u00fcks\u00e9ges a teljes Screen tesztel\u00e9se.
    • A TodoEditor megh\u00edv\u00e1sakor ki kell t\u00f6lten\u00fcnk a k\u00f6telez\u0151 param\u00e9tereit, de mivel ezekre a tesztben nem t\u00e1maszkodunk, ez\u00e9rt \u00fcres sztringeket, \u00fcres f\u00fcggv\u00e9nyeket haszn\u00e1lhatunk helyett\u00fck.
    • L\u00e1ssuk el test taggel a leg\u00f6rd\u00fcl\u0151 list\u00e1t.
    • Fogalmazzuk meg, hogy a v\u00e1lasztott alap\u00e9rtelmezett priorit\u00e1s l\u00e1that\u00f3 a k\u00e9perny\u0151n, de a t\u00f6bbi sz\u00f6vege nem.
    • Emul\u00e1ljunk kattint\u00e1st!
    • Fogalmazzuk meg, hogy most a t\u00f6bbi priorit\u00e1s is megjelent!
    • A teszt a k\u00f6z\u00f6s p\u00e9lda szerint is m\u0171k\u00f6dik, de itt el\u00e9g lehet az egyszer\u0171bb createComposeRule(), majd a composeTestRule.setContent is.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lefutott teszt, a hozz\u00e1 tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

    A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

    A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/firebase/","title":"Labor07 - Firebase","text":""},{"location":"laborok/firebase/#bevezeto","title":"Bevezet\u0151","text":"

    A labor sor\u00e1n a megl\u00e9v\u0151 feladatkezel\u0151 alkalmaz\u00e1s ker\u00fcl tov\u00e1bbfejleszt\u00e9sre a Firebase Backend as a Service (BaaS) felhaszn\u00e1l\u00e1s\u00e1val. A feladat c\u00e9lja, hogy szeml\u00e9ltesse, hogyan lehet k\u00f6z\u00f6s backendet haszn\u00e1l\u00f3 alkalmaz\u00e1st fejleszteni saj\u00e1t backend k\u00f3d fejleszt\u00e9se n\u00e9lk\u00fcl.

    A Firebase manaps\u00e1g az egyik legn\u00e9pszer\u0171bb Backend as a Service megold\u00e1s Android, iOS \u00e9s web kliensek t\u00e1mogat\u00e1s\u00e1val, mely sz\u00e1mos szolg\u00e1ltat\u00e1st biztos\u00edt, p\u00e9ld\u00e1ul: - real-time adatb\u00e1ziskezel\u00e9s - storage - authentik\u00e1ci\u00f3 - push \u00e9rtes\u00edt\u00e9sek - analytics - crash reporting

    Tov\u00e1bbi \u00e1ltal\u00e1nos inform\u00e1ci\u00f3k a Firebase-r\u0151l: https://firebase.google.com/.

    A laborfoglalkoz\u00e1s c\u00e9lja, hogy bemutassa a Firebase legfontosabb szolg\u00e1ltat\u00e1sait egy komplett alkalmaz\u00e1s megval\u00f3s\u00edt\u00e1sa keret\u00e9ben. Az alkalmaz\u00e1s az al\u00e1bbi f\u0151 funkci\u00f3kat t\u00e1mogatja: - regisztr\u00e1ci\u00f3, bejelentkez\u00e9s - \u00fczenetek list\u00e1z\u00e1sa - \u00fczenet\u00edr\u00e1s - k\u00e9pek csatol\u00e1sa \u00fczenetekhez - \u00fczenetek megjelen\u00edt\u00e9se val\u00f3s id\u0151ben - crash reporting - analitika

    Az anyag r\u00e9szletes meg\u00e9rt\u00e9s\u00e9hez javasoljuk, hogy figyelje a laborvezet\u0151 utas\u00edt\u00e1sait \u00e9s labor ut\u00e1n is 10-20 percet sz\u00e1njon a k\u00f3dr\u00e9szek meg\u00e9rt\u00e9s\u00e9re.

    "},{"location":"laborok/firebase/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/firebase/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    A Firebase megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektbe fogjuk integr\u00e1lni a k\u00fcl\u00f6nb\u00f6z\u0151 szolg\u00e1ltat\u00e1sokat. Ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl is, valamilyen probl\u00e9ma eset\u00e9n a kezd\u0151projektet err\u0151l a linkr\u0151l \u00e9rhet\u0151 el.

    Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a kicsomagolt projektet.

    FILE PATH

    A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

    Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

    "},{"location":"laborok/firebase/#projekt-elokeszitese-konfiguracio","title":"Projekt el\u0151k\u00e9sz\u00edt\u00e9se, konfigur\u00e1ci\u00f3","text":"

    Els\u0151 l\u00e9p\u00e9sk\u00e9nt l\u00e9tre kell hozni egy Firebase projektet a Firebase admin fel\u00fclet\u00e9n (Firebase console), majd egy Android Studio projektet \u00e9s a kett\u0151t \u00f6ssze kell k\u00f6tni: - Navig\u00e1ljunk a Firebase console fel\u00fclet\u00e9re: https://console.firebase.google.com/ ! - Jelentkezz\u00fcnk be! - Hozzunk l\u00e9tre egy \u00faj projektet a Create project elemet v\u00e1lasztva!

    • A projekt neve legyen BMETodoNEPTUN_KOD, ahol a NEPTUN_KOD hely\u00e9re a saj\u00e1t Neptun k\u00f3dunkat helyettes\u00edts\u00fck!
    • Az analitik\u00e1t most m\u00e9g nem sz\u00fcks\u00e9ges konfigur\u00e1lni.

    projekt n\u00e9v

    A Neptun k\u00f3dra az\u00e9rt van sz\u00fcks\u00e9g, mert ugyanazon laborg\u00e9p kulcs\u00e1val ugyanolyan nev\u0171 projektet nem hozhatunk l\u00e9tre t\u00f6bbsz\u00f6r, \u00e9s t\u00f6bb laborcsoport l\u00e9v\u00e9n ebb\u0151l probl\u00e9ma ad\u00f3dhatna. Ugyanerre lesz majd sz\u00fcks\u00e9g a package n\u00e9v eset\u00e9n is.

    Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n fuss\u00e1k \u00e1t a laborvezet\u0151vel k\u00f6z\u00f6sen a Firebase console fel\u00fclet\u00e9t az al\u00e1bbi elemekre kit\u00e9rve: - Authentication, Firestore \u00e9s Storage.

    N\u00e9zz\u00fck \u00e1t a megnyitott projektet! K\u00fcl\u00f6n\u00f6s figyelemmel vizsg\u00e1ljuk \u00e1t a projekt mostani fel\u00e9p\u00edt\u00e9s\u00e9t, az \u00faj r\u00e9szeket (todo_auth, data package), illetve hogyan lehets\u00e9ges ebben \u00e1t\u00e1llni a Firebase szolg\u00e1ltat\u00e1saira.

    Adjuk hozz\u00e1 az AndroidManifest.xml f\u00e1jlhoz az internet haszn\u00e1lati enged\u00e9lyt:

    <uses-permission android:name=\"android.permission.INTERNET\" />\n
    "},{"location":"laborok/firebase/#firebase-inicilaizacio-authentication","title":"Firebase inicilaiz\u00e1ci\u00f3, Authentication","text":"

    Ezek ut\u00e1n v\u00e1lasszuk Android Studioban a Tools -> Firebase men\u00fcpontot, melynek hat\u00e1s\u00e1ra jobb oldalt megny\u00edlik a Firebase Assistant funkci\u00f3.

    A Firebase Assistant akkor fogja megtal\u00e1lni a Firebase console-on l\u00e9trehozott projektet, ha Android Studioba is ugyanazzal a Google accounttal vagyunk bejelentkezve, mint amivel a console-on l\u00e9trehoztuk a projektet. Ellen\u0151rizz\u00fck ezt mindk\u00e9t helyen! Amennyiben a Firebase Assistant-ot nem siker\u00fcl be\u00fczemelni, manu\u00e1lisan is \u00f6sszek\u00f6thet\u0151 a k\u00e9t projekt. A le\u00edr\u00e1sban ismertetni fogjuk a l\u00e9p\u00e9seket, amelyeket az Assistant v\u00e9gez el.

    V\u00e1lasszuk az Assistant-ban az Authentication szakaszt \u00e9s azon bel\u00fcl az Authenticate using a custom authentication system-t, majd a Connect to Firebase gombot. Ezt k\u00f6vet\u0151en egy dialog ny\u00edlik meg, ahol ha megfelel\u0151ek az accountok, a m\u00e1sodik szakaszt (Choose an existing Firebase or Google project) v\u00e1lasztva kiv\u00e1laszthatjuk a projektet, amit a Firebase console-on m\u00e1r l\u00e9trehoztunk. Itt egy\u00e9bk\u00e9nt lehet\u0151s\u00e9g van \u00faj projektet is l\u00e9trehozni. (Ha els\u0151re hib\u00e1t l\u00e1tunk a projekttel val\u00f3 \u00f6sszekapcsol\u00e1sn\u00e1l, pr\u00f3b\u00e1ljuk \u00fajra, m\u00e1sodszorra \u00e1ltal\u00e1ban sikeresen megt\u00f6rt\u00e9nik az Android Studio projekt szinkroniz\u00e1l\u00e1sa a Firebase projekttel.)

    A h\u00e1tt\u00e9rben val\u00f3j\u00e1ban annyi t\u00f6rt\u00e9nik, hogy az alkalmaz\u00e1sunk package neve \u00e9s az al\u00e1\u00edr\u00f3 kulcs SHA-1 hash-e alapj\u00e1n hozz\u00e1ad\u00f3dik egy Android alkalmaz\u00e1s a Firebase console-on l\u00e9v\u0151 projekt\u00fcnkh\u00f6z, \u00e9s az ahhoz tartoz\u00f3 konfigur\u00e1ci\u00f3s (google-services.json) f\u00e1jl let\u00f6lt\u0151dik a projekt\u00fcnk k\u00f6nyvt\u00e1r\u00e1ba az alap\u00e9rtelmezett (app) modul al\u00e1.

    Ezt a l\u00e9p\u00e9ssorozatot manu\u00e1lisan is v\u00e9grehajthatjuk a Firebase console-on az Add Firebase to your Android app-et v\u00e1lasztva. A debug kulcs SHA-1 lenyomata ilyenkor a jobb oldalon tal\u00e1lhat\u00f3 Gradle f\u00fcl\u00f6n gener\u00e1lhat\u00f3. A fenti sorban kattintsunk az Execute Gradle Task men\u00fcpontra, majd a felugr\u00f3 ablakban \u00cdrjuk be a gradle signingreport-ot, \u00e9s nyomjunk egy entert. Ezek ut\u00e1naz als\u00f3 Run ablakban megtal\u00e1lhat\u00f3 az SHA-1 kulcs.

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sben szint\u00e9n az Assistant-ban az Authenticate using a custom authentication system alatt v\u00e1lasszuk az Add the Firebase Authentication SDK to your app elemet, itt l\u00e1that\u00f3 is, hogy milyen m\u00f3dos\u00edt\u00e1sok t\u00f6rt\u00e9nnek a projekt \u00e9s modul szint\u0171 build.gradle.kts f\u00e1jlokban.

    Sajnos a Firebase plugin nincs rendszeresen friss\u00edtve, \u00e9s \u00edgy el\u0151fordul, hogy a f\u00fcgg\u0151s\u00e9gek r\u00e9gi verzi\u00f3j\u00e1t adja hozz\u00e1 a build.gradle.kts f\u00e1jlokhoz. Ez\u00e9rt most friss\u00edteni fogjuk az im\u00e9nt automatikusan felvett f\u00fcgg\u0151s\u00e9geket, valamint innent\u0151l manu\u00e1lisan fogjuk hozz\u00e1adni az \u00fajabbakat az Assistant haszn\u00e1lata helyett. Fontos, hogy mindenb\u0151l az itt le\u00edrt verzi\u00f3t haszn\u00e1ljuk.

    Ellen\u0151rizz\u00fck a projekt szint\u0171 build.gradle f\u00e1jlban a google-services-t, hogy az al\u00e1bbi verzi\u00f3val rendelkezik:

    classpath 'com.google.gms:google-services:4.4.2'\n

    A Firebase BoM seg\u00edts\u00e9g\u00e9vel egys\u00e9gesen tudjuk kezelni az \u00f6sszes firebase k\u00f6nyvt\u00e1runk verzi\u00f3sz\u00e1m\u00e1t.

    Cser\u00e9lj\u00fck le a modul szint\u0171 build.gradle-ben a firebase-auth verzi\u00f3t a k\u00f6vetkez\u0151re:

    val firebaseBom = platform(\"com.google.firebase:firebase-bom:33.5.1\")\nimplementation(firebaseBom)\nimplementation(\"com.google.firebase:firebase-auth-ktx\")\n

    A gener\u00e1lt projektv\u00e1z t\u00f6bbi \u00e1ltal\u00e1nos f\u00fcgg\u0151s\u00e9ge (pl. appcompat \u00e9s ktx-core k\u00f6nyvt\u00e1rak) is elavult lehet, ezt az Android Studio jelzi is s\u00f6t\u00e9ts\u00e1rga h\u00e1tt\u00e9rrel. Ezekre r\u00e1\u00e1llva a kurzorral az Alt-Enter gyorsbillenyt\u0171vel kiv\u00e1laszthatjuk ezeknek a friss\u00edt\u00e9s\u00e9t.

    Ahhoz, hogy az e-mail alap\u00fa regisztr\u00e1ci\u00f3 \u00e9s authentik\u00e1ci\u00f3 megfelel\u0151en m\u0171k\u00f6dj\u00f6n, a Firebase console-ban az Authentication -> Sign-in method alatt az Email/Password providert enged\u00e9lyezni kell.

    K\u00e9sz\u00edts\u00fck el a megfelel\u0151 Service oszt\u00e1lyt. Hozzunk l\u00e9tre a data/auth package-en bel\u00fcl a FirebaseAuthService oszt\u00e1lyt! Val\u00f3s\u00edtsuk meg az AuthService interf\u00e9sz egyes met\u00f3dusait! Ehhez sz\u00fcks\u00e9g\u00fcnk lesz egy FirebaseAuth objektumra, melyet k\u00fcls\u0151 forr\u00e1sb\u00f3l fogunk megkapni:

    package hu.bme.aut.android.todo.data.auth  import com.google.firebase.auth.FirebaseAuth  import com.google.firebase.auth.UserProfileChangeRequest  import hu.bme.aut.android.todo.domain.model.User  import kotlinx.coroutines.channels.awaitClose  import kotlinx.coroutines.flow.Flow  import kotlinx.coroutines.flow.callbackFlow  import kotlinx.coroutines.tasks.await\n\nclass FirebaseAuthService(private val firebaseAuth: FirebaseAuth) : AuthService {\noverride val currentUserId: String? get() = firebaseAuth.currentUser?.uid\noverride val hasUser: Boolean get() = firebaseAuth.currentUser != null\noverride val currentUser: Flow<User?> get() = callbackFlow {\nthis.trySend(currentUserId?.let { User(it) })\nval listener =\nFirebaseAuth.AuthStateListener { auth ->\nthis.trySend(auth.currentUser?.let { User(it.uid) })\n}\nfirebaseAuth.addAuthStateListener(listener)\nawaitClose { firebaseAuth.removeAuthStateListener(listener) }\n}\n\n\noverride suspend fun signUp(email: String, password: String) {\nfirebaseAuth.createUserWithEmailAndPassword(email,password)\n.addOnSuccessListener {  result ->\nval user = result.user\nval profileChangeRequest = UserProfileChangeRequest.Builder()\n.setDisplayName(user?.email?.substringBefore('@'))\n.build()\nuser?.updateProfile(profileChangeRequest)\n}.await()\n}\n\noverride suspend fun authenticate(email: String, password: String) {\nfirebaseAuth.signInWithEmailAndPassword(email, password).await()\n}\n\noverride suspend fun sendRecoveryEmail(email: String) {\nfirebaseAuth.sendPasswordResetEmail(email).await()\n}\n\noverride suspend fun deleteAccount() {\nfirebaseAuth.currentUser!!.delete().await()\n}\n\noverride suspend fun signOut() {\nfirebaseAuth.signOut()\n}\n}\n
    N\u00e9zz\u00fck \u00e1t, hogyan tudtuk az \u00e1ltalunk defini\u00e1lt AuthService interf\u00e9szhez illeszteni a Firebase \u00e1ltal biztos\u00edtott API-t!

    Sokszor a biztos\u00edtott API k\u00f6zvetlen\u00fcl megfeleltethet\u0151 az \u00e1ltalunk defini\u00e1lt szolg\u00e1ltat\u00e1sokkal, mint p\u00e9ld\u00e1ul a currentUser vagy hasUser mez\u0151kn\u00e9l. Itt egyed\u00fcl arra kell figyeln\u00fcnk, hogy ne maradjon le a get() defin\u00edci\u00f3, akkor ugyanis a Service l\u00e9trej\u00f6ttekor t\u00f6rt\u00e9nne egy \u00e9rt\u00e9kad\u00e1s, nem pedig minden egyes kiolvas\u00e1sn\u00e1l egy f\u00fcggv\u00e9nyh\u00edv\u00e1s.

    Ha szerencs\u00e9nk van, akkor a felhaszn\u00e1lt API be\u00e9p\u00edtetten t\u00e1mogatni fogja a Kotlin k\u00fcl\u00f6nb\u00f6z\u0151 funkcionalit\u00e1sait, mint p\u00e9ld\u00e1ul a Kotlinos t\u00edpusok, contract-ok \u00e9s coroutine-ok. Sokszor viszont Java alap\u00fa k\u00f6nyvt\u00e1rakat kapunk, amiket nek\u00fcnk kell adapt\u00e1lni Kotlin k\u00f6rnyezetre. Erre egy j\u00f3 p\u00e9lda a currentUser mez\u0151. Ez a callbackFlow met\u00f3dust haszn\u00e1lja, melynek seg\u00edts\u00e9g\u00e9vel a callback jelleg\u0171 API-t tudjuk \u00e1talak\u00edtani Flow jelleg\u0171 eredm\u00e9nny\u00e9. A blokkon bel\u00fcl egy listenert regisztr\u00e1lunk be, mellyel meg tudjuk figyelni, ha v\u00e1ltoz\u00e1s t\u00f6rt\u00e9nik az aktu\u00e1lis felhaszn\u00e1l\u00f3k k\u00f6r\u00e9ben. Ekkor a trySend() met\u00f3dussal tudjuk a Flow-ra feliratkoz\u00f3 fel\u00e9 elk\u00fcldeni az \u00faj felhaszn\u00e1l\u00f3 adatait. \u00c9rdemes arra is figyelni, hogy ez a listener csak a feliratkoz\u00e1s ut\u00e1n kezd el \u00e9rt\u00e9keket kik\u00fcldeni. Annak \u00e9rdek\u00e9ben, hogy a UI egyb\u0151l megkapja az aktu\u00e1lis \u00e9rt\u00e9ket, a feliratkoz\u00e1s el\u0151tt kik\u00fcldj\u00fck az aktu\u00e1lis felhaszn\u00e1l\u00f3 adatait is.

    A t\u00f6bbi utas\u00edt\u00e1sn\u00e1l a Firebase egy Task t\u00edpus\u00fa eredm\u00e9ny objektummal t\u00e9r vissza. A Java vil\u00e1g\u00e1ban erre fel tudunk iratkozni, \u00e9s az eredm\u00e9ny\u00e9t egy Callback-ben le tudjuk kezelni. Szerencs\u00e9re azonban a Firebase \u00e1ltal\u00e1nos k\u00f6nytv\u00e1r\u00e1ban megtal\u00e1lhat\u00f3 egy Kotlin kieg\u00e9sz\u00edt\u0151 met\u00f3dus, mellyel a Task bev\u00e1rhat\u00f3 coroutine kontextusban az await() kulcssz\u00f3val. A teljess\u00e9g kedv\u00e9\u00e9rt n\u00e9zz\u00fck meg az al\u00e1bbi p\u00e9ld\u00e1t, hogyan lehet egy Callback jelleg\u0171 API-t \u00e1talak\u00edtani suspend fajt\u00e1j\u00fa f\u00fcggv\u00e9nny\u00e9:

    override suspend fun authenticate(email: String, password: String) = suspendCoroutine { continuation ->\nfirebaseAuth\n.signInWithEmailAndPassword(email, password)\n.addOnSuccessListener { continuation.resume(Unit) }\n.addOnFailureListener { continuation.resumeWithException(it) }\n}\n

    A suspendCoroutine met\u00f3dus le fogja futtatni a benne megadott blokkot, majd addig v\u00e1rakoztatja a coroutine-t, ameddig a blokkban megkapott Continuation objektumon kereszt\u00fcl nem jelezz\u00fck a h\u00edv\u00e1s v\u00e9geredm\u00e9ny\u00e9t. Ezzel a f\u00fcggv\u00e9nnyel k\u00f6nnyed\u00e9n \u00e1t tudjuk alak\u00edtani a Callback jelleg\u0171 m\u0171k\u00f6d\u00e9seket suspend alap\u00fara. Arra azonban figyelj\u00fcnk, hogy minden esetben megh\u00edv\u00f3djon a Continuation valamelyik resume met\u00f3dusa, ellenkez\u0151 esetben ugyanis befagy az adott coroutine, sose fog tudni tov\u00e1bbl\u00e9pni. Hasonl\u00f3an hasznos f\u00fcggv\u00e9ny a suspendCancellableCoroutine, mellyel azokat az eseteket is le tudjuk kezelni, ha a coroutine-t a folyamat k\u00f6zben t\u00f6rlik.

    \u00c1ll\u00edtsuk \u00e1t az alkalmaz\u00e1sunkat, hogy ezt az \u00faj FirebaseAuthService-t haszn\u00e1lja! Ehhez m\u00f3dos\u00edtsuk a TodoApplication oszt\u00e1lyunkat:

    package hu.bme.aut.android.todo  import android.app.Application  import com.google.firebase.auth.FirebaseAuth  import hu.bme.aut.android.todo.data.auth.AuthService  import hu.bme.aut.android.todo.data.auth.FirebaseAuthService  import hu.bme.aut.android.todo.data.todos.MemoryTodoService  import hu.bme.aut.android.todo.data.todos.TodoService\n\nclass TodoApplication : Application(){\noverride fun onCreate() {\nsuper.onCreate()\nauthService = FirebaseAuthService(FirebaseAuth.getInstance())\ntodoService = MemoryTodoService()\n}\n\ncompanion object{\nlateinit var authService: AuthService\nlateinit var todoService: TodoService\n}\n}\n

    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Hozzunk l\u00e9tre egy \u00faj felhaszn\u00e1l\u00f3t!

    jelsz\u00f3

    Ugyan nem kapunk semmi visszajelz\u00e9st, de a Firebase nem fogad el 6 karaktern\u00e9l r\u00f6videbb jelsz\u00f3t. \u00cdgy amennyiben r\u00f6vid a jelszavunk, \u00fagy t\u0171nhet, hogy a gombnyom\u00e1s hat\u00e1s\u00e1ra nem t\u00f6rt\u00e9nik semmi, nem m\u0171k\u00f6dik a regisztr\u00e1ci\u00f3. Ilyenkor ellen\u0151rizz\u00fck, hogy mindenk\u00e9ppen legal\u00e1bb 6 hossz\u00fa jelsz\u00f3t adtunk-e meg.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik Firebase Authentication oldal\u00e1n a beregisztr\u00e1lt felhaszn\u00e1l\u00f3, illetve a FirebaseAuthService forr\u00e1sk\u00f3dja, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/firebase/#feladatok-listazasa-keszitese","title":"Feladatok list\u00e1z\u00e1sa, k\u00e9sz\u00edt\u00e9se","text":"

    K\u00f6vetkez\u0151 l\u00e9p\u00e9sben a feladatok list\u00e1z\u00e1s\u00e1t fogjuk implement\u00e1lni a projekten bel\u00fcl.

    Adjuk hozz\u00e1 a projekthez a Cloud Firestore t\u00e1mogat\u00e1st:

    implementation(\"com.google.firebase:firebase-firestore-ktx\")\n

    Kapcsoljuk be a Cloud Firestore-t a Firebase console-on is . Az adatb\u00e1zist test mode-ban fogjuk haszn\u00e1lni, \u00edgy egyel\u0151re publikusan \u00edrhat\u00f3/olvashat\u00f3 lesz, de cser\u00e9be nem kell konfigur\u00e1lnunk a hozz\u00e1f\u00e9r\u00e9s-szab\u00e1lyoz\u00e1st. Ezt term\u00e9szetesen k\u00e9s\u0151bb mindenk\u00e9pp meg kellene tenni egy \u00e9les projektben.

    Locationnek v\u00e1lasszunk egy hozz\u00e1nk k\u00f6zel es\u0151 opci\u00f3t.

    Hozzuk l\u00e9tre a todos package-en bel\u00fcl a firebase package-et. Ebben k\u00e9t oszt\u00e1lyt fogunk defini\u00e1lni: a Firestore-ban t\u00e1rolt adatobjektum oszt\u00e1ly modellj\u00e9t, illetve a kommunik\u00e1ci\u00f3t megval\u00f3s\u00edt\u00f3 service k\u00f3dj\u00e1t.

    Hozzuk el\u0151sz\u00f6r l\u00e9tre az adatot reprezent\u00e1l\u00f3 oszt\u00e1lyt FirebaseTodo n\u00e9ven:

    package hu.bme.aut.android.todo.data.todos.firebase\n\nimport com.google.firebase.Timestamp\nimport com.google.firebase.firestore.DocumentId\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.datetime.*\nimport java.time.Instant\nimport java.time.LocalDateTime\nimport java.time.ZoneId\nimport java.util.Date\n\ndata class FirebaseTodo(\n@DocumentId val id: String = \"\",\nval title: String = \"\",\nval priority: Priority = Priority.NONE,\nval dueDate: Timestamp = Timestamp.now(),\nval description: String = \"\"\n)\n\nfun FirebaseTodo.asTodo() = Todo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = LocalDateTime\n.ofInstant(Instant.ofEpochSecond(dueDate.seconds), ZoneId.systemDefault())\n.toKotlinLocalDateTime()\n.date,\ndescription = description,\n)\n\nfun Todo.asFirebaseTodo() = FirebaseTodo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = Timestamp(Date.from(dueDate.atStartOfDayIn(TimeZone.currentSystemDefault()).toJavaInstant())),\ndescription = description,\n)\n
    Ebben a f\u00e1jlban defini\u00e1ltuk a k\u00e9t \u00e1talak\u00edt\u00f3 f\u00fcggv\u00e9nyt is, mellyel a Firebase \u00e9s az alkalmaz\u00e1s t\u00f6bbi r\u00e9sz\u00e9ben haszn\u00e1lt Todo oszt\u00e1ly k\u00f6z\u00f6tt tudunk \u00e1talak\u00edtani. Az egyed\u00fcli bonyolult r\u00e9sz a Firebase \u00e1ltal haszn\u00e1lt Timestamp oszt\u00e1ly haszn\u00e1lata az id\u0151pont elt\u00e1rol\u00e1s\u00e1ra, erre most r\u00e9szletesen nem t\u00e9r\u00fcnk ki.

    Hozzuk l\u00e9tre a feladatok t\u00e1rol\u00e1s\u00e1t v\u00e9gz\u0151 FirebaseTodoService oszt\u00e1lyt is ebben a package-ben:

    package hu.bme.aut.android.todo.data.todos.firebase\n\nimport com.google.firebase.firestore.FirebaseFirestore\nimport com.google.firebase.firestore.ktx.snapshots\nimport com.google.firebase.firestore.ktx.toObjects\nimport com.google.firebase.firestore.ktx.toObject\nimport hu.bme.aut.android.todo.data.auth.AuthService\nimport hu.bme.aut.android.todo.data.todos.TodoService\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.tasks.await\n\nclass FirebaseTodoService(\nprivate val firestore: FirebaseFirestore,\nprivate val authService: AuthService\n) : TodoService {\n\noverride val todos: Flow<List<Todo>> = authService.currentUser.flatMapLatest { user ->\nif (user == null) flow { emit(emptyList()) }\nelse currentCollection(user.id)\n.snapshots()\n.map { snapshot ->\nsnapshot\n.toObjects<FirebaseTodo>()\n.map {\nit.asTodo()\n}\n}\n}\n\noverride suspend fun getTodo(id: String): Todo? =\nauthService.currentUserId?.let {\ncurrentCollection(it).document(id).get().await().toObject<FirebaseTodo>()?.asTodo()\n}\n\noverride suspend fun saveTodo(todo: Todo) {\nauthService.currentUserId?.let {\ncurrentCollection(it).add(todo.asFirebaseTodo()).await()\n}\n}\n\noverride suspend fun updateTodo(todo: Todo) {\nauthService.currentUserId?.let {\ncurrentCollection(it).document(todo.id).set(todo.asFirebaseTodo()).await()\n}\n}\n\noverride suspend fun deleteTodo(id: String) {\nauthService.currentUserId?.let {\ncurrentCollection(it).document(id).delete().await()\n}\n}\n\nprivate fun currentCollection(userId: String) =\nfirestore.collection(USER_COLLECTION).document(userId).collection(TODO_COLLECTION)\n\ncompanion object {\nprivate const val USER_COLLECTION = \"users\"\nprivate const val TODO_COLLECTION = \"todos\"\n}\n}\n
    V\u00e9g\u00fcl ne felejts\u00fck el befriss\u00edteni a TodoApplication oszt\u00e1lyunkat, hogy a Firestoreban t\u00e1rolt feladatokat haszn\u00e1lja az alkalmaz\u00e1s:

    package hu.bme.aut.android.todo\n\nimport android.app.Application\nimport com.google.firebase.auth.FirebaseAuth\nimport com.google.firebase.firestore.FirebaseFirestore\nimport hu.bme.aut.android.todo.data.auth.AuthService\nimport hu.bme.aut.android.todo.data.auth.FirebaseAuthService\nimport hu.bme.aut.android.todo.data.todos.TodoService\nimport hu.bme.aut.android.todo.data.todos.firebase.FirebaseTodoService\n\nclass TodoApplication : Application(){\noverride fun onCreate() {\nsuper.onCreate()\nauthService = FirebaseAuthService(FirebaseAuth.getInstance())\ntodoService = FirebaseTodoService(FirebaseFirestore.getInstance(), authService)\n}\n\ncompanion object{\nlateinit var authService: AuthService\nlateinit var todoService: TodoService\n}\n}\n

    Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1sunkat! Ellen\u0151rizz\u00fck, hogy t\u00e9nyleg l\u00e9trej\u00f6nnek az adatb\u00e1zisban is a feladatok.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik Firebase Firestore oldal\u00e1n a l\u00e9trehozott feladat, illetve a fut\u00f3 alkalmaz\u00e1s, melyben az egyik l\u00e9trehozott feladat tartalmazza a Neptun-k\u00f3dot. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    Messaging, Crashlytics, Analytics

    A k\u00f6vezkez\u0151 technol\u00f3gi\u00e1k \u00e1tfut\u00e1si ideje sajnos hosszabb, \u00edgy az eredm\u00e9nyre nem ritk\u00e1n \u00f3r\u00e1kat is v\u00e1rni kell. (A notificationnek n\u00e9h\u00e1ny perc alatt meg kell j\u00f6nnie.)

    "},{"location":"laborok/firebase/#push-ertesitesek","title":"Push \u00e9rtes\u00edt\u00e9sek","text":"

    Adjuk hozz\u00e1 a projekt\u00fcnkh\u00f6z a Firebase Messaging f\u00fcgg\u0151s\u00e9get:

    implementation(\"com.google.firebase:firebase-messaging-ktx\")\n

    Csup\u00e1n ennyi elegend\u0151 a push alapvet\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez, ha \u00edgy \u00fajraford\u00edtjuk az alkalmaz\u00e1st, a Firebase fel\u00fclet\u00e9r\u0151l vagy API-j\u00e1val k\u00fcld\u00f6tt push \u00fczeneteket automatikusan megkapj\u00e1k a mobil kliensek \u00e9s egy Notification-ben megjelen\u00edtik.

    Pr\u00f3b\u00e1ljuk ki a push k\u00fcld\u00e9st a Firebase console-r\u00f3l (Cloud messaging men\u00fcpont alatt Send your first message), \u00e9s vizsg\u00e1ljuk meg, hogyan \u00e9rkezik meg telefonra, ha nem fut az alkalmaz\u00e1s. (Amikor fut az alkalmaz\u00e1s, akkor t\u0151l\u00fcnk v\u00e1rja az \u00fczenet lekezel\u00e9s\u00e9t az API.) A Notification szekci\u00f3 alatt \u00edrjuk be az \u00fczenet c\u00edm\u00e9t \u00e9s sz\u00f6veg\u00e9t, a Target r\u00e9szn\u00e9l pedig v\u00e1lasszuk ki az alkalmaz\u00e1st, hogy minden fut\u00f3 p\u00e9ld\u00e1ny megkapja az \u00fczenetet.

    Term\u00e9szetesen lehet\u0151s\u00e9g van saj\u00e1t push \u00fczenet feldolgoz\u00f3 szolg\u00e1ltat\u00e1s k\u00e9sz\u00edt\u00e9s\u00e9re is egy FirebaseMessagingService l\u00e9trehoz\u00e1s\u00e1val, melyr\u0151l tov\u00e1bbi r\u00e9szletek itt olvashat\u00f3k.

    "},{"location":"laborok/firebase/#crashlytics","title":"Crashlytics","text":"

    A Firebase Console-on el\u0151sz\u00f6r navig\u00e1ljunk a Crashlytics men\u00fcpontra, \u00e9s kapcsoljuk be a funkci\u00f3t. V\u00e1lasszuk az \u00faj Firebase alkalmaz\u00e1s integr\u00e1ci\u00f3j\u00e1t.

    Adjuk hozz\u00e1 a projekthez a f\u00fcgg\u0151s\u00e9geket a projekt szint\u0171 build.gradle.kts f\u00e1jlba:

    id(\"com.google.firebase.crashlytics\") version \"3.0.2\" apply false\n

    Ezekkel a m\u00f3dos\u00edt\u00e1sokkal egy Gradle plugint adtunk hozz\u00e1 a projekt\u00fcnkh\u00f6z, amit a modul szint\u0171 build.gradle f\u00e1jl elej\u00e9n be kell kapcsolnunk a m\u00e1r megl\u00e9v\u0151k ut\u00e1n:

    id(\"com.google.firebase.crashlytics\")\n

    V\u00e9g\u00fcl pedig sz\u00fcks\u00e9g\u00fcnk van k\u00e9t egyszer\u0171 Gradle f\u00fcgg\u0151s\u00e9gre is, amit a megl\u00e9v\u0151 Firebase f\u00fcgg\u0151s\u00e9gek mell\u00e9 helyezhet\u00fcnk, a modul szint\u0171 build.gradle f\u00e1jlban:

    implementation(\"com.google.firebase:firebase-crashlytics-ktx\")\nimplementation(\"com.google.firebase:firebase-analytics-ktx\")\n

    Vegy\u00fcnk fel egy \u00faj akci\u00f3t TodosScreen TodoAppBar r\u00e9sz\u00e9be, amivel az alkalmaz\u00e1st hib\u00e1val be tudjuk z\u00e1rni:

    TodoAppBar(\ntitle = stringResource(id = StringResources.app_bar_title_todos),\nactions = {\nIconButton(onClick = {\nviewModel.signOut()\nonSignOut()\n}) {\nIcon(imageVector = Icons.Default.Logout, contentDescription = null)\n}\nIconButton(onClick = {\nthrow RuntimeException(\"Test crash!\")\n}) {\nIcon(imageVector = Icons.Default.Close, contentDescription = null)\n}\n}\n)\n

    V\u00e9g\u00fcl a Firebase console-ban is enged\u00e9lyezz\u00fck a funkci\u00f3t a Crashlytics men\u00fcpont alatt.

    Pr\u00f3b\u00e1ljuk ki saj\u00e1t hibajelz\u00e9sek k\u00e9sz\u00edt\u00e9s\u00e9t a men\u00fc esem\u00e9nykezel\u0151j\u00e9ben. Vizsg\u00e1ljuk meg, meg\u00e9rkezik-e a Firebase Console-ba a hiba\u00fczenet!

    "},{"location":"laborok/firebase/#analitika","title":"Analitika","text":"

    Most enged\u00e9lyezz\u00fck az analitik\u00e1t a Firebase console Analytics men\u00fcpontja alatt!

    Az SDK-t m\u00e1r be\u00e1ll\u00edtottuk, ez\u00e9rt a megfelel\u0151 Account kiv\u00e1laszt\u00e1sa ut\u00e1n a Nextre, majd a Finishre kattinhatunk.

    Ezut\u00e1n az alkalmaz\u00e1s m\u00e1r napl\u00f3z alapvet\u0151 analitik\u00e1kat, haszn\u00e1lati statisztik\u00e1kat, melyek ugyanezen men\u00fcpont alatt lesznek el\u00e9rhet\u0151k.

    Emellett term\u00e9szetesen lehet\u0151s\u00e9g van az analitika kib\u0151v\u00edt\u00e9s\u00e9re \u00e9s testreszab\u00e1s\u00e1ra is. Az ehhez sz\u00fcks\u00e9ges Firebase f\u00fcgg\u0151s\u00e9get a Crashlyticsn\u00e9l m\u00e1r felvett\u00fck.

    K\u00e9sz\u00edts\u00fcnk saj\u00e1t analitika \u00fczeneteket egy \u00fajabb akci\u00f3b\u00f3l k\u00fcldve, ami szint\u00e9n a TopAppBar-ba ker\u00fcl:

    IconButton(onClick = {\n    val bundle = Bundle()\n    bundle.putString(\"demo_key\", \"idabc\")\n    bundle.putString(\"data_key\", \"mydata\")\n\n    FirebaseAnalytics.getInstance(context)\n        .logEvent(FirebaseAnalytics.Event.LOGIN, bundle)\n}) {\n    Icon(imageVector = Icons.Default.Message, contentDescription = null)\n}\n

    Fontos kiemelni, hogy nem garant\u00e1lt, hogy az analitika val\u00f3s id\u0151ben l\u00e1tszik a Firebase console-on. 30 percig vagy ak\u00e1r tov\u00e1bb is tarthat, mire egy-egy esem\u00e9ny itt megjelenik.

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, illetve a k\u00e9t \u00faj akci\u00f3gomb forr\u00e1sk\u00f3dja, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/firebase/#onallo-feladatok","title":"\u00d6n\u00e1ll\u00f3 feladatok","text":""},{"location":"laborok/firebase/#automatikus-bejelentkezes","title":"Automatikus bejelentkez\u00e9s","text":"

    Val\u00f3s\u00edtsuk meg, hogy a bejelentkez\u0151 k\u00e9perny\u0151 helyett egyb\u0151l a lista oldalra ugorjunk, ha a felhaszn\u00e1l\u00f3 kijelentkez\u00e9s helyett csak bez\u00e1rta az alkalmaz\u00e1st!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, az automatikus bejelentkez\u00e9st megval\u00f3s\u00edt\u00f3 forr\u00e1sk\u00f3d, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/firebase/#navigacio-esemeny-jelzese","title":"Navig\u00e1ci\u00f3 esem\u00e9ny jelz\u00e9se","text":"

    K\u00fcldj\u00fcnk egy analitikai esem\u00e9nyt abban az esetben, ha a felhaszn\u00e1l\u00f3 megnyitja valamelyik feladat\u00e1t! Az esem\u00e9ny tartalmazza a feladat azonos\u00edt\u00f3j\u00e1t is. Figyelj\u00fcnk arra, hogy megfelel\u0151 n\u00e9vvel k\u00fcldj\u00fck el az esem\u00e9nyt!

    BEADAND\u00d3 (1 pont)

    K\u00e9sz\u00edts egy k\u00e9perny\u0151 k\u00e9pet, amin l\u00e1tsz\u00f3dik a fut\u00f3 alkalmaz\u00e1s a lista oldalon, a navig\u00e1ci\u00f3 sor\u00e1n az esem\u00e9nyt kik\u00fcld\u0151 forr\u00e1sk\u00f3d, melyben a Neptun-k\u00f3d komment form\u00e1j\u00e1ban l\u00e1that\u00f3. A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

    "},{"location":"laborok/network/","title":"Labor09 - Network \u00e9s Paging (Unsplash)","text":""},{"location":"laborok/network/#bevezetes","title":"Bevezet\u00e9s","text":"

    Ezen a laboron megismerked\u00fcnk Android a h\u00e1l\u00f3zati h\u00edv\u00e1sok elv\u00e9gz\u00e9s\u00e9nek egyik legelterjedtebb megval\u00f3s\u00edt\u00e1s\u00e1val (Retrofit), a Paging Library-vel, illetve azzal, hogy a Compose keretrendszerben hogyan tudunk k\u00fcl\u00f6nb\u00f6z\u0151 k\u00e9perny\u0151m\u00e9reteket t\u00e1mogatni.

    "},{"location":"laborok/network/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

    A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

    "},{"location":"laborok/network/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
    1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

    2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

      Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

    3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

    4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

    Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a repository-ban l\u00e9v\u0151 projektet.

    "},{"location":"laborok/network/#meglevo-projekt-attekintese","title":"Megl\u00e9v\u0151 projekt \u00e1ttekint\u00e9se","text":"

    A megl\u00e9v\u0151 projektben megtal\u00e1lhat\u00f3 az elk\u00e9sz\u00edtett fel\u00fclet, illetve a hozz\u00e1 tartoz\u00f3 Viewmodellek. Tekints\u00fck \u00e1t a laborvezet\u0151vel a megl\u00e9v\u0151 k\u00f3dot!

    "},{"location":"laborok/network/#adat-es-halozati-reteg","title":"Adat- \u00e9s h\u00e1l\u00f3zati r\u00e9teg","text":""},{"location":"laborok/network/#konyvtarak","title":"K\u00f6nyvt\u00e1rak","text":"

    Retrofit

    A Retrofit egy \u00e1ltal\u00e1nos c\u00e9l\u00fa HTTP k\u00f6nyvt\u00e1r Java \u00e9s K\u00f6tlin k\u00f6rnyezetben. Sz\u00e9les k\u00f6rben haszn\u00e1lj\u00e1k, sz\u00e1mos projektben bizony\u00edtott m\u00e1r (kv\u00e1zi ipari standard). Az\u00e9rt haszn\u00e1ljuk, hogy ne kelljen alacsony sz\u00ednt\u0171 h\u00e1l\u00f3zati h\u00edv\u00e1sokat implement\u00e1lni.

    Seg\u00edts\u00e9g\u00e9vel el\u00e9g egy interface-ben annot\u00e1ci\u00f3k seg\u00edts\u00e9g\u00e9vel le\u00edrni az API-t (ez pl. a Swagger eszk\u00f6zzel gener\u00e1lhat\u00f3 is), majd e m\u00f6g\u00e9 k\u00e9sz\u00edt a Retrofit egy olyan oszt\u00e1lyt, mely a sz\u00fcks\u00e9ges h\u00e1l\u00f3zati h\u00edv\u00e1sokat elv\u00e9gzi. A Retrofit a h\u00e1tt\u00e9rben az OkHttp3-at haszn\u00e1lja, valamint az objektumok JSON form\u00e1tumba t\u00f6rt\u00e9n\u0151 soros\u00edt\u00e1s\u00e1t a Moshi libraryvel v\u00e9gzi. Ez\u00e9rt ezeket is be kell hivatkozni.

    Paging 3.0

    A Paging Library az Android Jetpack r\u00e9sze. Seg\u00edt az adatok oldalank\u00e9nti bet\u00f6lt\u00e9s\u00e9ben \u00e9s megjelen\u00edt\u00e9s\u00e9ben nagyobb adatk\u00e9szletb\u0151l, helyi t\u00e1rol\u00f3b\u00f3l vagy h\u00e1l\u00f3zatr\u00f3l. Ez a megk\u00f6zel\u00edt\u00e9s lehet\u0151v\u00e9 teszi az alkalmaz\u00e1sunk sz\u00e1m\u00e1ra, hogy mind a h\u00e1l\u00f3zati s\u00e1vsz\u00e9less\u00e9get, mind pedig a rendszer er\u0151forr\u00e1sait hat\u00e9konyabban haszn\u00e1lja.

    A Paging Library haszn\u00e1lat\u00e1nak el\u0151nyei:

    • Az oldalank\u00e9nti adatok mem\u00f3ri\u00e1ban t\u00f6rt\u00e9n\u0151 gyors\u00edt\u00f3t\u00e1raz\u00e1sa. Ez biztos\u00edtja, hogy az alkalmaz\u00e1s hat\u00e9konyan haszn\u00e1lja a rendszer er\u0151forr\u00e1sait oldalank\u00e9nti adatokkal val\u00f3 munka sor\u00e1n.
    • Be\u00e9p\u00edtett k\u00e9r\u00e9sek duplik\u00e1l\u00f3d\u00e1s\u00e1nak megakad\u00e1lyoz\u00e1sa, hogy az alkalmaz\u00e1s hat\u00e9konyan haszn\u00e1lja a h\u00e1l\u00f3zati s\u00e1vsz\u00e9less\u00e9get \u00e9s a rendszer er\u0151forr\u00e1sait.
    • Konfigur\u00e1lhat\u00f3 RecyclerView adapterek, amelyek automatikusan lek\u00e9rik az adatokat, amikor a felhaszn\u00e1l\u00f3 g\u00f6rget a bet\u00f6lt\u00f6tt adatok v\u00e9g\u00e9re.
    • Els\u0151oszt\u00e1ly\u00fa t\u00e1mogat\u00e1s Kotlin coroutines \u00e9s Flow, valamint LiveData \u00e9s RxJava sz\u00e1m\u00e1ra.
    • Be\u00e9p\u00edtett hibakezel\u00e9s-t\u00e1mogat\u00e1s, bele\u00e9rtve a friss\u00edt\u00e9si \u00e9s \u00fajrapr\u00f3b\u00e1l\u00e1si k\u00e9pess\u00e9geket.

    A Paging Library f\u0151bb elemei:

    • PagingData - egy t\u00e1rol\u00f3 'oldalazott' adatok sz\u00e1m\u00e1ra. Az adatok friss\u00edt\u00e9sekor k\u00fcl\u00f6n PagingData tart\u00e1lyt haszn\u00e1lunk.
    • PagingSource - a PagingSource az alap oszt\u00e1ly az adatok r\u00e9szletekben val\u00f3 bet\u00f6lt\u00e9s\u00e9hez PagingData streambe.
    • Pager.flow - egy Flow-t hoz l\u00e9tre, amely a PagingConfig \u00e9s egy f\u00fcggv\u00e9ny alapj\u00e1n konstru\u00e1lja a megval\u00f3s\u00edtott PagingSource-t.
    • RemoteMediator - seg\u00edt az oldalaz\u00e1s megval\u00f3s\u00edt\u00e1s\u00e1ban h\u00e1l\u00f3zatr\u00f3l \u00e9s adatb\u00e1zisb\u00f3l.
    • Coil

      A Coil (Coroutine Image Loader) egy k\u00e9p bet\u00f6lt\u0151 k\u00f6nyvt\u00e1r Androidra, amelyet a Kotlin koroutinokra \u00e9p\u00fcl. A Coil haszn\u00e1lat\u00e1nak el\u0151nyei:

      • Gyors: A Coil sz\u00e1mos optimaliz\u00e1l\u00e1st v\u00e9gez, bele\u00e9rtve a mem\u00f3ria- \u00e9s lemezt\u00e1rol\u00f3 gyors\u00edt\u00f3t\u00e1raz\u00e1st, az \u00e1tm\u00e9retez\u00e9st a mem\u00f3ri\u00e1ban, az automatikus k\u00e9r\u00e9sek sz\u00fcneteltet\u00e9s\u00e9t/le\u00e1ll\u00edt\u00e1s\u00e1t \u00e9s m\u00e9g sok m\u00e1st.
      • K\u00f6nny\u0171: A Coil kb. 2000 met\u00f3dust ad az APK-hoz (azoknak az alkalmaz\u00e1soknak, amelyek m\u00e1r haszn\u00e1lj\u00e1k az OkHttp \u00e9s a Coroutines k\u00f6nyvt\u00e1rakat), ami hasonl\u00f3 a Picasso-hoz \u00e9s jelent\u0151sen kevesebb, mint a Glide \u00e9s a Fresco k\u00f6nyvt\u00e1rak.
      • K\u00f6nnyen haszn\u00e1lhat\u00f3: A Coil API-ja a Kotlin nyelv funkci\u00f3it haszn\u00e1lja a k\u00f6nny\u0171 haszn\u00e1lat \u00e9s a minim\u00e1lis boilerplate k\u00f3d \u00e9rdek\u00e9ben.
      • Modern: A Coil a Kotlin nyelv\u0171s\u00e9get helyezi el\u0151t\u00e9rbe \u00e9s a modern k\u00f6nyvt\u00e1rakat haszn\u00e1lja, bele\u00e9rtve a Coroutines-t, az OkHttp-t, az Okio-t \u00e9s az AndroidX Lifecycles-t.
      "},{"location":"laborok/network/#fuggosegek","title":"F\u00fcgg\u0151s\u00e9gek","text":"

      A Retrofit, Paging \u00e9s Coil k\u00f6nyvt\u00e1rak haszn\u00e1lat\u00e1hoz a k\u00f6vetkez\u0151 f\u00fcgg\u0151s\u00e9gek sz\u00fcks\u00e9gesek (ezek m\u00e1r szerepelnek a projektben, ne vegy\u00fck fel \u0151ket \u00fajra):

      [versions]\nretrofit = \"2.11.0\"\npaging= \"3.3.2\"\ncoil = \"2.5.0\"\n\n[libraries]\nretrofit = { group = \"com.squareup.retrofit2\", name = \"retrofit\", version.ref = \"retrofit\" }\nretrofit-moshi = { group = \"com.squareup.retrofit2\", name = \"converter-moshi\", version.ref = \"retrofit\" }\npaging = { group = \"androidx.paging\", name = \"paging-compose\", version.ref = \"paging\" }\ncoil = { group = \"io.coil-kt\", name = \"coil-compose\", version.ref = \"coil\" }\n\ndependencies {\nimplementation(libs.retrofit)\nimplementation(libs.retrofit.moshi)\nimplementation(libs.paging)\nimplementation(libs.coil)\n}\n

      A data.model package-be hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt, melyek az API haszn\u00e1lat\u00e1hoz sz\u00fcks\u00e9gesek:

      UnsplashPhoto.kt:

      @Entity(tableName = \"photos\")\ndata class UnsplashPhoto(\n@PrimaryKey(autoGenerate = false)\nval id: String,\nval likes: Int,\n@Embedded\nval user: UserData,\n@Embedded\nval urls: Urls\n)\n\ndata class UserData(\nval username: String,\nval name: String,\n@Json(name = \"total_likes\")\nval totalLikes: Int,\n@Json(name = \"total_photos\")\nval totalPhotos: Int,\n@Json(name = \"profile_image\") @Embedded\nval profileImage: UserProfileImage,\n)\n\ndata class UserProfileImage(\nval small: String,\nval medium: String,\nval large: String\n)\n\ndata class Urls(\nval regular: String,\nval full: String\n)\n

      Az @Embedded annot\u00e1ci\u00f3val megjelel\u0151t mez\u0151k oszt\u00e1lyainak mez\u0151i k\u00f6zvetlen\u00fcl hivatkozhat\u00f3k az SQL querykben.

      A Moshi automatikusan megoldja majd az egyes tagv\u00e1ltoz\u00f3k szerializ\u00e1l\u00e1s\u00e1t, kiv\u00e9ve ott, ahol elt\u00e9r a n\u00e9v. Ezt a Json annot\u00e1ci\u00f3val jelezhetj\u00fck.

      SearchResult.kt:

      data class SearchResult(\n@Json(name = \"results\") val photos: List<UnsplashPhoto>\n)\n

      Hozzuk l\u00e9tre az els\u0151 DAO-t a data.local.dao package-ben:

      UnsplashPhotoDao.kt:

      @Dao\ninterface UnsplashPhotoDao {\n@Insert\nsuspend fun insertPhotos(photos: List<UnsplashPhoto>)\n\n@Query(\"SELECT EXISTS (SELECT * FROM photos WHERE id = :id)\")\nfun exists(id: String): Flow<Boolean>\n\n@Query(\"DELETE FROM photos\")\nsuspend fun deleteAllPhotos()\n\n@Query(\"SELECT * FROM photos\")\nfun getAllPhotos(): PagingSource<Int, UnsplashPhoto>\n\n@Query(\"SELECT * FROM photos WHERE id = :id\")\nfun getPhotoById(id: String): Flow<UnsplashPhoto>\n}\n

      Majd a data.local.database package-ben hozzuk l\u00e9tre az adatb\u00e1zist (az UnsplashPhotoRemoteKeysDao oszt\u00e1lyt k\u00e9s\u0151bb hozzuk l\u00e9tre):

      UnsplashDatabase.kt:

      @Database(entities = [UnsplashPhoto::class, UnsplashPhotoRemoteKeys::class], version = 1)\nabstract class UnsplashDatabase : RoomDatabase() {\nabstract val photosDao: UnsplashPhotoDao\nabstract val remoteKeysDao: UnsplashPhotoRemoteKeysDao\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, az adat\u00e1bzishoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/network/#a-retrofit-interfesz","title":"A Retrofit interf\u00e9sz","text":"

      Az Unsplash API h\u00e1rom v\u00e9gpontj\u00e1t fogjuk haszn\u00e1lni a k\u00e9pek, egy adott k\u00e9p lek\u00e9r\u00e9s\u00e9re, valamint a keres\u00e9s v\u00e9grehajt\u00e1s\u00e1ra. Hozzuk l\u00e9tre a lenti interface-t a data.remote.api package-ben.

      UnsplashApi.kt:

      interface UnsplashApi {\n\n@GET(\"/photos\")\nsuspend fun getPhotosFromEditorialFeed(\n@Query(\"page\") page: Int = 1,\n@Query(\"per_page\") perPage: Int = 10,\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n): Response<List<UnsplashPhoto>>\n\n@GET(\"/photos/{id}\")\nsuspend fun getPhotoById(\n@Path(\"id\") id: String,\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n): Response<UnsplashPhoto>\n\n@GET(\"/search/photos\")\nsuspend fun getSearchResults(\n@Query(\"client_id\") clientId: String = \"ACCESS_KEY\",\n@Query(\"page\") page: Int = 1,\n@Query(\"per_page\") perPage: Int = 10,\n@Query(\"query\") searchTerms: String,\n): Response<SearchResult>\n\n}\n

      Ha a Reponse-t nem tudn\u00e1 beimport\u00e1lni az Android Studio, vegy\u00fck fel k\u00e9zzel az import retrofit2.Response sort.

      A Retrofit annot\u00e1ci\u00f3i seg\u00edts\u00e9g\u00e9vel egyszer\u0171en tudjuk defini\u00e1lni a k\u00e9r\u00e9seinket. Cser\u00e9lj\u00fck le az itt szerepl\u0151 ACCESS_KEY stringet az Unsplashen regisztr\u00e1ci\u00f3 ut\u00e1n el\u00e9rhet\u0151 saj\u00e1t kulcsunkra. Az https://unsplash.com/oauth/applications oldalon regisztr\u00e1ci\u00f3 ut\u00e1n egy \u00faj applik\u00e1ci\u00f3t kell l\u00e9trehozni, \u00e9s azt megnyitva tal\u00e1lhatjuk meg a kulcsot.

      "},{"location":"laborok/network/#a-pagingsource-osztaly","title":"A PagingSource oszt\u00e1ly","text":"

      Az els\u0151 l\u00e9p\u00e9s egy PagingSource implement\u00e1ci\u00f3 meghat\u00e1roz\u00e1sa, hogy az adatforr\u00e1s azonos\u00edthat\u00f3 legyen. A PagingSource API oszt\u00e1lya tartalmazza a load() met\u00f3dust, amelyet fel\u00fcl kell \u00edrni, hogy jelentse, hogyan lehet lapozott adatokat visszanyerni a megfelel\u0151 adatforr\u00e1sb\u00f3l.

      Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.paging package-ben:

      SearchPagingSource.kt:

      class SearchPagingSource(\nprivate val api: UnsplashApi,\nprivate val searchTerms: String\n): PagingSource<Int, UnsplashPhoto>() {\n\noverride fun getRefreshKey(state: PagingState<Int, UnsplashPhoto>): Int? = state.anchorPosition\n\noverride suspend fun load(params: LoadParams<Int>): LoadResult<Int, UnsplashPhoto> {\nval currentPage = params.key ?: 1\nreturn try {\nval response = api.getSearchResults(searchTerms = searchTerms, perPage = INITIAL_PAGE_SIZE, page = currentPage)\nif (response.isSuccessful) {\nval photos = response.body()?.photos ?: emptyList()\nval endOfPaginationReached = photos.isEmpty()\nLoadResult.Page(\ndata = photos,\nprevKey = if (currentPage == 1) null else currentPage - 1,\nnextKey = if (endOfPaginationReached) null else currentPage + 1\n)\n} else throw Exception(response.errorBody()?.string() ?: \"Unsuccessful request.\")\n} catch (e: Exception) {\nLog.e(\"error\",e.stackTraceToString())\nLoadResult.Error(e)\n}\n}\n}\n

      A PagingSource<Key, Value> k\u00e9t t\u00edpusparam\u00e9tert tartalmaz: Key \u00e9s Value. A kulcs meghat\u00e1rozza az azonos\u00edt\u00f3t, amelyet az adat bet\u00f6lt\u00e9s\u00e9hez haszn\u00e1lnak, \u00e9s az \u00e9rt\u00e9k maga az adat t\u00edpusa.

      Egy tipikus PagingSource implement\u00e1ci\u00f3 a konstruktor\u00e1ban megadott param\u00e9tereket tov\u00e1bb\u00edtja a load() met\u00f3dusnak, hogy bet\u00f6lthesse a megfelel\u0151 adatokat egy lek\u00e9rdez\u00e9shez.

      A LoadParams objektum inform\u00e1ci\u00f3kat tartalmaz az elv\u00e9gzend\u0151 bet\u00f6lt\u00e9si m\u0171veletr\u0151l. Ide tartozik a bet\u00f6ltend\u0151 kulcs \u00e9s a bet\u00f6ltend\u0151 elemek sz\u00e1ma.

      A LoadResult objektum az adatbet\u00f6lt\u00e9s eredm\u00e9ny\u00e9t tartalmazza. A LoadResult egy z\u00e1rt oszt\u00e1ly, amely k\u00e9t form\u00e1ban jelenhet meg att\u00f3l f\u00fcgg\u0151en, hogy a load() h\u00edv\u00e1s sikeres volt-e vagy sem:

      • Sikeres esetben LoadResult.Page objektum
      • Sikertelen esetben LoadResult.Error objektum.

      A try-catch blokkban l\u00e1that\u00f3, hogy hogyan tudjuk haszn\u00e1lni a Retrofit interf\u00e9sz\u00fcnket h\u00e1l\u00f3zati h\u00edv\u00e1s elv\u00e9gz\u00e9s\u00e9re.

      A PagingSource implement\u00e1ci\u00f3ja emellett tartalmaznia kell egy getRefreshKey() met\u00f3dust, amely egy PagingState objektumot v\u00e1r param\u00e9terk\u00e9nt, \u00e9s visszaadja a kulcsot, amelyet \u00e1t kell adni a load() met\u00f3dusnak, amikor az adat friss\u00edt\u00e9se vagy \u00e9rv\u00e9nytelen\u00edt\u00e9se t\u00f6rt\u00e9nik az els\u0151 bet\u00f6lt\u00e9s ut\u00e1n. A Paging k\u00f6nyvt\u00e1r automatikusan megh\u00edvja ezt a met\u00f3dust az adat k\u00e9s\u0151bbi friss\u00edt\u00e9sekor.

      "},{"location":"laborok/network/#a-remotemediator-osztaly","title":"A RemoteMediator oszt\u00e1ly","text":"

      Jobb felhaszn\u00e1l\u00f3i \u00e9lm\u00e9nyt biztos\u00edthatunk azzal, ha gondoskodunk arr\u00f3l, hogy az alkalmaz\u00e1sunk haszn\u00e1lhat\u00f3 legyen akkor is, ha az internetkapcsolat instabil vagy ha a felhaszn\u00e1l\u00f3 offline. Az egyik m\u00f3dja ennek, hogy egyszerre lapozunk a h\u00e1l\u00f3zatr\u00f3l \u00e9s egy helyi adatb\u00e1zisb\u00f3l is. Ezzel az alkalmaz\u00e1s az UI-t egy helyi adatb\u00e1zisb\u00f3l vez\u00e9rli \u00e9s csak akkor k\u00e9r le adatokat a h\u00e1l\u00f3zatr\u00f3l, ha m\u00e1r nincs t\u00f6bb adat az adatb\u00e1zisban.

      A Paging k\u00f6nyvt\u00e1r a RemoteMediator komponenst biztos\u00edtja ehhez az esethez. A RemoteMediator a Paging k\u00f6nyvt\u00e1r jelz\u00e9sek\u00e9nt m\u0171k\u00f6dik, amikor az alkalmaz\u00e1s kimer\u00fcl az el\u0151t\u00e1rolt adatb\u00f3l. Ezt a jelz\u00e9st lehet haszn\u00e1lni tov\u00e1bbi adatok bet\u00f6lt\u00e9s\u00e9re a h\u00e1l\u00f3zatr\u00f3l, \u00e9s azokat helyi adatb\u00e1zisban t\u00e1rolni, ahol egy PagingSource bet\u00f6ltheti \u00e9s a felhaszn\u00e1l\u00f3i fel\u00fcleten megjelen\u00edtheti.

      Ha tov\u00e1bbi adatokra van sz\u00fcks\u00e9g, a Paging k\u00f6nyvt\u00e1r megh\u00edvja a RemoteMediator implement\u00e1ci\u00f3j\u00e1b\u00f3l a load() met\u00f3dust. Ez egy suspending f\u00fcggv\u00e9ny, \u00edgy hossz\u00fa ideig fut\u00f3 munk\u00e1t v\u00e9gezhet biztons\u00e1gosan. Ez a f\u00fcggv\u00e9ny \u00e1ltal\u00e1ban az \u00faj adatokat egy h\u00e1l\u00f3zati forr\u00e1sb\u00f3l szedi le, majd elmenti helyi t\u00e1rol\u00f3ba.

      Ez a folyamat \u00faj adatokkal m\u0171k\u00f6dik, de id\u0151vel az adatok t\u00e1rol\u00e1sa az adatb\u00e1zisban \u00e9rv\u00e9nytelen\u00edt\u00e9st ig\u00e9nyelhet, p\u00e9ld\u00e1ul amikor a felhaszn\u00e1l\u00f3 k\u00e9zzel ind\u00edtja el a friss\u00edt\u00e9st. Ezt a LoadType tulajdons\u00e1g jelzi, amelyet \u00e1t kell adni a load() met\u00f3dusnak. A LoadType t\u00e1j\u00e9koztatja a RemoteMediator-t arr\u00f3l, hogy a megl\u00e9v\u0151 adatokat friss\u00edteni kell-e, vagy olyan tov\u00e1bbi adatokat kell-e lek\u00e9rni, amelyeket a megl\u00e9v\u0151 list\u00e1hoz kell hozz\u00e1adni.

      \u00cdgy a RemoteMediator biztos\u00edtja, hogy az alkalmaz\u00e1s azokat az adatokat t\u00f6ltse be, amelyeket a felhaszn\u00e1l\u00f3k a megfelel\u0151 sorrendben szeretn\u00e9nek l\u00e1tni.

      Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.paging package-ben:

      UnsplashRemoteMediator.kt:

      @ExperimentalPagingApi\nclass UnsplashRemoteMediator(\nprivate val api: UnsplashApi,\nprivate val db: UnsplashDatabase\n): RemoteMediator<Int, UnsplashPhoto>() {\n\noverride suspend fun load(\nloadType: LoadType,\nstate: PagingState<Int, UnsplashPhoto>\n): MediatorResult {\nreturn try {\nval page = when (loadType) {\nLoadType.REFRESH -> {\nval remoteKeys = getRemoteKeysForClosestToPosition(state)\nremoteKeys?.nextKey?.minus(1) ?: INITIAL_PAGE\n}\nLoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)\nLoadType.APPEND -> {\nval remoteKeys = getRemoteKeysForLastItem(state) ?: throw InvalidObjectException(\"Result is empty\")\nremoteKeys.nextKey ?: return MediatorResult.Success(endOfPaginationReached = true)\n}\n}\n\nval response = api.getPhotosFromEditorialFeed(page = page, perPage = INITIAL_PAGE_SIZE)\nvar endOfPaginationReached = false\nif (response.isSuccessful) {\nval photos = response.body() ?: emptyList()\nendOfPaginationReached = photos.isEmpty()\ndb.withTransaction {\nif (loadType == LoadType.REFRESH) {\ndb.photosDao.deleteAllPhotos()\ndb.remoteKeysDao.deleteAllKeys()\n}\nval prevKey = if (page == INITIAL_PAGE) null else page - 1\nval nextKey = if (photos.isEmpty()) null else page + 1\nval keys = photos.map { photo ->\nUnsplashPhotoRemoteKeys(\nid = photo.id,\nprevKey = prevKey,\nnextKey = nextKey\n)\n}\ndb.remoteKeysDao.insertAllKeys(keys)\ndb.photosDao.insertPhotos(photos)\n}\n}\n\nMediatorResult.Success(endOfPaginationReached = endOfPaginationReached)\n\n} catch (e: IOException) {\nMediatorResult.Error(e)\n} catch (e: HttpException) {\nMediatorResult.Error(e)\n} catch (e: InvalidObjectException) {\nMediatorResult.Error(e)\n}\n}\n\nprivate suspend fun getRemoteKeysForLastItem(\nstate: PagingState<Int, UnsplashPhoto>\n): UnsplashPhotoRemoteKeys? {\nreturn state.lastItemOrNull()?.let { photo ->\ndb.withTransaction {\ndb.remoteKeysDao.getKeysById(photo.id)\n}\n}\n}\n\nprivate suspend fun getRemoteKeysForClosestToPosition(\nstate: PagingState<Int, UnsplashPhoto>\n): UnsplashPhotoRemoteKeys? {\nreturn state.anchorPosition?.let { position ->\nstate.closestItemToPosition(position)?.id?.let { id ->\ndb.withTransaction {\ndb.remoteKeysDao.getKeysById(id)\n}\n}\n}\n}\n}\n

      A load() met\u00f3dus visszat\u00e9r\u00e9si \u00e9rt\u00e9ke egy MediatorResult objektum. A MediatorResult lehet MediatorResult.Error (amely tartalmazza az hiba le\u00edr\u00e1s\u00e1t) vagy MediatorResult.Success (amely tartalmaz egy jelz\u00e9st arr\u00f3l, hogy van-e m\u00e9g t\u00f6bb adat bet\u00f6lt\u00e9sre).

      A load() met\u00f3dusnak a k\u00f6vetkez\u0151 l\u00e9p\u00e9seket kell v\u00e9grehajtania:

      • Meg kell hat\u00e1rozni, hogy melyik oldalt kell a h\u00e1l\u00f3zatr\u00f3l bet\u00f6lteni a bet\u00f6lt\u00e9si t\u00edpus \u00e9s az eddig bet\u00f6lt\u00f6tt adatok alapj\u00e1n.
      • Kiv\u00e1ltani a h\u00e1l\u00f3zati k\u00e9r\u00e9st.
      • V\u00e9grehajtani a bet\u00f6lt\u00e9si m\u0171velet kimenet\u00e9t\u0151l f\u00fcgg\u0151 cselekv\u00e9seket:
      • Ha a bet\u00f6lt\u00e9s sikeres \u00e9s az kapott elemek list\u00e1ja nem \u00fcres, akkor t\u00e1rolja el a lista elemeit az adatb\u00e1zisban, majd t\u00e9rjen vissza a MediatorResult.Success (endOfPaginationReached = false) \u00e9rt\u00e9kkel. Az adatok t\u00e1rol\u00e1sa ut\u00e1n \u00e9rv\u00e9nytelen\u00edtse az adatforr\u00e1st, hogy \u00e9rtes\u00edtse a Paging k\u00f6nyvt\u00e1rat az \u00faj adatokr\u00f3l.
      • Ha a bet\u00f6lt\u00e9s sikeres \u00e9s a kapott elemek list\u00e1ja \u00fcres vagy az utols\u00f3 oldal indexe, akkor t\u00e9rjen vissza a MediatorResult.Success (endOfPaginationReached = true) \u00e9rt\u00e9kkel. Az adatok t\u00e1rol\u00e1sa ut\u00e1n \u00e9rv\u00e9nytelen\u00edtse az adatforr\u00e1st, hogy \u00e9rtes\u00edtse a Paging k\u00f6nyvt\u00e1rat az \u00faj adatokr\u00f3l.
      • Ha a k\u00e9r\u00e9s hib\u00e1t okoz, akkor t\u00e9rjen vissza a MediatorResult.Error \u00e9rt\u00e9kkel.
      "},{"location":"laborok/network/#tavoli-kulcsok","title":"T\u00e1voli kulcsok","text":"

      Kezeln\u00fcnk kell azt a helyzetet, amikor a helyi gyors\u00edt\u00f3t\u00e1rban t\u00e1rolt adatok elavultak a t\u00e1voli adatforr\u00e1shoz k\u00e9pest.

      A t\u00e1voli kulcsok lehet\u0151v\u00e9 teszik, hogy inform\u00e1ci\u00f3t menthess\u00fcnk el a legut\u00f3bbi oldalr\u00f3l, amelyet a szerverr\u0151l k\u00e9rtek le. Az alkalmaz\u00e1s felhaszn\u00e1lhatja ezt az inform\u00e1ci\u00f3t a k\u00f6vetkez\u0151 bet\u00f6ltend\u0151 adatoldal azonos\u00edt\u00e1s\u00e1hoz \u00e9s k\u00e9r\u00e9s\u00e9hez.

      A t\u00e1voli kulcsok olyan kulcsok, amelyeket a RemoteMediator implement\u00e1ci\u00f3 arra haszn\u00e1l, hogy k\u00f6z\u00f6lje a backend szolg\u00e1ltat\u00e1ssal, melyik adatot kell legk\u00f6zelebb bet\u00f6lteni. A legegyszer\u0171bb esetben minden lapozott adat elemhez tartozik egy t\u00e1voli kulcs, amelyre k\u00f6nnyen hivatkozhat. Azonban ha a t\u00e1voli kulcsok nem feleltethet\u0151ek meg a konkr\u00e9t elemeknek, nek\u00fcnk kell \u0151ket kezelni a load h\u00edv\u00e1sban.

      Amikor a t\u00e1voli kulcsok nem k\u00f6zvetlen\u00fcl kapcsol\u00f3dnak a listaelemekhez, c\u00e9lszer\u0171 \u0151ket k\u00fcl\u00f6n t\u00e1bl\u00e1zatban t\u00e1rolni a helyi adatb\u00e1zisban. Defini\u00e1lni kell egy Room entit\u00e1st, amely egy t\u00e1voli kulcsokb\u00f3l \u00e1ll\u00f3 t\u00e1bl\u00e1zatot reprezent\u00e1l.

      Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.model package-ben:

      UnsplashPhotoRemoteKeys.kt:

      @Entity(tableName = \"remote_keys\")\ndata class UnsplashPhotoRemoteKeys(\n@PrimaryKey val id: String,\nval prevKey: Int?,\nval nextKey: Int?\n)\n

      Emellett defini\u00e1lni kell egy DAO-t a RemoteKey entit\u00e1sra.

      Hozzuk l\u00e9tre az al\u00e1bbi oszt\u00e1lyt a data.local.dao package-ben:

      UnsplashPhotoRemoteKeysDao.kt:

      @Dao\ninterface UnsplashPhotoRemoteKeysDao {\n\n@Insert(onConflict = OnConflictStrategy.REPLACE)\nsuspend fun insertAllKeys(keys: List<UnsplashPhotoRemoteKeys>)\n\n@Query(\"SELECT * FROM remote_keys WHERE id = :id\")\nsuspend fun getKeysById(id: String): UnsplashPhotoRemoteKeys\n\n@Query(\"DELETE FROM remote_keys\")\nsuspend fun deleteAllKeys()\n}\n

      "},{"location":"laborok/network/#pagingutil","title":"PagingUtil","text":"

      Hozzuk l\u00e9tre a PagingUtil oszt\u00e1lyt az util package-ben a paging konfigur\u00e1l\u00e1s\u00e1ra \u00e9s placeholderek be\u00e1ll\u00edt\u00e1s\u00e1ra:

      PagingUtil.kt:

      object PagingUtil {\nconst val INITIAL_PAGE_SIZE = 10\nconst val INITIAL_PAGE = 1\n\nfun <T: Any> LazyGridScope.items(\nitems: LazyPagingItems<T>,\nkey: ((item: T) -> Any)? = null,\nitemContent: @Composable LazyGridItemScope.(value: T?) -> Unit\n) {\nitems(\ncount = items.itemCount,\nkey = if (key == null) null else { index ->\nval item = items.peek(index)\nif (item == null) {\nPagingPlaceholderKey(index)\n} else {\nkey(item)\n}\n}\n) { index ->\nitemContent(items[index])\n}\n}\n\ndata class PagingPlaceholderKey(private val index: Int) : Parcelable {\noverride fun writeToParcel(parcel: Parcel, flags: Int) {\nparcel.writeInt(index)\n}\n\noverride fun describeContents(): Int {\nreturn 0\n}\n\ncompanion object {\n@Suppress(\"unused\")\n@JvmField\nval CREATOR: Parcelable.Creator<PagingPlaceholderKey> =\nobject : Parcelable.Creator<PagingPlaceholderKey> {\noverride fun createFromParcel(parcel: Parcel) =\nPagingPlaceholderKey(parcel.readInt())\n\noverride fun newArray(size: Int) = arrayOfNulls<PagingPlaceholderKey?>(size)\n}\n}\n}\n}\n

      A data.repository package-be vegy\u00fck fel a lenti DataSource oszt\u00e1lyt, melyen kereszt\u00fcl a ViewModel el\u00e9ri a k\u00e9r\u00e9seinket:

      UnsplashPhotoDataSource.kt:

      @ExperimentalPagingApi\nclass UnsplashPhotoDataSource(\nprivate val api: UnsplashApi,\nprivate val db: UnsplashDatabase\n) {\n\nfun getAllPhotos(): Flow<PagingData<UnsplashPhoto>> {\nreturn Pager(\nconfig = PagingConfig(pageSize = INITIAL_PAGE_SIZE),\nremoteMediator = UnsplashRemoteMediator(api, db),\npagingSourceFactory = {  db.photosDao.getAllPhotos() }\n).flow\n}\n\nfun getPhotoByIdFromDatabase(id: String): Flow<UnsplashPhoto> = db.photosDao.getPhotoById(id)\n\nfun getSearchResults(searchTerms: String): Flow<PagingData<UnsplashPhoto>> {\nreturn Pager(\nconfig = PagingConfig(pageSize = INITIAL_PAGE_SIZE),\npagingSourceFactory = {\nSearchPagingSource(\napi = api,\nsearchTerms = searchTerms\n)\n}\n).flow\n}\n\nsuspend fun exists(id: String): Boolean = db.photosDao.exists(id).first()\n\nsuspend fun getPhotoByIdFromApi(id: String): Flow<UnsplashPhoto> {\nval response = api.getPhotoById(id)\nreturn if (response.isSuccessful) {\nflow { emit(response.body() ?: throw NullPointerException()) }\n} else throw Exception(\"Unsuccessful request\")\n}\n}\n

      Ha a flow-emit r\u00e9szn\u00e9l hib\u00e1t kapunk, cser\u00e9lj\u00fck le az importot a k\u00f6vetkez\u0151re: import kotlinx.coroutines.flow.flow.

      Itt l\u00e1thatjuk a Pager oszt\u00e1ly haszn\u00e1lat\u00e1t a lapozott adatok folyam\u00e1nak be\u00e1ll\u00edt\u00e1s\u00e1hoz.

      V\u00e9g\u00fcl hozzuk l\u00e9tre a saj\u00e1t Application oszt\u00e1lyunkat a gy\u00f6k\u00e9r package-ben:

      UnsplashApplication.kt:

      @ExperimentalPagingApi\nclass UnsplashApplication : Application() {\ncompanion object {\nlateinit var photoDataSource: UnsplashPhotoDataSource\n}\n\noverride fun onCreate() {\nsuper.onCreate()\nval db = Room.databaseBuilder(\nthis.baseContext,\nUnsplashDatabase::class.java,\n\"unsplash_db\"\n).build()\n\nval client = OkHttpClient.Builder()\n.readTimeout(15, TimeUnit.SECONDS)\n.connectTimeout(15, TimeUnit.SECONDS)\n.build()\n\nval moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()\nval retrofit = Retrofit.Builder()\n.baseUrl(\"https://api.unsplash.com/\")\n.client(client)\n.addConverterFactory(MoshiConverterFactory.create(moshi))\n.build()\n\nval api = retrofit.create(UnsplashApi::class.java)\n\nphotoDataSource = UnsplashPhotoDataSource(api,db)\n\n}\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s list\u00e1z\u00f3 k\u00e9perny\u0151je (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a Paging-hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s r\u00e9szletes k\u00e9perny\u0151je (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a Paging-hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/network/#onallo-feladat-1-kulonbozo-kepernyomeretek-tamogatasa","title":"\u00d6n\u00e1ll\u00f3 feladat 1 - K\u00fcl\u00f6nb\u00f6z\u0151 k\u00e9perny\u0151m\u00e9retek t\u00e1mogat\u00e1sa","text":"

      Szeretn\u00e9nk felk\u00e9sz\u00edteni az alkalmaz\u00e1sunkat, hogy k\u00fcl\u00f6nb\u00f6z\u0151 m\u00e9ret\u0171 k\u00e9perny\u0151k eset\u00e9n m\u00e1shogy, az adott k\u00e9sz\u00fcl\u00e9k sz\u00e1m\u00e1ra optim\u00e1lis m\u00f3don jelenleg meg. Ezzel els\u0151k\u00e9nt a util package-ben hozzunk l\u00e9tre egy oszt\u00e1lyt a kateg\u00f3ri\u00e1inkra:

      WindowSize.kt:

      enum class WindowSize { Compact, Medium, Expanded }\n

      Ezt k\u00f6vet\u0151en hozzuk l\u00e9tre a marad\u00e9k k\u00e9t elrendez\u00e9s\u00fcnket a feature.photos_feed.screensbysize package-ben:

      PhotosFeed_Compact.kt:

      @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@Composable\nfun PhotosFeedScreen_Compact(\nrefreshState: PullRefreshState,\nrefreshing: Boolean,\nonPhotoItemClick: (String) -> Unit,\nphotos: LazyPagingItems<UnsplashPhotoUiModel>,\nvalue: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\n) {\nval state = rememberLazyListState()\nScaffold(\nmodifier = modifier,\ntopBar = {\nSearchTopAppBar(\nisScrollInProgress = state.isScrollInProgress,\nvalue = value,\nonValueChange = onValueChange\n)\n}\n) {\nBox(\nmodifier = modifier\n.fillMaxSize()\n.pullRefresh(refreshState)\n.padding(it)\n) {\nif (!refreshing) {\nLazyColumn(\nmodifier = modifier.fillMaxSize()\n) {\nitems(photos.itemCount) { index ->\nphotos[index]?.let { model ->\nPhotoItem(\nphoto = model,\nonClick = onPhotoItemClick\n)\n}\n}\n}\n}\nPullRefreshIndicator(\nrefreshing = refreshing,\nstate = refreshState,\nmodifier = Modifier.align(Alignment.TopCenter)\n)\n}\n}\n}\n

      PhotosFeed_Expanded.kt:

      @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@Composable\nfun PhotosFeedScreen_Expanded(\nonPhotoItemClick: (String) -> Unit,\nphotos: LazyPagingItems<UnsplashPhotoUiModel>,\nvalue: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\nloadedPhoto: UnsplashPhotoUiModel? = null,\n) {\nval density = LocalDensity.current\nval configuration = LocalConfiguration.current\nval screenWidth = configuration.screenWidthDp.dp\nval state = rememberLazyGridState()\n\nRow(modifier = modifier.fillMaxSize()) {\nRow(\nmodifier = Modifier.fillMaxSize()\n) {\nScaffold(\nmodifier = Modifier.fillMaxSize(),\ntopBar = {\nSearchTopAppBar(\nisScrollInProgress = state.isScrollInProgress,\nvalue = value,\nonValueChange = onValueChange\n)\n}\n) {\nLazyVerticalGrid(\nstate = state,\ncolumns = GridCells.Adaptive(240.dp),\nmodifier = Modifier\n.fillMaxSize()\n.padding(it)\n.weight(1f)\n) {\nitems(photos.itemCount) { index ->\nphotos[index]?.let { model ->\nPhotoItem(\nphoto = model,\nonClick = onPhotoItemClick\n)\n}\n}\n}\n}\n\nAnimatedVisibility(\nvisible = loadedPhoto != null && screenWidth >= 1000.dp,\nenter = slideInHorizontally {\nwith(density) { 40.dp.roundToPx() }\n} + expandHorizontally(\nexpandFrom = Alignment.End\n) + fadeIn(\ninitialAlpha = 0.3f\n),\nexit = slideOutHorizontally() + shrinkHorizontally() + fadeOut(),\nmodifier = Modifier\n.fillMaxSize()\n.padding(5.dp)\n.weight(1f)\n) {\nPhotoDetails(\nloadedPhoto = loadedPhoto!!,\nwindowSize = WindowSize.Expanded\n)\n}\n}\n}\n}\n

      Friss\u00edts\u00fck a Screen\u00fcnket, hogy haszn\u00e1lja a fenti Composable-\u00f6ket:

      PhotosFeedScreen.kt:

      @ExperimentalMaterial3Api\n@ExperimentalMaterialApi\n@ExperimentalPagingApi\n@Composable\nfun PhotosFeedScreen(\nmodifier: Modifier = Modifier,\nwindowSize: WindowSize = WindowSize.Compact,\nonPhotoItemClick: (String) -> Unit = {},\nviewModel: PhotosFeedViewModel = viewModel(factory = PhotosFeedViewModel.Factory)\n) {\n\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval photos = state.photos?.collectAsLazyPagingItems()\nval selectedPhoto = state.photo?.collectAsStateWithLifecycle(null)\n\nval configuration = LocalConfiguration.current\nval screenWidth = configuration.screenWidthDp.dp\n\nval refreshState = rememberPullRefreshState(\nrefreshing = state.isLoading,\nonRefresh = viewModel::refreshPhotos\n)\n\nif (photos != null) {\nwhen(windowSize) {\nWindowSize.Compact -> {\nPhotosFeedScreen_Compact(\nrefreshState = refreshState,\nrefreshing = state.isLoading,\nonPhotoItemClick = onPhotoItemClick,\nphotos = photos,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\nWindowSize.Medium -> {\nPhotosFeedScreen_Medium(\nrefreshState = refreshState,\nrefreshing = state.isLoading,\nonPhotoItemClick = onPhotoItemClick,\nphotos = photos,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\nWindowSize.Expanded -> {\nPhotosFeedScreen_Expanded(\nonPhotoItemClick = if (screenWidth >= 1000.dp) {\nviewModel::loadSelectedPhoto\n} else onPhotoItemClick,\nphotos = photos,\nloadedPhoto = selectedPhoto?.value,\nvalue = state.searchTerms,\nonValueChange = viewModel::onSearchTermsChange\n)\n}\n}\n} else if (state.isError) {\nBox(modifier = modifier.fillMaxSize()) {\nText(text = state.throwable?.message.toString())\n}\n}\n}\n

      A PhotoDetails.kt-ban sz\u00fcntess\u00fck meg a h\u00e1rom windowsize-ot tartalmaz\u00f3 sor kommentez\u00e9s\u00e9t.

      Friss\u00edts\u00fck a NavGraph-ot: NavGraph.kt:

      @ExperimentalMaterial3Api\n@ExperimentalPagingApi\n@ExperimentalMaterialApi\n@Composable\nfun NavGraph(\nwindowSize: WindowSize = WindowSize.Compact,\n) {\nval navController = rememberNavController()\n\nNavHost(\nnavController = navController,\nstartDestination = Screen.PhotosFeed.route\n) {\ncomposable(route = Screen.PhotosFeed.route) {\nPhotosFeedScreen(\nwindowSize = windowSize,\nonPhotoItemClick = { photoId ->\nnavController.navigate(Screen.LoadedPhoto.passPhotoId(photoId))\n}\n)\n}\ncomposable(\nroute = Screen.LoadedPhoto.route,\narguments = listOf(\nnavArgument(\"photoId\") {\ntype = NavType.StringType\n}\n)\n) {\nLoadedPhotoScreen()\n}\n}\n}\n
      Vegy\u00fck fel a material3 k\u00f6nyvt\u00e1r mint\u00e1j\u00e1ra a material3-window-size-class f\u00fcgg\u0151s\u00e9get (link)[https://developer.android.com/jetpack/androidx/releases/compose-material3] Majd az Activity-t is \u00e1ll\u00edtsuk be ezek haszn\u00e1lat\u00e1ra:

      MainActivity.kt:

      class MainActivity : ComponentActivity() {\n@OptIn(\nExperimentalMaterial3WindowSizeClassApi::class,\nExperimentalMaterial3Api::class,\nExperimentalPagingApi::class,\nExperimentalMaterialApi::class\n)\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nUnsplashTheme {\nval windowSize = when (calculateWindowSizeClass(this).widthSizeClass) {\nWindowWidthSizeClass.Compact -> WindowSize.Compact\nWindowWidthSizeClass.Medium -> WindowSize.Medium\nWindowWidthSizeClass.Expanded -> WindowSize.Expanded\nelse -> WindowSize.Compact\n}\n\nNavGraph(windowSize = windowSize)\n}\n}\n}\n}\n
      Alt+Enterrel vegy\u00fck fel az annot\u00e1ci\u00f3ra kattintva a hi\u00e1nyz\u0151 f\u00fcgg\u0151s\u00e9get.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s a m\u00e1s m\u00e9retben megjelen\u0151 k\u00e9pekkel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a m\u00e1s m\u00e9ret\u0171 k\u00e9perny\u0151kh\u00f6z tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/network/#onallo-feladat-2-dependency-injection","title":"\u00d6n\u00e1ll\u00f3 feladat 2 - Dependency Injection","text":"

      \u00cdrjuk \u00e1t a projektet \u00fagy, hogy az el\u0151z\u0151 laboron megismert Dependency Injection keretrendszereket haszn\u00e1lja!

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a m\u0171k\u00f6d\u0151 alkalmaz\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a dependency injectionj\u00f6z tartoz\u00f3 relev\u00e1ns k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/permissions/","title":"Labor11 - Fut\u00e1sidej\u0171 enged\u00e9lyek (Contacts)","text":"

      A labor c\u00e9lja, hogy bemutassa, hogyan lehet a Compose keretrendszerben fut\u00e1sidej\u0171 enged\u00e9lyeket kezelni.

      "},{"location":"laborok/permissions/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

      A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

      "},{"location":"laborok/permissions/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
      1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

      2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

        Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

      3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

      4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

      Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd:

      1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
      2. A projekt neve legyen Contacts, a kezd\u0151 package pedig hu.bme.aut.android.contacts.
      3. A projektet a repository-n bel\u00fcl egy k\u00fcl\u00f6n mapp\u00e1ban hozzuk l\u00e9tre.
      4. Nyelvnek v\u00e1lasszuk a Kotlin-t.
      5. A minimum API szint legyen 24 (Android 7.0).

      FILE PATH

      A projekt a repository-ban l\u00e9v\u0151 Contacts k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

      Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

      "},{"location":"laborok/permissions/#verziok-frissitese","title":"Verzi\u00f3k friss\u00edt\u00e9se","text":"

      Vegy\u00fck fel az al\u00e1bbi f\u00fcgg\u0151s\u00e9geket a modul szint\u0171 build.gradle f\u00e1jlunkba, majd a laborvezet\u0151vel tekints\u00fck \u00e1t \u0151ket.

      dependencies {\n// Compose Bill of Materials\nval composeBom = platform(\"androidx.compose:compose-bom:2024.05.00\")\nimplementation(composeBom)\nandroidTestImplementation(composeBom)\n\n// Compose\nimplementation(\"androidx.compose.material3:material3\")\nimplementation(\"androidx.compose.ui:ui\")\nimplementation(\"androidx.compose.ui:ui-tooling-preview\")\nimplementation(\"androidx.compose.material:material-icons-extended\")\n\n// Compose testing\nandroidTestImplementation(\"androidx.compose.ui:ui-test-junit4\")\ndebugImplementation(\"androidx.compose.ui:ui-test-manifest\")\ndebugImplementation(\"androidx.compose.ui:ui-tooling\")\n\n// Core\nimplementation(\"androidx.core:core-ktx:1.13.1\")\nimplementation(\"androidx.activity:activity-compose:1.9.0\")\n\n// Lifecycle, Viewmodel\nval lifecycle_version = \"2.7.0\"\nimplementation(\"androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version\")\nimplementation(\"androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version\")\n\n// Navigation\nimplementation(\"androidx.navigation:navigation-compose:2.7.7\")\n\n// Permissions\nimplementation(\"com.google.accompanist:accompanist-permissions:0.35.0-alpha\")\n\n// Coil\nimplementation(\"io.coil-kt:coil-compose:2.5.0\")\n\n//Testing\ntestImplementation(\"junit:junit:4.13.2\")\nandroidTestImplementation(\"androidx.test.ext:junit:1.1.5\")\nandroidTestImplementation(\"androidx.test.espresso:espresso-core:3.5.1\")\n}\n

      Ezek mellett ellen\u0151rizz\u00fck a kotlin plugin \u00e9s a compose verzi\u00f3j\u00e1t. A labor k\u00e9sz\u00edt\u00e9sekor a k\u00f6vetkez\u0151ek voltak \u00e9rv\u00e9nyben:

      • Projekt szint\u0171 build.gradle:
        plugins {  \n  ...\n  id 'org.jetbrains.kotlin.android' version '1.8.10' apply false  \n}\n
      • Modul szint\u0171 build.gradle:
        android {\n    ...\n    composeOptions {\n        kotlinCompilerExtensionVersion '1.4.3'\n    }\n}\n

      A modul szint\u0171 build.gradle f\u00e1jlban \u00e1lll\u00edtsuk \u00e1t a compileSdk \u00e9rt\u00e9k\u00e9t 34-re!

      V\u00e9g\u00fcl vegy\u00fck fel el\u0151re az alkalmaz\u00e1shoz sz\u00fcks\u00e9ges sz\u00f6veges er\u0151forr\u00e1sokat:

      strings.xml:

      <resources>\n<string name=\"app_name\">Contacts</string>\n<string name=\"button_label_request_permission\">Grant permission</string>\n<string name=\"permission_grant_ask_beginning\">\"The\"</string>\n<string name=\"permission_grant_ask_separator_before_last_permission\">, and</string>\n<string name=\"permission_grant_ask_separator\">,</string>\n<string name=\"permission_grant_ask_singular_permission\">\" permission is\"</string>\n<string name=\"permission_grant_ask_multiple_permissions\">\" permissions are\"</string>\n<string name=\"permission_grant_ask_rationale\">\" important. Please grant all of them for the app to function properly.\"</string>\n<string name=\"permission_grant_ask_denied\">\" denied. The app cannot function without them. Please grant all of them.\"</string>\n<string name=\"contact_data_label_name\">Name</string>\n<string name=\"contact_data_label_phonenumber\">Number</string>\n<string name=\"contact_data_label_email\">Email</string>\n<string name=\"some_error_message\">Error</string>\n</resources>\n

      "},{"location":"laborok/permissions/#kontaktok-listaja","title":"Kontaktok list\u00e1ja","text":"

      Hozzunk l\u00e9tre egy \u00faj domain package-t l\u00e9tre a projekt\u00fcnk gy\u00f6ker\u00e9ben, mely az alkalmaz\u00e1sunk adatr\u00e9teg\u00e9nek r\u00e9szeit fogja tartalmazni, majd ezen bel\u00fcl hozzunk l\u00e9tre egy model package-et, mely az adatmodellek oszt\u00e1ly megfelel\u0151it fogja tartalmazni. Ebben hozzuk l\u00e9tre az al\u00e1bbi f\u00e1jlt:

      Contact.kt:

      import android.graphics.Bitmap\n\ndata class Contact(\nval id: String = \"\",\nval name: String = \"\",\nvar phoneNumber: String = \"\",\nvar emailAddress: String = \"\",\nvar photo: Bitmap? = null\n)\n

      "},{"location":"laborok/permissions/#navigacio-kialakitasa","title":"Navig\u00e1ci\u00f3 kialak\u00edt\u00e1sa","text":"

      Az el\u0151z\u0151 laborhoz hasonl\u00f3an alak\u00edtsuk ki a projektben a navig\u00e1ci\u00f3n\u00e1l haszn\u00e1lt oszt\u00e1lyokat!

      Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban l\u00e9tre egy \u00faj package-et navigation n\u00e9ven, majd hozzuk l\u00e9tre benne az \u00fatvonalakat reprezent\u00e1l\u00f3 Screen oszt\u00e1lyt:

      sealed class Screen(val route: String) {  }\n
      Illetve hozzuk l\u00e9tre a navig\u00e1ci\u00f3t v\u00e9gz\u0151 Composable f\u00fcggv\u00e9nyt is a NavGraph.kt f\u00e1jlban:
      @Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = \"\"\n) {\n\n}\n}\n

      A NavGraph Composable szerepe, hogy karban tartsa az \u00fatvonalakat, itt fogjuk a navig\u00e1ci\u00f3s esem\u00e9nyeket feldolgozni.

      V\u00e9g\u00fcl friss\u00edts\u00fck a MainActivity tartalm\u00e1t \u00fagy, hogy a NavGraph Composable-t haszn\u00e1lja:

      class MainActivity : ComponentActivity() {\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nContactsTheme {\nNavGraph()\n}\n}\n}\n}\n
      "},{"location":"laborok/permissions/#use-casek","title":"Use-casek","text":"

      A n\u00e9vjegyek kezel\u00e9s\u00e9t \u00e9rint\u0151 m\u0171veletek use-casekbe fogjuk kiszervezni. Az els\u0151 h\u00e1rom use-case, amire sz\u00fcks\u00e9g\u00fcnk lesz: - N\u00e9vjegyek list\u00e1z\u00e1sa - H\u00edv\u00e1s ind\u00edt\u00e1sa - SMS k\u00fcld\u00e9se

      Ut\u00f3bbi k\u00e9t funkci\u00f3ra nem k\u00e9sz\u00edt\u00fcnk saj\u00e1t implement\u00e1ci\u00f3t, hanem a k\u00e9sz\u00fcl\u00e9ken el\u00e9rhet\u0151 alkalmaz\u00e1sokat fogjuk elind\u00edtani implicit intent seg\u00edts\u00e9g\u00e9vel. A kontextuson v\u00e9gzett egy\u00e9ni f\u00fcggv\u00e9nyh\u00edv\u00e1sokat ezt k\u00f6vet\u0151en fogjuk implement\u00e1lni.

      Vegy\u00fck fel az al\u00e1bbi use-case-eket a domain.usecase package-be.

      LoadContactsUseCase.kt:

      class LoadContactsUseCase(\nprivate val context: Context\n) {\noperator fun invoke(): Flow<ArrayList<Contact>> = context.getContacts()\n}\n

      MakeACallUseCase.kt:

      class MakeACallUseCase(\nprivate val context: Context\n) {\noperator fun invoke(phoneNumber: String) {\nval callIntent = Intent(Intent.ACTION_CALL).apply {\ndata = Uri.parse(\"tel:$phoneNumber\")\nflags = FLAG_ACTIVITY_NEW_TASK\n}\ncontext.startActivity(callIntent)\n}\n}\n

      SendSMSUseCase.kt:

      class SendSMSUseCase(\nprivate val context: Context\n) {\noperator fun invoke(phoneNumber: String) {\nval intent = Intent(Intent.ACTION_VIEW).apply {\ndata = Uri.parse(\"sms:$phoneNumber\")\nflags = Intent.FLAG_ACTIVITY_NEW_TASK\n}\ncontext.startActivity(intent)\n}\n}\n

      R\u00f6viden tekints\u00fck \u00e1t a use-case-eket a laborvezet\u0151vel.

      "},{"location":"laborok/permissions/#nevjegymuveletek","title":"N\u00e9vjegym\u0171veletek","text":"

      Hozzuk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban a data package-et, majd benne a n\u00e9vjegyek kezel\u00e9s\u00e9vel kapcsolatos m\u0171veleteket elv\u00e9gz\u0151 seg\u00e9doszt\u00e1lyt. A m\u0171veletekben a ContentProvider \u00e1ltal adott adatok el\u00e9r\u00e9s\u00e9hez a ContentResolvert \u00e9s Cursort haszn\u00e1lunk.

      ContactsOperations.kt:

      object ContactsOperations {\n\nfun Context.getContacts(): Flow<ArrayList<Contact>> = flow {\nval contacts = ArrayList<Contact>()\nthis@getContacts.contentResolver?.performQuery(\nuri = ContactsContract.Contacts.CONTENT_URI,\nsortOrder = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + \" ASC\"\n).use { cursor ->\nif (cursor != null && cursor.count > 0) {\nval idColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID)\nval nameColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)\nwhile (cursor.moveToNext()) {\nval contactId = cursor.getString(idColumnIndex)\nval name = cursor.getString(nameColumnIndex)\nif (name != null) {\ncontacts.add(Contact(contactId, name))\n}\n}\n}\n}\ncontacts.forEach {\nit.phoneNumber = getContactPhoneNumber(it.id)\nit.emailAddress = getContactEmail(it.id)\nit.photo = getContactPhoto(it.id)\n}\nemit(contacts)\n}.flowOn(Dispatchers.IO)\n\nprivate fun Context.getContactName(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval displayNameColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)\ncursor.getString(displayNameColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactPhoneNumber(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval phoneNumberColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)\ncursor.getString(phoneNumberColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactEmail(id: String): String {\nthis.contentResolver?.performQuery(\nuri = ContactsContract.CommonDataKinds.Email.CONTENT_URI,\nselection = \"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?\",\nselectionArgs = arrayOf(id),\n).use { cursor ->\nreturn if (cursor == null || !cursor.moveToNext()) {\n\"\"\n} else {\nval emailColumnIndex =\ncursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)\ncursor.getString(emailColumnIndex)\n}\n}\n}\n\nprivate fun Context.getContactPhoto(id: String): Bitmap? {\nvar photo = BitmapFactory.decodeResource(this.resources, R.drawable.ic_launcher_foreground)\nContactsContract.Contacts.openContactPhotoInputStream(\nthis.contentResolver,\nContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id.toLong())\n).use {\nif (it != null) {\nphoto = BitmapFactory.decodeStream(it)\n}\n}\nreturn photo\n}\n\nprivate fun ContentResolver.performQuery(\n@RequiresPermission.Read uri: Uri,\nprojection: Array<String>? = null,\nselection: String? = null,\nselectionArgs: Array<String>? = null,\nsortOrder: String? = null\n): Cursor? {\nreturn query(uri, projection, selection, selectionArgs, sortOrder)\n}\n\n}\n

      "},{"location":"laborok/permissions/#engedelykezeles","title":"Enged\u00e9lykezel\u00e9s","text":""},{"location":"laborok/permissions/#hatter","title":"H\u00e1tt\u00e9r","text":"

      Android 6.0 (API level 23, Marshmallow) verzi\u00f3t\u00f3l kezdve a felhaszn\u00e1l\u00f3 fut\u00e1sid\u0151ben adhatja meg, vagy utas\u00edthatja el az alkalmaz\u00e1s \u00e1ltal k\u00e9rt enged\u00e9lyeket, \u00e9s nem az alkalmaz\u00e1s telep\u00edt\u00e9sekor vagy friss\u00edt\u00e9sekor. D\u00f6nthet \u00fagy, hogy bizonyos enged\u00e9lyeket nem ad meg egy alkalmaz\u00e1snak, \u00edgy nagyobb fok\u00fa ir\u00e1ny\u00edt\u00e1s ker\u00fcl a kez\u00e9be. Az alkalmaz\u00e1senged\u00e9lyeket k\u00e9s\u0151bb b\u00e1rmikor m\u00f3dos\u00edthatja a rendszerszint\u0171 alkalmaz\u00e1s be\u00e1ll\u00edt\u00e1sokn\u00e1l.

      Az enged\u00e9lyek k\u00e9t kateg\u00f3ri\u00e1ba vannak sorolva: normal \u00e9s dangerous.

      A normal kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyek nem jelentenek k\u00f6zvetlen kock\u00e1zatot a felhaszn\u00e1l\u00f3 szem\u00e9lyes adataira, ezeket az enged\u00e9lyeket a rendszer automatikusan megadja az alkalmaz\u00e1snak, ha sz\u00fcks\u00e9ge van r\u00e1.

      A dangerous kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyek lehet\u0151s\u00e9get adhatnak az alkalmaz\u00e1snak a felhaszn\u00e1l\u00f3 szem\u00e9lyes adataihoz val\u00f3 hozz\u00e1f\u00e9r\u00e9shez. Ebben az esetben a felhaszn\u00e1l\u00f3nak kell megadni az enged\u00e9lyt az alkalmaz\u00e1s sz\u00e1m\u00e1ra. Ennek a k\u00f6zvetlen k\u00f6vetkezm\u00e9nye az, hogy az alkalmaz\u00e1sokat fel kell k\u00e9sz\u00edteni arra az esetre, ha nincs megadva egy adott funkci\u00f3 m\u0171k\u00f6d\u00e9s\u00e9hez elengedhetetlen enged\u00e9ly.

      Ezen az oldalon tal\u00e1lhat\u00f3 az \u00f6sszes enged\u00e9ly kateg\u00f3ri\u00e1nk\u00e9nt.

      Az AndroidManifest.xml f\u00e1jlban kateg\u00f3ri\u00e1t\u00f3l f\u00fcggetlen\u00fcl meg kell adni az alkalmaz\u00e1s sz\u00e1m\u00e1ra sz\u00fcks\u00e9ges \u00f6sszes enged\u00e9lyt, de ennek hat\u00e1sa elt\u00e9r a futtat\u00f3 rendszer verzi\u00f3j\u00e1t\u00f3l \u00e9s a target SDK verzi\u00f3t\u00f3l f\u00fcgg\u0151en:

      • Ha az eszk\u00f6z Android 5.1 (API level 22) vagy alacsonyabb verzi\u00f3t futtat, VAGY az alkalmaz\u00e1s target SDK szintje 22 vagy kisebb, akkor a rendszer telep\u00edt\u00e9skor k\u00e9ri el az \u00f6sszes sz\u00fcks\u00e9ges enged\u00e9lyt. Ha a felhaszn\u00e1l\u00f3 nem fogadja el egyben az \u00f6sszes k\u00e9r\u00e9st, akkor a telep\u00edt\u00e9si folyamat le\u00e1ll.

      • Ha az eszk\u00f6z Android 6.0 (API level 23) vagy nagyobb verzi\u00f3t futtat \u00c9S az alkalmaz\u00e1s target SDK szintje 23 vagy nagyobb, akkor az alkalmaz\u00e1s a fut\u00e1sa sor\u00e1n fogja elk\u00e9rni a dangerous kateg\u00f3ri\u00e1ba tartoz\u00f3 enged\u00e9lyeket, a normal enged\u00e9lyeket pedig a rendszer automatikusan megadja. Ebben az esetben a felhaszn\u00e1l\u00f3 b\u00e1rmikor b\u00e1rmelyik enged\u00e9lyt megadhatja, vagy visszavonhatja. Megtagadott enged\u00e9lyekkel az alkalmaz\u00e1s limit\u00e1lt funkcionalit\u00e1ssal futhat tov\u00e1bb, erre a helyzetre is fel kell k\u00e9sz\u00fclni.

      Megjegyz\u00e9s: a Google Play 2018 augusztus\u00e1t\u00f3l megk\u00f6veteli a legal\u00e1bb 26-os target SDK verzi\u00f3t \u00faj alkalmaz\u00e1sokra, 2018 november\u00e9t\u0151l pedig m\u00e1r megl\u00e9v\u0151 alkalmaz\u00e1sok friss\u00edt\u00e9seire is. Ezzel a legal\u00e1bb 6.0-s Androidot futtat\u00f3 eszk\u00f6z\u00f6k\u00f6n elker\u00fclhetetlenn\u00e9 v\u00e1lt a fut\u00e1sidej\u0171 enged\u00e9lyek kezel\u00e9se.

      Az enged\u00e9lyek kezel\u00e9s\u00e9re most az Accompanist k\u00f6nyvt\u00e1rat fogjuk most haszn\u00e1lni, mely a Jetpack Compose keretrenszerben t\u00e1mogatja az enged\u00e9lyek k\u00f6nny\u0171 kezel\u00e9s\u00e9t.

      "},{"location":"laborok/permissions/#engedelyek-kezelese","title":"Enged\u00e9lyek kezel\u00e9se","text":"

      Az alkalmaz\u00e1sunknak sz\u00fcks\u00e9ge lesz enged\u00e9lyekre a kapcsolatok el\u00e9r\u00e9s\u00e9hez, szerkeszt\u00e9s\u00e9hez, valamint h\u00edv\u00e1s ind\u00edt\u00e1s\u00e1hoz, ezeket vegy\u00fck fel a manifest f\u00e1jlba az application tagen k\u00edv\u00fclre:

          <uses-feature android:name=\"android.hardware.telephony\" android:required=\"false\" />\n<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>\n<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>\n<uses-permission android:name=\"android.permission.CALL_PHONE\"/>\n

      Az enged\u00e9lykezel\u00e9s fontosabb elemei: - A rememberPermissionState API \u00e1ltal tudunk a felhaszn\u00e1l\u00f3t\u00f3l egy enged\u00e9lyre r\u00e1k\u00e9rdezni, \u00e9s az enged\u00e9ly st\u00e1tusz\u00e1t ellen\u0151rizni. - Egyes esetekben sz\u00fcks\u00e9g lehet arra, hogy t\u00e1j\u00e9koztassuk a felhaszn\u00e1l\u00f3t arr\u00f3l, hogy mi\u00e9rt k\u00e9r az alkalmaz\u00e1s bizonyos dangerous enged\u00e9lyeket. Ez n\u00f6velheti a felhaszn\u00e1l\u00f3 bizalm\u00e1t az alkalmaz\u00e1ssal szemben. - Ha egy jogosults\u00e1got a felhaszn\u00e1l\u00f3 egyszer elutas\u00edtott, a shouldShowRationale v\u00e1ltoz\u00f3 \u00e9rt\u00e9ke alapj\u00e1n eld\u00f6nthet\u0151, hogy a k\u00e9rd\u00e9ses enged\u00e9ly \u00fajra elk\u00e9r\u00e9se a rendszer szerint szorul-e r\u00e9szletes magyar\u00e1zatra.

      Hozzuk l\u00e9tre a gy\u00f6k\u00e9r k\u00f6nyvt\u00e1rban az util package-t, majd benne az al\u00e1bbi, enged\u00e9lykezel\u00e9sek kiszolg\u00e1l\u00f3 seg\u00e9doszt\u00e1lyt.

      PermissionsUtil.kt:

      object PermissionsUtil {\n@ExperimentalPermissionsApi\nfun getTextToShowGivenPermissions(\npermissions: List<PermissionState>,\nshouldShowRationale: Boolean,\ncontext: Context\n): String {\nval revokedPermissionsSize = permissions.size\nif (revokedPermissionsSize == 0) return \"\"\nval textToShow = StringBuilder()\n\nwith(context) {\ntextToShow.apply {\nappend(getString(R.string.permission_grant_ask_beginning))\n}\n\nfor (i in permissions.indices) {\nval permission = permissions[i].permission\n.replace(Regex(\"[a-z]|[.]\"), \"\")\ntextToShow.append(\" $permission\")\nwhen {\nrevokedPermissionsSize > 1 && i == revokedPermissionsSize - 2 -> {\ntextToShow.append(getString(R.string.permission_grant_ask_separator_before_last_permission))\n}\nelse -> {\ntextToShow.append(getString(R.string.permission_grant_ask_separator))\n}\n}\n}\ntextToShow.append(\nif (revokedPermissionsSize == 1) {\ngetString(R.string.permission_grant_ask_singular_permission)\n} else getString(R.string.permission_grant_ask_multiple_permissions)\n)\ntextToShow.append(\nif (shouldShowRationale) {\ngetString(R.string.permission_grant_ask_rationale)\n} else {\ngetString(R.string.permission_grant_ask_denied)\n}\n)\n}\n\nreturn textToShow.toString()\n}\n}\n

      "},{"location":"laborok/permissions/#lista-felulet-es-logika","title":"Lista fel\u00fclet \u00e9s logika","text":"

      Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl a feature package-et, mely az egyes oldalak Composable \u00e9s ViewModel oszt\u00e1lyait fogja tartalmazni k\u00fcl\u00f6n packagenk\u00e9nt, majd hozzuk l\u00e9tre ebben a contact_list package-t.

      El\u0151sz\u00f6r foglalkozzunk az oldalhoz tartoz\u00f3 ViewModel oszt\u00e1llyal. Hozzuk l\u00e9tre a ContactsListViewModel.kt f\u00e1jlt, majd m\u00e1soljuk be az al\u00e1bbi k\u00f3dr\u00e9szletet:

      ContactsViewModel.kt:

      class ContactsViewModel(\nprivate val loadContactsUseCase: LoadContactsUseCase,\nprivate val makeCall: MakeACallUseCase,\nprivate val sendSMS: SendSMSUseCase,\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(ContactsState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: ContactsEvent) {\nwhen(event) {\nis ContactsEvent.MakeCall -> {\nval phoneNumber = event.phoneNumber\nviewModelScope.launch {\nmakeCall(phoneNumber)\n}\n}\nis ContactsEvent.SendSms -> {\nval phoneNumber = event.phoneNumber\nviewModelScope.launch {\nsendSMS(phoneNumber)\n}\n}\n}\n}\n\nfun loadContacts() {\nviewModelScope.launch(Dispatchers.IO) {\n_state.update { it.copy(isLoading = true) }\nloadContactsUseCase().catch { e ->\n_state.update { it.copy(\nisLoading = false,\nerror = e\n) }\n}.collect { contacts ->\n_state.update { it.copy(\nisLoading = false,\ncontacts = contacts\n) }\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[APPLICATION_KEY] as Application).baseContext\nContactsViewModel(\nloadContactsUseCase = LoadContactsUseCase(context),\nmakeCall = MakeACallUseCase(context),\nsendSMS = SendSMSUseCase(context)\n)\n}\n}\n}\n\n}\n\ndata class ContactsState(\nval isLoading: Boolean = false,\nval error: Throwable? = null,\nval isError: Boolean = error != null,\nval contacts: List<Contact> = emptyList()\n)\n\nsealed class ContactsEvent {\ndata class MakeCall(val phoneNumber: String): ContactsEvent()\ndata class SendSms(val phoneNumber: String): ContactsEvent()\n}\n
      Az el\u0151z\u0151 laborhoz hasonl\u00f3an a fel\u00fcletet le\u00edr\u00f3 \u00e1llapot oszt\u00e1lyt sealed class-k\u00e9nt deklar\u00e1ljuk, \u00e9s j\u00f3l elk\u00fcl\u00f6n\u00edtett \u00e1llapot oszt\u00e1lyokat vesz\u00fcnk fel, \u00edgy is jelezve, hogy az egyes \u00e1llapotokban az oldalunkon mit kell megjelen\u00edteni. Ezeket egy MutableStateFlow seg\u00edts\u00e9g\u00e9vel kezelj\u00fck, melyet egy csak olvashat\u00f3 v\u00e1ltozat\u00e1ban osztunk meg az oldalt reprezent\u00e1l\u00f3 Composable-el.

      Mivel a ViewModel k\u00e9pes t\u00fal\u00e9lni az \u0151t l\u00e9trehoz\u00f3 komponenst, ez\u00e9rt a k\u00f3db\u00f3l mi nem a konstruktor h\u00edv\u00e1s\u00e1val fogjuk l\u00e9trehozni a p\u00e9ld\u00e1nyt, hanem a keretrendszernek tudunk \u00e1tadni egy speci\u00e1lis factory met\u00f3dust, amit a rendszer az els\u0151 alkalommal meg fog h\u00edvni. Ezt a met\u00f3dust szervezt\u00fck ki a companion object r\u00e9szbe, ami jelenleg csak l\u00e9trehoz egy p\u00e9ld\u00e1nyt, a k\u00e9s\u0151bbiekben azonban hasznos lesz k\u00fcl\u00f6nb\u00f6z\u0151 k\u00fcls\u0151 \u00e9rt\u00e9kek inicializ\u00e1l\u00e1s\u00e1ra.

      K\u00fcl\u00f6n oszt\u00e1lyokat hozunk l\u00e9tre a n\u00e9vjegyeken v\u00e9gzett esem\u00e9nyek rendszerez\u00e9s\u00e9re, illetve az \u00e1llapot t\u00e1rol\u00e1s\u00e1ra.

      A k\u00e9pek \u00e9s sz\u00f6vegek kezel\u00e9s\u00e9hez hozzuk l\u00e9tre az al\u00e1bbi seg\u00e9doszt\u00e1lyokat a ui.model package-ben:

      UiIcon.kt:

      import androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\n\nsealed class UiIcon {\n@Composable\nfun AsImage(\nmodifier: Modifier = Modifier,\ntint: Color = LocalContentColor.current,\n@StringRes contentDescriptionResId: Int? = null,\n) {\nwhen (this) {\nis ImageRes -> {\nIcon(\npainter = painterResource(id = drawableResId),\ntint = tint,\nmodifier = modifier,\ncontentDescription = if (contentDescriptionResId == null) {\nnull\n} else stringResource(id = contentDescriptionResId)\n)\n}\nis VectorImage -> {\nIcon(\nimageVector = imageVector,\ntint = tint,\nmodifier = modifier,\ncontentDescription = if (contentDescriptionResId == null) {\nnull\n} else stringResource(id = contentDescriptionResId)\n)\n}\n}\n}\n}\n\ndata class ImageRes(@DrawableRes val drawableResId: Int): UiIcon()\n\ndata class VectorImage(val imageVector: ImageVector): UiIcon()\n

      UiText.kt:

      import android.content.Context\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport hu.bme.aut.android.contacts.R\n\nsealed class UiText {\n\ndata class DynamicString(val text: String) : UiText()\n\ndata class StringResource(\n@StringRes val id: Int,\nval formatArgs: ArrayList<Any> = arrayListOf()\n) : UiText()\n\n@Composable\nfun AsText(\nmodifier: Modifier = Modifier,\ncolor: Color = Color.Unspecified,\nfontSize: TextUnit = TextUnit.Unspecified,\nfontStyle: FontStyle? = null,\nfontWeight: FontWeight? = null,\nfontFamily: FontFamily? = null,\nletterSpacing: TextUnit = TextUnit.Unspecified,\ntextDecoration: TextDecoration? = null,\ntextAlign: TextAlign? = null,\nlineHeight: TextUnit = TextUnit.Unspecified,\noverflow: TextOverflow = TextOverflow.Clip,\nsoftWrap: Boolean = true,\nmaxLines: Int = Int.MAX_VALUE,\nonTextLayout: (TextLayoutResult) -> Unit = {},\nstyle: TextStyle = LocalTextStyle.current\n) {\nText(\ntext = when(this) {\nis DynamicString -> {\ntext\n}\nis StringResource -> {\nstringResource(id = id, formatArgs)\n}\n},\nmodifier = modifier,\ncolor = color,\nfontSize = fontSize,\nfontStyle = fontStyle,\nfontWeight = fontWeight,\nfontFamily = fontFamily,\nletterSpacing = letterSpacing,\ntextDecoration = textDecoration,\ntextAlign = textAlign,\nlineHeight = lineHeight,\noverflow = overflow,\nsoftWrap = softWrap,\nmaxLines = maxLines,\nonTextLayout = onTextLayout,\nstyle = style\n)\n}\n\nfun asString(context: Context): String {\nreturn when(this) {\nis DynamicString -> {\ntext\n}\nis StringResource -> {\ncontext.getString(id,formatArgs)\n}\n}\n}\n\n}\n\n\nfun Throwable?.toUiText(): UiText {\nval message: String = this?.message.orEmpty()\nreturn if (message.isBlank()) {\nUiText.StringResource(R.string.some_error_message)\n} else {\nUiText.DynamicString(message)\n}\n}\n

      A ui.common packagebe hozzuk l\u00e9tre az al\u00e1bbi, listaelemeket reprez\u00e1nt\u00e1l\u00f3 f\u00e1jlt, \u00e9s tekints\u00fck \u00e1t az elrendez\u00e9s\u00e9t:

      ContactListItem.kt:

      import androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Call\nimport androidx.compose.material.icons.filled.Person\nimport androidx.compose.material.icons.filled.Sms\nimport androidx.compose.material3.Divider\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.contacts.domain.model.Contact\nimport hu.bme.aut.android.contacts.ui.model.VectorImage\n\n@ExperimentalMaterial3Api\n@Composable\nfun ContactListItem(\ncontact: Contact,\nmodifier: Modifier = Modifier,\nonMakeCall: (String) -> Unit,\nonSendSms: (String) -> Unit\n) {\nListItem(\nheadlineContent = { Text(text = contact.name) },\nsupportingContent = { Text(text = contact.phoneNumber) },\nleadingContent = {\nif (contact.photo != null) {\nImage(\nbitmap = contact.photo!!.asImageBitmap(),\ncontentDescription = null,\nmodifier = Modifier\n.size(50.dp)\n.clip(RoundedCornerShape(5.dp)),\ncontentScale = ContentScale.Crop\n)\n} else {\nIcon(\nimageVector = Icons.Default.Person,\ncontentDescription = null,\nmodifier = Modifier\n.size(50.dp)\n.clip(RoundedCornerShape(5.dp))\n.background(MaterialTheme.colorScheme.secondaryContainer),\ntint = MaterialTheme.colorScheme.background\n)\n}\n},\ntrailingContent = {\nRow {\nIconButton(onClick = { onMakeCall(contact.phoneNumber) }) {\nVectorImage(Icons.Default.Call)\n.AsImage(tint = MaterialTheme.colorScheme.primary)\n}\nSpacer(modifier = Modifier.width(5.dp))\nIconButton(onClick = { onSendSms(contact.phoneNumber) }) {\nVectorImage(Icons.Default.Sms)\n.AsImage(tint = MaterialTheme.colorScheme.primary)\n}\n}\n},\nmodifier = modifier\n)\nDivider(color = MaterialTheme.colorScheme.primaryContainer)\n}\n

      Ezt k\u00f6vet\u0151en l\u00e9trehozhatjuk a k\u00e9perny\u0151nkh\u00f6z tartoz\u00f3 elrendez\u00e9st a feature.contact_list package-ben. Itt megfigyelhet\u0151 a kor\u00e1bban eml\u00edtett rememberMultiplePermissionsState haszn\u00e1lata, illetve az enged\u00e9lyek megl\u00e9t\u00e9nek ellen\u0151rz\u00e9se. Az enged\u00e9lyek elk\u00e9r\u00e9se egy AlertDialog seg\u00edts\u00e9g\u00e9vel t\u00f6rt\u00e9nik.

      ContactsScreen.kt:

      import androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLifecycleOwner\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.rememberMultiplePermissionsState\nimport hu.bme.aut.android.contacts.R\nimport hu.bme.aut.android.contacts.ui.common.ContactListItem\nimport hu.bme.aut.android.contacts.ui.model.VectorImage\nimport hu.bme.aut.android.contacts.ui.model.toUiText\nimport hu.bme.aut.android.contacts.util.PermissionsUtil.getTextToShowGivenPermissions\nimport androidx.compose.foundation.lazy.items\n\n@ExperimentalPermissionsApi\n@ExperimentalMaterial3Api\n@Composable\nfun ContactsScreen(\nmodifier: Modifier = Modifier,\nviewModel: ContactsViewModel = viewModel(factory = ContactsViewModel.Factory),\nonListItemClick: (String) -> Unit,\nonFabClick: () -> Unit\n) {\nval context = LocalContext.current\nval lifecycle = LocalLifecycleOwner.current.lifecycle\n\nval contactsPermissions = rememberMultiplePermissionsState(\npermissions = listOf(\nandroid.Manifest.permission.READ_CONTACTS,\nandroid.Manifest.permission.WRITE_CONTACTS,\nandroid.Manifest.permission.CALL_PHONE\n)\n)\n\nif (contactsPermissions.allPermissionsGranted) {\n\nLaunchedEffect(key1 = contactsPermissions.allPermissionsGranted) {\nviewModel.loadContacts()\n}\n\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nScaffold(\nmodifier = modifier,\nfloatingActionButton = {\nLargeFloatingActionButton(onClick = onFabClick) {\nVectorImage(Icons.Default.Add).AsImage()\n}\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nif (state.isLoading) {\nCircularProgressIndicator(color = MaterialTheme.colorScheme.primary)\n} else if (state.isError) {\nText(text = state.error.toUiText().asString(context))\n} else {\nLazyColumn(\nmodifier = Modifier\n.fillMaxSize()\n) {\nitems(state.contacts) { contact ->\nContactListItem(\ncontact = contact,\nmodifier = Modifier.clickable {\nonListItemClick(contact.id)\n},\nonMakeCall = { viewModel.onEvent(ContactsEvent.MakeCall(it)) },\nonSendSms = { viewModel.onEvent(ContactsEvent.SendSms(it)) }\n)\n}\n}\n}\n}\n}\n\n} else {\nAlertDialog(\nonDismissRequest = { },\nconfirmButton = {\nButton(onClick = { contactsPermissions.launchMultiplePermissionRequest() }) {\nText(stringResource(id = R.string.button_label_request_permission))\n}\n},\ntext = {\nText(\ngetTextToShowGivenPermissions(\ncontactsPermissions.revokedPermissions,\ncontactsPermissions.shouldShowRationale,\ncontext\n)\n)\n}\n)\n}\n}\n
      Ezt k\u00f6vet\u0151en friss\u00edthetj\u00fck a Screen oszt\u00e1lyunk az \u00faj \u00fatvonallal:

      object Contacts: Screen(route = \"contacts\")\n

      Illetve kieg\u00e9sz\u00edthetj\u00fck a NavGraph oszt\u00e1lyt is a listan\u00e9zet\u00fcnkkel:

      @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.Contacts.route\n) {\ncomposable(route = Screen.Contacts.route) {\nContactsScreen(\nonListItemClick = {\n//TODO\n},\nonFabClick = {\n//TODO\n}\n)\n}\n}\n}\n

      Ha hib\u00e1t dob az Android Studio, adjuk hozz\u00e1 a hi\u00e1nyz\u00f3 annot\u00e1ci\u00f3kat.

      "},{"location":"laborok/permissions/#vcf-file-importalasa","title":"VCF file import\u00e1l\u00e1sa","text":"

      A k\u00f6vetkez\u0151 l\u00e9p\u00e9seket csak emul\u00e1toron sz\u00fcks\u00e9ges elv\u00e9gezni, ha nem \u00e1llnak rendelkez\u00e9sre n\u00e9vjegyek: - T\u00f6lts\u00fck le a repository-ban el\u00e9rhet\u0151 .vcf f\u00e1jlt: n\u00e9vjegyek. - A Device Explorer tool haszn\u00e1lat\u00e1val m\u00e1soljuk be a let\u00f6lt\u00f6tt f\u00e1jlt az sdcard/Download k\u00f6nyvt\u00e1rba. - Nyissuk meg az emul\u00e1tor gy\u00e1ri N\u00e9vjegyek alkalmaz\u00e1s\u00e1t, \u00e9s import\u00e1ljuk a vcf f\u00e1jlt.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lista n\u00e9zet n\u00e9vjegyekkel (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a folyamatban lev\u0151 h\u00edv\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a megnyitott SMS k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/permissions/#onallo-feladatok","title":"\u00d6n\u00e1ll\u00f3 feladatok","text":""},{"location":"laborok/permissions/#kontaktok-hozzaadasa","title":"Kontaktok hozz\u00e1ad\u00e1sa","text":"

      Vegy\u00fcnk fel egy \u00faj use-case-t a n\u00e9vjegyek ment\u00e9s\u00e9re:

      SaveContactUseCase.kt:

      class SaveContactUseCase (\nprivate val context: Context\n) {\noperator fun invoke(\nname: String,\nphoneNumber: String,\nemailAddress: String,\nphotoUri: Uri?\n) {\ncontext.addNewContact(name, phoneNumber, emailAddress, photoUri)\n}\n}\n

      Eg\u00e9sz\u00edts\u00fck ki a ContactOperations oszt\u00e1lyunkat az al\u00e1bbi, hozz\u00e1ad\u00e1shoz sz\u00fcks\u00e9ges f\u00fcggv\u00e9nyekkel:

      fun Context.addNewContact(\nname: String,\nphoneNumber: String,\nemailAddress: String,\nphotoUri: Uri?\n) {\nval id = 0\nval contentProviderOperations = arrayListOf<ContentProviderOperation>()\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)\n.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)\n.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber)\n.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MAIN)\n.build()\n)\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, emailAddress)\n.withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_OTHER)\n.build()\n)\n\nif (photoUri != null) {\nval photo = if (Build.VERSION.SDK_INT < 28) {\nMediaStore.Images.Media.getBitmap(contentResolver, photoUri)\n} else {\nval source = ImageDecoder.createSource(contentResolver, photoUri)\nImageDecoder.decodeBitmap(source)\n}\n\ncontentProviderOperations.add(\nContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)\n.withValueBackReference(ContactsContract.RawContacts.Data.RAW_CONTACT_ID,id)\n.withValue(ContactsContract.RawContacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)\n.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bitmapToByteArray(photo))\n.build()\n)\n}\n\ncontentResolver.applyBatch(ContactsContract.AUTHORITY, contentProviderOperations)\n}\n\n\n\nprivate fun bitmapToByteArray(bitmap: Bitmap): ByteArray {\nval stream = ByteArrayOutputStream()\nbitmap.compress(Bitmap.CompressFormat.PNG, 90, stream)\nreturn stream.toByteArray()\n}\n

      Hozzuk l\u00e9tre a ui.common package-ben az al\u00e1bbi oszt\u00e1lyt az adataink \u00fajrafelhaszn\u00e1lhat\u00f3 m\u00f3don t\u00f6rt\u00e9n\u0151 megjelen\u00edt\u00e9s\u00e9hez:

      ContactDataItem.kt:

      @ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDataItem(\nmodifier: Modifier = Modifier,\nenabled: Boolean = false,\nleadingIcon: UiIcon,\nlabel: UiText,\nvalue: String,\nonValueChange: (String) -> Unit\n) {\nSurface(\nmodifier = modifier\n.fillMaxWidth()\n.padding(bottom = 10.dp),\ncolor = MaterialTheme.colorScheme.primaryContainer,\nshape = RoundedCornerShape(5.dp)\n) {\nRow(\nmodifier = Modifier\n.fillMaxWidth()\n.height(IntrinsicSize.Min),\nverticalAlignment = Alignment.CenterVertically,\nhorizontalArrangement = Arrangement.Start\n) {\nleadingIcon.AsImage(modifier = Modifier.padding(15.dp))\nColumn(\nmodifier = Modifier\n.height(IntrinsicSize.Min)\n.fillMaxWidth()\n.padding(end = 15.dp)\n) {\nTextField(\nvalue = value,\nlabel = {\nlabel.AsText(\nstyle = MaterialTheme.typography.labelSmall,\nfontWeight = FontWeight.Black,\nmaxLines = 1\n)\n},\nonValueChange = onValueChange,\nsingleLine = true,\nshape = RectangleShape,\ncolors = TextFieldDefaults.textFieldColors(\nfocusedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,\ndisabledTextColor = MaterialTheme.colorScheme.onPrimaryContainer,\ncontainerColor = Color.Transparent,\nfocusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\nunfocusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\ndisabledLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\nfocusedIndicatorColor = Color.Transparent,\nunfocusedIndicatorColor = Color.Transparent,\ndisabledIndicatorColor = Color.Transparent,\nerrorIndicatorColor = Color.Transparent,\n),\nenabled = enabled,\nmodifier = Modifier.fillMaxWidth()\n)\n}\n}\n}\n}\n\n@Preview(showBackground = true)\n@ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDataItem_Preview() {\nvar value by remember {\nmutableStateOf(\"data value\")\n}\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Warning),\nlabel = UiText.DynamicString(\"data name\"),\nvalue = value,\nonValueChange = { value = it }\n)\n}\n

      A feature.contact_add package-ben hozzuk l\u00e9tre a hozz\u00e1 tartoz\u00f3 ViewModel oszt\u00e1lyt:

      AddNewContactViewModel.kt:

      class AddNewContactViewModel(\nprivate val saveContact: SaveContactUseCase\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(AddNewContactState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: AddNewContactEvent) {\nwhen(event) {\nis AddNewContactEvent.ChangeContactName -> {\n_state.update { it.copy(contactName = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactNumber -> {\n_state.update { it.copy(contactNumber = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactEmail -> {\n_state.update { it.copy(contactEmail = event.newValue) }\n}\nis AddNewContactEvent.ChangeContactPhoto -> {\n_state.update { it.copy(contactPhoto = event.newValue) }\n}\n}\n}\n\nfun addContact() {\nviewModelScope.launch(Dispatchers.IO) {\nsaveContact(\nstate.value.contactName,\nstate.value.contactNumber,\nstate.value.contactEmail,\nstate.value.contactPhoto\n)\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application).baseContext\nAddNewContactViewModel(saveContact = SaveContactUseCase(context))\n}\n}\n}\n}\n\ndata class AddNewContactState(\nval contactName: String = \"\",\nval contactNumber: String = \"\",\nval contactEmail: String = \"\",\nval contactPhoto: Uri? = null\n)\n\nsealed class AddNewContactEvent {\ndata class ChangeContactName(val newValue: String): AddNewContactEvent()\ndata class ChangeContactNumber(val newValue: String): AddNewContactEvent()\ndata class ChangeContactEmail(val newValue: String): AddNewContactEvent()\n\ndata class ChangeContactPhoto(val newValue: Uri): AddNewContactEvent()\n}\n

      Ezt k\u00f6vet\u0151en a fel\u00fcletet is elk\u00e9sz\u00edthetj\u00fck:

      AddNewContactScreen.kt:

      @ExperimentalFoundationApi\n@ExperimentalMaterial3Api\n@Composable\nfun AddNewContactScreen(\nmodifier: Modifier = Modifier,\nonNavigateBack: () -> Unit,\nviewModel: AddNewContactViewModel = viewModel(factory = AddNewContactViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nScaffold(\nmodifier = modifier.fillMaxSize(),\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = \"Contacts\")\n},\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nVectorImage(Icons.Default.ArrowBack).AsImage()\n}\n},\ncolors = TopAppBarDefaults.smallTopAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n},\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = {\nviewModel.addContact()\nonNavigateBack()\n}\n) {\nIcon(\nimageVector = Icons.Default.Save,\ncontentDescription = null\n)\n}\n}\n) { padding ->\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nPhotoPicker(\naddImageUri = {\nviewModel.onEvent(AddNewContactEvent.ChangeContactPhoto(it))\n},\nimageUri = state.contactPhoto\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Person),\nlabel = UiText.StringResource(R.string.contact_data_label_name),\nvalue = state.contactName,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactName(it)) }\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Phone),\nlabel = UiText.StringResource(R.string.contact_data_label_phonenumber),\nvalue = state.contactNumber,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactNumber(it)) }\n)\n\nContactDataItem(\nenabled = true,\nleadingIcon = VectorImage(Icons.Default.Email),\nlabel = UiText.StringResource(R.string.contact_data_label_email),\nvalue = state.contactEmail,\nonValueChange = { viewModel.onEvent(AddNewContactEvent.ChangeContactEmail(it)) }\n)\n}\n}\n\n}\n\n@Composable\nfun PhotoPicker(\naddImageUri: (Uri) -> Unit,\nimageUri: Uri?\n) {\nval photoPicker = rememberLauncherForActivityResult(\ncontract = ActivityResultContracts.PickVisualMedia()\n) {\nif (it != null) {\naddImageUri(it)\n}\n}\n\nBox(\ncontentAlignment = Alignment.Center,\nmodifier = Modifier\n.padding(15.dp)\n.size(120.dp)\n.border(\nborder = BorderStroke(2.dp, MaterialTheme.colorScheme.primary),\nshape = RoundedCornerShape(100.dp)\n)\n.clip(shape = RoundedCornerShape(100.dp))\n) {\nIcon(\nimageVector = Icons.Default.Image,\ncontentDescription = null,\ntint = MaterialTheme.colorScheme.primary,\nmodifier = Modifier.size(40.dp)\n)\nAsyncImage(\nmodifier = Modifier\n.size(120.dp)\n.clip(shape = RoundedCornerShape(100.dp))\n.clickable {\nphotoPicker.launch(\nPickVisualMediaRequest(\nActivityResultContracts.PickVisualMedia.ImageOnly\n)\n)\n},\nmodel = ImageRequest.Builder(LocalContext.current)\n.data(imageUri)\n.crossfade(enable = true)\n.build(),\ncontentDescription = null,\ncontentScale = ContentScale.Crop,\n)\n}\n\n}\n

      Eg\u00e9sz\u00edts\u00fck ki a Screen oszt\u00e1lyunkat az \u00fajabb \u00fatvonallal:

      object AddContact: Screen(route = \"add_contact\")\n

      A Navgraph-ban kezelj\u00fck a FAB-unk \u00e9rint\u00e9s\u00e9t, \u00e9s vegy\u00fck fel az \u00faj k\u00e9perny\u0151t:

      onFabClick = {\nnavController.navigate(Screen.AddContact.route)\n}\n
      composable(route = Screen.AddContact.route) {\nAddNewContactScreen(\nonNavigateBack = {\nnavController.popBackStack(route = Screen.Contacts.route, inclusive = false)\n}\n)\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a saj\u00e1t neveddel kit\u00f6lt\u00f6tt hozz\u00e1ad\u00e1si k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

      A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/permissions/#kontaktok-szerkesztese","title":"Kontaktok szerkeszt\u00e9se","text":"

      Vegy\u00fcnk fel egy \u00faj use-case-t a n\u00e9vjegyek r\u00e9szleteinek megjelen\u00edt\u00e9s\u00e9re:

      GetContactDetailsUseCase.kt:

      class GetContactDetailsUseCase (\nprivate val context: Context\n) {\noperator fun invoke(id: String): Contact {\nreturn context.getContactDetails(id)\n}\n}\n

      Eg\u00e9sz\u00edts\u00fck ki a ContactOperations oszt\u00e1lyunkat az al\u00e1bbiakkal:

      ContactsOperations.kt:

          fun Context.getContactDetails(id: String): Contact {\nreturn Contact(\nid = id,\nname = getContactName(id),\nphoneNumber = getContactPhoneNumber(id),\nemailAddress = getContactEmail(id),\nphoto = getContactPhoto(id)\n)\n}\n

      A feature.contact_details package-ben hozzuk l\u00e9tre a hozz\u00e1 tartoz\u00f3 ViewModel oszt\u00e1lyt:

      ContactDetailsViewModel.kt:

      class ContactDetailsViewModel(\ngetContactDetails: GetContactDetailsUseCase,\nprivate val makeCall: MakeACallUseCase,\nprivate val sendSMS: SendSMSUseCase,\nsavedStateHandle: SavedStateHandle\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(ContactDetailsState())\nval state = _state.asStateFlow()\n\nfun onEvent(event: ContactDetailsEvent) {\nwhen(event) {\nis ContactDetailsEvent.MakeCall -> {\nmakeCall(state.value.contact.phoneNumber)\n}\nis ContactDetailsEvent.SendSms -> {\nsendSMS(state.value.contact.phoneNumber)\n}\n}\n}\n\ninit {\nval id = checkNotNull<String>(savedStateHandle[\"id\"])\nviewModelScope.launch(Dispatchers.IO) {\ntry {\n_state.update { it.copy(isLoading = true) }\nval contact = getContactDetails(id)\n_state.update { it.copy(\nisLoading = false,\ncontact = contact\n) }\n} catch (e: IOException) {\n_state.update { it.copy(\nisLoading = false,\nerror = e\n) }\n}\n\n}\n}\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval context = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application).baseContext\nval savedStateHandle = createSavedStateHandle()\nContactDetailsViewModel(\ngetContactDetails = GetContactDetailsUseCase(context),\nmakeCall = MakeACallUseCase(context),\nsendSMS = SendSMSUseCase(context),\nsavedStateHandle = savedStateHandle\n)\n}\n}\n}\n}\n\ndata class ContactDetailsState(\nval isLoading: Boolean = false,\nval error: Throwable? = null,\nval isError: Boolean = error != null,\nval contact: Contact = Contact()\n)\n\nsealed class ContactDetailsEvent {\nobject MakeCall: ContactDetailsEvent()\nobject SendSms: ContactDetailsEvent()\n\n}\n

      Hozzuk l\u00e9tre a ui.common package-ben az al\u00e1bbi oszt\u00e1lyt a gombjainknak:

      ActionButton.kt:

      @Composable\nfun ActionButton(\nmodifier: Modifier = Modifier,\nonClick: () -> Unit,\nicon: UiIcon,\nlabel: UiText? = null\n) {\n\nSurface(\nmodifier = modifier\n.clickable { onClick() },\nshape = RoundedCornerShape(5.dp),\ncolor = MaterialTheme.colorScheme.secondary\n) {\nColumn(\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nicon.AsImage(\nmodifier = Modifier\n.padding(\nstart = 10.dp,\nend = 10.dp,\ntop = 5.dp,\nbottom = if (label == null) 5.dp else 0.dp\n)\n.size(if (label == null) 40.dp else 20.dp),\ntint = MaterialTheme.colorScheme.onSecondary\n)\n\nlabel?.AsText(\nmodifier = Modifier.padding(bottom = 5.dp),\ncolor = MaterialTheme.colorScheme.onSecondary\n)\n}\n}\n}\n\n@Preview\n@Composable\nfun ActionButton_Preview() {\nActionButton(\nonClick = { /*TODO*/ },\nicon = VectorImage(Icons.Default.Warning),\nlabel = UiText.DynamicString(\"Warning\")\n)\n}\n

      Ezt k\u00f6vet\u0151en a fel\u00fcletet is elk\u00e9sz\u00edthetj\u00fck:

      ContactDetailsScreen.kt:

      @ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Composable\nfun ContactDetailsScreen(\nmodifier: Modifier = Modifier,\nonNavigateBack: () -> Unit,\nviewModel: ContactDetailsViewModel = viewModel(factory = ContactDetailsViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval context = LocalContext.current\n\nScaffold(\ntopBar = {\nTopAppBar(\ntitle = {\nText(text = \"Contacts\")\n},\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nVectorImage(Icons.Default.ArrowBack).AsImage()\n}\n},\ncolors = TopAppBarDefaults.smallTopAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nif (state.isLoading) {\nCircularProgressIndicator(color = MaterialTheme.colorScheme.primary)\n} else if (state.isError) {\nText(text = state.error.toUiText().asString(context))\n} else {\nval contact = state.contact\nval photo = contact.photo\nColumn(\nmodifier = Modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.background),\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nif (photo != null) {\nImage(\nbitmap = photo.asImageBitmap(),\ncontentDescription = null,\nmodifier = Modifier\n.padding(15.dp)\n.size(120.dp)\n.clip(shape = RoundedCornerShape(100.dp))\n.background(Color.Black),\nalignment = Alignment.Center\n)\n}\nText(\ntext = contact.name,\nstyle = MaterialTheme.typography.titleLarge,\ntextAlign = TextAlign.Center\n)\nSpacer(modifier = Modifier.height(15.dp))\nRow(modifier = Modifier\n.fillMaxWidth()\n.padding(horizontal = 5.dp)) {\nActionButton(\nonClick = { viewModel.onEvent(ContactDetailsEvent.MakeCall) },\nicon = VectorImage(Icons.Default.Call),\nmodifier = Modifier.weight(1f)\n)\nActionButton(\nonClick = { viewModel.onEvent(ContactDetailsEvent.SendSms) },\nicon = VectorImage(Icons.Default.Sms),\nmodifier = Modifier\n.weight(1f)\n.padding(horizontal = 5.dp)\n)\nActionButton(\nonClick = { /*TODO*/ },\nicon = VectorImage(Icons.Default.Mail),\nmodifier = Modifier.weight(1f)\n)\n}\nSpacer(modifier = Modifier.height(15.dp))\nContactDataItem(\nleadingIcon = VectorImage(Icons.Default.Home),\nlabel = UiText.StringResource(R.string.contact_data_label_phonenumber),\nvalue = contact.phoneNumber,\nonValueChange = { },\nmodifier = Modifier.padding(horizontal = 5.dp)\n)\nContactDataItem(\nleadingIcon = VectorImage(Icons.Default.Home),\nlabel = UiText.StringResource(R.string.contact_data_label_email),\nvalue = contact.emailAddress,\nonValueChange = { },\nmodifier = Modifier.padding(horizontal = 5.dp)\n)\n}\n}\n}\n}\n}\n\n@ExperimentalMaterial3Api\n@ExperimentalFoundationApi\n@Preview(showBackground = true)\n@Composable\nfun ContactDetailsScreen_Preview() {\nContactDetailsScreen(onNavigateBack = { })\n}\n

      Eg\u00e9sz\u00edts\u00fck ki a Screen oszt\u00e1lyunkat az \u00fajabb \u00fatvonallal:

      object ContactDetails: Screen(route = \"contact/{id}\") {\nfun passId(id: String) = \"contact/$id\"\n}\n

      A Navgraph-ban kezelj\u00fck a listaelemek \u00e9rint\u00e9s\u00e9t, \u00e9s vegy\u00fck fel az \u00faj k\u00e9perny\u0151t:

      onListItemClick = { id ->\nnavController.navigate(Screen.ContactDetails.passId(id))\n},\n
      composable(\nroute = Screen.ContactDetails.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.StringType\n}\n)\n) {\nContactDetailsScreen(\nonNavigateBack = {\nnavController.popBackStack(route = Screen.Contacts.route, inclusive = false)\n}\n)\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a saj\u00e1t neveddel kit\u00f6lt\u00f6tt r\u00e9szletek k\u00e9perny\u0151 (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet.

      A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/persistence/","title":"Labor06 - Perzisztens adatt\u00e1rol\u00e1s a Room k\u00f6nyvt\u00e1rral","text":""},{"location":"laborok/persistence/#bevezetes","title":"Bevezet\u00e9s","text":"

      A labor c\u00e9lja a perzisztens adatt\u00e1rol\u00e1s megismer\u00e9se ORM technik\u00e1val, a Room k\u00f6nyvt\u00e1r seg\u00edts\u00e9g\u00e9vel. A labor egy\u00fattal azt is bemutatja, hogy egy modern, \u00f6sszetett alkalamz\u00e1s k\u00fcl\u00f6nb\u00f6z\u0151 r\u00e9szeit (adatel\u00e9r\u00e9s, \u00fczleti logika, felhaszn\u00e1l\u00f3i fel\u00fclet) hogyan tudunk megfelel\u0151 r\u00e9tegez\u00e9ssel, \u00e1ttekinthet\u0151 \u00e9s j\u00f3l karban tarthat\u00f3 architekt\u00far\u00e1val kifejleszteni.

      Ezeknek az elveknek a megismer\u00e9s\u00e9hez az \u00f6t\u00f6dik laboron megismert Todo alkalmaz\u00e1s kidolgozottabb verzi\u00f3j\u00e1t k\u00e9sz\u00edtj\u00fck el.

      "},{"location":"laborok/persistence/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

      A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

      "},{"location":"laborok/persistence/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
      1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

      2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

        Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

      3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

      4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

      A Room megismer\u00e9s\u00e9hez ebben a laborban egy el\u0151re elk\u00e9sz\u00edtett projektben fogunk dolgozni, ez megtal\u00e1lhat\u00f3 a repository-n bel\u00fcl. Ind\u00edtsuk el az Android Studio-t, majd nyissuk meg a projektet.

      FILE PATH

      A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

      Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

      "},{"location":"laborok/persistence/#a-reteges-architektura-kialakitasa","title":"A r\u00e9teges architekt\u00fara kialak\u00edt\u00e1sa","text":"

      Egy \u00f6sszetettebb alkalmaz\u00e1son bel\u00fcl a k\u00f3dot r\u00e9tegekbe szervezz\u00fck, hogy a m\u0171k\u00f6d\u00e9s j\u00f3l \u00e1tl\u00e1that\u00f3 legyen, illetve hogy az alkalmaz\u00e1s egyes r\u00e9szei kev\u00e9sb\u00e9 f\u00fcggjenek a t\u00f6bbit\u0151l. A r\u00e9teges architekt\u00far\u00e1nkban lesz egy domain modell \u00e9s \u00fczleti logika, amely a megval\u00f3s\u00edtott funkcionalit\u00e1s megjelen\u00e9st\u0151l \u00e9s adatb\u00e1ziskezel\u00e9st\u0151l f\u00fcggetlen r\u00e9sz\u00e9t k\u00e9pezi. A felhaszn\u00e1l\u00f3i fel\u00fcleten bevitt adatoknak k\u00fcl\u00f6n modellje lesz, \u00e9s ezt majd \u00e1t kell alak\u00edtanunk a f\u00fcggetlen domain modellre. Az adatb\u00e1zisba ment\u00e9shez szint\u00e9n k\u00fcl\u00f6n modellt haszn\u00e1lunk, \u00e9s ebben az esetben is konverzi\u00f3ra lesz sz\u00fcks\u00e9g\u00fcnk a domain modell \u00e9s az adatb\u00e1zismodell k\u00f6z\u00f6tt. Az \u00edgy kialak\u00edtott r\u00e9tegez\u00e9ssel a megjelen\u00edt\u00e9s \u00e9s az adatb\u00e1zismodell sem f\u00fcggenek egym\u00e1st\u00f3l, csup\u00e1n a f\u00fcggetlen domain modellt\u0151l. \u00cdgy mind a megjelen\u00e9s, mind az adab\u00e1ziskezel\u0151 r\u00e9teg k\u00f6nnyebben m\u00f3dos\u00edthat\u00f3 a m\u00e1sikt\u0151l f\u00fcggetlen\u00fcl.

      "},{"location":"laborok/persistence/#a-domainmodell-es-az-uzleti-logika-elkeszitese","title":"A domainmodell \u00e9s az \u00fczleti logika elk\u00e9sz\u00edt\u00e9se","text":"

      El\u0151sz\u00f6r a domain r\u00e9teggel foglalkozunk, ez tulajdonk\u00e9ppen m\u00e1r rendelkez\u00e9sre \u00e1ll a domain.model package-ben. Ez a domain (a megoldand\u00f3 feladat) nagyj\u00e1b\u00f3l technol\u00f3giaf\u00fcggetlen r\u00e9sze, amelybe m\u00e9g nem vegy\u00fclnek a konkr\u00e9t adatt\u00e1rol\u00e1si technol\u00f3gi\u00e1val vagy megjelen\u00edt\u00e9ssel kapcsolatos r\u00e9szletek. Ezzel a k\u00f6zb\u00fcls\u0151 r\u00e9teggel az alkalmaz\u00e1sunk komponensei laz\u00e1bban csatoltt\u00e1 v\u00e1lnak, \u00e9s megk\u00f6nny\u00edtik, hogy kev\u00e9s m\u00f3dos\u00edt\u00e1ssal lecser\u00e9lj\u00fck ak\u00e1r az adatb\u00e1ziskezel\u00e9s\u00e9rt felel\u0151s Roomot, ak\u00e1r a megjelen\u00edt\u00e9st. Az itt megval\u00f3s\u00edtott \u00fczleti logika m\u0171veletek nem f\u00fcggenek k\u00f6zvetlen a Roomt\u00f3l, csak a reposiory komponensekt\u0151l, \u00e9s mivel a tennival\u00f3k f\u00fcggetlen domainmodellj\u00e9vel dolgoznak, a megjelen\u00edt\u00e9st\u0151l is f\u00fcggetlenek.

      Term\u00e9szetesen m\u00e1s architekt\u00far\u00e1val is lehet m\u0171k\u00f6d\u0151k\u00e9pes alkalmaz\u00e1st k\u00e9sz\u00edteni, de ez a megold\u00e1s v\u00e1lt Android platformon konvencion\u00e1liss\u00e1, ez\u00e9rt ha ezt k\u00f6vetj\u00fck, akkor k\u00f6nnyebben tudunk egy\u00fctt dolgozni m\u00e1s fejleszt\u0151kkel. A hivatalos dokument\u00e1ci\u00f3 is szentel ennek a k\u00e9rd\u00e9snek egy fejezetet: https://developer.android.com/topic/architecture/domain-layer

      Tekints\u00fck \u00e1t a domain.model package-et, n\u00e9zz\u00fck meg, hogyan \u00e9p\u00fcl fel a domainmodell!

      "},{"location":"laborok/persistence/#a-felhasznaloi-felulet-elkeszitese","title":"A felhaszn\u00e1l\u00f3i fel\u00fclet elk\u00e9sz\u00edt\u00e9se","text":"

      Most a felhaszn\u00e1l\u00f3i fel\u00fclet modellj\u00e9vel haladunk tov\u00e1bb. Ezek a kor\u00e1bban l\u00e9trehozott domainmodellhez igen hasonlatosak, de a rugalmasabb architekt\u00fara \u00e9s a laza csatol\u00e1s megval\u00f3s\u00edt\u00e1sa miatt k\u00fcl\u00f6n modelleket k\u00e9sz\u00edt\u00fcnk a fel\u00fcleten megjelen\u00edtett adatokhoz. Ez egy ilyen egyszer\u0171 alkalmaz\u00e1sn\u00e1l el\u0151sz\u00f6r indokolatlan duplik\u00e1ci\u00f3nak t\u0171nhet, az az \u00e9rz\u00e9s\u00fcnk, hogy bizonyos dolgokat t\u00f6bbsz\u00f6r implement\u00e1lunk. Azonban ahogy egy alkalmaz\u00e1s fejl\u0151dik, b\u0151v\u00fcl, egy ilyen laz\u00e1n csatolt \u00e9s \u00e1tl\u00e1that\u00f3 architekt\u00fara mindenk\u00e9pp kifizet\u0151d\u0151v\u00e9 v\u00e1lik.

      A felhaszn\u00e1l\u00f3i fel\u00fclet modellje is m\u00e1r rendelkez\u00e9sre \u00e1ll a kiindul\u00f3 projektben. Tekints\u00fck \u00e1t a ui.model package-et! Ebben m\u00e1r rendelkez\u00e9sre \u00e1ll a PriorityUi \u00e9s a TodoUi oszt\u00e1ly, illetve konverzi\u00f3s logika is van a f\u00e1jloban a domain modellbe/modellb\u0151l t\u00f6rt\u00e9n\u0151 konvert\u00e1l\u00e1shoz. Tal\u00e1lunk m\u00e9g itt egy UiText oszt\u00e1lyt a felhaszn\u00e1l\u00f3i fel\u00fcleten megjelen\u0151 sz\u00f6veges \u00fczenetek k\u00f6nnyebb kezel\u00e9s\u00e9hez.

      A fent \u00e1ttekintett UI modellekre \u00e9p\u00fclnek a felhaszn\u00e1l\u00f3i fel\u00fclet megjelen\u00edtett r\u00e9szei. Ezek a ui.common package-ben m\u00e1r szint\u00e9n rendelkez\u00e9sre \u00e1llnak. Tekints\u00fck \u00e1t ezeket is!

      Most az elemi fel\u00fcleti elemekkel v\u00e9gezt\u00fcnk, most j\u00f6nnek a t\u00e9nyleges k\u00e9perny\u0151k. Ezek a feature package-ben vannak. Ezen bel\u00fcl h\u00e1rom f\u0151 funkci\u00f3t fogunk megk\u00fcl\u00f6nb\u00f6ztetni: l\u00e9trehoz\u00e1s, list\u00e1z\u00e1s, megjelen\u00edt\u00e9s. Ezek egy-egy subpackage-ben vannak, \u00e9s a kiindul\u00f3 projektben ez is mind rendelkez\u00e9sre \u00e1llnak. Tekints\u00fck \u00e1t ezeket is, \u00e9s eleven\u00edts\u00fck fel a funkci\u00f3jukat.

      A fel\u00fcleti elemek elk\u00e9sz\u00edt\u00e9se ut\u00e1n gondoskodni kell a k\u00f6zt\u00fck t\u00f6rt\u00e9n\u0151 navig\u00e1ci\u00f3r\u00f3l is. Ez is m\u00e1r rendelkez\u00e9sre \u00e1ll a navigation package-ben. N\u00e9zz\u00fck \u00e1t ezeket is!

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a teend\u0151k list\u00e1ja a fut\u00f3 alkalmaz\u00e1sban, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/persistence/#az-adatreteg-elkeszitese","title":"Az adatr\u00e9teg elk\u00e9sz\u00edt\u00e9se","text":"

      Most elk\u00e9sz\u00edtj\u00fck az adatb\u00e1zis kezel\u00e9s\u00e9\u00e9rt felel\u0151s komponenseket. Most is n\u00e9hol \u00fagy t\u0171nhet majd, hogy bizonyos dolgokat \"dupl\u00e1n\" val\u00f3s\u00edtunk meg, azonban ennek az el\u0151nyei egy val\u00f3s komplex alkalmaz\u00e1sban mindig \u00e9rv\u00e9nyes\u00fclnek, ez\u00e9rt \u00e9rdemes megismern\u00fcnk, \u00e9s haszn\u00e1lnunk ezt az architektur\u00e1lis szervez\u00e9st.

      Az els\u0151 l\u00e9p\u00e9s, hogy a Roomot mint f\u00fcgg\u0151s\u00e9get vegy\u00fck fel a projekt\u00fcnkbe. Ehhez el\u0151sz\u00f6r a projekt szint\u0171 build.gradle.kts f\u00e1jlban \u00e1ll\u00edtsuk be a haszn\u00e1lni k\u00edv\u00e1nt kapt plugin verzi\u00f3j\u00e1t a f\u00fcgg\u0151s\u00e9gek k\u00f6zt:

      kotlin(\"kapt\") version \"1.9.10\" apply false\n

      Majd a modul szint\u0171 build.gradle.kts f\u00e1jlban enged\u00e9lyezz\u00fck a kapt plugint:

      plugins {\nid(\"com.android.application\")\nid(\"org.jetbrains.kotlin.android\")\nkotlin(\"kapt\")\n}\n

      \u00c9s ugyanitt vegy\u00fck is fel a Room k\u00f6nyvt\u00e1rat:

          // Room\nval room_version = \"2.6.0\"\nimplementation(\"androidx.room:room-runtime:$room_version\")\nkapt(\"androidx.room:room-compiler:$room_version\")\nimplementation(\"androidx.room:room-ktx:$room_version\")\n

      Most sz\u00fcks\u00e9g\u00fcnk van az elmentett tennival\u00f3k adatmodellj\u00e9re. Mivel a megk\u00f6zel\u00edt\u00e9s\u00fcnkben a Room k\u00f6nyvt\u00e1rat haszn\u00e1ljuk, ez azt jelenti, hogy egy olyan oszt\u00e1lyt k\u00e9sz\u00edt\u00fcnk, amellyel a szoftver\u00fcnkben fut\u00e1sid\u0151ben egy teend\u0151 j\u00f3l modellezhet\u0151, \u00e9s ezt az oszt\u00e1lyt megfeleltetj\u00fck az SQLite adatb\u00e1zisunk egy t\u00e1bl\u00e1j\u00e1val. Ez \u00edgy k\u00e9nyelmes, hiszen a rel\u00e1ci\u00f3s adatmodell kiforrott, k\u00f6zismert, ez\u00e9rt az adatokat gyakran t\u00e1bl\u00e1kban akarjuk t\u00e1rolni, ugyanakkor a programunkban az objektumorient\u00e1lt szeml\u00e9letben mozgunk otthonosan, \u00e9s az adatokat ez\u00e9rt objektumokban szeretj\u00fck t\u00e1rolni. Ezeket az oszt\u00e1lyokat a szoftverfejleszt\u00e9si terminol\u00f3gi\u00e1ban entit\u00e1soknak szoktuk nevezni.

      Hozzunk l\u00e9tre ez\u00e9rt egy data.entities package-et, \u00e9s ebbe vegy\u00fck fel a k\u00f6vetkez\u0151t:

      @Entity(tableName = \"todo_table\")\ndata class TodoEntity(\n@PrimaryKey(autoGenerate = true) val id: Int,\nval title: String,\nval priority: Priority,\nval dueDate: LocalDate,\nval description: String\n)\n\nfun TodoEntity.asTodo(): Todo = Todo(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = dueDate,\ndescription = description\n)\n\nfun Todo.asTodoEntity(): TodoEntity = TodoEntity(\nid = id,\ntitle = title,\npriority = priority,\ndueDate = dueDate,\ndescription = description\n)\n

      Ebben a k\u00f3dban a Room k\u00f6nyvt\u00e1r annot\u00e1ci\u00f3val meg van jel\u00f6lve, hogy az oszt\u00e1ly egy entit\u00e1s lesz, \u00e9s a todo_table nev\u0171 t\u00e1bl\u00e1ba lesznek a p\u00e9ld\u00e1nyai lek\u00e9pezve, valamint az id nev\u0171 tagv\u00e1ltoz\u00f3j\u00e1nak megfelel\u0151 oszlop lesz az els\u0151dleges kulcs, \u00e9s ennek \u00e9rt\u00e9keit besz\u00far\u00e1skor fogja egyedi \u00e9rt\u00e9kk\u00e9nt gener\u00e1lni a k\u00f6rnyezet, vagyis nem kell nek\u00fcnk gondoskodnunk r\u00f3la, hogy minden \u00faj teend\u0151 \u00faj egyedi azonos\u00edt\u00f3t kapjon.

      A k\u00f6vetkez\u0151 l\u00e9p\u00e9s, hogy az entit\u00e1shoz kapcsol\u00f3d\u00f3 alapm\u0171veleteket is t\u00e1mogassuk a Room k\u00f6nyvt\u00e1r seg\u00edts\u00e9g\u00e9vel. Ezt egy DAO (Data Access Object) komponenssel fogjuk megval\u00f3s\u00edtani. A DAO egy - szint\u00e9n nem csak Android alatt alkalmazott - tervez\u00e9si minta, amelynek a l\u00e9nyege, hogy az egy entit\u00e1shoz kapcsol\u00f3d\u00f3 \u00f6sszes adatb\u00e1zism\u0171veleteket egy komponensbe gy\u0171jtj\u00fck \u00f6ssze. Ez egyr\u00e9szt j\u00f3l \u00e1ttekinthet\u0151, illetve ha az adatb\u00e1zist le szeretn\u00e9nk cser\u00e9lni m\u00e1s technol\u00f3gi\u00e1ra, akkor elvileg elegend\u0151 lenne a DAO komponens m\u00f3dos\u00edt\u00e1sa, b\u00e1r ilyen jelleg\u0171 m\u00f3dos\u00edt\u00e1sra manaps\u00e1g \u00e1ltal\u00e1ban nincs sz\u00fcks\u00e9g.

      Hozzunk l\u00e9tre egy data.dao package-et, \u00e9s ebbe vegy\u00fck fel az al\u00e1bbit:

      @Dao\ninterface TodoDao {\n\n@Insert(onConflict = OnConflictStrategy.REPLACE)\nsuspend fun insertTodo(todo: TodoEntity)\n\n@Query(\"SELECT * FROM todo_table\")\nfun getAllTodos(): Flow<List<TodoEntity>>\n\n@Query(\"SELECT * FROM todo_table WHERE id = :id\")\nfun getTodoById(id: Int): Flow<TodoEntity>\n\n@Update\nsuspend fun updateTodo(todo: TodoEntity)\n\n@Query(\"DELETE FROM todo_table WHERE id = :id\")\nsuspend fun deleteTodo(id: Int)\n}\n

      L\u00e1thatjuk, hogy egyr\u00e9szt maga az interf\u00e9sz is meg van jel\u00f6lve, mint DAO komponens, m\u00e1sr\u00e9szt az egyes m\u0171veleteken is Room annot\u00e1ci\u00f3k vannak. A Room az annot\u00e1ci\u00f3b\u00f3l, illetve az annot\u00e1lt met\u00f3dus param\u00e9tereib\u0151l \u00e9s visszat\u00e9r\u00e9si \u00e9rt\u00e9k\u00e9b\u0151l ki tudja k\u00f6vetkeztetni a sz\u00e1nd\u00e9kunkat. Besz\u00e9lj\u00fck \u00e1t az egyes met\u00f3dusok jelent\u00e9s\u00e9t a gyakorlatvezet\u0151vel! Mivel ez a komponens egy interf\u00e9sz, ezt nem mi fogjuk implement\u00e1lni, hanem a Room k\u00e9sz\u00edti el fut\u00e1sid\u0151ben az implement\u00e1ci\u00f3j\u00e1t.

      Ezut\u00e1n egy repository komponenst k\u00e9sz\u00edt\u00fcnk. Ez n\u00e9mileg \u00fagy t\u0171nik, mintha nem adna hozz\u00e1 t\u00fal sokat a DAO-hoz, azonban fontos c\u00e9lja, hogy a fels\u0151bb r\u00e9tegeket f\u00fcggetlen\u00edtse a Roomt\u00f3l, hogy ne k\u00f6zvetlen att\u00f3l f\u00fcggjenek. Tulajdonk\u00e9ppen a kiindul\u00f3 projektben m\u00e1r l\u00e9tezik egy ilyen komponens, de ezt \u00e1t kell alak\u00edtanunk, mert a kor\u00e1bbi verzi\u00f3 m\u00e9g nem haszn\u00e1lt f\u00fcggetlen dom\u00e9n- \u00e9s adatb\u00e1zismodelleket.

      El\u0151sz\u00f6r k\u00e9sz\u00edts\u00fcnk egy data.repository package-et, \u00e9s ebbe mozgassuk \u00e1t az interf\u00e9szt, majd cser\u00e9lj\u00fck le az al\u00e1bbira:

      interface TodoRepository {\nfun getAllTodos(): Flow<List<TodoEntity>>\n\nfun getTodoById(id: Int): Flow<TodoEntity>\n\nsuspend fun insertTodo(todo: TodoEntity)\n\nsuspend fun updateTodo(todo: TodoEntity)\n\nsuspend fun deleteTodo(id: Int)\n}\n

      Majd pedig ennek az implement\u00e1ci\u00f3j\u00e1t is k\u00e9sz\u00edts\u00fck el:

      class TodoRepositoryImpl(private val dao: TodoDao) : TodoRepository {\n\noverride fun getAllTodos(): Flow<List<TodoEntity>> = dao.getAllTodos()\n\noverride fun getTodoById(id: Int): Flow<TodoEntity> = dao.getTodoById(id)\n\noverride suspend fun insertTodo(todo: TodoEntity) { dao.insertTodo(todo) }\n\noverride suspend fun updateTodo(todo: TodoEntity) { dao.updateTodo(todo) }\n\noverride suspend fun deleteTodo(id: Int) { dao.deleteTodo(id) }\n}\n

      A kor\u00e1bbi implement\u00e1ci\u00f3t, a MemoryTodoRepository oszt\u00e1lyt most t\u00f6r\u00f6lj\u00fck ki, erre nem lesz m\u00e1r sz\u00fcks\u00e9g.

      A feature package-ekben most a viewmodelek is elt\u00f6rtek, mert egyr\u00e9szt a repository m\u00e1sik package-be ker\u00fclt, m\u00e1sr\u00e9szt a kor\u00e1bbi repository implement\u00e1ci\u00f3t t\u00f6r\u00f6lt\u00fck. Ezt nem tudjuk k\u00f6nnyen kijav\u00edtani, mert az \u00faj repository a konstruktor\u00e1ban a DAO komponenst v\u00e1rja, azt viszont nem konstruktorh\u00edv\u00e1ssal hozzuk l\u00e9tre, hanem a Room gy\u00e1rtja majd le, ez\u00e9rt ehhez m\u00e9g kell n\u00e9mi k\u00f3dot \u00edrnunk. M\u00e1sr\u00e9szt pedig nem is k\u00edv\u00e1natos, hogy a viewmodel k\u00f6zvetlen a repository-t h\u00edvja, hiszen ahogy fentebb indokoltuk, nem el\u0151ny\u00f6s, ha a felhaszn\u00e1l\u00f3i fel\u00fclet komponensek k\u00f6zvetlen az adatb\u00e1ziskezel\u00e9si r\u00e9teggel is f\u00fcggnek egym\u00e1st\u00f3l. Ez\u00e9rt majd a dom\u00e9nmodellhez kapcsol\u00f3d\u00f3 \u00fczletilogika-komponenseket fogunk bevezetni. De el\u0151bb fejezz\u00fck be az adatb\u00e1ziskezel\u00e9si r\u00e9teg implement\u00e1ci\u00f3j\u00e1t!

      M\u00e9g h\u00e1rom feladatunk van az adatb\u00e1ziskezel\u0151 r\u00e9teg kialak\u00edt\u00e1s\u00e1ban. Az els\u0151, hogy a let\u00e1rolni k\u00edv\u00e1nt Java-t\u00edpusok \u00e9s az SQLite be\u00e9p\u00edtett t\u00edpusai k\u00f6zt nem teljes az egyez\u00e9s. Ezt konverterekkel kell \u00e1thidalnunk. K\u00e9sz\u00edts\u00fcnk egy data.converters package-et, \u00e9s ebbe el\u0151sz\u00f6r a d\u00e1tumokkal kapcsolatos konverterek implement\u00e1ci\u00f3j\u00e1t:

      object LocalDateConverter {\n\n@TypeConverter\nfun LocalDate.asString(): String = this.toString()\n\n@TypeConverter\nfun String.asLocalDateTime(): LocalDate = this.toLocalDate()\n}\n

      A met\u00f3dusokon lev\u0151 @TypeConverter annot\u00e1ci\u00f3 jelzi a Room sz\u00e1m\u00e1ra, hogy ezeket a f\u00fcggv\u00e9nyeket konverzi\u00f3hoz haszn\u00e1lhatja, a szignat\u00far\u00e1b\u00f3l pedig egy\u00e9rtelm\u0171en kik\u00f6vetkeztethet\u0151, hogy milyen t\u00edpusok k\u00f6zt tud vel\u00fck konvert\u00e1lni. Most a priorit\u00e1s enumer\u00e1ci\u00f3t is t\u00e1mogassuk a megfelel\u0151 konverterekkel:

      object TodoPriorityConverter {\n\n@TypeConverter\nfun Priority.asString(): String = this.name\n\n@TypeConverter\nfun String.asPriority(): Priority {\nreturn when(this) {\nPriority.LOW.name -> Priority.LOW\nPriority.MEDIUM.name -> Priority.MEDIUM\nPriority.HIGH.name -> Priority.HIGH\nelse -> Priority.LOW\n}\n}\n}\n

      A m\u00e1sodik l\u00e9p\u00e9s, hogy az elk\u00e9sz\u00fclt komponensekb\u0151l \u00f6ssze kell \u00e1ll\u00edtanunk az adatb\u00e1ziskezel\u00e9s glob\u00e1lis be\u00e1ll\u00edt\u00e1sait \u00f6sszefog\u00f3 RoomDatabase implement\u00e1ci\u00f3nkat. Ezt tegy\u00fck a data package gy\u00f6ker\u00e9be:

      @Database(entities = [TodoEntity::class], version = 1)\n@TypeConverters(TodoPriorityConverter::class, LocalDateConverter::class)\nabstract class TodoDatabase : RoomDatabase() {\nabstract val dao: TodoDao\n}\n

      Figyelj\u00fck meg az annot\u00e1ci\u00f3kat! Itt meg vannak hivatkozva a haszn\u00e1lni k\u00edv\u00e1nt entit\u00e1sok \u00e9s konverterek, illetve az adatb\u00e1ziss\u00e9ma egy verzi\u00f3sz\u00e1mot is kap. Ez az\u00e9rt hasznos, mert ahogy fejl\u0151dik az alkalmaz\u00e1s, az adatb\u00e1zis s\u00e9m\u00e1ja is v\u00e1ltozhat, fejl\u0151dhet. Ilyen esetekben arra is lehet\u0151s\u00e9get ad a Room, hogy migr\u00e1ci\u00f3kat biztos\u00edtsunk a r\u00e9gebbi adatb\u00e1ziss\u00e9m\u00e1kr\u00f3l t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9sre. Ha telep\u00edtve van az alkalmaz\u00e1s r\u00e9gi verzi\u00f3ja, amely m\u00e1r mentett el adatokat az eszk\u00f6zre, \u00e9s friss\u00edtj\u00fck az alkalmaz\u00e1st, akkor a k\u00f6vetkez\u0151 indul\u00e1s ut\u00e1n a Room megvizsg\u00e1lja, hogy t\u00f6rt\u00e9nt-e v\u00e1ltoz\u00e1s az adatb\u00e1zis verzi\u00f3j\u00e1ban, \u00e9s sz\u00fcks\u00e9g eset\u00e9n futtatja a migr\u00e1ci\u00f3kat.

      Az utols\u00f3 l\u00e9p\u00e9s az adatb\u00e1ziskezel\u00e9s implement\u00e1ci\u00f3j\u00e1hoz, hogy az alkalmaz\u00e1s indul\u00e1sakor inicializ\u00e1ljuk az adatb\u00e1zist. Ehhez egy Application oszt\u00e1llyal kell kieg\u00e9sz\u00edten\u00fcnk az alkalmaz\u00e1sunkat. Az Application oszt\u00e1ly a teljes alkalmaz\u00e1s \u00e9letciklus-esem\u00e9nyeit tudja kezelni, illetve arra is alkalmas, hogy itt glob\u00e1lis adatokat ments\u00fcnk el, amelyeket majd az alkalmaz\u00e1s tetsz\u0151leges komponenseib\u0151l el\u00e9rhet\u0151v\u00e9 akarunk tenni. Ezt az alkalmaz\u00e1s \"root package\"-\u00e9be, a MainActivity mell\u00e9 tegy\u00fck:

      class TodoApplication : Application() {\n\ncompanion object {\nprivate lateinit var db: TodoDatabase\n\nlateinit var repository: TodoRepositoryImpl\n}\n\noverride fun onCreate() {\nsuper.onCreate()\ndb = Room.databaseBuilder(\napplicationContext,\nTodoDatabase::class.java,\n\"todo_database\"\n).fallbackToDestructiveMigration().build()\n\nrepository = TodoRepositoryImpl(db.dao)\n}\n}\n

      L\u00e1that\u00f3, hogy az alkalmaz\u00e1s indul\u00e1sakor l\u00e9trehozzuk az adatb\u00e1zist \u00e9s a TodoRepositoryImpl-et, majd ezeket az oszt\u00e1ly companion objectj\u00e9be el is mentj\u00fck. Hogy az Application oszt\u00e1ly t\u00e9nyleg az elv\u00e1s\u00e1runk szerint m\u0171k\u00f6dj\u00fcnk, m\u00e9g meg is kell hivatkozni a Manifest.xml f\u00e1jl application elem\u00e9ben. Cser\u00e9lj\u00fck az application elem nyit\u00f3 tagj\u00e9t az al\u00e1bbira:

          <application\nandroid:name=\".TodoApplication\"\nandroid:allowBackup=\"true\"\nandroid:dataExtractionRules=\"@xml/data_extraction_rules\"\nandroid:fullBackupContent=\"@xml/backup_rules\"\nandroid:icon=\"@mipmap/ic_launcher\"\nandroid:label=\"@string/app_name\"\nandroid:roundIcon=\"@mipmap/ic_launcher_round\"\nandroid:supportsRtl=\"true\"\nandroid:theme=\"@style/Theme.Todo\"\ntools:targetApi=\"31\">\n

      Ezzel \u00edgy m\u00e1r \u00f6ssze\u00e1llt az adatb\u00e1ziskezel\u0151 r\u00e9teg, de m\u00e9g fel kell oldanunk a komponensek k\u00f6zti kommunik\u00e1ci\u00f3t.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a TodoDatabase k\u00f3dr\u00e9sz\u00e9lete, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/persistence/#uzleti-logika","title":"\u00dczleti logika","text":"

      Most k\u00e9sz\u00edts\u00fck el a domain.usecases package-et. Ebbe ker\u00fclnek az egyes \u00fczletilogika-m\u0171veletek megval\u00f3s\u00edt\u00e1sai. Kezdj\u00fck a tennival\u00f3 l\u00e9trehoz\u00e1s\u00e1val:

      class SaveTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(todo: Todo) {\nrepository.insertTodo(todo.asTodoEntity())\n}\n\n}\n

      Ennek a k\u00f3dr\u00e9szletnek a szerepe, hogy - ak\u00e1rcsak a domainmodell - lev\u00e1lasztja az \u00fczleti logik\u00e1t az adatr\u00e9tegr\u0151l. Jelen esetben az \u00fczleti logik\u00e1nk igen egyszer\u0171, \u00e9s ez\u00e9rt ezek a m\u0171veletek tulajdonk\u00e9ppen csak megh\u00edvj\u00e1k az adatr\u00e9teget a repository komponenseken kereszt\u00fcl, illetve konvert\u00e1lj\u00e1k a domainmodelleket entit\u00e1sokk\u00e1. Egy \u00f6sszetettebb alkalmaz\u00e1sban ez nem felt\u00e9tlen van \u00edgy, \u00e9s ez a r\u00e9teg ak\u00e1r bonyolultabb is lehet, t\u00f6bb adatm\u0171veletb\u0151l nagyobb l\u00e9pt\u00e9k\u0171, \u00f6sszetettebb m\u0171veleteket val\u00f3s\u00edthat meg.

      A fentihez hasonl\u00f3 k\u00e9sz\u00edts\u00fck el a m\u00f3dos\u00edt\u00e1s use case oszt\u00e1ly\u00e1t:

      class UpdateTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(todo: Todo) {\nrepository.updateTodo(todo.asTodoEntity())\n}\n\n}\n

      Majd a lek\u00e9rdez\u00e9st:

      class LoadTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(id: Int): Result<Todo> {\nreturn try {\nResult.success(repository.getTodoById(id).first().asTodo())\n} catch (e: IOException) {\nResult.failure(e)\n}\n}\n\n}\n

      A t\u00f6rl\u00e9st:

      class DeleteTodoUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(id: Int) {\nrepository.deleteTodo(id)\n}\n\n}\n

      \u00c9s legyen egy list\u00e1z\u00e1sunk is, amikor minden tennival\u00f3t bet\u00f6lt\u00fcnk:

      class LoadTodosUseCase(private val repository: TodoRepository) {\n\nsuspend operator fun invoke(): Result<List<Todo>> {\nreturn try {\nval todos = repository.getAllTodos().first()\nResult.success(todos.map { it.asTodo() })\n} catch (e: IOException) {\nResult.failure(e)\n}\n}\n}\n

      V\u00e9g\u00fcl ezeket \u00f6sszefogjuk egy oszt\u00e1ly tagv\u00e1ltoz\u00f3iban:

      class TodoUseCases(repository: TodoRepository) {\nval loadTodos = LoadTodosUseCase(repository)\nval loadTodo = LoadTodoUseCase(repository)\nval saveTodo = SaveTodoUseCase(repository)\nval updateTodo = UpdateTodoUseCase(repository)\nval deleteTodo = DeleteTodoUseCase(repository)\n}\n

      Most m\u00f3dos\u00edtanunk kell a viewmodeljeinket. Kezdj\u00fck a l\u00e9trehoz\u00e1s m\u0171velettel! El\u0151sz\u00f6r is konstruktorban m\u00e1r nem a repository-t kapjuk meg, hanem a TodoUseCases oszt\u00e1lyra kapunk referenci\u00e1t:

      class TodoCreateViewModel(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n...\n}\n

      Majd ennek megfelel\u0151en m\u00f3dos\u00edtsuk a ment\u00e9st \u00e9s az inicializ\u00e1l\u00e1st is:

          private fun onSave() {\nviewModelScope.launch {\ntry {\ntodoOperations.saveTodo(state.value.todo.asTodo())\n_uiEvent.send(TodoCreateUiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(TodoCreateUiEvent.Failure(e.toUiText()))\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodoCreateViewModel(\ntodoOperations = todoOperations\n)\n}\n}\n}\n

      Folytassuk a r\u00e9szletez\u0151 n\u00e9zettel, itt is hasonl\u00f3ak lesznek a v\u00e1ltoz\u00e1sok:

      class TodoDetailViewModel(\nprivate val todoOperations: TodoUseCases,\nprivate val savedStateHandle: SavedStateHandle) : ViewModel() {\n...\n}\n

      Majd:

          private fun loadTodos() {\nval id = checkNotNull<Int>(savedStateHandle[\"id\"])\nviewModelScope.launch {\ntry {\n_state.value = TodoDetailState.Loading\nval todo = todoOperations.loadTodo(id)\n_state.value = TodoDetailState.Result(\ntodo = todo.getOrThrow().asTodoUi()\n)\n} catch (e: Exception) {\n_state.value = TodoDetailState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nval savedStateHandle = createSavedStateHandle()\nTodoDetailViewModel(\ntodoOperations,\nsavedStateHandle\n)\n}\n}\n}\n

      V\u00e9g\u00fcl a list\u00e1z\u00f3 n\u00e9zet k\u00f6vetkezik:

      class TodoListViewModel(\nprivate val todoOperations: TodoUseCases\n) : ViewModel() {\n...\n}\n

      Majd:

          fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\nval todos = todoOperations.loadTodos().getOrThrow().map { it.asTodoUi() }\n_state.value = TodoListState.Result(\ntodoList = todos\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval todoOperations = TodoUseCases(TodoApplication.repository)\nTodoListViewModel(\ntodoOperations\n)\n}\n}\n}\n

      Most m\u00e1r kipr\u00f3b\u00e1lhat\u00f3 az alkalmaz\u00e1s, \u00e9s a l\u00e9trehozott teend\u0151k t\u00e9nylegesen az adatb\u00e1zisba ment\u0151dnek.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a teend\u0151k list\u00e1ja, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt, illetve egy teend\u0151 c\u00edm\u00e9ben.

      A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/persistence/#onallo-feladat-1","title":"\u00d6n\u00e1ll\u00f3 feladat 1","text":"

      Val\u00f3s\u00edtsd meg az \u00f6sszes tennival\u00f3 t\u00f6rl\u00e9s\u00e9t, pl. az AppBaron elhelyezett gombbal! A laboron l\u00e1tott architekt\u00far\u00e1hoz hasonl\u00f3an r\u00e9tegr\u0151l-r\u00e9tegre val\u00f3s\u00edtsd meg a sz\u00fcks\u00e9ges funkci\u00f3kat.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a mindent t\u00f6r\u00f6l funkci\u00f3, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/persistence/#onallo-feladat-2","title":"\u00d6n\u00e1ll\u00f3 feladat 2","text":"

      Hossz\u00fa kattint\u00e1sra leny\u00edl\u00f3 men\u00fcb\u0151l lehessen megosztani a tennival\u00f3kat m\u00e1s alkalmaz\u00e1sokkal sz\u00f6veges \u00fczenetk\u00e9nt. Az \u00fczenet tartalmazza a tennival\u00f3 jellemz\u0151it.

      Seg\u00edts\u00e9g: https://developer.android.com/training/sharing/send

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1sban a megoszt\u00e1s funkci\u00f3, az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt.

      A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/tictactoe/","title":"Labor02 - Egyszer\u0171 felhaszn\u00e1l\u00f3i fel\u00fclet t\u00f6bb Activity seg\u00edts\u00e9g\u00e9vel (TicTacToe)","text":""},{"location":"laborok/tictactoe/#bevezetes","title":"Bevezet\u00e9s","text":"

      A labor c\u00e9lja egy t\u00f6bb Activity-b\u0151l \u00e1ll\u00f3 Android alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9se, valamint az egyszer\u0171 rajzol\u00e1s bemutat\u00e1sa egy TicTacToe j\u00e1t\u00e9k seg\u00edts\u00e9g\u00e9vel.

      A labor sor\u00e1n a k\u00f6vetkez\u0151 funkci\u00f3kat fogjuk megval\u00f3s\u00edtani:

      • Men\u00fc Activity
      • J\u00e1t\u00e9kt\u00e9r Activity
      • TicTacToe n\u00e9zet
      • J\u00e1t\u00e9k logika elkezd\u00e9se

      A laborhoz kapcsol\u00f3d\u00f3 \u00f6n\u00e1ll\u00f3 feladat:

      • J\u00e1t\u00e9k logika megval\u00f3s\u00edt\u00e1sa: gy\u0151zelem ellen\u0151rz\u00e9se

      A megval\u00f3s\u00edtand\u00f3 j\u00e1t\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9t az al\u00e1bbi k\u00e9perny\u0151k\u00e9pek szeml\u00e9ltetik:

      "},{"location":"laborok/tictactoe/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

      A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

      "},{"location":"laborok/tictactoe/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
      1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

      2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

        Checkout

        Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

      3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

      4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

      Android, Java, Kotlin

      Az Android hagyom\u00e1nyosan Java nyelven volt fejleszthet\u0151, azonban az ut\u00f3bbi \u00e9vekben a Google \u00e1t\u00e1llt a Kotlin nyelvre. Ez egy sokkal modernebb nyelv, mint a Java, sok olyan nyelvi elemet ad, amit k\u00e9nyelmes haszn\u00e1lni, valamint \u00faj nyelvi szab\u00e1lyokat, amikkel p\u00e9ld\u00e1ul elker\u00fclhet\u0151ek a Java nyelven gyakori NullPointerException jelleg\u0171 hib\u00e1k.

      M\u00e1sr\u00e9szr\u0151l viszont a nyelv sok mindenben t\u00e9r el a hagyom\u00e1nyosan C jelleg\u0171 szintaktik\u00e1t k\u00f6vet\u0151 nyelvekt\u0151l, amit majd l\u00e1tni is fogunk. A labor el\u0151tt \u00e9rdemes megismerkedni a nyelvvel, egyr\u00e9szt a fent l\u00e1that\u00f3 linken, m\u00e1sr\u00e9szt ezt az \u00f6sszefoglal\u00f3 cikket \u00e1tolvasva.

      "},{"location":"laborok/tictactoe/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

      Els\u0151 l\u00e9p\u00e9sk\u00e9nt ind\u00edtsuk el az Android Studio-t, majd:

      1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Views Activity lehet\u0151s\u00e9get.
      2. A projekt neve legyen TicTacToe, a kezd\u0151 package hu.bme.aut.android.tictactoe, a ment\u00e9si hely pedig a kicheckoutolt repository-n bel\u00fcl a TicTacToe mappa.
      3. Nyelvnek v\u00e1lasszuk a Kotlin-t.
      4. A minimum API szint legyen API24: Android 7.0.
      5. A Build configuration language Kotlin DSL legyen.

      FILE PATH

      A projekt mindenk\u00e9ppen a repository-ban l\u00e9v\u0151 TicTacToe k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

      Sikeres projekt l\u00e9trehoz\u00e1s ut\u00e1n a laborvezet\u0151 vezet\u00e9s\u00e9vel vizsg\u00e1lja meg a forr\u00e1s fel\u00e9p\u00edt\u00e9s\u00e9t.

      A projekt l\u00e9trehoz\u00e1sakor, a ford\u00edt\u00f3 keretrendszernek rengeteg f\u00fcgg\u0151s\u00e9get kell let\u00f6ltenie. Am\u00edg ez nem t\u00f6rt\u00e9nt meg, addig a projektben neh\u00e9zkes navig\u00e1lni, hi\u00e1nyzik a k\u00f3dkieg\u00e9sz\u00edt\u00e9s, stb... \u00c9ppen ez\u00e9rt ezt tan\u00e1csos kiv\u00e1rni, azonban ez ak\u00e1r 5 percet is ig\u00e9nybe vehet az els\u0151 alkalommal! Az ablak alj\u00e1n l\u00e1that\u00f3 inform\u00e1ci\u00f3s s\u00e1vot kell figyelni.

      "},{"location":"laborok/tictactoe/#az-alkalmazas-mukodese","title":"Az alkalmaz\u00e1s m\u0171k\u00f6d\u00e9se","text":"

      A megval\u00f3s\u00edtand\u00f3 alkalmaz\u00e1s m\u0171k\u00f6d\u00e9si elve a k\u00f6vetkez\u0151:

      1. Az alkalmaz\u00e1s ind\u00edt\u00e1sakor a MainActivity jelenik meg.
      2. A MainActivity-r\u0151l lehet \u00faj j\u00e1t\u00e9kot ind\u00edtani az \u00daj j\u00e1t\u00e9k men\u00fcpont hat\u00e1s\u00e1ra, ez \u00e1tnavig\u00e1l a GameActivity-re.
      3. A MainActivity-r\u0151l meg lehet tekinteni az Eredm\u00e9nyek-et, ami jelenleg csak egy Toast-ot dob fel egy \u00fczenettel (ezt a funkci\u00f3t opcion\u00e1lisan k\u00e9s\u0151bb meg lehet val\u00f3s\u00edtani, ha a perzisztencia t\u00e9mak\u00f6rt m\u00e1r vett\u00fck el\u0151ad\u00e1son).
      4. A MainActivity-r\u0151l meg lehet n\u00e9zni az alkalmaz\u00e1s k\u00e9sz\u00edt\u0151ir\u0151l sz\u00f3l\u00f3 inform\u00e1ci\u00f3kat az Inf\u00f3 men\u00fct v\u00e1lasztva. Ez a funkci\u00f3 \u00e1tnavig\u00e1l az AboutActivity-re, ami dial\u00f3gus form\u00e1ban fog megjelenni.
      "},{"location":"laborok/tictactoe/#szoveges-eroforrasok","title":"Sz\u00f6veges er\u0151forr\u00e1sok","text":"

      Navig\u00e1ljunk a res/values/strings.xml-re, ahol a projekt sz\u00f6veges er\u0151forr\u00e1sai tal\u00e1lhat\u00f3ak. Haszn\u00e1ljuk a k\u00f6vetkez\u0151 sz\u00f6veges er\u0151forr\u00e1sokat:

      <resources>\n<string name=\"app_name\">TicTacToe</string>\n<string name=\"btn_start\">\u00daj j\u00e1t\u00e9k</string>\n<string name=\"btn_highscore\">Eredm\u00e9nyek</string>\n<string name=\"btn_about\">Inf\u00f3</string>\n<string name=\"toast_highscore\">Eredm\u00e9nyek</string>\n<string name=\"txt_about\">Made by Hallgat\u00f3</string>\n</resources>\n
      "},{"location":"laborok/tictactoe/#szukseges-tovabbi-activityk-letrehozasa","title":"Sz\u00fcks\u00e9ges tov\u00e1bbi Activityk l\u00e9trehoz\u00e1sa","text":"

      A fentiek alapj\u00e1n l\u00e1that\u00f3 teh\u00e1t, hogy a megl\u00e9v\u0151 MainActivity mellett m\u00e9g k\u00e9t m\u00e1sik Activity-t, a GameActivity-t \u00e9s az AboutActivity-t kell l\u00e9trehoznunk.

      Activity l\u00e9trehoz\u00e1sakor tipikusan az al\u00e1bbi forr\u00e1s \u00e1llom\u00e1nyok v\u00e1ltoznak:

      • L\u00e9trej\u00f6n az Activity-hez tartoz\u00f3 Kotlin f\u00e1jl.
      • L\u00e9trej\u00f6n az Activity-hez tartoz\u00f3 layout XML.
      • Az AndroidManifest.xml-be beker\u00fcl az Activity az <application> tag-en bel\u00fcl.

      Az Activity l\u00e9trehoz\u00e1st azonban megk\u00f6nny\u00edti az Android Studio \u00e9s a fenti l\u00e9p\u00e9seket nem kell egyes\u00e9vel elv\u00e9geznie a fejleszt\u0151nek.

      1. A megl\u00e9v\u0151 Activity-t tartalmaz\u00f3 package-re jobb eg\u00e9rgombbal kattintva v\u00e1lasszuk a New -> Activity -> Empty Views Activity opci\u00f3t \u00e9s hozzuk l\u00e9tre a m\u00e1sik k\u00e9t Activity-t (AboutActivity, GameActivity), Source Language-nek v\u00e1lasszuk a Kotlint.
      2. L\u00e9trehoz\u00e1s ut\u00e1n a res/values/strings.xml-ben a <resources> tagen bel\u00fcl vegy\u00fck fel a k\u00e9t \u00faj Activity c\u00edm\u00e9t:
            <string name=\"title_activity_about\">Az alkalmaz\u00e1sr\u00f3l</string>\n<string name=\"title_activity_game\">J\u00e1t\u00e9k</string>\n
      3. \u00c1ll\u00edtsuk be a Manifestben azt, hogy az AboutActivity dial\u00f3gus form\u00e1ban jelenjen meg, a theme attrib\u00fatum be\u00e1ll\u00edt\u00e1s\u00e1val

        A k\u00f3dkieg\u00e9sz\u00edt\u00e9s seg\u00edt megtal\u00e1lni a megfelel\u0151 t\u00e9m\u00e1t a lehet\u0151s\u00e9gek k\u00f6z\u00fcl, kezdj\u00fck el a kezd\u0151 bet\u0171ket be\u00edrni!

            <activity\nandroid:name=\".AboutActivity\"\nandroid:exported=\"false\"\nandroid:label=\"@string/title_activity_about\"\nandroid:parentActivityName=\".MainActivity\"\nandroid:theme=\"@style/Theme.AppCompat.Light.Dialog\">\n<meta-data\nandroid:name=\"android.support.PARENT_ACTIVITY\"\nandroid:value=\".MainActivity\" />\n</activity>\n

        A fenti k\u00f3dr\u00e9szletben az AboutActivity c\u00edm\u00e9t is be\u00e1ll\u00edtjuk a label attrib\u00fatum be\u00e1ll\u00edt\u00e1s\u00e1val

      4. \u00c1ll\u00edtsuk be a GameActivity c\u00edm\u00e9t is

            <activity\nandroid:name=\".GameActivity\"\nandroid:exported=\"false\"\nandroid:label=\"@string/title_activity_game\" />\n

      L\u00e9trehoz\u00e1s ut\u00e1n ellen\u0151rizz\u00fck a laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel a l\u00e9trej\u00f6tt k\u00f3dokat!

      "},{"location":"laborok/tictactoe/#mainactivity-felulet","title":"MainActivity fel\u00fclet","text":"

      A MainActivity a fenti \u00e1bra alapj\u00e1n h\u00e1rom men\u00fcpontot tartalmaz k\u00f6z\u00e9pre igaz\u00edtva. Ezt a fel\u00fcletet a hozz\u00e1 tartoz\u00f3 res/layout/activity_main.xml-ben hozhatjuk l\u00e9tre. Mivel az AndroidStudio m\u00e1r alap\u00e9rtelmezetten ConstraintLayout alap\u00fa n\u00e9zetet gener\u00e1l, \u00edgy most ezt fogjuk haszn\u00e1lni a megval\u00f3s\u00edt\u00e1sra. Az anyagban ennek m\u0171k\u00f6d\u00e9se csak k\u00e9s\u0151bb k\u00f6vetkezik, \u00edgy al\u00e1bb megtal\u00e1lhat\u00f3 a k\u00e9sz XML le\u00edr\u00f3. Akinek van kedve, a gif alapj\u00e1n kipr\u00f3b\u00e1lhatja a haszn\u00e1lat\u00e1t:

      Tipp: Shift + Kattint\u00e1ssal lehet t\u00f6bb elemet kijel\u00f6lni

      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".MainActivity\">\n\n<Button\nandroid:id=\"@+id/btnStart\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_start\"\napp:layout_constraintBottom_toTopOf=\"@+id/btnHighScores\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:layout_constraintVertical_chainStyle=\"packed\" />\n\n<Button\nandroid:id=\"@+id/btnHighScores\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_highscore\"\napp:layout_constraintBottom_toTopOf=\"@+id/btnAbout\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@+id/btnStart\" />\n\n<Button\nandroid:id=\"@+id/btnAbout\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:text=\"@string/btn_about\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintHorizontal_bias=\"0.5\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toBottomOf=\"@+id/btnHighScores\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n

      N\u00e9zz\u00fck \u00e1t a laborvezet\u0151vel a fel\u00fclet fel\u00e9p\u00edt\u00e9s\u00e9t!

      "},{"location":"laborok/tictactoe/#viewbinding","title":"ViewBinding","text":"

      A fel\u00fcleti elemeink el\u00e9r\u00e9s\u00e9re \"n\u00e9zetk\u00f6t\u00e9st\", azaz ViewBinding-ot fogunk haszn\u00e1lni. A ViewBinding a k\u00f3d\u00edr\u00e1st k\u00f6nny\u00edti meg sz\u00e1munkra. Amennyiben ezt haszn\u00e1ljuk, az automatikusan gener\u00e1l\u00f3d\u00f3 binding oszt\u00e1lyokon kereszt\u00fcl k\u00f6zvetlen referenci\u00e1n kereszt\u00fcl tudunk el\u00e9rni minden ID-val rendelkez\u0151 er\u0151forr\u00e1st az XML f\u00e1jljainkban.

      El\u0151sz\u00f6r is be kell kapcsolnunk a modulunkra a ViewBinding-ot. Az app modulhoz tartoz\u00f3 build.gradle.kts f\u00e1jlban az android tagen bel\u00fclre illessz\u00fck be az enged\u00e9lyez\u00e9st:

      android {\n...\nbuildFeatures {\nviewBinding = true\n}\n}\n

      Majd nyomjunk a fels\u0151 k\u00e9k s\u00e1von jobb oldalon megjelen\u0151 Sync Now gombra. Ezzel a gradle bet\u00f6lti sz\u00fcks\u00e9ges v\u00e1ltoztat\u00e1sokat.

      ViewBinding

      Ebben az esetben a modul minden egyes XML layout f\u00e1jlj\u00e1hoz gener\u00e1l\u00f3dik egy \u00fagynevezett binding oszt\u00e1ly. Minden binding oszt\u00e1ly tartalmaz referenci\u00e1t az adott XML layout er\u0151forr\u00e1s gy\u00f6k\u00e9r elem\u00e9re \u00e9s az \u00f6sszes ID-val rendelkez\u0151 view-ra. A gener\u00e1lt oszt\u00e1ly neve \u00fagy \u00e1ll el\u0151, hogy az XML layout nev\u00e9t Pascal form\u00e1tumba alak\u00edtja a rendszer \u00e9s a v\u00e9g\u00e9re illeszti, hogy Binding. Azaz p\u00e9ld\u00e1ul a activity_login.xml er\u0151forr\u00e1sf\u00e1jlb\u00f3l az al\u00e1bbi binding oszt\u00e1ly gener\u00e1l\u00f3dik: ActivityLoginBinding.

      <LinearLayout ... >\n<TextView android:id=\"@+id/name\" />\n<ImageView android:cropToPadding=\"true\" />\n<Button android:id=\"@+id/button\"\nandroid:background=\"@drawable/rounded_button\" />\n</LinearLayout>\n

      A gener\u00e1lt oszt\u00e1lynak k\u00e9t mez\u0151je van. A name id-val rendelkez\u0151 TextView \u00e9s a button id-j\u00fa Button. A layout-ban szerepl\u0151 ImageView-nak nincs id-ja, ez\u00e9rt nem szerepel a binding oszt\u00e1lyban.

      Minden gener\u00e1lt oszt\u00e1ly tartalmaz egy getRoot() met\u00f3dust, amely direkt referenciak\u00e9nt szolg\u00e1l a layout gy\u00f6ker\u00e9re. A p\u00e9ld\u00e1ban a getRoot() met\u00f3dus a LinearLayout-tal t\u00e9r vissza.

      Ezzel ut\u00e1n m\u00e1r a teljes modulunkban automatikusan el\u00e9rhet\u0151v\u00e9 v\u00e1lt a ViewBinding. Haszn\u00e1lat\u00e1hoz az Activity-nkben csak p\u00e9ld\u00e1nyos\u00edtanunk kell a binding objektumot, amin kereszt\u00fcl majd el\u00e9rhetj\u00fck az er\u0151forr\u00e1sainkat. A binding p\u00e9ld\u00e1ny m\u0171k\u00f6d\u00e9s\u00e9hez h\u00e1rom dolgot kell tenn\u00fcnk:

      1. A gener\u00e1lt binding oszt\u00e1ly statikus inflate f\u00fcggv\u00e9ny\u00e9vel p\u00e9ld\u00e1nyos\u00edtjuk a binding oszt\u00e1lyunkat az Activity-hez,
      2. Szerz\u00fcnk egy referenci\u00e1t a gy\u00f6k\u00e9r n\u00e9zetre a getRoot() f\u00fcggv\u00e9nnyel,
      3. Ezt a gy\u00fck\u00e9relemet odaadjuk a setContentView() f\u00fcggv\u00e9nynek, hogy ez legyen az akt\u00edv view a k\u00e9perny\u0151n:
      package hu.bme.aut.android.tictactoe\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport hu.bme.aut.android.tictactoe.databinding.ActivityMainBinding\n\nclass MainActivity : AppCompatActivity() {\nprivate lateinit var binding: ActivityMainBinding\n\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nbinding = ActivityMainBinding.inflate(layoutInflater)\nsetContentView(binding.root)\n}\n}\n

      lateinit

      A lateinit kulcssz\u00f3val megjel\u00f6lt property-ket a ford\u00edt\u00f3 megengedi inicializ\u00e1latlanul hagyni az oszt\u00e1ly konstruktor\u00e1nak lefut\u00e1sa ut\u00e1nig, an\u00e9lk\u00fcl, hogy nullable-k\u00e9nt k\u00e9ne azokat megjel\u00f6ln\u00fcnk (ami k\u00e9s\u0151bb k\u00e9nyelmetlenn\u00e9 tenn\u00e9 a haszn\u00e1latukat, mert mindig ellen\u0151rizn\u00fcnk k\u00e9ne, hogy null-e az \u00e9rt\u00e9k\u00fck). Ez praktikus olyan esetekben, amikor egy oszt\u00e1ly inicializ\u00e1l\u00e1sa nem a konstruktor\u00e1ban t\u00f6rt\u00e9nik (p\u00e9ld\u00e1ul ahogy az Activity-k eset\u00e9ben az onCreate-ben), mert k\u00e9s\u0151bb az esetleges null eset lekezel\u00e9se n\u00e9lk\u00fcl haszn\u00e1lhatjuk majd a property-t. A lateinit haszn\u00e1lat\u00e1val \u00e1tv\u00e1llaljuk a felel\u0151ss\u00e9get a ford\u00edt\u00f3t\u00f3l, hogy a property-t az els\u0151 haszn\u00e1lata el\u0151tt inicializ\u00e1lni fogjuk - ellenkez\u0151 esetben kiv\u00e9telt kapunk.

      Cser\u00e9lj\u00fck le a fenti minta alapj\u00e1n a m\u00e1sik k\u00e9t Activity k\u00f3dj\u00e1t is ViewBinding alap\u00fa megold\u00e1sra!

      "},{"location":"laborok/tictactoe/#highscore-gomb-esemenykezelo","title":"Highscore gomb esem\u00e9nykezel\u0151","text":"

      Az Eredm\u00e9nyek men\u00fcpontra kattintva egy Toast \u00fczenetet kell megjelen\u00edteni. Ehhez meg kell keresni az Eredm\u00e9nyek men\u00fcpont gombj\u00e1t \u00e9s be kell \u00e1ll\u00edtani neki az al\u00e1bbi esem\u00e9nykezel\u0151t a MainActivity onCreate() f\u00fcggv\u00e9ny\u00e9n bel\u00fcl:

      binding.btnHighScores.setOnClickListener {\nToast.makeText(\nthis@MainActivity,\ngetString(R.string.toast_highscore),\nToast.LENGTH_LONG\n).show()\n}\n

      onClickListener

      A setOnClickListener f\u00fcggv\u00e9ny val\u00f3j\u00e1ban egy View.OnClickListener interf\u00e9szt megval\u00f3s\u00edt\u00f3 objektumot v\u00e1r param\u00e9terk\u00e9nt, amelynek egyetlen megval\u00f3s\u00edtand\u00f3 f\u00fcggv\u00e9nye van. Ezt l\u00e9trehozhatn\u00e1nk a Java-s anonim oszt\u00e1lyok st\u00edlus\u00e1ban is, de helyette kihaszn\u00e1ljuk, hogy a f\u00fcggv\u00e9nyek els\u0151rend\u0171 tagjai a Kotlin nyelvnek, \u00edgy rendelkez\u00fcnk igazi f\u00fcggv\u00e9ny t\u00edpusokkal. Jelen esetben a param\u00e9terben egy olyan lambda kifejez\u00e9st adunk \u00e1t, amely fejl\u00e9ce megegyezik az elv\u00e1rt interf\u00e9sz egyetlen f\u00fcggv\u00e9ny\u00e9nek fejl\u00e9c\u00e9vel, a SAM conversion nyelvi funkci\u00f3 pedig a h\u00e1tt\u00e9rben a lambda alapj\u00e1n l\u00e9trehozza a megfelel\u0151 View.OnClickListener p\u00e9ld\u00e1nyt.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a highscores Toast \u00fczenet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      "},{"location":"laborok/tictactoe/#aboutactivity-felulet","title":"AboutActivity fel\u00fclet","text":"

      Ahogy kor\u00e1bban eml\u00edtett\u00fck, az Inf\u00f3 men\u00fc elind\u00edtja az AboutActivity-t. Els\u0151k\u00e9nt k\u00e9sz\u00edts\u00fck el az AboutActivity fel\u00fclet\u00e9t, melyet a res/layout/activity_about.xml \u00edr le. Mint kor\u00e1bban, itt is lehet ConstraintLayout-ot k\u00e9sz\u00edteni a seg\u00edts\u00e9ggel, vagy al\u00e1bb megtal\u00e1lhat\u00f3 az XML:

      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".AboutActivity\">\n\n<TextView\nandroid:id=\"@+id/textView\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_margin=\"8dp\"\nandroid:text=\"@string/txt_about\"\nandroid:textSize=\"32sp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n
      "},{"location":"laborok/tictactoe/#navigacio-megvalositasa-activityk-kozt","title":"Navig\u00e1ci\u00f3 megval\u00f3s\u00edt\u00e1sa Activityk k\u00f6zt","text":"

      A k\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt val\u00f3s\u00edtsuk meg a navig\u00e1ci\u00f3t (v\u00e1lt\u00e1st) az Activity-k k\u00f6z\u00f6tt. Az \u00daj j\u00e1t\u00e9k men\u00fcpont hat\u00e1s\u00e1ra a GameActivity-re, az Inf\u00f3 men\u00fcpont hat\u00e1s\u00e1ra pedig az AboutActivity-re kell \u00e1tv\u00e1ltanunk. Activity-k k\u00f6zti v\u00e1lt\u00e1st egy Intent seg\u00edts\u00e9g\u00e9vel tudunk implement\u00e1lni - besz\u00e9lj\u00fck meg a laborvezet\u0151vel az Intent-ek alapjait. Ezt a t\u00e9m\u00e1t el\u0151ad\u00e1son k\u00e9s\u0151bb m\u00e9lyebben fogjuk m\u00e9g \u00e9rinteni.

      Val\u00f3s\u00edtsuk meg ezen k\u00e9t gomb esem\u00e9nykezel\u0151j\u00e9t szint\u00e9n a MainActivity onCreate() f\u00fcggv\u00e9ny\u00e9ben!

      binding.btnStart.setOnClickListener {\nstartActivity(Intent(this@MainActivity, GameActivity::class.java))\n}\n\nbinding.btnAbout.setOnClickListener {\nstartActivity(Intent(this@MainActivity, AboutActivity::class.java))\n}\n

      setContentView

      Gyakori hiba, hogy a setContentView f\u00fcggv\u00e9nynek a gy\u00f6k\u00e9r n\u00e9zet helyett v\u00e9letlen\u00fcl az ID-val hivatkozott layout-ot adjuk oda. (R.layout.activity_main.xml). Ilyenkor k\u00e9tszer is p\u00e9ld\u00e1nyosodik a fel\u00fclet, r\u00e1ad\u00e1sul a k\u00e9perny\u0151n az egyik jelenik meg, m\u00edg a binding-ok a m\u00e1sikra lesznek be\u00e1ll\u00edtva.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a az AboutActivity (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), egy ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a TextView sz\u00f6vegek\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      "},{"location":"laborok/tictactoe/#jatek-logika","title":"J\u00e1t\u00e9k logika","text":"

      A 3x3-as TicTacToe t\u00e1blaj\u00e1t\u00e9k logik\u00e1j\u00e1t k\u00fcl\u00f6n oszt\u00e1lyban val\u00f3s\u00edtjuk meg egy Singleton form\u00e1j\u00e1ban, \u00edgy k\u00f6nnyen hozz\u00e1f\u00e9rhet\u00fcnk majd.

      Amennyiben nem ismeri ezt a tervez\u00e9si mint\u00e1t, \u00e9rdemes ut\u00e1nan\u00e9zni, illetve r\u00e1k\u00e9rdezni a laborvezet\u0151n\u00e9l.

      K\u00e9sz\u00edts\u00fcnk a tictactoe package-en bel\u00fcl egy model package-et, majd abban egy TicTacToeModel oszt\u00e1lyt (a package-en jobb eg\u00e9rgomb, majd New -> Kotlin File/Class). Az oszt\u00e1ly egy 3x3-as m\u00e1trixban t\u00e1rolja a j\u00e1t\u00e9kt\u00e9r mez\u0151inek tartalm\u00e1t \u00e9s k\u00fcl\u00f6nf\u00e9le publikus f\u00fcggv\u00e9nyeket biztos\u00edt a j\u00e1t\u00e9kt\u00e9r lek\u00e9rdez\u00e9s\u00e9hez \u00e9s m\u00f3dos\u00edt\u00e1s\u00e1hoz.

      package hu.bme.aut.android.tictactoe.model\n\nobject TicTacToeModel {\n\nconst val EMPTY: Byte = 0\nconst val CIRCLE: Byte = 1\nconst val CROSS: Byte = 2\n\nvar nextPlayer: Byte = CIRCLE\n\nprivate var model: Array<ByteArray> = arrayOf(\nbyteArrayOf(EMPTY, EMPTY, EMPTY),\nbyteArrayOf(EMPTY, EMPTY, EMPTY),\nbyteArrayOf(EMPTY, EMPTY, EMPTY))\n\nfun resetModel() {\nfor (i in 0 until 3) {\nfor (j in 0 until 3) {\nmodel[i][j] = EMPTY\n}\n}\n}\n\nfun getFieldContent(x: Int, y: Int): Byte {\nreturn model[x][y]\n}\n\nfun changeNextPlayer() {\nif (nextPlayer == CIRCLE) {\nnextPlayer = CROSS\n} else {\nnextPlayer = CIRCLE\n}\n}\n\nfun setFieldContent(x: Int, y: Int, content: Byte): Byte {\nchangeNextPlayer()\nmodel[x][y] = content\nreturn content\n}\n\n}\n

      Singleton

      Kotlinban nyelvi szint\u0171 t\u00e1mogat\u00e1s van a singletonok l\u00e9trehoz\u00e1s\u00e1ra. Ahelyett, hogy nek\u00fcnk k\u00e9ne egyetlen statikus p\u00e9ld\u00e1nyt felvenn\u00fcnk, el\u00e9g csak a class kulcssz\u00f3 helyett az object kulcssz\u00f3val l\u00e9trehoznunk az oszt\u00e1lyt hogy egy singletont kapjunk.

      const

      A ford\u00edt\u00e1s id\u0151ben konstans \u00e9rt\u00e9keket \u00e9rdemes a const kulcssz\u00f3val megjel\u00f6ln\u00fcnk (erre a fejleszt\u0151k\u00f6rnyezet is figyelmeztet, ha nem tenn\u00e9nk), ezzel teljes\u00edtm\u00e9ny optimaliz\u00e1ci\u00f3kat \u00e9rhet\u00fcnk el, illetve a sz\u00e1nd\u00e9kainkat is tiszt\u00e1bban jelezz\u00fck.

      collection

      A Kotlin standard library sz\u00e1mos f\u00fcggv\u00e9nyt ny\u00fajt k\u00fcl\u00f6nb\u00f6z\u0151 collection-\u00f6k egyszer\u0171 l\u00e9trehoz\u00e1s\u00e1ra. Figyelj\u00fck meg a k\u00f3dban az arrayOf \u00e9s a byteArrayOf haszn\u00e1lat\u00e1t, amelyek megh\u00edv\u00e1s\u00e1val l\u00e9trehozunk t\u00f6mb\u00f6ket, \u00e9s azonnal fel is t\u00f6ltj\u00fck \u0151ket elemekkel.

      "},{"location":"laborok/tictactoe/#jatekter-kirajzolasa","title":"J\u00e1t\u00e9kt\u00e9r kirajzol\u00e1sa","text":"

      A k\u00f6vetkez\u0151 l\u00e9p\u00e9s a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1sa \u00e9s annak hozz\u00e1rendel\u00e9se a GameActivity-hez.

      Els\u0151 l\u00e9p\u00e9sk\u00e9nt a megl\u00e9v\u0151 tictactoe package-ben hozzunk l\u00e9tre egy view package-et , majd abban egy TicTacToeView oszt\u00e1lyt, mely a View \u0151soszt\u00e1lyb\u00f3l sz\u00e1rmazik:

      package hu.bme.aut.android.tictactoe.view\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.view.View\nimport kotlin.math.min\n\nclass TicTacToeView : View {\n\nprivate val paintBg = Paint()\nprivate val paintLine = Paint()\n\nconstructor(context: Context?) : super(context)\nconstructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n\ninit {\npaintBg.color = Color.BLACK\npaintBg.style = Paint.Style.FILL\n\npaintLine.color = Color.WHITE\npaintLine.style = Paint.Style.STROKE\npaintLine.strokeWidth = 5F\n}\n\noverride fun onDraw(canvas: Canvas) {\ncanvas.drawRect(0F, 0F, width.toFloat(), height.toFloat(), paintBg)\n\ndrawGameArea(canvas)\ndrawPlayers(canvas)\n}\n\nprivate fun drawGameArea(canvas: Canvas) {\n//TODO\n}\n\nprivate fun drawPlayers(canvas: Canvas) {\n//TODO\n}\n\noverride fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\nval w = MeasureSpec.getSize(widthMeasureSpec)\nval h = MeasureSpec.getSize(heightMeasureSpec)\nval d: Int\n\nwhen {\nw == 0 -> { d = h }\nh == 0 -> { d = w }\nelse -> { d = min(w, h) }\n}\n\nsetMeasuredDimension(d, d)\n}\n\noverride fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\n// TODO\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n\n}\n

      L\u00e1that\u00f3, hogy az oszt\u00e1ly egy n\u00e9zet rajzol\u00e1s\u00e1\u00e9rt felel\u0151s. L\u00e9trehozunk k\u00e9t Paint objektumot, melyek a h\u00e1tt\u00e9r, illetve a p\u00e1lyaelemek rajzol\u00e1s\u00e1hoz lesznek haszn\u00e1lva. A konstruktorok, mint l\u00e1tjuk csak egy super() h\u00edv\u00e1st val\u00f3s\u00edtanak meg, mivel ebben a megval\u00f3s\u00edt\u00e1sban az init blokk v\u00e9gzi az oszt\u00e1ly inicializ\u00e1l\u00e1s\u00e1t. Fontos, hogy az onDraw()-ban ne hozzunk l\u00e9tre objektumokat, hiszen az onDraw() minden k\u00e9pkocka kirajzol\u00e1sakor megh\u00edv\u00f3dik \u00e9s sokszor hozn\u00e1 l\u00e9tre feleslegesen \u0151ket, lass\u00edtva ezzel a m\u0171k\u00f6d\u00e9st \u00e9s megnehez\u00edtve a garbage collector dolg\u00e1t.

      Az oszt\u00e1ly egyik legl\u00e9nyegesebb f\u00fcggv\u00e9nye az onDraw, mely a kapott canvas objektumra rajzolja ki a n\u00e9zet tartalm\u00e1t. A jelenlegi implement\u00e1ci\u00f3 feket\u00e9re festi a ter\u00fcletet \u00e9s megh\u00edvja a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1s\u00e9rt (n\u00e9gyzetr\u00e1cs) \u00e9s a j\u00e1t\u00e9kosok (X \u00e9s O) kirajzol\u00e1s\u00e1\u00e9rt felel\u0151s \u2013 egyel\u0151re m\u00e9g \u00fcres \u2013 f\u00fcggv\u00e9nyeket.

      Az onMeasure f\u00fcggv\u00e9ny fel\u00fcldefini\u00e1l\u00e1s\u00e1val biztos\u00edthat\u00f3, hogy a n\u00e9zet mindig n\u00e9gyzetes form\u00e1ban jelenjen meg, azaz ugyanakkora legyen a sz\u00e9less\u00e9ge, mint a magass\u00e1ga.

      V\u00e9g\u00fcl az onTouchEvent f\u00fcggv\u00e9nyben tudjuk kezelni az \u00e9rint\u00e9s esem\u00e9nyeket. Jelenleg az ACTION_DOWN esem\u00e9nyt vizsg\u00e1ljuk, de m\u00e1s \u00e9rint\u00e9s esem\u00e9nyek is hasonl\u00f3an kezelhet\u0151k itt.

      init

      Az init blokkban v\u00e9gezhetj\u00fck el az oszt\u00e1lyunk olyan inicializ\u00e1l\u00e1si feladatait, amelyekre b\u00e1rmilyen konstruktor megh\u00edv\u00e1sakor sz\u00fcks\u00e9g\u00fcnk van.

      when

      Figyelj\u00fck meg a when k\u00e9tf\u00e9le haszn\u00e1lat\u00e1t. Az onTouchEvent f\u00fcggv\u00e9nyben egy Java-s switch-hez hasonl\u00f3an futtat k\u00f3dot a param\u00e9terk\u00e9nt megkapott kifejez\u00e9s \u00e9rt\u00e9k\u00e9t\u0151l f\u00fcgg\u0151en, m\u00edg az onMeasure f\u00fcggv\u00e9nyben egy kev\u00e9sb\u00e9 olvashat\u00f3 if-else l\u00e1nc helyett haszn\u00e1ljuk, param\u00e9ter n\u00e9lk\u00fcl.

      kasztol\u00e1s

      Kotlinban a (float) x \u00e9s (int) y st\u00edlus\u00fa castol\u00e1sok helyett a numerikus t\u00edpusok k\u00f6z\u00f6tt a toInt(), toFloat(), \u00e9s hasonl\u00f3 f\u00fcggv\u00e9nyekkel v\u00e9gezhet\u00fcnk konverzi\u00f3t.

      Ahhoz, hogy a GameActivity ezt a j\u00e1t\u00e9kteret megjelen\u00edtse, m\u00f3dos\u00edtsuk a hozz\u00e1 tartoz\u00f3 res/layout/activity_game.xml f\u00e1jlt. A fel\u00fclet egy Fragment kont\u00e9nert tartalmaz, amibe majd a j\u00e1t\u00e9kt\u00e9r ker\u00fcl:

      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nxmlns:tools=\"http://schemas.android.com/tools\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\ntools:context=\".GameActivity\">\n\n<androidx.fragment.app.FragmentContainerView\nandroid:id=\"@+id/fragmentGameArea\"\nandroid:name=\"hu.bme.aut.android.tictactoe.fragments.GameFragment\"\nandroid:layout_width=\"0dp\"\nandroid:layout_height=\"0dp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\ntools:layout=\"@layout/fragment_game\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

      K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck el a j\u00e1t\u00e9kteret tartalmaz\u00f3 Fragmentet. Ezt k\u00e9sz\u00edthetj\u00fck az Activity-hez hasonl\u00f3an var\u00e1zsl\u00f3val is, (jobb klinn -> New -> Fragment...) azonban ez t\u00fal sok olyan k\u00f3dot gener\u00e1lna, amire nek\u00fcnk most nincs sz\u00fcks\u00e9g\u00fcnk. Csin\u00e1ljunk teh\u00e1t egy \u00faj layout f\u00e1jlt, aminek a neve legyen fragment_game. Ez egy sz\u00fcrk\u00e9s h\u00e1tter\u0171 ConstraintLayout k\u00f6zep\u00e9n jelen\u00edtse meg a TicTacToeView n\u00e9zetet:

      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\nxmlns:app=\"http://schemas.android.com/apk/res-auto\"\nandroid:layout_width=\"match_parent\"\nandroid:layout_height=\"match_parent\"\nandroid:background=\"#888888\">\n\n<hu.bme.aut.android.tictactoe.view.TicTacToeView\nandroid:id=\"@+id/ticTacToeView\"\nandroid:layout_width=\"wrap_content\"\nandroid:layout_height=\"wrap_content\"\nandroid:layout_marginStart=\"8dp\"\nandroid:layout_marginTop=\"8dp\"\nandroid:layout_marginEnd=\"8dp\"\nandroid:layout_marginBottom=\"8dp\"\napp:layout_constraintBottom_toBottomOf=\"parent\"\napp:layout_constraintEnd_toEndOf=\"parent\"\napp:layout_constraintStart_toStartOf=\"parent\"\napp:layout_constraintTop_toTopOf=\"parent\"\napp:layout_constraintVertical_bias=\"0.495\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n

      package

      Fontos, hogy az itt szerepl\u0151 package n\u00e9v a saj\u00e1t TicTacToeView oszt\u00e1lyunk neve el\u0151tt azonos legyen a n\u00e9zet forr\u00e1s\u00e1nak tetej\u00e9n szerepl\u0151 package n\u00e9vvel, egy\u00e9bk\u00e9nt hib\u00e1t fogunk kapni, amikor megpr\u00f3b\u00e1ljuk megnyitni ezt a k\u00e9perny\u0151t. - De szerencs\u00e9re a k\u00f3dkieg\u00e9sz\u00edt\u0151 ebben is seg\u00edt.

      A fel\u00fclet ut\u00e1n k\u00e9sz\u00edts\u00fck el egy k\u00fcl\u00f6n fragments package-be mag\u00e1t a GameFragment-et is, aminek egyetlen feladata, hogy megjelen\u00edtse a fel\u00fclet\u00fcnket:

      package hu.bme.aut.android.tictactoe.fragments\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport hu.bme.aut.android.tictactoe.databinding.FragmentGameBinding\n\nclass GameFragment : Fragment() {\n\nprivate lateinit var binding: FragmentGameBinding\n\noverride fun onCreateView(\ninflater: LayoutInflater,\ncontainer: ViewGroup?,\nsavedInstanceState: Bundle?\n): View {\nbinding = FragmentGameBinding.inflate(layoutInflater, container, false)\nreturn binding.root\n}\n}\n
      Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Most m\u00e1r az \u00daj j\u00e1t\u00e9k gombra nyomva meg kell, hogy jelenjen a (m\u00e9g er\u0151sen hi\u00e1nyos) j\u00e1t\u00e9kter\u00fcnk.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9kt\u00e9r (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a GameFragmenthez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt val\u00f3s\u00edtsuk meg a j\u00e1t\u00e9kt\u00e9r kirajzol\u00e1s\u00e1t a TicTacToeView drawGameArea f\u00fcggv\u00e9ny\u00e9ben, azaz rajzoljuk meg a v\u00edzszintes \u00e9s f\u00fcgg\u0151leges vonalakat:

      private fun drawGameArea(canvas: Canvas) {\nval widthFloat: Float = width.toFloat()\nval heightFloat: Float = height.toFloat()\n\n// border\ncanvas.drawRect(0F, 0F, widthFloat, heightFloat, paintLine)\n\n// two horizontal lines\ncanvas.drawLine(0F, heightFloat / 3, widthFloat, widthFloat / 3, paintLine)\ncanvas.drawLine(0F, 2 * heightFloat / 3, widthFloat, 2 * heightFloat / 3, paintLine)\n\n// two vertical lines\ncanvas.drawLine(widthFloat / 3, 0F, widthFloat / 3, heightFloat, paintLine)\ncanvas.drawLine(2 * widthFloat / 3, 0F, 2 * widthFloat / 3, heightFloat, paintLine)\n}\n

      Ezt k\u00f6vet\u0151en val\u00f3s\u00edtsuk meg a modell alapj\u00e1n a j\u00e1t\u00e9kt\u00e9rben az X-ek \u00e9s O-k kirajzol\u00e1s\u00e1t az drawPlayers f\u00fcggv\u00e9nyben. A megval\u00f3s\u00edt\u00e1s sor\u00e1n v\u00e9gigmegy\u00fcnk a j\u00e1t\u00e9kt\u00e9r m\u00e1trix elemein \u00e9s a benne tal\u00e1lhat\u00f3 \u00e9rt\u00e9kek szerint O-t vagy X-et rajzolunk az adott mez\u0151be:

      private fun drawPlayers(canvas: Canvas) {\n// draw a circle at the center of the field\n// X coordinate: left side of the square + half width of the square\nfor (i in 0 until 3) {\nfor (j in 0 until 3) {\nwhen (TicTacToeModel.getFieldContent(i, j)) {\nTicTacToeModel.CIRCLE -> {\nval centerX = i * width / 3 + width / 6\nval centerY = j * height / 3 + height / 6\nval radius = height / 6 - 2\ncanvas.drawCircle(centerX.toFloat(), centerY.toFloat(), radius.toFloat(), paintLine)\n}\nTicTacToeModel.CROSS -> {\ncanvas.drawLine(\n(i * width / 3).toFloat(),\n(j * height / 3).toFloat(),\n((i + 1) * width / 3).toFloat(),\n((j + 1) * height / 3).toFloat(),\npaintLine\n)\ncanvas.drawLine(\n((i + 1) * width / 3).toFloat(),\n(j * height / 3).toFloat(),\n(i * width / 3).toFloat(),\n((j + 1) * height / 3).toFloat(),\npaintLine\n)\n}\n}\n}\n}\n}\n

      for ciklus

      A Kotlin for ciklus\u00e1nak nincs h\u00e1rom r\u00e9szre bontott, ;-vel elv\u00e1lasztott verzi\u00f3ja. Csak a fenti k\u00f3dban is l\u00e1that\u00f3 for each st\u00edlus\u00fa for ciklust t\u00e1mogatja a nyelv, amellyel azonban b\u00e1rmilyen iter\u00e1lhat\u00f3 objektumon ugyan\u00fagy tudunk iter\u00e1lni. Ha egyszer\u0171en sz\u00e1mokon szeretn\u00e9nk ezt megtenni, l\u00e9trehozhatunk egy iter\u00e1lhat\u00f3 Range-et p\u00e9ld\u00e1ul a 0..3 szintaxissal amivel egy z\u00e1rt intervallumot kapunk, vagy a fent haszn\u00e1lt 0 until 3 szintaxissal, ami egy jobbr\u00f3l ny\u00edlt intervallumot hoz l\u00e9tre, teh\u00e1t a 3 \u00e9rt\u00e9ket m\u00e1r nem fogja felvenni a ciklus v\u00e1ltoz\u00f3.

      V\u00e9g\u00fcl val\u00f3s\u00edtsuk meg az \u00e9rint\u00e9s esem\u00e9nyre val\u00f3 reag\u00e1l\u00e1st \u00fagy, hogy a megfelel\u0151 mez\u0151be \u2013 ha az \u00fcres \u2013 elhelyezz\u00fck az aktu\u00e1lis j\u00e1t\u00e9kost, melyet a modell nextPlayer v\u00e1ltoz\u00f3ja reprezent\u00e1l.

      A modell friss\u00edt\u00e9se ut\u00e1n az \u00fajrarajzol\u00e1st az invalidate() f\u00fcggv\u00e9ny megh\u00edv\u00e1s\u00e1val tudjuk el\u00e9rni.

      override fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\nval tX: Int = (event.x / (width / 3)).toInt()\nval tY: Int = (event.y / (height / 3)).toInt()\nif (tX < 3 && tY < 3 && TicTacToeModel.getFieldContent(tX, tY) == TicTacToeModel.EMPTY) {\nTicTacToeModel.setFieldContent(tX, tY, TicTacToeModel.nextPlayer)\ninvalidate()\n}\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n
      "},{"location":"laborok/tictactoe/#alkalmazas-ikon-lecserelese-onallo-feladat","title":"Alkalmaz\u00e1s ikon lecser\u00e9l\u00e9se - \u00d6n\u00e1ll\u00f3 feladat","text":"

      Az alkalmaz\u00e1s ikonj\u00e1t jelenleg a res/mipmap[-ldpi/mdpi/hdpi/xhdpi/...] mapp\u00e1kban tal\u00e1lhat\u00f3 ic_launcher.png jelk\u00e9pezi. Keress\u00fcnk egy \u00faj ikont \u00e9s cser\u00e9lj\u00fck le. Nem musz\u00e1j az ikont minden felbont\u00e1sban elk\u00e9sz\u00edteni, egyszer\u0171en elhelyezhet\u00f3nk egy m\u00e9retet a mipmap mapp\u00e1ban is (melyet l\u00e9tre kell hozni), ekkor term\u00e9szetesen k\u00fcl\u00f6nb\u00f6z\u0151 felbont\u00e1s\u00fa eszk\u00f6z\u00f6k\u00f6n torzulhat az ikon k\u00e9pe. (Ha marad id\u0151, a be\u00e9p\u00edtett Asset Studio-val elk\u00e9sz\u00edthetj\u00fck az \u00f6sszes sz\u00fcks\u00e9ges v\u00e1ltozatot.)

      Ikon gener\u00e1l\u00e1sa

      Ikon gener\u00e1l\u00e1s\u00e1ra haszn\u00e1lhatjuk p\u00e9ld\u00e1ul a k\u00f6vetkez\u0151 oldalt: https://icon.kitchen/

      Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st!

      \u00c9szrevehetj\u00fck, hogy ha a j\u00e1t\u00e9kt\u00e9rr\u0151l visszal\u00e9p\u00fcnk \u00e9s megint \u00faj j\u00e1t\u00e9kot kezd\u00fcnk, a j\u00e1t\u00e9kt\u00e9r nem t\u00f6rl\u0151dik. Ez\u00e9rt a GameActivity-re val\u00f3 navig\u00e1ci\u00f3 el\u0151tt a TicTacToeModel-t alap\u00e1llapotba kell \u00e1ll\u00edtanunk, hogy \u00faj j\u00e1t\u00e9k kezd\u0151dj\u00f6n (MainActivity.kt):

      binding.btnStart.setOnClickListener {\nTicTacToeModel.resetModel()\nstartActivity(Intent(this@MainActivity, GameActivity::class.java))\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9kt\u00e9r j\u00e1t\u00e9k k\u00f6zben (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a TicTacToeView k\u00f3dj\u00e1nak egy r\u00e9szlete, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      "},{"location":"laborok/tictactoe/#jateklogika-ellenorzese-onallo-feladat","title":"J\u00e1t\u00e9klogika ellen\u0151rz\u00e9se - \u00d6n\u00e1ll\u00f3 feladat","text":"

      Val\u00f3s\u00edtson meg egy f\u00fcggv\u00e9nyt, mely minden l\u00e9p\u00e9s ut\u00e1n leellen\u0151rzi, hogy gy\u0151z\u00f6tt-e valamelyik j\u00e1t\u00e9kos, vagy nincs-e d\u00f6ntetlen. Amennyiben v\u00e9ge a j\u00e1t\u00e9knak, egy Toast \u00fczenettel jelezze ezt a felhaszn\u00e1l\u00f3nak \u00e9s l\u00e9pjen vissza a f\u0151men\u00fcbe. A laborvezet\u0151 seg\u00edts\u00e9g\u00e9vel vizsg\u00e1lja meg, hogy a View oszt\u00e1lyb\u00f3l hogyan \u00e9rhet\u0151 el az \u0151t tartalmaz\u00f3 \"host\" Activity, aminek \u00edgy p\u00e9ld\u00e1ul egy gameOver() f\u00fcggv\u00e9nye megh\u00edvhat\u00f3, ami megval\u00f3s\u00edtja a fent le\u00edrt j\u00e1t\u00e9k befejez\u00e9st.

      J\u00f3 munk\u00e1t k\u00edv\u00e1nunk!

      Seg\u00edts\u00e9g

      A j\u00e1t\u00e9k\u00e1llapot ellen\u0151rz\u00e9se a TicTacToeModel feladata, \u00edgy oda k\u00e9sz\u00edts\u00fcnk egy f\u00fcggv\u00e9nyt, ami ezt teszi meg:

      fun checkGameState(): Byte { ///TODO 4 \u00e1llapottal t\u00e9rhet vissza: \n// k\u00f6r nyert\n// kereszt nyert\n// d\u00f6ntetlen\n// m\u00e9g nincs v\u00e9ge\nreturn CIRCLE\n}\n

      J\u00e1t\u00e9k \u00e1llapotot ellen\u0151rizni \u00faj jel lehelyez\u00e9se ut\u00e1n \u00e9rdemes, teh\u00e1t pl. a TicTacToeView onTouchEvent() f\u00fcggv\u00e9ny\u00e9ben:

      override fun onTouchEvent(event: MotionEvent?): Boolean {\nwhen (event?.action) {\nMotionEvent.ACTION_DOWN -> {\nval tX: Int = (event.x / (width / 3)).toInt()\nval tY: Int = (event.y / (height / 3)).toInt()\nif (tX < 3 && tY < 3 && TicTacToeModel.getFieldContent(tX, tY) == TicTacToeModel.EMPTY) {\nTicTacToeModel.setFieldContent(tX, tY, TicTacToeModel.nextPlayer)\ninvalidate()\nval result = TicTacToeModel.checkGameState()\n///v\u00e9ge van, teh\u00e1t tov\u00e1bb h\u00edvunk\nif (result != TicTacToeModel.EMPTY) {\n(context as GameActivity).gameOver(result)\n}\n}\nreturn true\n}\nelse -> return super.onTouchEvent(event)\n}\n}\n

      A context ilyen kasztol\u00e1sa nem sz\u00e9p, \u00e9s vesz\u00e9lyes is. Itt csak az egyszer\u0171s\u00e9ge miatt haszn\u00e1ljuk. A f\u00e9l\u00e9v k\u00e9s\u0151bbi r\u00e9sz\u00e9ben tanulunk szebb megold\u00e1st erre a probl\u00e9m\u00e1ra.

      A Toast-ot pedig a GameActivity-b\u0151l dobjuk az eredm\u00e9ny alapj\u00e1n, majd bez\u00e1rjuk az Activity-t:

      fun gameOver(result: Byte) {\nwhen (result) {\n///TODO t\u00f6bb eset\nTicTacToeModel.CIRCLE -> {\nToast.makeText(this@GameActivity, \"A k\u00f6r nyert\", Toast.LENGTH_LONG).show()\n}\n}\nfinish()\n}\n

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a j\u00e1t\u00e9k v\u00e9g\u00e9t jelz\u0151 Toast \u00fczenet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), a j\u00e1t\u00e9k\u00e1llapot ellen\u0151rz\u00e9s\u00e9hez tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt. A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l.

      "},{"location":"laborok/timetable/","title":"LaborExtra - \u00d3rarend","text":""},{"location":"laborok/timetable/#bevezeto","title":"Bevezet\u0151","text":"

      A labor sor\u00e1n a feladat egy \u00f3rarend alkalmaz\u00e1s elk\u00e9sz\u00edt\u00e9se, ahol a felhaszn\u00e1l\u00f3 grafikus fel\u00fcleten szerkesztheti \u00e9s l\u00e1thatja a napi, valamint heti beoszt\u00e1s\u00e1t.

      Az Play Store-ban sz\u00e1mos ilyen szoftver tal\u00e1lhat\u00f3, melyeket \u00e9rdemes megvizsg\u00e1lni \u00e9s \u00f6tleteket mer\u00edteni a megval\u00f3s\u00edt\u00e1shoz.

      "},{"location":"laborok/timetable/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

      A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

      "},{"location":"laborok/timetable/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
      1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

      2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

        Checkout

        Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

      3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

      4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

      FILE PATH

      A projekt mindenk\u00e9ppen egy repository-ban l\u00e9v\u0151 k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

      "},{"location":"laborok/timetable/#kovetelmenyek","title":"K\u00f6vetelm\u00e9nyek","text":"

      Az alkalmaz\u00e1s megtervez\u00e9se \u00e9s megval\u00f3s\u00edt\u00e1sa teljes m\u00e9rt\u00e9kben k\u00f6tetlen, az elv\u00e1rt minim\u00e1lis funkcionalit\u00e1s:

      • \u00d3ra (esem\u00e9ny) l\u00e9trehoz\u00e1sa, szerkeszt\u00e9se, t\u00f6rl\u00e9se. Attrib\u00fatumai (legal\u00e1bb):
        • Kezd\u00e9si, befejez\u00e9si id\u0151 (pl. 8:15-11:45),
        • T\u00e1rgy (pl. Android alap\u00fa szoftverfejleszt\u00e9s),
        • Helysz\u00edn (pl. QBF08),
        • Sz\u00edn, amellyel megjelenik a napt\u00e1rban (pl. #234567)
      • \u00d3r\u00e1k \u00e1tl\u00e1that\u00f3 megjelen\u00edt\u00e9se napi \u00e9s heti bont\u00e1sban teljes k\u00e9perny\u0151n
      • Egy \u00f3ra r\u00e9szletes adatlapja
      • Adatok perzisztens t\u00e1rol\u00e1sa

      A fenti specifik\u00e1ci\u00f3 megval\u00f3s\u00edt\u00e1sa 4-es oszt\u00e1lyzatot jelent, jobb min\u0151s\u00edt\u00e9shez tov\u00e1bbi kreat\u00edv funkci\u00f3k be\u00e9p\u00edt\u00e9se \u00e9s ig\u00e9nyes felhaszn\u00e1l\u00f3i fel\u00fclet sz\u00fcks\u00e9ges.

      N\u00e9h\u00e1ny \u00f6tlet:

      • Widget
      • Eml\u00e9keztet\u0151 be\u00e1ll\u00edt\u00e1sa az \u00f3r\u00e1khoz
      • A-h\u00e9t, B-h\u00e9t t\u00e1mogat\u00e1sa
      • Vizsg\u00e1k, ZH-k be\u00e9p\u00edt\u00e9se
      • telefon \u00e9s tablet, \u00e1ll\u00f3-fekv\u0151 felhaszn\u00e1l\u00f3i fel\u00fclet
      • nem default UI elemek haszn\u00e1lata (mint a School Helper-ben)
      • Esem\u00e9nyek import\u00e1l\u00e1sa napt\u00e1rb\u00f3l, vagy iCal f\u00e1jlb\u00f3l
      • Teljes \u00f3rarend export\u00e1l\u00e1sa-import\u00e1l\u00e1sa saj\u00e1t tervez\u00e9s\u0171 f\u00e1jlba
      • T\u00f6bb skin t\u00e1mogat\u00e1sa

      BEADAND\u00d3 (5 pont)

      A rep\u00f3ba felt\u00f6ltend\u0151 az alkalmaz\u00e1s projekt k\u00f6nyvt\u00e1r\u00e1n fel\u00fcl egy felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv is (README.md), ami le\u00edrja az elk\u00e9sz\u00edtett szoftver funkci\u00f3it, \u00e9s k\u00e9peket is tartalmaz minden relev\u00e1ns k\u00e9perny\u0151r\u0151l.

      "},{"location":"laborok/todo_compose_basics/","title":"Labor05 - Todo Alkalmaz\u00e1s","text":"

      A labor c\u00e9lja, hogy bemutassa, hogyan lehet egy egyszer\u0171 ToDo alkalmaz\u00e1st megval\u00f3s\u00edtani a Compose keretrendszerben.

      "},{"location":"laborok/todo_compose_basics/#elokeszuletek","title":"El\u0151k\u00e9sz\u00fcletek","text":"

      A feladatok megold\u00e1sa sor\u00e1n ne felejtsd el k\u00f6vetni a feladat bead\u00e1s folyamat\u00e1t.

      "},{"location":"laborok/todo_compose_basics/#git-repository-letrehozasa-es-letoltese","title":"Git repository l\u00e9trehoz\u00e1sa \u00e9s let\u00f6lt\u00e9se","text":"
      1. Moodle-ben keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-j\u00e9t \u00e9s annak seg\u00edts\u00e9g\u00e9vel hozd l\u00e9tre a saj\u00e1t repository-dat.

      2. V\u00e1rd meg, m\u00edg elk\u00e9sz\u00fcl a repository, majd checkout-old ki.

        Egyetemi laborokban, ha a checkout sor\u00e1n nem k\u00e9r a rendszer felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t, \u00e9s nem siker\u00fcl a checkout, akkor val\u00f3sz\u00edn\u0171leg a g\u00e9pen kor\u00e1bban megjegyzett felhaszn\u00e1l\u00f3n\u00e9vvel pr\u00f3b\u00e1lkozott a rendszer. El\u0151sz\u00f6r t\u00f6r\u00f6ld ki a mentett bel\u00e9p\u00e9si adatokat (l\u00e1sd itt), \u00e9s pr\u00f3b\u00e1ld \u00fajra.

      3. Hozz l\u00e9tre egy \u00faj \u00e1gat megoldas n\u00e9ven, \u00e9s ezen az \u00e1gon dolgozz.

      4. A neptun.txt f\u00e1jlba \u00edrd bele a Neptun k\u00f3dodat. A f\u00e1jlban semmi m\u00e1s ne szerepeljen, csak egyetlen sorban a Neptun k\u00f3d 6 karaktere.

      "},{"location":"laborok/todo_compose_basics/#projekt-letrehozasa","title":"Projekt l\u00e9trehoz\u00e1sa","text":"

      Ezut\u00e1n ind\u00edtsuk el az Android Studio-t, majd:

      1. Hozzunk l\u00e9tre egy \u00faj projektet, v\u00e1lasszuk az Empty Activity lehet\u0151s\u00e9get.
      2. A projekt neve legyen Todo, a kezd\u0151 package pedig hu.bme.aut.android.todo.
      3. A projektet a repository-n bel\u00fcl egy k\u00fcl\u00f6n mapp\u00e1ban hozzuk l\u00e9tre.
      4. A minimum API szint legyen 26 (Android 8.0).
      5. A Build configuration language-n\u00e9l v\u00e1lasszuk a Kotlin DSL-t.

      FILE PATH

      A projekt a repository-ban l\u00e9v\u0151 Todo k\u00f6nyvt\u00e1rba ker\u00fclj\u00f6n, \u00e9s bead\u00e1sn\u00e1l legyen is felpusholva! A k\u00f3d n\u00e9lk\u00fcl nem tudunk maxim\u00e1lis pontot adni a laborra!

      Ellen\u0151r\u00edzz\u00fck, hogy a l\u00e9trej\u00f6tt projekt lefordul \u00e9s helyesen m\u0171k\u00f6dik!

      "},{"location":"laborok/todo_compose_basics/#verziok-frissitese","title":"Verzi\u00f3k friss\u00edt\u00e9se","text":"

      Vegy\u00fck fel a sz\u00fcks\u00e9ges k\u00f6nyvt\u00e1rakat a libs.versions.toml f\u00e1jlban:

      [versions]\n...\ncomposeBom = \"2024.09.03\"\nkotlinxDatetime = \"0.4.1\"\nlifecycleVersion = \"2.8.6\"\nnavigationCompose = \"2.8.2\"\n\n[libraries]\n...\nandroidx-lifecycle-runtime-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-runtime-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-lifecycle-viewmodel-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-viewmodel-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\" }\nandroidx-navigation-compose = { group = \"androidx.navigation\", name=\"navigation-compose\", version.ref = \"navigationCompose\" }\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\n

      Majd pedig haszn\u00e1ljuk is ezeket a modul szint\u0171 build.gradle.kts f\u00e1jlban:

      dependencies {\n    ...\n\n    //Compose Bill of Materials\n    implementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n\n    //ViewModel Lifecycle\n    implementation(libs.androidx.lifecycle.runtime.compose)\n    implementation(libs.androidx.lifecycle.viewmodel.compose)\n\n    //Kotlin Extensions DateTime - LocalDate\n    implementation(libs.kotlinx.datetime)\n\n    //Compose Navigation\n    implementation(libs.androidx.navigation.compose)\n\n    //Material Icons\n    implementation(libs.androidx.material.icons.extended)\n}\n

      F\u00fcgg\u0151s\u00e9gek

      Az itt tal\u00e1lhat\u00f3 k\u00f3dban minden f\u00fcgg\u0151s\u00e9g szerepel, a labor sor\u00e1n \u00fajat hozz\u00e1adni nem kell. Azonban az egy\u00e9rtelm\u0171s\u00e9g kedv\u00e9\u00e9rt a k\u00e9s\u0151bbiekben mindenhol felt\u00fcntetj\u00fck az adott ter\u00fclethez sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket.

      A fenti f\u00fcgg\u0151s\u00e9gekhez 34-es SDK-val kell ford\u00edtanunk a projektet, ha a legener\u00e1lt alkalmaz\u00e1sban kor\u00e1bbi lenne megadva, akkor friss\u00edts\u00fck ezt is a modul szint\u0171 build.gradle.kts f\u00e1jlunkban:

          compileSdk = 34\n
      "},{"location":"laborok/todo_compose_basics/#szoveges-eroforrasok-definialasa","title":"Sz\u00f6veges er\u0151forr\u00e1sok defini\u00e1l\u00e1sa","text":"

      A strings.xml f\u00e1jl m\u0171k\u00f6d\u00e9s\u00e9t m\u00e1r ismerj\u00fck, t\u00f6lts\u00fck fel ezt el\u0151re a k\u00e9s\u0151bb sz\u00fcks\u00e9ges sz\u00f6veges c\u00edmk\u00e9kkel, hogy k\u00e9s\u0151bb a l\u00e9nyeges elemekre tudjunk koncentr\u00e1lni:

      <resources>\n<string name=\"app_name\">Todo</string>\n<string name=\"some_error_message\">Error</string>\n<string name=\"priority_title_none\">none</string>\n<string name=\"priority_title_low\">low</string>\n<string name=\"priority_title_medium\">medium</string>\n<string name=\"priority_title_high\">high</string>\n<string name=\"text_empty_todo_list\">\"You haven\\\\'t added any todos yet. \"</string>\n<string name=\"text_your_todo_list\">Your todos</string>\n<string name=\"list_item_supporting_text\">The due date is: %1$s</string>\n<string name=\"textfield_label_description\">Description</string>\n<string name=\"textfield_label_title\">Title</string>\n<string name=\"app_bar_title_create_todo\">Create todo</string>\n<string name=\"dialog_ok_button_text\">OK</string>\n<string name=\"dialog_dismiss_button_text\">Close</string>\n</resources>\n
      "},{"location":"laborok/todo_compose_basics/#adatosztalyok-letrehozasa","title":"Adatoszt\u00e1lyok l\u00e9trehoz\u00e1sa","text":"

      Miel\u0151tt nekil\u00e1tn\u00e1nk az alkalmaz\u00e1s fel\u00fcleteinek, illetve logik\u00e1j\u00e1nak kialak\u00edt\u00e1s\u00e1ba, \u00e9rdemes l\u00e9trehozni azokat a modelloszt\u00e1lyokat, amiket az alkalmaz\u00e1son bel\u00fcl haszn\u00e1lni fogunk. Az alkalmaz\u00e1sunkban feladatokat akarunk t\u00e1rolni, melyek a k\u00f6vetkez\u0151 tulajdons\u00e1gokkal fognak rendelkezni:

      • N\u00e9v
      • Le\u00edr\u00e1s
      • Feladat hat\u00e1rideje
      • Fontoss\u00e1g
      • Azonos\u00edt\u00f3

      Hozzunk l\u00e9tre egy \u00faj domain package-t l\u00e9tre a projekt\u00fcnk gy\u00f6ker\u00e9ben, mely az alkalmaz\u00e1sunk adatr\u00e9teg\u00e9nek r\u00e9szeit fogja tartalmazni, majd ezen bel\u00fcl hozzunk l\u00e9tre egy model package-et, mely az adatmodellek oszt\u00e1ly megfelel\u0151it fogja tartalmazni. Ebben hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt: Todo.kt:

      package hu.bme.aut.android.todo.domain.model\n\nimport kotlinx.datetime.LocalDate\n\ndata class Todo(\nval id: Int,\nval title: String,\nval priority: Priority,\nval dueDate: LocalDate,\nval description: String  )\n

      Priority.kt:

      package hu.bme.aut.android.todo.domain\n\nenum class Priority {  NONE,  LOW,  MEDIUM,  HIGH,  }\n

      LocalDate

      A LocalDate egy \u00e1ltal\u00e1nos implement\u00e1ci\u00f3ja az id\u0151 kezel\u00e9s\u00e9nek, mely multiplatform k\u00f6rnyezetben is haszn\u00e1lhat\u00f3, ehhez a k\u00f6vetkez\u0151 f\u00fcgg\u0151s\u00e9get kell hozz\u00e1adnunk a modul szint\u0171 build.gradle f\u00e1jlhoz:

      [versions]\nkotlinxDatetime = \"0.4.1\"\n\n[libraries]\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\n

      Id\u0151 oszt\u00e1lyok kezel\u00e9se

      A labor sor\u00e1n a LocalDate mindig a kotlinx, mig a LocalDateTime mindig a java k\u00f6nyvt\u00e1rb\u00f3l legyen import\u00e1lva.

      Az adat t\u00edpus\u00fa oszt\u00e1lyok eset\u00e9ben a Kotlin automatikusan deklar\u00e1l gyakran haszn\u00e1lt f\u00fcggv\u00e9nyeket, mint p\u00e9ld\u00e1ul az equals() \u00e9s hashCode() f\u00fcggv\u00e9nyeket k\u00fcl\u00f6nb\u00f6z\u0151 objektumok \u00f6sszehasonl\u00edt\u00e1s\u00e1hoz, illetve egy toString() f\u00fcggv\u00e9nyt, mely visszaadja a t\u00e1rolt v\u00e1ltoz\u00f3k \u00e9rt\u00e9k\u00e9t.

      A felhaszn\u00e1l\u00f3i fel\u00fclet k\u00f3dj\u00e1nak egyszer\u0171s\u00edt\u00e9se \u00e9rd\u00e9k\u00e9ben \u00e9rdemes olyan seg\u00e9doszt\u00e1lyokat is defini\u00e1lni, melyek m\u00e1r k\u00f6zvetlen\u00fcl a fel\u00fcleten haszn\u00e1lt \u00e9rt\u00e9keket fogj\u00e1k haszn\u00e1lni. Ehhez deifini\u00e1ljuk a ui package-en bel\u00fcl a model package-et, \u00e9s vegy\u00fck fel a k\u00f6vetkez\u0151 oszt\u00e1lyokat: UiText.kt:

      package hu.bme.aut.android.todo.ui.model\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport hu.bme.aut.android.todo.R\n\nsealed class UiText {\ndata class DynamicString(val value: String): UiText()\ndata class StringResource(@StringRes val id: Int): UiText()\n\nfun asString(context: Context): String {\nreturn when(this) {\nis DynamicString -> this.value\nis StringResource -> context.getString(this.id)\n}\n}\n}\n\nfun Throwable.toUiText(): UiText {\nval message = this.message.orEmpty()\nreturn if (message.isBlank()) {\nUiText.StringResource(R.string.some_error_message)\n} else {\nUiText.DynamicString(message)\n}\n}\n

      Vizsg\u00e1ljuk meg, hogy tudjuk a sealed class seg\u00edts\u00e9g\u00e9vel \u00e1ltal\u00e1nosan defini\u00e1lni a sz\u00f6vegeket, melyek \u00edgy j\u00f6hetnek a be\u00e9getett er\u0151forr\u00e1sb\u00f3l, vagy \u00e9rkezhetnek a szerveren kereszt\u00fcl egy k\u00fcls\u0151 forr\u00e1sb\u00f3l.

      PriorityUi.kt:

      package hu.bme.aut.android.todo.ui.model\n\nimport androidx.compose.ui.graphics.Color\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.domain.model.Priority\n\nenum class PriorityUi(\nval title: Int,\nval color: Color\n) {\nNone(\ntitle =  R.string.priority_title_none,\ncolor = Color(0xFFE6E4E4)\n),\nLow(\ntitle = R.string.priority_title_low,\ncolor = Color(0xFF8BC34A)\n),\nMedium(\ntitle = R.string.priority_title_medium,\ncolor = Color(0xFFFFC107)\n),\nHigh(\ntitle = R.string.priority_title_high,\ncolor = Color(0xFFF44336)\n),\n}\n\nfun PriorityUi.asPriority(): Priority {\nreturn when(this) {\nPriorityUi.None -> Priority.NONE\nPriorityUi.Low -> Priority.LOW\nPriorityUi.Medium -> Priority.MEDIUM\nPriorityUi.High -> Priority.HIGH\n}\n}\n\nfun Priority.asPriorityUi(): PriorityUi {\nreturn when(this) {\nPriority.NONE -> PriorityUi.None\nPriority.LOW -> PriorityUi.Low\nPriority.MEDIUM -> PriorityUi.Medium\nPriority.HIGH -> PriorityUi.High\n}\n}\n

      TodoUi.kt

      package hu.bme.aut.android.todo.ui.model\n\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.toLocalDate\nimport java.time.LocalDateTime\n\ndata class TodoUi(\nval id: Int = 0,\nval title: String = \"\",\nval priority: PriorityUi = PriorityUi.None,\nval dueDate: String = LocalDate(  LocalDateTime.now().year,  LocalDateTime.now().monthValue,  LocalDateTime.now().dayOfMonth  ).toString(),\nval description: String = \"\"  )  fun Todo.asTodoUi(): TodoUi = TodoUi(  id = id,  title = title,  priority = priority.asPriorityUi(),  dueDate = dueDate.toString(),  description = description  )  fun TodoUi.asTodo(): Todo = Todo(  id = id,  title = title,  priority = priority.asPriority(),  dueDate = dueDate.toLocalDate(),  description = description  )\n
      "},{"location":"laborok/todo_compose_basics/#navigacio-kialakitasa","title":"Navig\u00e1ci\u00f3 kialak\u00edt\u00e1sa","text":"

      Az el\u0151z\u0151 laborhoz hasonl\u00f3an alak\u00edtsuk ki a projektben a navig\u00e1ci\u00f3n\u00e1l haszn\u00e1lt oszt\u00e1lyokat!

      Navig\u00e1ci\u00f3

      Itt is a Compose Navigation k\u00f6nyvt\u00e1rat fogjuk haszn\u00e1lni, ez\u00e9rt adjuk ezt hozz\u00e1 a modul szint\u0171 build.gradle f\u00e1jlunkhoz.

      [versions]\nnavigationCompose = \"2.8.1\"\n\n[libraries]\nandroidx-navigation-compose = { group = \"androidx.navigation\", name=\"navigation-compose\", version.ref = \"navigationCompose\" }\n

      Hozzunk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1rban egy \u00faj package-et navigation n\u00e9ven, majd hozzuk l\u00e9tre benne az \u00fatvonalakat reprezent\u00e1l\u00f3 Screen oszt\u00e1lyt:

      package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {  }\n

      Illetve hozzuk l\u00e9tre a navig\u00e1ci\u00f3t v\u00e9gz\u0151 Composable f\u00fcggv\u00e9nyt is a NavGraph.kt f\u00e1jlban:

      package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.rememberNavController\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = \"\"\n) {\n\n}\n}\n

      A NavGraph Composable szerepe, hogy karban tartsa az \u00fatvonalakat, itt fogjuk a navgi\u00e1ci\u00f3s esem\u00e9nyeket feldolgozni.

      V\u00e9g\u00fcl friss\u00edts\u00fck a MainActivity tartalm\u00e1t \u00fagy, hogy a NavGraph Composable-t haszn\u00e1lja:

      package hu.bme.aut.android.todo\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport hu.bme.aut.android.todo.navigation.NavGraph\nimport hu.bme.aut.android.todo.ui.theme.TodoTheme\n\nclass MainActivity : ComponentActivity() {\noverride fun onCreate(savedInstanceState: Bundle?) {\nsuper.onCreate(savedInstanceState)\nsetContent {\nTodoTheme {\nNavGraph()\n}\n}\n}\n}\n
      "},{"location":"laborok/todo_compose_basics/#lista-oldal-kialakitasa","title":"Lista oldal kialak\u00edt\u00e1sa","text":"

      Ahhoz, hogy az alkalmaz\u00e1sunk m\u0171k\u00f6dj\u00f6n, sz\u00fcks\u00e9g\u00fcnk lesz egy oldalra, amit indul\u00e1skor meg tudunk jelen\u00edteni. Az els\u0151 oldal, melyet l\u00e9trehozunk, a feladatokat megjelen\u00edt\u0151 lista oldal lesz. Gondoljuk v\u00e9gig, milyen feladatokat kell elv\u00e9gezni, illetve milyen interakci\u00f3k t\u00f6rt\u00e9nnek ezen a fel\u00fcleten:

      • Az oldalra val\u00f3 navig\u00e1l\u00e1skor be kell t\u00f6lteni az \u00f6sszes feladatot.
      • Egy feladatra val\u00f3 kattint\u00e1s ut\u00e1n el kell navig\u00e1lni egy r\u00e9szletez\u0151 oldalra.
      • El\u00e9rhet\u0151v\u00e9 kell tenni egy \u00faj feladat l\u00e9trehoz\u00e1s\u00e1t, melynek hat\u00e1s\u00e1ra \u00faj oldalra kell navig\u00e1lnunk.

      Az \u00faj oldalakra val\u00f3 navig\u00e1l\u00e1shoz sz\u00fcks\u00e9g\u00fcnk van a navig\u00e1ci\u00f3t vez\u00e9rl\u0151 kontrollerre, melyet a NavGraph Composable kezel, ez\u00e9rt ezekn\u00e9l az esem\u00e9nyekn\u00e9l az oldal olyan f\u00fcggv\u00e9ny callback objektumokat fog megh\u00edvni, melyeket a konstruktor\u00e1n kereszt\u00fcl kap meg, \u00edgy a NavGraph k\u00f6nnyen tud \u00e9rtes\u00fclni r\u00f3luk.

      Az adatok kezel\u00e9s\u00e9hez tipikusan a ViewModel oszt\u00e1lyt haszn\u00e1ljuk. A ViewModel seg\u00edts\u00e9g\u00e9vel biztos\u00edtjuk azt, hogy elk\u00fcl\u00f6n\u00fcljenek az alkalmaz\u00e1sunk megjelen\u00edt\u00e9s\u00e9rt szolg\u00e1l\u00f3 k\u00f3djai az alkalmaz\u00e1s logik\u00e1j\u00e1t biztos\u00edt\u00f3 k\u00f3djait\u00f3l. M\u00edg az el\u0151bbiek a fel\u00fclet megjelen\u00e9s\u00e9\u00e9rt felelnek, a ViewModel t\u00e1rolja \u00e9s dolgozza fel a UI-nak sz\u00fcks\u00e9ges adatokat.

      ViewModel Lifecycle

      Vegy\u00fck fel a sz\u00fcks\u00e9ges f\u00fcgg\u0151s\u00e9geket:

      [versions]\nlifecycleVersion = \"2.8.6\"\n[libraries]\nandroidx-lifecycle-runtime-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-runtime-compose\", version.ref = \"lifecycleVersion\" }\nandroidx-lifecycle-viewmodel-compose = { group = \"androidx.lifecycle\", name=\"lifecycle-viewmodel-compose\", version.ref = \"lifecycleVersion\" }\n

      Hozzuk l\u00e9tre a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl a feature package-et, mely az egyes oldalak Composable \u00e9s ViewModel oszt\u00e1lyait fogja tartalmazni funkci\u00f3nk\u00e9nt k\u00fcl\u00f6n package-ben, majd hozzuk l\u00e9tre ebben a todo_list package-t.

      El\u0151sz\u00f6r foglalkozzunk az oldalhoz tartoz\u00f3 ViewModel oszt\u00e1llyal. Hozzuk l\u00e9tre a TodoListViewModel.kt f\u00e1jlt, majd m\u00e1soljuk be az al\u00e1bbi k\u00f3dr\u00e9szletet:

      package hu.bme.aut.android.todo.feature.todo_list\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nsealed class TodoListState {\nobject Loading : TodoListState()\ndata class Error(val error: Throwable) : TodoListState()\ndata class Result(val todoList : List<TodoUi>) : TodoListState()\n}\n\nclass TodoListViewModel() : ViewModel() {\nprivate val _state = MutableStateFlow<TodoListState>(TodoListState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\ndelay(2000)\n//TODO: Add todo loading logic\n_state.value = TodoListState.Result(\ntodoList = listOf(\nTodoUi(\nid = 1,\ntitle = \"Teszt feladat 1\",\npriority = PriorityUi.Low,\ndescription = \"Feladat le\u00edr\u00e1s 1\",\n),\nTodoUi(\nid = 2,\ntitle = \"Teszt feladat 2\",\npriority = PriorityUi.Medium,\ndescription = \"Feladat le\u00edr\u00e1s 2\",\n),\nTodoUi(\nid = 3,\ntitle = \"Teszt feladat 3\",\npriority = PriorityUi.High,\ndescription = \"Feladat le\u00edr\u00e1s 3\",\n),\n),\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoListViewModel()\n}\n}\n}\n}\n

      A fel\u00fcletet le\u00edr\u00f3 \u00e1llapot oszt\u00e1lyt sealed class-k\u00e9nt deklar\u00e1ljuk, \u00e9s j\u00f3l elk\u00fcl\u00f6n\u00edtett \u00e1llapot oszt\u00e1lyokat vesz\u00fcnk fel, \u00edgy is jelezve, hogy az egyes \u00e1llapotokban az oldalunkon mit kell megjelen\u00edteni. Ezeket egy MutableStateFlow seg\u00edts\u00e9g\u00e9vel kezelj\u00fck, melyet egy csak olvashat\u00f3 v\u00e1ltozat\u00e1ban osztunk meg az oldalt reprezent\u00e1l\u00f3 Composable-el. Az adatok bet\u00f6lt\u00e9s\u00e9t egyel\u0151re a ViewModel mag\u00e1ban v\u00e9gzi el, ez azonban hamarosan ki lesz b\u0151v\u00edtve k\u00fcls\u0151 adatbet\u00f6lt\u00e9s t\u00e1mogat\u00e1s\u00e1val.

      Mivel a ViewModel k\u00e9pes t\u00fal\u00e9lni az \u0151t l\u00e9trehoz\u00f3 komponenst, ez\u00e9rt a k\u00f3db\u00f3l mi nem a konstruktor h\u00edv\u00e1s\u00e1val fogjuk l\u00e9trehozni a p\u00e9ld\u00e1nyt, hanem a keretrendszernek tudunk \u00e1tadni egy speci\u00e1lis factory met\u00f3dust, amit a rendszer az els\u0151 alkalommal meg fog h\u00edvni. Ezt a met\u00f3dust szervezt\u00fck ki a companion object r\u00e9szbe, ami jelenleg csak l\u00e9trehoz egy p\u00e9ld\u00e1nyt, a k\u00e9s\u0151bbiekben azonban hasznos lesz k\u00fcl\u00f6nb\u00f6z\u0151 k\u00fcls\u0151 \u00e9rt\u00e9kek inicializ\u00e1l\u00e1s\u00e1ra.

      Hozzuk l\u00e9tre a fel\u00fcletet megval\u00f3s\u00edt\u00f3 TodoListScreen.kt f\u00e1jlt is ugyanebben a packageben:

      package hu.bme.aut.android.todo.feature.todo_list\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.ui.model.toUiText\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Composable\nfun TodoListScreen(\nonListItemClick: (Int) -> Unit,\nonFabClick: () -> Unit,\nviewModel: TodoListViewModel = viewModel(factory = TodoListViewModel.Factory),\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\nval context = LocalContext.current\n\nScaffold(\nmodifier = Modifier.fillMaxSize(),\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = onFabClick,\ncontainerColor = MaterialTheme.colorScheme.primary,\ncontentColor = MaterialTheme.colorScheme.onPrimary\n) {\nIcon(imageVector = Icons.Default.Add, contentDescription = null)\n}\n}\n) { innerPadding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(innerPadding)\n.padding(8.dp)\n.background(\ncolor = if (state is TodoListState.Loading || state is TodoListState.Error) {\nMaterialTheme.colorScheme.secondaryContainer\n} else {\nMaterialTheme.colorScheme.background\n}\n),\ncontentAlignment = Alignment.Center\n) {\nwhen (state) {\nis TodoListState.Loading -> CircularProgressIndicator(\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\n\nis TodoListState.Error -> Text(\ntext = state.error.toUiText().asString(context)\n)\n\nis TodoListState.Result -> {\nif (state.todoList.isEmpty()) {\nText(text = stringResource(id = R.string.text_empty_todo_list))\n} else {\n///TODO: handle list\n}\n}\n}\n}\n}\n}\n

      Mint a legt\u00f6bb esetben, itt is egy Scaffold-ot haszn\u00e1lunk az oldalunk kezel\u00e9s\u00e9re, melyhez most egy LargeFloatingActionButton-t is adunk, amellyel majd \u00faj feladatokat lehet l\u00e9trehozni. Ne felejts\u00fck el a Scaffold f\u0151 tartalm\u00e1ban innerPadding n\u00e9vvel megkapott PaddingValues \u00e9rt\u00e9keket a megfelel\u0151 helyre besz\u00farni (ez ebben az esetben a f\u0151 Box k\u00f6r\u00e9 ker\u00fcl. Ezek mellett l\u00e1that\u00f3, hogyan tudunk az aktu\u00e1lis \u00e1llapot k\u00fcl\u00f6nb\u00f6z\u0151 \u00e9rt\u00e9keinek f\u00fcggv\u00e9ny\u00e9ben el\u00e1gazni, \u00e9s k\u00fcl\u00f6nb\u00f6z\u0151 elemeket megjelen\u00edteni.

      Vizsg\u00e1ljuk meg, hogyan t\u00f6rt\u00e9nik az oldal friss\u00edt\u00e9se! A collectAsStateWithLifecycle() f\u00fcggv\u00e9nyh\u00edv\u00e1ssal automatikusan feliratkozunk a ViewModel-ben t\u00e1rolt \u00e1llapotra. Ha v\u00e1ltoz\u00e1s t\u00f6rt\u00e9nik ebben, \u00fajra le fog futni a Composable, mely \u00edgy m\u00e1r a frisebb \u00e1llapotot fogja megjelen\u00edteni.

      Val\u00f3s\u00edtsuk meg a lista megjelen\u00edt\u00e9s\u00e9t is! M\u00e1soljuk be az al\u00e1bbi k\u00f3dot a megfelel\u0151 else \u00e1gba:

      Column {\nText(\ntext = stringResource(id = R.string.text_your_todo_list),\nfontSize = 24.sp\n)\nLazyColumn(\nmodifier = Modifier\n.fillMaxSize()\n) {\nitems(state.todoList, key = { todo -> todo.id }) { todo ->\nListItem(\nleadingContent = {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = todo.priority.color,\nmodifier = Modifier\n.size(64.dp)\n)\n},\nheadlineContent = {\nText(text = todo.title)\n},\nsupportingContent = {\nText(\ntext = stringResource(\nid = R.string.list_item_supporting_text,\ntodo.dueDate\n)\n)\n},\nmodifier = Modifier.clickable(onClick = {\nonListItemClick(\ntodo.id\n)\n})\n)\nif (state.todoList.last() != todo) {\nHorizontalDivider(\nthickness = 2.dp,\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\n}\n}\n}\n}\n
      Material Icon

      A Circle ikon csak a kieg\u00e9sz\u00edt\u0151 Material Icon k\u00f6nyvt\u00e1rban tal\u00e1lhat\u00f3 meg, melyet az al\u00e1bbi f\u00fcgg\u0151s\u00e9ggel tudunk hozz\u00e1adni a projekthez:

      [libraries]\nandroidx-material-icons-extended = { group = \"androidx.compose.material\", name=\"material-icons-extended\" }\n

      items

      Ha hib\u00e1t dobna az items-re, \u00e9s nem tal\u00e1lja az importot, adjuk hozz\u00e1 az al\u00e1bbi importot a f\u00e1jl tetej\u00e9hez:

      import androidx.compose.foundation.lazy.items\n

      L\u00e1that\u00f3, hogy a lista megjelen\u00edt\u00e9s\u00e9re a LazyColumn Composable-t haszn\u00e1ljuk, mely k\u00e9pes nagy elemsz\u00e1m\u00fa list\u00e1t hat\u00e9konyan megjelen\u00edteni. Ahhoz, hogy j\u00f3l m\u0171k\u00f6dj\u00f6n a lista m\u00f3dos\u00edt\u00e1sa eset\u00e9n is (pl. hozz\u00e1ad\u00e1s, t\u00f6rl\u00e9s, \u00e1trendez\u00e9s), mindenk\u00e9pp \u00e9rdemes a key param\u00e9tert \u00fagy defini\u00e1lni, hogy az adott listaelemet egy\u00e9rtelm\u0171en beazonos\u00edtsa.

      Lista elemnek most egy egyszer\u0171 ListItem-et haszn\u00e1lunk, ami eset\u00fcnkben t\u00f6k\u00e9letesen el\u00e9g. A leadingContent hely\u00e9re a priority ikon, a headlineContent hely\u00e9re a todo c\u00edme, a supportingContent hely\u00e9re a hat\u00e1rid\u0151 ker\u00fcl. Term\u00e9szetesen komplexebb listaelemek eset\u00e9n k\u00fcl\u00f6n Composable k\u00e9sz\u00edt\u00e9se aj\u00e1nlott.

      Az oldal elk\u00e9sz\u00fclt, m\u00e1r csak a navig\u00e1ci\u00f3t kell friss\u00edteni az oldalhoz. Vegy\u00fck fel az \u00fatvonalat a Screen oszt\u00e1lyba:

      package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\n}\n

      Illetve a NavGraph Composable-t:

      package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\n//TODO: Navigate to detailed screen\n},\nonFabClick = {\n//TODO: Navigate to create screen\n}\n)\n}\n}\n}\n

      Futtassuk az alkalmaz\u00e1s!

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a fut\u00f3 alkalmaz\u00e1s (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

      A k\u00e9pet a megold\u00e1sban a repository-ba f1.png n\u00e9ven t\u00f6ltsd f\u00f6l!

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/todo_compose_basics/#adatreteg-kialakitasa","title":"Adatr\u00e9teg kialak\u00edt\u00e1sa","text":"

      Ezen a laboron egy egyszer\u0171s\u00edtett megold\u00e1st mutatunk be a feladatok t\u00e1rol\u00e1s\u00e1ra, mely csak a mem\u00f3ri\u00e1ban menti el az \u00e9rt\u00e9keket. Hozzunk l\u00e9tre egy data package-et a gy\u00f6k\u00e9rk\u00f6nyvt\u00e1ron bel\u00fcl, majd hozzuk l\u00e9tre az al\u00e1bbi k\u00e9t f\u00e1jlt:

      TodoRepository.kt:

      package hu.bme.aut.android.todo.data\n\nimport hu.bme.aut.android.todo.domain.model.Todo\n\ninterface ITodoRepository {\nsuspend fun insertTodo(todo: Todo)\nsuspend fun deleteTodo(todo: Todo)\nsuspend fun getTodoById(id: Int): Todo\nsuspend fun getAllTodos(): List<Todo>\nsuspend fun updateTodo(updatedTodo: Todo)\n}\n

      MemoryTodoRepository.kt :

      package hu.bme.aut.android.todo.data\n\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.domain.model.Todo\nimport kotlinx.coroutines.delay\nimport kotlinx.datetime.toKotlinLocalDateTime\nimport java.time.LocalDateTime\n\nobject MemoryTodoRepository : ITodoRepository {\nprivate val todos = mutableListOf(\nTodo(\nid = 1,\ntitle = \"Teszt feladat 1\",\npriority = Priority.LOW,\ndescription = \"Feladat le\u00edr\u00e1s 1\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 2,\ntitle = \"Teszt feladat 2\",\npriority = Priority.MEDIUM,\ndescription = \"Feladat le\u00edr\u00e1s 2\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 3,\ntitle = \"Teszt feladat 3\",\npriority = Priority.HIGH,\ndescription = \"Feladat le\u00edr\u00e1s 3\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 4,\ntitle = \"Teszt feladat 4 hossz\u0171 sz\u00f6veg, hogy t\u00f6bb sorba kelljen \u00edrni\",\npriority = Priority.HIGH,\ndescription = \"Feladat le\u00edr\u00e1s 4\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 5,\ntitle = \"Teszt feladat 5\",\npriority = Priority.LOW,\ndescription = \"Feladat le\u00edr\u00e1s 5\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n),\nTodo(\nid = 6,\ntitle = \"Teszt feladat 6\",\npriority = Priority.MEDIUM,\ndescription = \"Feladat le\u00edr\u00e1s 6\",\ndueDate = LocalDateTime.now().toKotlinLocalDateTime().date,\n)\n)\n\noverride suspend fun insertTodo(todo: Todo) {\ndelay(1000)\ntodos.add(todo)\n}\n\noverride suspend fun deleteTodo(todo: Todo) {\ndelay(1000)\ntodos.remove(todo)\n}\n\noverride suspend fun getTodoById(id: Int): Todo {\ndelay(1000)\nfor (todo in todos) {\nif (todo.id == id) return todo\n}\nreturn todos.first()\n}\n\noverride suspend fun getAllTodos(): List<Todo> {\ndelay(1000)\nreturn todos.toList()\n}\n\noverride suspend fun updateTodo(updatedTodo: Todo) {\ndelay(1000)\nfor (todo in todos) {\nif (todo.id == updatedTodo.id)\ntodos[todos.indexOf(todo)] = updatedTodo\n}\n}\n}\n

      Az ITodoRepository egy \u00e1ltal\u00e1nos interf\u00e9szt \u00edr le, mellyel el\u00e9rhet\u0151v\u00e9 v\u00e1lnak a feladatok az alkalmaz\u00e1s sz\u00e1m\u00e1ra, m\u00edg a MemoryTodoRepository egy mem\u00f3ria alap\u00fa megval\u00f3s\u00edt\u00e1s\u00e1t mutatja be. B\u00e1r itt most nem lenne sz\u00fcks\u00e9g a suspend kulcssz\u00f3 haszn\u00e1lat\u00e1ra, ezzel tudjuk biztos\u00edtani, hogy a k\u00e9s\u0151bbiekben egy adatb\u00e1zis vagy h\u00e1l\u00f3zati TodoRepository elk\u00e9sz\u00edt\u00e9se ut\u00e1n k\u00f6nnyed\u00e9n tudjuk migr\u00e1lni a projektet, ezt a k\u00e9sleltet\u00e9st imit\u00e1ljuk a delay() f\u00fcggv\u00e9ny h\u00edv\u00e1s\u00e1val is. Az object kulcssz\u00f3val a Singleton mint\u00e1t tudjuk egyszer\u0171en megval\u00f3s\u00edtani.

      Friss\u00edts\u00fck a TodoListViewModel oszt\u00e1lyt, hogy ezt a mem\u00f3ria alap\u00fa megval\u00f3s\u00edt\u00e1st haszn\u00e1lja:

      class TodoListViewModel(private val repository: ITodoRepository) : ViewModel() {\nprivate val _state = MutableStateFlow<TodoListState>(TodoListState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nviewModelScope.launch {\ntry {\n_state.value = TodoListState.Loading\ndelay(2000)\nval list = repository.getAllTodos()\n_state.value = TodoListState.Result(\ntodoList = list.map { it.asTodoUi() }\n)\n} catch (e: Exception) {\n_state.value = TodoListState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoListViewModel(\nMemoryTodoRepository\n)\n}\n}\n}\n}\n

      Futassuk az alkalmaz\u00e1st, \u00e9s ellen\u0151rizz\u00fck, hogy tov\u00e1bbra is megjelennek a feladatok a list\u00e1ban!

      "},{"location":"laborok/todo_compose_basics/#reszletes-feladat-felulet","title":"R\u00e9szletes feladat fel\u00fclet","text":"

      K\u00f6vetkez\u0151 l\u00e9p\u00e9sk\u00e9nt k\u00e9sz\u00edts\u00fck fel a r\u00e9szletez\u0151 fel\u00fcletet, melyen a feladat le\u00edr\u00e1s\u00e1t tudjuk megn\u00e9zni. K\u00e9sz\u00edts\u00fck el az oldalt a lista oldal mint\u00e1j\u00e1ra.

      Kezdj\u00fck a navig\u00e1ci\u00f3 implement\u00e1l\u00e1s\u00e1val. Ebben az esetben az \u00fatvonal fogja tartalmazni az azonos\u00edt\u00f3j\u00e1t a feladatnak az al\u00e1bbi m\u00f3don: Screen.kt:

      package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\nobject TodoDetail : Screen(\"todo_detail/{id}\"){\nfun passId(id: Int) = \"todo_detail/$id\"\n}\n}\n
      A feladat azonos\u00edt\u00f3j\u00e1t egy / jellel elv\u00e1lasztva tessz\u00fck be az \u00fatvonalba.

      NavGraph.kt:

      package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navArgument\nimport hu.bme.aut.android.todo.feature.todo_detail.TodoDetailScreen\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\nnavController.navigate(Screen.TodoDetail.passId(it))\n},\nonFabClick = {\n//TODO: Navigate to create screen\n}\n)\n}\ncomposable(\nroute = Screen.TodoDetail.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.IntType\n}\n)\n) {\nTodoDetailScreen(onNavigateBack = { navController.popBackStack() })\n}\n}\n}\n

      Az azonos\u00edt\u00f3t a composable-ben is fel kell t\u00fcntetn\u00fcnk az arguments param\u00e9terben. Itt tudjuk megadni, hogy milyen t\u00edpus\u00fa lesz az \u00e9rt\u00e9k, amit \u00e1tadunk a param\u00e9terben, \u00edgy a keretrendszer automatikusan \u00e1t tudja alak\u00edtani a megfelel\u0151 t\u00edpuss\u00e1. Hozzunk l\u00e9tre egy \u00faj package-et a feature package-en bel\u00fcl todo_detail n\u00e9ven.

      TodoDetailViewModel.kt:

      package hu.bme.aut.android.todo.feature.todo_detail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.createSavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.data.ITodoRepository\nimport hu.bme.aut.android.todo.data.MemoryTodoRepository\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport hu.bme.aut.android.todo.ui.model.asTodoUi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nsealed class TodoDetailState {\nobject Loading : TodoDetailState()\ndata class Error(val error: Throwable) : TodoDetailState()\ndata class Result(val todo: TodoUi) : TodoDetailState()\n}\n\nclass TodoDetailViewModel(private val repository: ITodoRepository, private val savedStateHandle: SavedStateHandle) : ViewModel() {\n\nprivate val _state = MutableStateFlow<TodoDetailState>(TodoDetailState.Loading)\nval state = _state.asStateFlow()\n\ninit {\nloadTodos()\n}\n\nprivate fun loadTodos() {\nval id = checkNotNull<Int>(savedStateHandle[\"id\"])\nviewModelScope.launch {\ntry {\n_state.value = TodoDetailState.Loading\ndelay(2000)\nval todo = repository.getTodoById(id)\n_state.value = TodoDetailState.Result(\ntodo.asTodoUi()\n)\n} catch (e: Exception) {\n_state.value = TodoDetailState.Error(e)\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nval savedStateHandle = createSavedStateHandle()\nTodoDetailViewModel(\nMemoryTodoRepository,\nsavedStateHandle\n)\n}\n}\n}\n}\n
      Az \u00fatvonalban \u00e1tadott param\u00e9ter kiolvas\u00e1s\u00e1hoz a SavedStateHandle oszt\u00e1lyt haszn\u00e1ljuk. Ennek az oszt\u00e1lynak a szerepe az olyan adatok ment\u00e9se, melyet az alkalmaz\u00e1s h\u00e1tt\u00e9rben t\u00f6rt\u00e9n\u0151 megsemmis\u00edt\u00e9se \u00e9s \u00fajraind\u00edt\u00e1sa ut\u00e1n is ki akarunk olvasni. Ezt a funkci\u00f3j\u00e1t most nem haszn\u00e1ljuk ki, viszont a keretrendszer ebbe t\u00f6lti be az \u00fatvonal param\u00e9tereket is, melyekhez \u00edgy k\u00f6nnyen hozz\u00e1f\u00e9r\u00fcnk, amikor az \u00faj feladatot kell bet\u00f6lteni.

      TodoDetailScreen.kt:

      package hu.bme.aut.android.todo.feature.todo_detail\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.ui.model.toUiText\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TodoDetailScreen(\nonNavigateBack: () -> Unit,\nviewModel: TodoDetailViewModel = viewModel(factory = TodoDetailViewModel.Factory)\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\n\nval context = LocalContext.current\n\nScaffold(\ntopBar = {\nif (state is TodoDetailState.Result) {\nTopAppBar(\ntitle = { Text(state.todo.title) },\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nIcon(imageVector = Icons.Default.ArrowBack, contentDescription = null)\n}\n},\ncolors = TopAppBarDefaults.topAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary\n),\n)\n}\n},\n) {\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(it),\ncontentAlignment = Alignment.Center\n) {\nwhen (state) {\nis TodoDetailState.Loading -> CircularProgressIndicator(\ncolor = MaterialTheme.colorScheme.secondaryContainer\n)\nis TodoDetailState.Error -> Text(\ntext = state.error.toUiText().asString(context)\n)\nis TodoDetailState.Result -> {\nval todo = state.todo\nColumn(\nmodifier = Modifier.fillMaxSize().padding(all = 8.dp)\n) {\nText(\ntodo.dueDate,\nstyle = MaterialTheme.typography.titleMedium\n)\nRow(\nmodifier = Modifier\n.height(TextFieldDefaults.MinHeight)\n.fillMaxWidth()\n.clip(shape = RoundedCornerShape(size = 5.dp)),\nverticalAlignment = Alignment.CenterVertically\n) {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = todo.priority.color,\nmodifier = Modifier\n.size(24.dp)\n)\nSpacer(modifier = Modifier.width(8.dp))\nText(\nmodifier = Modifier\n.weight(weight = 8f),\ntext = stringResource(id = todo.priority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\n}\nText(\ntodo.description\n)\n}\n}\n}\n}\n}\n}\n

      V\u00e9g\u00fcl a lista oldalhoz hasonl\u00f3an kiolvassuk a ViewModel-ben t\u00e1rolt \u00e1llapotot \u00e9s megjelen\u00edtj\u00fck a megfelel\u0151 fel\u00fcleti elemeket.

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a r\u00e9szletes n\u00e9zet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

      A k\u00e9pet a megold\u00e1sban a repository-ba f2.png n\u00e9ven t\u00f6ltsd f\u00f6l!

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/todo_compose_basics/#feladat-letrehozasa-felulet-komponensek","title":"Feladat l\u00e9trehoz\u00e1sa fel\u00fclet komponensek","text":"

      Az utols\u00f3 fel\u00fclet, melyet elk\u00e9sz\u00edt\u00fcnk az alkalmaz\u00e1shoz, a feladat l\u00e9trehoz\u00e1sa fel\u00fclet lesz. Ehhez t\u00f6bb \u00f6n\u00e1ll\u00f3 fel\u00fcleti elemre lesz sz\u00fcks\u00e9g\u00fcnk, melyeket az oldal el\u0151tt l\u00e9trehozunk. Hozzuk l\u00e9tre a ui package-en bel\u00fcl a common package-et, mely az olyan Composable elemeket tartalmazza, melyeket ak\u00e1r t\u00f6bb oldalon is fel tudn\u00e1nk haszn\u00e1lni. Ezen bel\u00fcl hozzuk l\u00e9tre az al\u00e1bbi elemeket:

      DateSelector.kt:

      package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.EditCalendar\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport kotlinx.datetime.LocalDate\nimport java.time.LocalDateTime\n\n@Composable\nfun DateSelector(\npickedDate: LocalDate,\nonClick: () -> Unit,\nmodifier: Modifier = Modifier,\nenabled: Boolean = true\n) {\nval shape = RoundedCornerShape(5.dp)\n\nSurface(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.background(MaterialTheme.colorScheme.background)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape)\n.clickable(enabled = enabled, onClick = onClick),\nshape = shape\n) {\nRow(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape),\nverticalAlignment = Alignment.CenterVertically\n) {\nText(\nmodifier = Modifier\n.weight(weight = 8f)\n.padding(start = 20.dp),\ntext = pickedDate.toString(),\nstyle = MaterialTheme.typography.labelMedium\n)\nIconButton(\nmodifier = Modifier\n.weight(weight = 1.5f),\nonClick = onClick\n) {\nIcon(\nimageVector = Icons.Default.EditCalendar,\ncontentDescription = null,\ntint = MaterialTheme.colorScheme.primary\n)\n}\n}\n}\n}\n\n@Preview\n@Composable\nfun DateSelectorPreview() {\nval d = LocalDateTime.now()\nDateSelector(\npickedDate = LocalDate(d.year, d.month, d.dayOfMonth),\nonClick = { }\n)\n}\n

      NormalTextField.kt:

      package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActionScope\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun NormalTextField(\nvalue: String,\nlabel: String,\nonValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\nleadingIcon: @Composable (() -> Unit)? = null,\ntrailingIcon: @Composable (() -> Unit)? = null,\nsingleLine: Boolean = false,\nenabled: Boolean = true,\nonDone: (KeyboardActionScope.() -> Unit)?\n) {\nval shape = RoundedCornerShape(5.dp)\n\nTextField(\nvalue = value,\nonValueChange = onValueChange,\nlabel = { Text(text = label) },\nleadingIcon = leadingIcon,\ntrailingIcon = trailingIcon,\nmodifier = modifier.clip(shape),\nsingleLine = singleLine,\nenabled = enabled,\nkeyboardOptions = KeyboardOptions(\nkeyboardType = KeyboardType.Text,\nimeAction = ImeAction.Done\n),\nkeyboardActions = KeyboardActions(\nonDone = onDone\n),\nshape = shape\n)\n}\n\n@Preview\n@Composable\nfun NormalTextFieldPreview() {\nNormalTextField(\nvalue = \"name\",\nlabel = \"P\u00e9ter\",\nonValueChange = {}) {\n\n}\n}\n

      PriorityDropdown.kt:

      package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.icons.filled.Circle\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\n\n@Composable\nfun PriorityDropDown(\npriorities: List<PriorityUi>,\nselectedPriority: PriorityUi,\nonPrioritySelected: (PriorityUi) -> Unit,\nmodifier: Modifier = Modifier,\nenabled: Boolean = true\n) {\nvar expanded by remember { mutableStateOf(false) }\nval angle: Float by animateFloatAsState(\ntargetValue = if (expanded) 180f else 0f,\nlabel = \"Priority arrow angle animation\"\n)\n\nval shape = RoundedCornerShape(5.dp)\n\nSurface(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape)\n.background(MaterialTheme.colorScheme.background)\n.clickable(enabled = enabled) { expanded = true },\nshape = shape\n) {\nRow(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth)\n.height(TextFieldDefaults.MinHeight)\n.clip(shape = shape),\nverticalAlignment = Alignment.CenterVertically\n) {\nSpacer(modifier = Modifier.width(20.dp))\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = selectedPriority.color,\nmodifier = Modifier\n.size(20.dp)\n)\nSpacer(modifier = Modifier.width(5.dp))\nText(\nmodifier = Modifier\n.weight(weight = 8f),\ntext = stringResource(id = selectedPriority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\nIconButton(\nmodifier = Modifier\n.weight(weight = 1.5f)\n.rotate(degrees = angle),\nonClick = { expanded = true }\n) {\nIcon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n)\n}\nDropdownMenu(\nmodifier = modifier\n.width(TextFieldDefaults.MinWidth),\nexpanded = expanded,\nonDismissRequest = { expanded = false }\n) {\npriorities.forEach { priority ->\nDropdownMenuItem(\ntext = {\nText(\ntext = stringResource(id = priority.title),\nstyle = MaterialTheme.typography.labelMedium\n)\n},\nonClick = {\nexpanded = false\nonPrioritySelected(priority)\n},\nleadingIcon = {\nIcon(\nimageVector = Icons.Default.Circle,\ncontentDescription = null,\ntint = priority.color,\nmodifier = Modifier.size(22.dp)\n)\n}\n)\n}\n}\n}\n}\n}\n\n@Composable\n@Preview\nfun PriorityDropdown_Preview() {\nval priorities = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High)\nvar selectedPriority by remember { mutableStateOf(priorities[0]) }\n\nColumn(\nmodifier = Modifier.fillMaxSize(),\nverticalArrangement = Arrangement.Center,\nhorizontalAlignment = Alignment.CenterHorizontally\n) {\nPriorityDropDown(\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = {\nselectedPriority = it\n}\n)\n}\n}\n

      Ezt a h\u00e1rom elemet fogjuk \u00f6ssze a TodoEditor komponenssel, melyet ugyanitt hozzunk l\u00e9tre:

      package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport kotlinx.datetime.LocalDate\nimport java.time.LocalDateTime\n\n@Composable\nfun TodoEditor(\ntitleValue: String,\ntitleOnValueChange: (String) -> Unit,\ndescriptionValue: String,\ndescriptionOnValueChange: (String) -> Unit,\nmodifier: Modifier = Modifier,\npriorities: List<PriorityUi> = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High),\nselectedPriority: PriorityUi,\nonPrioritySelected: (PriorityUi) -> Unit,\npickedDate: LocalDate,\nonDateSelectorClicked: () -> Unit,\nenabled: Boolean = true,\n) {\nval fraction = 0.95f\n\nval keyboardController = LocalSoftwareKeyboardController.current\n\nColumn(\nmodifier = modifier\n.fillMaxSize()\n.background(MaterialTheme.colorScheme.secondaryContainer),\nhorizontalAlignment = Alignment.CenterHorizontally,\nverticalArrangement = Arrangement.SpaceAround,\n) {\nif (enabled) {\nNormalTextField(\nvalue = titleValue,\nlabel = stringResource(id = R.string.textfield_label_title),\nonValueChange = titleOnValueChange,\nsingleLine = true,\nonDone = { keyboardController?.hide() },\nmodifier = Modifier\n.fillMaxWidth(fraction)\n.padding(top = 5.dp)\n)\n}\nSpacer(modifier = Modifier.height(5.dp))\nPriorityDropDown(\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = onPrioritySelected,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction),\nenabled = enabled\n)\nSpacer(modifier = Modifier.height(5.dp))\nDateSelector(\npickedDate = pickedDate,\nonClick = onDateSelectorClicked,\nmodifier = Modifier\n.weight(1f)\n.fillMaxWidth(fraction),\nenabled = enabled\n)\nSpacer(modifier = Modifier.height(5.dp))\nNormalTextField(\nvalue = descriptionValue,\nlabel = stringResource(id = R.string.textfield_label_description),\nonValueChange = descriptionOnValueChange,\nsingleLine = false,\nonDone = { keyboardController?.hide() },\nmodifier = Modifier\n.weight(10f)\n.fillMaxWidth(fraction)\n.padding(bottom = 5.dp),\nenabled = enabled\n)\n}\n}\n\n@Composable\n@Preview(showBackground = true)\nfun TodoEditor_Preview() {\nvar title by remember { mutableStateOf(\"\") }\nvar description by remember { mutableStateOf(\"\") }\n\nval priorities = listOf(PriorityUi.Low, PriorityUi.Medium, PriorityUi.High)\nvar selectedPriority by remember { mutableStateOf(priorities[0]) }\n\nval c = LocalDateTime.now()\nval pickedDate by remember { mutableStateOf(LocalDate(c.year, c.month, c.dayOfMonth)) }\n\nBox(Modifier.fillMaxSize()) {\nTodoEditor(\ntitleValue = title,\ntitleOnValueChange = { title = it },\ndescriptionValue = description,\ndescriptionOnValueChange = { description = it },\npriorities = priorities,\nselectedPriority = selectedPriority,\nonPrioritySelected = { selectedPriority = it },\npickedDate = pickedDate,\nonDateSelectorClicked = {\n\n},\n)\n}\n}\n

      Ezek mellett a l\u00e9trehoz\u00e1s oldalon sz\u00fcks\u00e9g\u00fcnk lesz egy TopAppBar elemre is. Egy ilyet m\u00e1r l\u00e9trehoztunk a r\u00e9szletes n\u00e9zeten, ezt kiemelve \u00e9s \u00e1ltal\u00e1nos\u00edtva hozzuk l\u00e9tre az \u00faj TodoAppBar elemet ugyanebbe a package-be:

      package hu.bme.aut.android.todo.ui.common\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TodoAppBar(\nmodifier: Modifier = Modifier,\ntitle: String,\nactions: @Composable() RowScope.() -> Unit = {},\nonNavigateBack: () -> Unit\n) {\nTopAppBar(\nmodifier = modifier,\ntitle = { Text(text = title) },\nnavigationIcon = {\nIconButton(onClick = onNavigateBack) {\nIcon(imageVector = Icons.Default.ArrowBack, contentDescription = null)\n\n}\n},\nactions = actions,\ncolors = TopAppBarDefaults.topAppBarColors(\ncontainerColor = MaterialTheme.colorScheme.primary,\ntitleContentColor = MaterialTheme.colorScheme.onPrimary,\nactionIconContentColor = MaterialTheme.colorScheme.onPrimary,\nnavigationIconContentColor = MaterialTheme.colorScheme.onPrimary\n)\n)\n}\n\n@Composable\n@Preview\nfun TodoAppBar_Preview() {\nTodoAppBar(\ntitle = \"Title\",\nactions = {},\nonNavigateBack = {}\n)\n}\n

      Ezzel a TodoDetailScreen TopAppBar r\u00e9sze az al\u00e1bbi egyszer\u0171bb deklar\u00e1ci\u00f3ra cser\u00e9lhet\u0151:

      TodoAppBar(\ntitle = state.todo.title,\nonNavigateBack = onNavigateBack,\n)\n
      "},{"location":"laborok/todo_compose_basics/#feladat-keszitese-oldal","title":"Feladat k\u00e9sz\u00edt\u00e9se oldal","text":"

      Hozzuk l\u00e9tre a feature package-en bel\u00fcl a todo_create package-et. Ezen bel\u00fcl k\u00e9sz\u00edts\u00fck el az oldal logik\u00e1j\u00e1t megval\u00f3s\u00edt\u00f3 TodoCreateViewModel oszt\u00e1lyt:

      package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport hu.bme.aut.android.todo.data.ITodoRepository\nimport hu.bme.aut.android.todo.data.MemoryTodoRepository\nimport hu.bme.aut.android.todo.ui.model.PriorityUi\nimport hu.bme.aut.android.todo.ui.model.TodoUi\nimport hu.bme.aut.android.todo.ui.model.UiText\nimport hu.bme.aut.android.todo.ui.model.asTodo\nimport hu.bme.aut.android.todo.ui.model.toUiText\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.LocalDate\n\ndata class TodoCreateState(\nval todo: TodoUi = TodoUi()\n)\n\nsealed class TodoCreateUiEvent {\nobject Success : TodoCreateUiEvent()\ndata class Failure(val error: UiText) : TodoCreateUiEvent()\n}\n\nsealed class TodoCreateEvent {\ndata class ChangeTitle(val text: String) : TodoCreateEvent()\ndata class ChangeDescription(val text: String) : TodoCreateEvent()\ndata class SelectPriority(val priority: PriorityUi) : TodoCreateEvent()\ndata class SelectDate(val date: LocalDate) : TodoCreateEvent()\nobject SaveTodo : TodoCreateEvent()\n}\n\nclass TodoCreateViewModel(\nprivate val todoRepository: ITodoRepository\n) : ViewModel() {\n\nprivate val _state = MutableStateFlow(TodoCreateState())\nval state = _state.asStateFlow()\n\nprivate val _uiEvent = Channel<TodoCreateUiEvent>()\nval uiEvent = _uiEvent.receiveAsFlow()\n\nfun onEvent(event: TodoCreateEvent) {\nwhen (event) {\nis TodoCreateEvent.ChangeTitle -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo.copy(title = newValue)\n)\n}\n}\n\nis TodoCreateEvent.ChangeDescription -> {\nval newValue = event.text\n_state.update {\nit.copy(\ntodo = it.todo.copy(description = newValue)\n)\n}\n}\n\nis TodoCreateEvent.SelectPriority -> {\nval newValue = event.priority\n_state.update {\nit.copy(\ntodo = it.todo.copy(priority = newValue)\n)\n}\n}\n\nis TodoCreateEvent.SelectDate -> {\nval newValue = event.date\n_state.update {\nit.copy(\ntodo = it.todo.copy(dueDate = newValue.toString())\n)\n}\n}\n\nTodoCreateEvent.SaveTodo -> {\n_state.update {\nit.copy(\ntodo = it.todo.copy(id = (Math.random()*Int.MAX_VALUE).toInt())\n)\n}\nonSave()\n}\n}\n}\n\nprivate fun onSave() {\nviewModelScope.launch {\ntry {\ntodoRepository.insertTodo(state.value.todo.asTodo())\n_uiEvent.send(TodoCreateUiEvent.Success)\n} catch (e: Exception) {\n_uiEvent.send(TodoCreateUiEvent.Failure(e.toUiText()))\n}\n}\n}\n\ncompanion object {\nval Factory: ViewModelProvider.Factory = viewModelFactory {\ninitializer {\nTodoCreateViewModel(\ntodoRepository = MemoryTodoRepository\n)\n}\n}\n}\n}\n

      Ebben a ViewModel oszt\u00e1lyban k\u00e9t \u00faj architekt\u00fara mint\u00e1t is megfigyelhet\u00fcnk:

      • A felhaszn\u00e1l\u00f3i fel\u00fcletr\u0151l \u00e9rkez\u0151 esem\u00e9nyeknek egy \u00faj oszt\u00e1lyt defini\u00e1ltunk TodoCreateEvent n\u00e9ven. Ezeket az esem\u00e9nyeket egy \u00e1ltal\u00e1nos, onEvent() met\u00f3dusban kezelj\u00fck le, \u00edgy k\u00f6nnyebben k\u00f6vethet\u0151, milyen interakci\u00f3kra sz\u00e1m\u00edthatunk a ViewModel oldal\u00e1r\u00f3l. Sz\u00fcks\u00e9g eset\u00e9n az egyes esem\u00e9nyek kezel\u00e9s\u00e9re l\u00e9trehozhat\u00f3 egyedi priv\u00e1t met\u00f3dus, de a UI csak az onEvent()-et h\u00edvja meg.
      • Vannak olyan esem\u00e9nyek, melyekre a UI r\u00e9tegnek reag\u00e1lnia kell a megjelen\u00edt\u00e9s helyett. P\u00e9ld\u00e1ul egy feladat sikeres l\u00e9trehoz\u00e1sa ut\u00e1n azt szeretn\u00e9nk, hogy az alkalmaz\u00e1s navig\u00e1ljon vissza az el\u0151z\u0151 oldalra. Ilyenkor azt akarjuk, hogy ez az esem\u00e9ny csak \u00e9s kiz\u00e1r\u00f3lag egyszer ker\u00fclj\u00f6n feldolgoz\u00e1sra. Ezeket egy Channel seg\u00edts\u00e9g\u00e9vel osztjuk meg.

      Az ehhez tartoz\u00f3 oldalhoz hozzuk l\u00e9tre a TodoCreateScreen.kt f\u00e1jlt ebbe a package-be:

      package hu.bme.aut.android.todo.feature.todo_create\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFloatingActionButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport hu.bme.aut.android.todo.R\nimport hu.bme.aut.android.todo.domain.model.Priority\nimport hu.bme.aut.android.todo.ui.common.TodoAppBar\nimport hu.bme.aut.android.todo.ui.common.TodoEditor\nimport hu.bme.aut.android.todo.ui.model.asPriorityUi\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.toLocalDate\n\n@Composable\nfun TodoCreateScreen(\nonNavigateBack: () -> Unit,\nviewModel: TodoCreateViewModel = viewModel(factory = TodoCreateViewModel.Factory)\n) {\nval state by viewModel.state.collectAsStateWithLifecycle()\n\nval hostState = remember { SnackbarHostState() }\n\nval scope = rememberCoroutineScope()\n\nval context = LocalContext.current\n\nLaunchedEffect(key1 = true) {\nviewModel.uiEvent.collect { uiEvent ->\nwhen(uiEvent) {\nis TodoCreateUiEvent.Success -> { onNavigateBack() }\nis TodoCreateUiEvent.Failure -> {\nscope.launch {\nhostState.showSnackbar(uiEvent.error.asString(context))\n}\n}\n}\n}\n}\n\nScaffold(\nsnackbarHost = { SnackbarHost(hostState) },\ntopBar = {\nTodoAppBar(\ntitle = stringResource(id = R.string.app_bar_title_create_todo),\nonNavigateBack = onNavigateBack,\nactions = { }\n)\n},\nfloatingActionButton = {\nLargeFloatingActionButton(\nonClick = { viewModel.onEvent(TodoCreateEvent.SaveTodo) },\ncontainerColor = MaterialTheme.colorScheme.primary,\ncontentColor = MaterialTheme.colorScheme.onPrimary\n) {\nIcon(imageVector = Icons.Default.Save, contentDescription = null)\n}\n}\n) { padding ->\nBox(\nmodifier = Modifier\n.fillMaxSize()\n.padding(padding),\ncontentAlignment = Alignment.Center\n) {\nTodoEditor(\ntitleValue = state.todo.title,\ntitleOnValueChange = { viewModel.onEvent(TodoCreateEvent.ChangeTitle(it)) },\ndescriptionValue = state.todo.description,\ndescriptionOnValueChange = { viewModel.onEvent(TodoCreateEvent.ChangeDescription(it)) },\npriorities = Priority.entries.map { it.asPriorityUi() },\nselectedPriority = state.todo.priority,\nonPrioritySelected = { viewModel.onEvent(TodoCreateEvent.SelectPriority(it)) },\npickedDate = state.todo.dueDate.toLocalDate(),\nonDateSelectorClicked = {\n//TODO: Open date picker dialog\n},\nmodifier = Modifier\n)\n}\n}\n}\n

      Utols\u00f3 l\u00e9p\u00e9sk\u00e9nt k\u00f6ss\u00fck be a navig\u00e1ci\u00f3t is ehhez az oldalhoz. Friss\u00edts\u00fck a Screen.kt f\u00e1jlt az al\u00e1bbi k\u00f3ddal:

      package hu.bme.aut.android.todo.navigation\n\nsealed class Screen(val route: String) {\nobject TodoList : Screen(\"todo_list\")\nobject TodoDetail : Screen(\"todo_detail/{id}\"){\nfun passId(id: Int) = \"todo_detail/$id\"\n}\nobject TodoCreate : Screen(\"todo_create\")\n}\n

      Val\u00f3s\u00edtsuk meg a navig\u00e1ci\u00f3t is a NavGraph.kt f\u00e1jlban:

      package hu.bme.aut.android.todo.navigation\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navArgument\nimport hu.bme.aut.android.todo.feature.todo_create.TodoCreateScreen\nimport hu.bme.aut.android.todo.feature.todo_detail.TodoDetailScreen\nimport hu.bme.aut.android.todo.feature.todo_list.TodoListScreen\n\n@Composable\nfun NavGraph(\nnavController: NavHostController = rememberNavController(),\n) {\nNavHost(\nnavController = navController,\nstartDestination = Screen.TodoList.route\n) {\ncomposable(Screen.TodoList.route) {\nTodoListScreen(\nonListItemClick = {\nnavController.navigate(Screen.TodoDetail.passId(it))\n},\nonFabClick = {\nnavController.navigate(Screen.TodoCreate.route)\n}\n)\n}\ncomposable(\nroute = Screen.TodoDetail.route,\narguments = listOf(\nnavArgument(\"id\") {\ntype = NavType.IntType\n}\n)\n) {\nTodoDetailScreen(onNavigateBack = { navController.popBackStack() })\n}\ncomposable(Screen.TodoCreate.route) {\nTodoCreateScreen(\nonNavigateBack = {\nnavController.popBackStack()\n}\n)\n}\n}\n}\n

      Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Mit tapasztalunk egy feladat l\u00e9trehoz\u00e1s\u00e1n\u00e1l?

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a feladat l\u00e9trehoz\u00e1sa n\u00e9zet (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

      A k\u00e9pet a megold\u00e1sban a repository-ba f3.png n\u00e9ven t\u00f6ltsd f\u00f6l!

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/todo_compose_basics/#kiegeszito-feladat-1-feladat-lista-frissitese","title":"Kieg\u00e9sz\u00edt\u0151 feladat 1 - Feladat lista friss\u00edt\u00e9se","text":"

      \u00c9szrevehetj\u00fck, hogy ha l\u00e9trehozunk egy \u00faj feladatot, az nem jelenik meg a list\u00e1ban. Ez az\u00e9rt t\u00f6rt\u00e9nik, mert a lista oldal tartalm\u00e1t csak az oldal l\u00e9trej\u00f6ttekor friss\u00edtj\u00fck be, a feladat l\u00e9trehoz\u00e1sa ut\u00e1n t\u00f6rt\u00e9n\u0151 visszal\u00e9p\u00e9s viszont a megl\u00e9v\u0151 oldalra l\u00e9p vissza, nem hoz l\u00e9tre egy \u00fajat. Ezt t\u00f6bb m\u00f3don is meg tudjuk oldani.

      • A visszal\u00e9p\u00e9s sor\u00e1n megh\u00edvunk egy met\u00f3dust, mely befriss\u00edti a lista oldal tartalm\u00e1t. Ez t\u00f6rt\u00e9nhet egy Channel-en kereszt\u00fcl az oldal fel\u00e9, vagy kiemelhetj\u00fck a lista n\u00e9zet ViewModel oszt\u00e1ly\u00e1t a navig\u00e1ci\u00f3s komponensbe, amin \u00edgy k\u00f6zvetlen\u00fcl meg tudjuk h\u00edvni a lista friss\u00edt\u00e9st.
      • A feladatokat t\u00e1rol\u00f3 Repository egy reakt\u00edv Flow-al t\u00e9rne vissza az egyszeri lista helyett, \u00edgy a lista oldal ezen kereszt\u00fcl tud \u00e9rtes\u00fclni a v\u00e1ltoz\u00e1sokr\u00f3l (pl. Firebase hasonl\u00f3 elven is tud m\u0171k\u00f6dni)
      • Az oldal akt\u00edvv\u00e1 v\u00e1l\u00e1sakor automatikusan friss\u00edtj\u00fck a lista tartalm\u00e1t is.

      Mi most a harmadik megold\u00e1st fogjuk alkalmazni. Ehhez \u00e9rtes\u00fcln\u00fcnk kell arr\u00f3l, amikor az adott oldal akt\u00edvv\u00e1 v\u00e1l\u00edk (hasonl\u00f3an az Activity onResume() \u00e9letciklus\u00e1hoz). Ezt az itt l\u00e1that\u00f3 le\u00edr\u00e1s alapj\u00e1n tudjuk megval\u00f3s\u00edtani.

      TodoListScreen.kt:

      @Composable\nfun TodoListScreen(\nonListItemClick: (Int) -> Unit,\nonFabClick: () -> Unit,\nviewModel: TodoListViewModel = viewModel(factory = TodoListViewModel.Factory),\n) {\nval state = viewModel.state.collectAsStateWithLifecycle().value\nval context = LocalContext.current\n\nval lifecycleOwner = LocalLifecycleOwner.current\nDisposableEffect(lifecycleOwner) {\nval observer = LifecycleEventObserver { _, event ->\nif (event == Lifecycle.Event.ON_RESUME) {\nviewModel.loadTodos()\n}\n}\nlifecycleOwner.lifecycle.addObserver(observer)\nonDispose {\nlifecycleOwner.lifecycle.removeObserver(observer)\n}\n}\n\nScaffold(\n...\n}\n

      Tegy\u00fck a ViewModel loadTodos() met\u00f3dus\u00e1t publikuss\u00e1, \u00e9s t\u00f6r\u00f6lj\u00fck az inicializ\u00e1l\u00f3 k\u00f3dblokkban t\u00f6rt\u00e9n\u0151 megh\u00edv\u00e1s\u00e1t. Pr\u00f3b\u00e1ljuk ki az alkalmaz\u00e1st! Ha zavar a t\u00f6lt\u00e9s miatti k\u00e9perny\u0151 bevillan\u00e1sa, akkor ak\u00e1r ki is vehetj\u00fck a loadTodos() met\u00f3dusb\u00f3l a Loading \u00e1llapot be\u00e1ll\u00edt\u00e1s\u00e1t.

      "},{"location":"laborok/todo_compose_basics/#kiegeszito-feladat-2-animacio-optimalizalas","title":"Kieg\u00e9sz\u00edt\u0151 feladat 2 - Anim\u00e1ci\u00f3 optimaliz\u00e1l\u00e1s","text":"

      Vizsg\u00e1ljuk meg, hogyan t\u00f6rt\u00e9nik a fontoss\u00e1got kiv\u00e1laszt\u00f3 fel\u00fcleti elemen a lenyit\u00e1st jelz\u0151 elem anim\u00e1ci\u00f3ja:

      PriorityDropdown.kt:

      @Composable\nfun PriorityDropDown(\n...\n) {\nvar expanded by remember { mutableStateOf(false) }\nval angle: Float by animateFloatAsState(\ntargetValue = if (expanded) 180f else 0f,\nlabel = \"Priority arrow angle animation\"\n)\n\nSurface(\n...\n) {\nRow(\n...\n) {\n...\nIconButton(\nmodifier = Modifier\n.rotate(degrees = angle),\nonClick = { expanded = true }\n) {\nIcon(\n...\n)\n}\n...\n}\n}\n}\n
      Az animateFloatAsState() egy nagyon hasznos State objektumot tesz el\u00e9rhet\u0151v\u00e9 az objektumonkun bel\u00fcl. A targetValue \u00e9rt\u00e9k\u00e9t \u00e1ll\u00edtva a kiolvasott \u00e9rt\u00e9k nem egyb\u0151l, hanem egy \u00e1tmenettel fogja megk\u00f6zel\u00edteni a c\u00e9l\u00e9rt\u00e9ket, mely \u00edgy k\u00f6nnyen felhaszn\u00e1lhat\u00f3 anim\u00e1ci\u00f3k k\u00e9sz\u00edt\u00e9s\u00e9re. R\u00e1ad\u00e1sul mivel az \u00f6sszes Composable egy Kotlin f\u00fcggv\u00e9nynek felel meg, tetsz\u0151leges el\u00e1gaz\u00e1st vagy fel\u00fcletet l\u00e9tre tudok hozni egy ilyen anim\u00e1ci\u00f3 seg\u00edts\u00e9g\u00e9vel. Arra viszont \u00e9rdemes \u00fcgyelni, hogy ezek az anim\u00e1ci\u00f3k min\u00e9l hat\u00e9konyabbak legyenek, mindig csak a sz\u00fcks\u00e9ges elemeket rajzolj\u00e1k \u00fajra.

      Egy State objektum \u00e9rt\u00e9knek a v\u00e1ltoz\u00e1sa sor\u00e1n minden kontextust, mely kiolvasta az \u00e9rt\u00e9k\u00e9t, \u00fajra fogja futtatni. Ebben a helyzetben az IconButton l\u00e9trehoz\u00e1sakor olvassuk ki az aktu\u00e1lis \u00e9rt\u00e9k\u00e9t, mely a Row elem f\u00fcggv\u00e9ny callbackj\u00e9ben t\u00f6rt\u00e9nik meg, teh\u00e1t a sz\u00f6g m\u00f3dos\u00edt\u00e1sa hat\u00e1s\u00e1ra ezt a k\u00f3dblokkot mindenk\u00e9pp \u00fajra kell futtatnia a Composenak.

      Els\u0151 optimaliz\u00e1ci\u00f3s l\u00e9p\u00e9sk\u00e9nt beljebb vihetj\u00fck a forgat\u00e1st elv\u00e9gz\u00f3 k\u00f3dr\u00e9szletet az IconButton belsej\u00e9ben tal\u00e1lhat\u00f3 Icon elemre:

      IconButton(\nmodifier = Modifier\n.weight(weight = 1.5f),\nonClick = { expanded = true }\n) {\nIcon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n.rotate(degrees = angle),\n)\n}\n
      \u00cdgy a sz\u00f6g kiolvas\u00e1sa az Icon l\u00e9trehoz\u00e1sakor t\u00f6rt\u00e9nik, mely az IconButton callbackj\u00e9ben t\u00f6rt\u00e9nik, melyben csak az Icon l\u00e9trehoz\u00e1sa t\u00f6rt\u00e9nik, \u00edgy kevesebb elemet kell l\u00e9trehozni ennek a m\u00f3dos\u00edt\u00e1s\u00e1ra.

      A kiolvas\u00e1s hely\u00e9nek m\u00f3dos\u00edt\u00e1sa mellett egy m\u00e1sik szempontra is \u00e9rdemes figyeln\u00fcnk a State objektumok haszn\u00e1lat\u00e1n\u00e1l: a Compose melyik f\u00e1zis\u00e1ban t\u00f6rt\u00e9nik a kiolvas\u00e1s. Ennek meg\u00e9rt\u00e9s\u00e9re n\u00e9zz\u00fck \u00e1t az al\u00e1bbi \u00e1br\u00e1t:

      Az aktu\u00e1lis helyzetben a Composition r\u00e9tegben olvassuk ki az \u00e9rt\u00e9k\u00e9t a sz\u00f6gnek, pedig val\u00f3j\u00e1ban csak a kirajzol\u00e1skor kellene egy forgat\u00e1si transzform\u00e1ci\u00f3t haszn\u00e1lni. A Compose sok esetben k\u00e9t megold\u00e1st biztos\u00edt egy param\u00e9ter megad\u00e1s\u00e1ra: a k\u00f6zvetlen \u00e9rt\u00e9kad\u00e1s, illetve a callbacken kereszt\u00fcli visszat\u00e9r\u00e9s.

      Jelenleg a k\u00f6zvetlen \u00e9rt\u00e9kad\u00e1st haszn\u00e1ljuk, mert akkor megadjuk a forgat\u00e1s \u00e9rt\u00e9k\u00e9t, amikor l\u00e9trehozzuk az adott Modifier objektumot. Ezt \u00e1ltal\u00e1ban egyszer\u0171bb, viszont cser\u00e9be kor\u00e1bban kiolvas\u00e1sra ker\u00fcl az \u00e9rt\u00e9k, mint sz\u00fcks\u00e9g lenne. A callbacken kereszt\u00fcli visszat\u00e9r\u00e9s eset\u00e9n a Compose garant\u00e1lja, hogy csak abban a f\u00e1zisban olvassa ki az adott \u00e9rt\u00e9ket, amikor m\u00e1r mindenk\u00e9pp sz\u00fcks\u00e9ges. Ezt forgat\u00e1sn\u00e1l is lehet haszn\u00e1lni a k\u00f6vetkez\u0151 m\u00f3don:

      Icon(\nimageVector = Icons.Default.ArrowDropDown,\ncontentDescription = null,\nmodifier = Modifier.padding(5.dp)\n.graphicsLayer {\nrotationZ = angle\n}\n)\n

      \u00cdgy ebben az esetben a sz\u00f6g kiolvas\u00e1sa m\u00e1r csak a Drawing f\u00e1zisban t\u00f6rt\u00e9nik, nem kell a teljes cikluson v\u00e9gigfutni az anim\u00e1ci\u00f3 sor\u00e1n.

      "},{"location":"laborok/todo_compose_basics/#onallo-feladat","title":"\u00d6n\u00e1ll\u00f3 feladat","text":""},{"location":"laborok/todo_compose_basics/#datumvalaszto-elkeszitese","title":"D\u00e1tumv\u00e1laszt\u00f3 elk\u00e9sz\u00edt\u00e9se","text":"

      Val\u00f3s\u00edtsuk meg a felugr\u00f3 d\u00e1tumv\u00e1laszt\u00f3 ablakot a TodoCreateScreen-en! Kor\u00e1bban ezt k\u00fcls\u0151 k\u00f6nyvt\u00e1rral kellett megoldanunk, azonban szerencs\u00e9re a Material3 m\u00e1r tartalmaz DatePickert \u00e9s DatePickerDialog-ot. A megval\u00f3s\u00edt\u00e1shoz el\u0151sz\u00f6r fel kell venn\u00fcnk egy showDialog v\u00e1ltoz\u00f3t az oldalon bel\u00fcl, melyet a TodoEditor megfelel\u0151 callbackj\u00e9ben be kell \u00e1ll\u00edtanunk. Ha pedig a showDialog true \u00e9rt\u00e9ket tartalmaz, akkor az oldalhoz tartoz\u00f3 Scaffold v\u00e9g\u00e9n jelen\u00edts\u00fck meg a dial\u00f3gust a megfelel\u0151 param\u00e9terez\u00e9s\u00e9vel. Ne felejts\u00fck el \u00e1tadni az aktu\u00e1lis d\u00e1tumot, illetve a k\u00e9t esem\u00e9nyt kezelj\u00fck le megfelel\u0151en:

      DatePickerDialog
      if (showDialog) {\nBox() {\n\nval datePickerState = rememberDatePickerState()\n\nDatePickerDialog(\nonDismissRequest = {\n// Dismiss the dialog when the user clicks outside the dialog or on the back\n// button. If you want to disable that functionality, simply use an empty\n// onDismissRequest.\nshowDialog = false\n},\nconfirmButton = {\nTextButton(\nonClick = {\nshowDialog = false\nval date =\nInstant.ofEpochMilli(datePickerState.selectedDateMillis!!)\n.atZone(ZoneId.systemDefault()).toLocalDateTime()\n\nviewModel.onEvent(\nTodoCreateEvent.SelectDate(\nLocalDate(date.year, date.month, date.dayOfMonth)\n)\n)\n}\n) {\nText(stringResource(R.string.dialog_ok_button_text))\n}\n},\ndismissButton = {\nTextButton(\nonClick = {\nshowDialog = false\n}\n) {\nText(stringResource(R.string.dialog_dismiss_button_text))\n}\n}\n) {\nDatePicker(state = datePickerState)\n}\n}\n}   

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a d\u00e1tumv\u00e1laszt\u00f3 dial\u00f3gus (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az ahhoz tartoz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

      A k\u00e9pet a megold\u00e1sban a repository-ba f4.png n\u00e9ven t\u00f6ltsd f\u00f6l!

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"laborok/todo_compose_basics/#lista-osszekeverese","title":"Lista \u00f6sszekever\u00e9se","text":"

      Adjunk hozz\u00e1 egy f\u00fcggv\u00e9nyt a TodoListViewModel-hez, mely megkeveri a lista elemeit! Haszn\u00e1ljuk ehhez a shuffled() f\u00fcggv\u00e9nyt. H\u00edvjuk meg ezt a f\u00fcggv\u00e9nyt egy \u00faj floating action button megnyom\u00e1s\u00e1ra (tegy\u00fck egy Column-be a l\u00e9trehoz\u00e1s gombot, \u00e9s f\u00f6l\u00e9 tegy\u00fcnk egy \u00faj gombot). Vegy\u00fck fel a list\u00e1n bel\u00fcl a ListItem modifier l\u00e1nc\u00e1hoz az animateItemPlacement() h\u00edv\u00e1st. Mit tapasztalunk, ha \u00edgy megkeverj\u00fck a lista tartalm\u00e1t? Mi t\u00f6rt\u00e9nik, ha kivessz\u00fck a LazyColumn items blokkj\u00e1b\u00f3l a key param\u00e9tert?

      BEADAND\u00d3 (1 pont)

      K\u00e9sz\u00edts egy k\u00e9perny\u0151k\u00e9pet, amelyen l\u00e1tszik a lista megkevert \u00e1llapot\u00e1ban (emul\u00e1toron, k\u00e9sz\u00fcl\u00e9ket t\u00fckr\u00f6zve vagy k\u00e9perny\u0151felv\u00e9tellel), az anim\u00e1ci\u00f3t tartalmaz\u00f3 k\u00f3dr\u00e9szlet, valamint a neptun k\u00f3dod a k\u00f3dban valahol kommentk\u00e9nt!

      A k\u00e9pet a megold\u00e1sban a repository-ba f5.png n\u00e9ven t\u00f6ltsd f\u00f6l!

      A k\u00e9perny\u0151k\u00e9p sz\u00fcks\u00e9ges felt\u00e9tele a pontsz\u00e1m megszerz\u00e9s\u00e9nek.

      "},{"location":"tudnivalok/github/GitHub-Actions/","title":"GitHub Actions ismertet\u0151","text":"

      A laborfeladatok ki\u00e9rt\u00e9kel\u00e9s\u00e9ben a GitHub Actions-re t\u00e1maszkodunk. Seg\u00edts\u00e9g\u00e9vel a git repository-kon m\u0171veleteket \u00e9s programokat tudunk futtatni. Ilyen m\u0171velet p\u00e9ld\u00e1ul a C# k\u00f3d leford\u00edt\u00e1sa, vagy a beadott k\u00f3d tesztel\u00e9se.

      A lefutott ki\u00e9rt\u00e9kel\u00e9sr\u0151l a pull request-ben fogsz \u00e9rtes\u00edt\u00e9st kapni. Ha meg szeretn\u00e9d n\u00e9zni r\u00e9szletesebben a h\u00e1tt\u00e9rben t\u00f6rt\u00e9nteket, vagy p\u00e9ld\u00e1ul az alkalmaz\u00e1s napl\u00f3kat, a GitHub fel\u00fclet\u00e9n az Actions alatt indulhatsz el.

      Az Actions fel\u00fclet\u00e9n un. Workflow-kat l\u00e1tsz; minden egyes ki\u00e9rt\u00e9kel\u00e9s futtat\u00e1s egy-egy elem lesz itt (teh\u00e1t historikusan is visszakereshet\u0151ek).

      Ezek k\u00f6z\u00fcl egyet kiv\u00e1lasztva (pl. a legfels\u0151 mindig a legutols\u00f3) l\u00e1thatod a workflow fut\u00e1s\u00e1nak r\u00e9szleteit. A fut\u00e1s napl\u00f3j\u00e1hoz a bal oldali list\u00e1ban m\u00e9g kattintani kell egyet. Jobb oldalon l\u00e1that\u00f3 a folyamat teljes napl\u00f3ja.

      Minden z\u00f6ld pipa egy-egy sikeres l\u00e9p\u00e9st jelent. Ezen l\u00e9p\u00e9sek nem azonosak a feladatokokkal, hanem a ki\u00e9rt\u00e9kel\u00e9s folyamat\u00e1nak l\u00e9p\u00e9sei lesznek. Ilyen l\u00e9p\u00e9s p\u00e9ld\u00e1ul a k\u00f6rnyezet el\u0151k\u00e9sz\u00edt\u00e9se, pl. a .NET SDK telep\u00edt\u00e9se (minden ki\u00e9rt\u00e9kel\u00e9s egy vadi\u00faj k\u00f6rnyezetben indul, \u00edgy mindent el\u0151 kell k\u00e9sz\u00edteni).

      Alapvet\u0151en a l\u00e9p\u00e9sek mindig sikeresek, akkor is, ha a megold\u00e1sodban hiba van, mert a ki\u00e9rt\u00e9kel\u00e9s erre fel van k\u00e9sz\u00edtve. Kiv\u00e9telt ez al\u00f3l csak a neptun.txt hi\u00e1nya ill. a C# k\u00f3d leford\u00edt\u00e1sa jelent. El\u0151bbi felt\u00e9tlen\u00fcl sz\u00fcks\u00e9ges, ez\u00e9rt semmilyen folyamatot nem hajtunk v\u00e9gre n\u00e9lk\u00fcle. Ut\u00f3bbi eset\u00e9ben a C# k\u00f3d ford\u00edt\u00e1sa szint\u00e9n sz\u00fcks\u00e9ges a tov\u00e1bbl\u00e9p\u00e9shez, ez\u00e9rt sikertelens\u00e9g eset\u00e9n le\u00e1ll a folyamat.

      N\u00e9ha el\u0151fordulhat azonban tranziens, id\u0151szakos hiba is. P\u00e9ld\u00e1ul a .NET k\u00f6rnyezet let\u00f6lt\u00e9se nem siker\u00fcl h\u00e1l\u00f3zati hiba miatt. Ilyen esetben a futtat\u00e1st k\u00e9zzel meg lehet ism\u00e9telni. Ez persze csak akkor seg\u00edt, ha t\u00e9nyleg \u00e1tmeneti hib\u00e1r\u00f3l van sz\u00f3, teh\u00e1t pl. egy C# ford\u00edt\u00e1si hib\u00e1n nem fog seg\u00edteni. (Ezt a hiba\u00fczenetb\u0151l illetve a l\u00e9p\u00e9s nev\u00e9b\u0151l tudod kider\u00edteni, vagy legal\u00e1bb is megtippelni kell\u0151 bizonyoss\u00e1ggal.)

      A feladat f\u00fcggv\u00e9ny\u00e9ben ak\u00e1r az alkalmaz\u00e1s napl\u00f3kat is meg tudod n\u00e9zni itt. Pl. amikor .NET alkalmaz\u00e1st k\u00e9sz\u00edtesz, az alkalmaz\u00e1st elind\u00edtjuk, \u00e9s minden, amit napl\u00f3z, itt megtekinthet\u0151.

      Az al\u00e1bbi p\u00e9ld\u00e1ul egy Entity Framework-\u00f6t haszn\u00e1l\u00f3 alkalmaz\u00e1s inicializ\u00e1s\u00e1t mutatja, k\u00f6zt\u00fck p\u00e9ld\u00e1ul a kiadott SQL parancsokat is. Debuggol\u00e1s k\u00f6zben a Visual Studio Output ablak\u00e1ban is hasonl\u00f3kat l\u00e1thatsz. Ez term\u00e9szetesen nagyban f\u00fcgg a konkr\u00e9t feladatt\u00f3l.

      "},{"location":"tudnivalok/github/GitHub-credentials/","title":"Egyetemi laborokban: GitHub bel\u00e9p\u00e9s","text":"

      Az egyetemi laborokban a g\u00e9pek megjegyzik a GitHub bel\u00e9p\u00e9si adatokat. Ezt a munka v\u00e9gezt\u00e9vel k\u00e9zzel kell t\u00f6r\u00f6lni.

      1. Nyisd meg a Credential Manager-t a Start men\u00fcb\u0151l.
      2. A Windows Credentials oldalon keresd meg a GitHubra mutat\u00f3 bejegyz\u00e9seket, \u00e9s t\u00f6r\u00f6ld \u0151ket.
      "},{"location":"tudnivalok/github/GitHub/","title":"Feladatok bead\u00e1sa (GitHub)","text":"

      A feladatok bead\u00e1s\u00e1hoz a GitHub platformot haszn\u00e1ljuk. Minden labor bead\u00e1sa egy-egy GitHub repository-ban t\u00f6rt\u00e9nik, melyet a feladatle\u00edr\u00e1sban tal\u00e1lhat\u00f3 linken kereszt\u00fcl kapsz meg. A labor feladatainak megold\u00e1s\u00e1t ezen repository-ban kell elk\u00e9sz\u00edtened, \u00e9s ide kell felt\u00f6ltened. A k\u00e9sz megold\u00e1s bead\u00e1sa a repository-ba val\u00f3 felt\u00f6lt\u00e9s ut\u00e1n egy un. pull request form\u00e1j\u00e1ban t\u00f6rt\u00e9nik, amelyet a laborvezet\u0151dh\u00f6z rendelsz.

      FONTOS

      Az itt le\u00edrt formai el\u0151\u00edr\u00e1sok betart\u00e1sa elv\u00e1r\u00e1s. A nem ilyen form\u00e1ban beadott megold\u00e1sokat nem \u00e9rt\u00e9kelj\u00fck.

      "},{"location":"tudnivalok/github/GitHub/#roviditett-verzio","title":"R\u00f6vid\u00edtett verzi\u00f3","text":"

      Al\u00e1bb r\u00e9szletesen bemutatjuk a bead\u00e1s menet\u00e9t. Itt egy r\u00f6vid \u00f6sszefoglal\u00f3 az \u00e1ttekint\u00e9shez, illetve a helyes bead\u00e1s ellen\u0151rz\u00e9s\u00e9hez.

      1. A munk\u00e1dat Moodle-ben tal\u00e1lhat\u00f3 GitHub Classroom megh\u00edv\u00f3 linken kereszt\u00fcl l\u00e9trehozott GitHub repository-ban kell elk\u00e9sz\u00edtsd.

      2. A megold\u00e1shoz k\u00e9sz\u00edts egy k\u00fcl\u00f6n \u00e1gat, ne a master-en dolgozz. Erre az \u00e1gra ak\u00e1rh\u00e1ny kommitot tehetsz. Mindenk\u00e9ppen pushold a megold\u00e1st.

      3. A bead\u00e1st egy pull request jelzi, amely pull request-et a laborvezet\u0151dh\u00f6z kell rendelned.

      4. Ha az eredm\u00e9nnyel vagy \u00e9rt\u00e9kel\u00e9ssel kapcsolatban k\u00e9rd\u00e9sed van, pull request kommentben k\u00e9rdezhetsz. A laborvezet\u0151 \u00e9rtes\u00edt\u00e9s\u00e9hez haszn\u00e1ld a @n\u00e9v c\u00edmz\u00e9st a komment sz\u00f6veg\u00e9ben.

      "},{"location":"tudnivalok/github/GitHub/#a-munka-elkezdese-git-checkout","title":"A munka elkezd\u00e9se: git checkout","text":"
      1. Regisztr\u00e1lj egy GitHub accountot, ha m\u00e9g nincs.

      2. Moodle-ben a kurzus oldal\u00e1n keresd meg a laborhoz tartoz\u00f3 megh\u00edv\u00f3 URL-t. Ez minden laborhoz m\u00e1s lesz, \u00fcgyelj r\u00e1, hogy a megfelel\u0151 linket haszn\u00e1ld.

      3. Ha k\u00e9ri, adj enged\u00e9lyt a GitHub Classroom alkalmaz\u00e1snak, hogy haszn\u00e1lja az account adataidat.

      4. L\u00e1tni fogsz egy oldalt, ahol elfogadhatod a feladatot (\"Accept the ... assignment\"). Kattints a gombra.

      5. V\u00e1rd meg, am\u00edg elk\u00e9sz\u00fcl a repository. A repository linkj\u00e9t itt kapod meg.

        Megjegyz\u00e9s

        A repository priv\u00e1t lesz, azaz az senki nem l\u00e1tja, csak te, \u00e9s az oktat\u00f3k.

      6. Nyisd meg a repository-t a webes fel\u00fcleten a linkre kattintva. Ezt az URL-t \u00edrd fel, vagy mentsd el.

      7. Kl\u00f3nozd le a repository-t. Ehhez sz\u00fcks\u00e9ges lesz a repository c\u00edm\u00e9re, amit a repository webes fel\u00fclet\u00e9n a Clone or download alatt tal\u00e1lsz.

        A git repository kezel\u00e9s\u00e9hez tetsz\u0151leges klienst haszn\u00e1lhatsz. Ha nincs kedvenced m\u00e9g, akkor legegyszer\u0171bb a GitHub Desktop. Ebben az alkalmaz\u00e1sban k\u00f6zvetlen\u00fcl tudod list\u00e1zni a repository-kat GitHub-r\u00f3l, vagy haszn\u00e1lhatod az URL-t is a kl\u00f3noz\u00e1shoz.

        Ha konzolt haszn\u00e1ln\u00e1l, az al\u00e1bbi parancs kl\u00f3nozza a repository-t (ha a git parancs el\u00e9rhet\u0151): git clone <repository link>

        Sikertelen kl\u00f3noz\u00e1s

        Amennyiben a bejelentkez\u00e9s sikertelen felhaszn\u00e1l\u00f3n\u00e9v/jelsz\u00f3 p\u00e1rossal a \"Clone with HTTPS\" eset\u00e9n, (r\u00e9gebb \u00f3ta haszn\u00e1lt felhaszn\u00e1l\u00f3n\u00e1l) \u00e9rdemes ellen\u0151rizni a git-en tal\u00e1lhat\u00f3 Personal Access token lej\u00e1rati d\u00e1tum\u00e1t.

        Jobb fels\u0151 sarokban a profilk\u00e9p melletti lefel\u00e9 mutat\u00f3 nyil > Settings > bal oldalon (legals\u00f3) Developer settings > ugyanitt Personal access tokens.

        Alternat\u00edv m\u00f3dszerk\u00e9nt: HTTP kl\u00f3noz\u00e1s helyett, SSH kulcs haszn\u00e1lat\u00e1hoz, angol nyelv\u0171 instrukci\u00f3k itt tal\u00e1lhat\u00f3ak.

      8. Ha siker\u00fclt a kl\u00f3noz\u00e1s, M\u00c9G NE KEZDJ EL DOLGOZNI! A megold\u00e1st ne a repository master/main \u00e1g\u00e1n k\u00e9sz\u00edtsd el. Hozz l\u00e9tre egy \u00faj \u00e1gat (branch) megoldas n\u00e9ven.

        GitHub Desktop-ban a Branch men\u00fcben teheted ezt meg.

        Ha konzolt haszn\u00e1lsz, az \u00faj \u00e1g elk\u00e9sz\u00edthet\u0151 ezzel a paranccsal: git checkout -b megoldas

      9. Ezen a megold\u00e1s \u00e1gon dolgozva k\u00e9sz\u00edtsd el a beadand\u00f3kat. Ak\u00e1rh\u00e1nyszor kommitolhatsz \u00e9s pusholhatsz. A megold\u00e1s r\u00e9sze a forr\u00e1sk\u00f3d \u00e9s a feladatokban elv\u00e1rt k\u00e9perny\u0151k\u00e9pek. Ha a feladat k\u00e9perny\u0151k\u00e9pet v\u00e1r el, akkor azt a repository gy\u00f6ker\u00e9be commitold az elv\u00e1rt n\u00e9ven.

        Egyetemi laborban

        Laborg\u00e9peken mindig ellen\u0151r\u00edzd, hogy a megfelel\u0151 n\u00e9vvel \u00e9s email c\u00edmmel kommitolsz-e. Ezt a k\u00f6vetkez\u0151 command line paranccsal tudod megtenni.

        git config user.name\ngit config user.email\n

        Ha ez nem megfelel\u0151 lenne, akkor add ki az al\u00e1bbi parancsokat a git repository mapp\u00e1j\u00e1ban. Ezzel az adott repository-ra fogod be\u00e1ll\u00edtani a k\u00edv\u00e1nt nevet \u00e9s email c\u00edmet. (\u00c9rdemes olyan email c\u00edmet, megadni ami a github useretekhez van rendelve)

        git config user.name \"John Doe\"\ngit config user.email \"john@doe.org\"\n

        Otthon

        Otthon a fentieket \u00e9rdemes lehet a glob\u00e1lisan vizsg\u00e1lni \u00e9s fel\u00fcl\u00edrni a --global kapcsol\u00f3val.

        GitHub Desktop-ban \u00edgy tudsz kommitolni. Mindig ellen\u0151rizd, hogy j\u00f3 \u00e1gon vagy-e. Els\u0151 alkalommal a megoldas \u00e1g csak helyben l\u00e9tezik, ez\u00e9rt publik\u00e1lni kell: Publish this branch.

        A tov\u00e1bbi kommitokn\u00e1l is mindig ellen\u0151rizd a megfelel\u0151 \u00e1gat. Ha egy kommit m\u00e9g nincs fel\u00f6ltve, azt a Push origin gombbal teheted meg. A kis sz\u00e1m a gombon jelzi, hogy h\u00e1ny, m\u00e9g nem pusholt kommit van.

        Ha konzolt haszn\u00e1lsz, akkor az al\u00e1bbi parancsokat haszn\u00e1ld (felt\u00e9ve, hogy a j\u00f3 \u00e1gon vagy):

        # Ellen\u0151rizd az \u00e1gat, \u00e9s hogy milyen f\u00e1jlok m\u00f3dosultak\ngit status\n\n# Minden v\u00e1ltoztat\u00e1st el\u0151k\u00e9sz\u00edt kommitol\u00e1sra\ngit add .\n\n# Kommit\ngit commit -m \"f1\"\n\n# Push els\u0151 alkalommal az \u00faj \u00e1g publik\u00e1l\u00e1s\u00e1hoz\ngit push --set-upstream origin megoldas\n\n# Push a tov\u00e1bbiakban, amikor az \u00e1g m\u00e1r nem \u00faj\ngit push\n
      "},{"location":"tudnivalok/github/GitHub/#a-megoldas-beadasa","title":"A megold\u00e1s bead\u00e1sa","text":"
      1. Ha v\u00e9gezt\u00e9l a megold\u00e1ssal, ellen\u0151rizd a GitHub webes fel\u00fclet\u00e9n, hogy mindent felt\u00f6lt\u00f6tt\u00e9l-e. Ehhez a webes fel\u00fcleten v\u00e1ltanod kell az \u00e1gak k\u00f6z\u00f6tt.

        Felt\u00f6lt\u00e9s a webes fel\u00fcleten

        Azt javasoljuk, hogy ne haszn\u00e1ld a GitHub f\u00e1jl felt\u00f6lt\u00e9s funkci\u00f3j\u00e1t. Ha valami hi\u00e1nyzik, a helyi git repository-ban p\u00f3told, \u00e9s kommitold majd pushold.

      2. Ha t\u00e9nyleg k\u00e9sz vagy, akkor nyiss egy pull request-et.

        Minek a pull request?

        Ez a pull request fogja \u00f6ssze a megold\u00e1sodat, \u00e9s annak \"v\u00e9geredm\u00e9ny\u00e9t\" mutatja. \u00cdgy a laborvezet\u0151nek nem az egyes kommitjaidat vagy f\u00e1jljaidat kell n\u00e9znie, hanem csak a relev\u00e1ns, v\u00e1ltozott r\u00e9szeket l\u00e1tja egyben. A pull request jelenti a feladatod bead\u00e1s\u00e1t is, \u00edgy ez a l\u00e9p\u00e9s nem hagyhat\u00f3 ki.

        A pull request nyit\u00e1s\u00e1hoz a GitHub webes fel\u00fclet\u00e9re kell menj. Itt, ha nem r\u00e9g pusholt\u00e1l, a GitHub fel is aj\u00e1nlja a pull request l\u00e9trehoz\u00e1s\u00e1t.

        A pull request-et a fenti men\u00fcben is l\u00e9trehozhatod. Fontos, hogy a megfelel\u0151 brancheket v\u00e1laszd ki: master-be megy a megoldas \u00e1g.

        Ha minden rendben siker\u00fclt, a men\u00fcben fent l\u00e1tod a kis \"1\" sz\u00e1mot a Pull request elem mellett, jelezve, hogy van egy nyitott pull request. DE M\u00c9G NEM V\u00c9GEZT\u00c9L!

      3. A pull request hat\u00e1s\u00e1ra le fog futni egy \u00e9rt\u00e9kel\u00e9s. Ennek eredm\u00e9ny\u00e9t a pull request alatt kommentben fogod l\u00e1tni.

        Ez az \u00e9rt\u00e9kel\u00e9s minden labor eset\u00e9ben m\u00e1s lesz. Egyes laborokn\u00e1l a programodat lefuttatjuk, \u00e9s el\u0151zetes pontsz\u00e1mot is kapsz. M\u00e1s laborokn\u00e1l csak \"szintaktikai ellen\u0151rz\u00e9st\" v\u00e9gz\u00fcnk.

        Ha a ki\u00e9rt\u00e9kel\u00e9s eredm\u00e9ny\u00e9vel kapcsolatban t\u00f6bb inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ged, mint amit itt l\u00e1tsz, a GitHub Actions webes fel\u00fclete seg\u00edts\u00e9g\u00fcl szolg\u00e1lhat. Err\u0151l itt tal\u00e1lsz egy r\u00f6vid ismertet\u0151t.

      4. Ha nem vagy megel\u00e9gedve a munk\u00e1ddal, akkor m\u00e9g jav\u00edthatsz rajta. Ehhez kommitolj \u00e9s pusholj \u00fajra. Ha tov\u00e1bbra is a megfelel\u0151 \u00e1gon dolgozol, akkor a pull request \u00fajb\u00f3l le fogja futtatni a ki\u00e9rt\u00e9kel\u00e9st. Arra k\u00e9r\u00fcnk, hogy MAXIMUM 5 alkalommal futtasd le a ki\u00e9rt\u00e9kel\u00e9st!

        Megold\u00e1s jav\u00edt\u00e1sa ki\u00e9rt\u00e9kel\u00e9s n\u00e9lk\u00fcl

        Ha \u00fagy l\u00e1tod, hogy a megold\u00e1sodat m\u00e9g jav\u00edtani akarod, \u00e9s nem szeretn\u00e9d, hogy mindig lefusson az \u00e9rt\u00e9kel\u00e9s, akkor \u00e1ll\u00edtsd \u00e1t a pull request-et a webes fel\u00fcleten draft \u00e1llapotra.

        Ezzel az \u00e1llapottal jelzed, hogy m\u00e9g dolgozol. Kommitolj \u00e9s pusholj. Ilyenkor nem fog futni ki\u00e9rt\u00e9kel\u00e9s. Ha v\u00e9gezt\u00e9l, akkor vissza kell \u00e1ll\u00edtanod a pull request-et: menj a PR alj\u00e1ra \u00e9s kattints a \"Ready for review\" gombra. Ennek hat\u00e1s\u00e1ra vissza\u00e1ll a PR \u00e9s le fog futni az automata \u00e9rt\u00e9kel\u00e9s.

        Maximum 5

        A maximum 5 alkalomba nem sz\u00e1moljuk bele az esetlegesen megszakadt, vagy tranziens hiba miatt sikertelen futtat\u00e1sokat. Ha viszont figyelmetlens\u00e9gb\u0151l, vagy sz\u00e1nd\u00e9kosan t\u00fall\u00e9ped az \u00f6t\u00f6t, akkor pontlevon\u00e1ssal szankcion\u00e1lunk. Arra k\u00e9r\u00fcnk, hogy bead\u00e1s el\u0151tt teszteld a megold\u00e1sod, ne a GitHub platformot \"dolgoztasd\" magad helyett!

      5. V\u00c9GEZET\u00dcL, ha k\u00e9sz vagy, a pull request-et rendeld a laborvezet\u0151dh\u00f6z. Ez a l\u00e9p\u00e9s felt\u00e9tlen\u00fcl fontos, ez jelzi a bead\u00e1st.

        Pull request n\u00e9lk\u00fcl

        Ha nincs pull request-ed, vagy nincs a laborvezet\u0151h\u00f6z rendelve, akkor \u00fagy tekintj\u00fck, hogy m\u00e9g nem vagy k\u00e9szen, \u00e9s nem adtad be a megold\u00e1st.

        V\u00e9gezt\u00e9l

        Miut\u00e1n a laborvezet\u0151h\u00f6z rendelted a pull request-et, m\u00e1r ne m\u00f3dos\u00edts semmin. A laborvezet\u0151 \u00e9rt\u00e9kelni fogja a munk\u00e1dat, \u00e9s a pull request lez\u00e1r\u00e1s\u00e1val kommentben jelzi a v\u00e9geredm\u00e9nyt.

      "},{"location":"tudnivalok/github/GitHub/#kapott-eredmennyel-kapcsolatban-kerdes-vagy-reklamacio","title":"Kapott eredm\u00e9nnyel kapcsolatban k\u00e9rd\u00e9s vagy reklam\u00e1ci\u00f3","text":"

      Ha a feladatok \u00e9rt\u00e9kel\u00e9s\u00e9vel vagy az eredm\u00e9nnyel kapcsolatban k\u00e9rd\u00e9st tenn\u00e9l fel, vagy reklam\u00e1ln\u00e1l, haszn\u00e1ld a Pull Request kommentel\u00e9si lehet\u0151s\u00e9g\u00e9t erre. Annak \u00e9rdek\u00e9ben, hogy a laborvezet\u0151 biztosan \u00e9rtes\u00fclj\u00f6n a k\u00e9rd\u00e9sr\u0151l haszn\u00e1ld a @n\u00e9v mention funkci\u00f3t a laborvezet\u0151d megnevez\u00e9s\u00e9hez. Err\u0151l automatikusan kapni fog egy email \u00e9rtes\u00edt\u00e9st.

      Reklam\u00e1ci\u00f3 csak indokl\u00e1ssal

      Ha nem \u00e9rtesz egyet az \u00e9rt\u00e9kel\u00e9ssel, a bizony\u00edt\u00e1s t\u00e9ged terhel, azaz al\u00e1 kell t\u00e1masztanod a reklam\u00e1ci\u00f3d (pl. annak le\u00edr\u00e1s\u00e1val, hogyan tesztelted a megold\u00e1sod, \u00e9s mi bizony\u00edtja a helyess\u00e9g\u00e9t).

      "},{"location":"tudnivalok/github/contributing/","title":"Hozz\u00e1j\u00e1rul\u00e1s az anyaghoz","text":"

      Az anyag terjedelm\u00e9b\u0151l adand\u00f3an apr\u00f3bb hib\u00e1k esetenk\u00e9nt hi\u00e1nyoss\u00e1gok jelentkezhetnek a laborokban. Ha egy ilyennel tal\u00e1lkozol \u00e9s \u00fagy d\u00f6ntesz szeretn\u00e9l seg\u00edteni hallgat\u00f3t\u00e1rsaidnak, azt a k\u00f6vetkez\u0151kben le\u00edrtak alapj\u00e1n tudod megtenni.

      Plusz pont jegyzet jav\u00edt\u00e1s\u00e9rt

      M\u00e1s tant\u00e1rgyak mint\u00e1j\u00e1ra itt is szeretn\u00e9nk plusz pontot adni a jegyzet open-source hozz\u00e1j\u00e1rul\u00e1sai\u00e9rt. Akik a t\u00e1rgyat jelenleg hallgatj\u00e1k, pontokat kaphatnak hozz\u00e1j\u00e1rul\u00e1saik\u00e9rrt.

      A f\u00e9l\u00e9v sor\u00e1n max 3 db plusz pontot lehet szerezni fejenk\u00e9nt olyan jav\u00edt\u00e1sok\u00e9rt, amik a trivi\u00e1lis 1-2 bet\u0171 elg\u00e9pel\u00e9sen t\u00fal \u00e9rdemben jav\u00edtanak a githubon tal\u00e1lhat\u00f3 labor jegyzetek min\u0151s\u00e9g\u00e9n. Pl.: jelent\u0151s mennyis\u00e9g\u0171 elg\u00e9pel\u00e9s jav\u00edt\u00e1sa, egy\u00e9rtelm\u0171s\u00edt\u00e9sek, illusztr\u00e1ci\u00f3k kieg\u00e9sz\u00edt\u00e9sek k\u00e9sz\u00edt\u00e9se vagy ak\u00e1r egy teljes kieg\u00e9sz\u00edt\u0151 jegyzet \u00edr\u00e1sa (term\u00e9szetesen nem azonos pont\u00e9rt\u00e9kkel).

      Persze a pont n\u00e9lk\u00fcl az 1-1 bet\u0171s elg\u00e9pel\u00e9seket is sz\u00edvesen fogadjuk, ami bemeleg\u00edt\u00e9snek is t\u00f6k\u00e9letes.

      "},{"location":"tudnivalok/github/contributing/#hibak-jelzese","title":"Hib\u00e1k jelz\u00e9se","text":"

      Amennyiben hib\u00e1t tal\u00e1lsz az anyagban, vagy szeretn\u00e9d b\u0151v\u00edteni, de nem \u00e1ll m\u00f3dodban jav\u00edtani, nyithatsz egy issue-t amiben le\u00edrod a hib\u00e1t.

      1. N\u00e9zd meg, hogy valaki nem jelezte-e, amit szeretn\u00e9l. Gyakran m\u00e1r l\u00e9tez\u0151 probl\u00e9m\u00e1kat tal\u00e1lnak, amire m\u00e1r van pull request, \u00edgy miel\u0151tt b\u00e1rmit tenn\u00e9l n\u00e9zd meg valaki nem el\u0151z\u00f6tt-e meg
      2. Az issues tabon a new issue gombbal hozz l\u00e9tre egy \u00faj issue-t.
      3. L\u00e1sd el a megfelel\u0151 c\u00edmk\u00e9kkel
        1. A labor t\u00edpusa (android az androidos laborokn\u00e1l)
        2. A hiba t\u00edpusa (clarification, typo, illustration vagy notes)
      4. \u00cdrd le, hogy mit k\u00e9ne tartalmaznia a jav\u00edt\u00e1snak

      Tip

      Az c\u00edme legyen r\u00f6vid \u00e9s l\u00e9nyegret\u00f6r\u0151, pl.: Megfogalmaz\u00e1s pontos\u00edt\u00e1sa a 4. laborban vagy A 6. laborban a le\u00edrt k\u00f3d hib\u00e1san m\u0171k\u00f6dik Android 12-n

      A issue descriptionj\u00e9ben pedig fejtsd ki, hol tal\u00e1lhat\u00f3 a hi\u00e1nyoss\u00e1g, illetve ha van r\u00e1 \u00f6tleted, hogy lehetne orvosolni ezt. Ha ezeken t\u00fal m\u00e9g screenshotot is tudsz mell\u00e9kelni, az nagyban megseg\u00edti a probl\u00e9ma mihamarabbi jav\u00edt\u00e1s\u00e1t.

      Warning

      A github issues nem a laborfeladatok megold\u00e1s\u00e1val kapcsolatos probl\u00e9m\u00e1k helye, \u00edgy a \"Nem tudom megoldani hogy az \u00e9rtes\u00edt\u00e9s meg\u00e9rkezzen\" jelleg\u0171 probl\u00e9m\u00e1kat ne itt jelezz\u00e9tek, erre vannak a laboralkalmak.

      "},{"location":"tudnivalok/github/contributing/#valtoztatasok-javaslasa","title":"V\u00e1ltoztat\u00e1sok javasl\u00e1sa","text":"

      Amennyiben a hozz\u00e1j\u00e1rul\u00e1sod meg tudod val\u00f3s\u00edtani ind\u00edts pull requestet

      1. Forkold a repository-t a Githubon jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 gombbal

      2. V\u00e9gezd el a v\u00e1ltoztat\u00e1sokat.

        Tip

        Ez nagyon hasonl\u00f3an m\u0171k\u00f6dik a laborok beada\u00e1s\u00e1hoz

        1. Hozz l\u00e9tre egy branchet a saj\u00e1t forkodon, amin a v\u00e1ltoztat\u00e1sokat el fogod v\u00e9gezni.

        2. Ezen a branchen k\u00e9sz\u00edtsd el a jav\u00edt\u00e1sokat

        3. Ellen\u0151rizd, hogy ne ker\u00fclj\u00f6n bele a commitba olyan file, amit az editor gener\u00e1lt (pl.: .idea mappa) illetve olyan file aminek nem k\u00e9ne kiker\u00fclnie, pl.: Github Private Access Token

        4. Ha k\u00e9sz vagy a laborok bead\u00e1s\u00e1hoz hasonl\u00f3an ind\u00edts egy pull requestet a VIAUAC00/laborok master branch\u00e9re.

        5. L\u00e1sd el a megfelel\u0151 c\u00edmk\u00e9kkel

          1. A labor t\u00edpusa (android az androidos laborokn\u00e1l \u00e9s web a webes laborokn\u00e1l)
          2. A hiba t\u00edpusa (clarification, typo, illustration vagy notes)
        6. A le\u00edr\u00e1sban r\u00e9szletezd v\u00e1ltoztat\u00e1sok ok\u00e1t. Ne felejtsd el bele\u00edrni a NEPTUN k\u00f3dod a le\u00edr\u00e1sba, mert \u00edgy fogjuk tudni megadni a pontokat.
      3. Valaki, akinek hozz\u00e1f\u00e9r\u00e9se van a repositoryhoz, ellen\u0151rzi a v\u00e1ltoztat\u00e1sok sz\u00fcks\u00e9gess\u00e9g\u00e9t, \u00e9s elb\u00edr\u00e1lja, hogy val\u00f3ban beker\u00fclhet az anyagba.

      4. A v\u00e1ltoztat\u00e1sokra review-t ind\u00edtunk \u00e9s ha kell m\u00f3dos\u00edt\u00e1sokat fogunk k\u00e9rni.
      5. Ha minden k\u00e9rt v\u00e1ltoztat\u00e1s megt\u00f6rt\u00e9nt, a hozz\u00e1j\u00e1rul\u00e1sod beleker\u00fcl az anyagba.
      "},{"location":"tudnivalok/github/contributing/#code-style","title":"Code style","text":"
      • Kotlin: a hivatalos style guide alapj\u00e1n
      • Markdown: Mivel az alap spec nem mindig a legtiszt\u00e1bban \u00e9rthet\u0151, a markdownlint szab\u00e1lyai alapj\u00e1n, az n\u00e9h\u00e1ny kiv\u00e9tel\u00e9vel. Ezeket a .markdownlint.yaml-ben tal\u00e1lod, ha VSCode-ot haszn\u00e1lsz automatikusan alkalmazza \u0151ket az editor \u00e9s jelzi ha nem megfelel\u0151 amit \u00edrsz.

      Ezek a st\u00edlusok a t\u00e1rgyban aj\u00e1nlott editorokban k\u00f6nnyen be\u00e1ll\u00edthat\u00f3ak.

      "},{"location":"tudnivalok/github/contributing/#vscode","title":"VSCode","text":"

      Aj\u00e1nlott extension\u00f6k:

      • yzhang.markdown-all-in-one: MD szinkroniz\u00e1lt live preview
      • DavidAnson.vscode-markdownlint: MD form\u00e1z\u00e1s, szab\u00e1lyok stb.
      • Prettier: HTML+CSS form\u00e1z\u00f3
      • Error Lens: Kiemeli a hib\u00e1kat hogy gyorsabben megtal\u00e1ljuk \u0151ket

      Az editor be\u00e1ll\u00edt\u00e1s\u00e1hoz nyisd meg a repo-t a gy\u00f6ker\u00e9ben VSCode-al. A VSCode fel fogja aj\u00e1nlani a k\u00e9t markdown extension-t.

      Ha ez megt\u00f6rt\u00e9nt, nyiss meg egy markdown dokumentumot, \u00e9s haszn\u00e1ld a Ctrl+Shift+P shortcutot, a command palette megnyit\u00e1s\u00e1hoz.

      Tip

      A command palette a VSCode parancsaihoz ny\u00fajt hozz\u00e1f\u00e9r\u00e9st, autocompleteeli a parancsokat \u00e9s egy minim\u00e1lis GUI-t is biztos\u00edt.

      A command palette-be keress\u00fck meg a Format Document With... men\u00fcpontot \u00e9s v\u00e1lasszuk ki. Ekkor egy almen\u00fcbe dob az editor \u00e9s kiv\u00e1laszthatjuk hogy melyik form\u00e1z\u00f3val form\u00e1zzuk a MD dokumentumokat. Legalul lesz egy Configure Default Formatter, v\u00e1lasszuk ezt. Ezut\u00e1n v\u00e1lasszuk a markdownlint extensiont, \u00e9s k\u00e9szen vagyunk.

      Megfelel\u0151 formatter kiv\u00e1laszt\u00e1sa

      Ne v\u00e1laszd ki a prettiert formatterk\u00e9nt, mert elt\u00f6ri a sz\u00f6vegbubor\u00e9kokat.

      Ezen fel\u00fcl \u00e9rdemes lehet bekapcsolni a ment\u00e9s el\u0151tti form\u00e1z\u00e1st.

      A Ctrl+, shortcuttal megnyitjuk a be\u00e1ll\u00edt\u00e1sokat, \u00e9s r\u00e1keres\u00fcnk arra, hogy format on save. Itt kipip\u00e1ljuk a checkboxot \u00e9s k\u00e9szen vagyunk.

      Ha ehhez nem lenne t\u00fcrelmed, itt a json amit a settings.json-ba illesztve be\u00e1ll\u00edt\u00f3dik minden.

      {\n\"[markdown]\": {\n\"editor.defaultFormatter\": \"DavidAnson.vscode-markdownlint\",\n\"editor.formatOnSave\": true\n}\n}\n
      "},{"location":"tudnivalok/github/contributing/#ajanlasok","title":"Aj\u00e1nl\u00e1sok","text":""},{"location":"tudnivalok/github/contributing/#android","title":"Android","text":"
      • Az androidos Kotlin \u00e9s XML fileokat illetve k\u00f3dr\u00e9szleteket Android Studioban form\u00e1zva \u00e9rdemes hozz\u00e1adni az anyaghoz
      • Ahhoz hogy biztosan form\u00e1zva legyenek a fileok haszn\u00e1ld a Ctrl+Alt+L shortcutot
      "},{"location":"tudnivalok/github/contributing/#markdown-fileok","title":"Markdown Fileok","text":"
      • A markdown fileokat se az Android Studio se a Visual Studio Code nem rendereli alaphelyzetben. Erre a feladatra a k\u00f6vetkez\u0151 extension\u00f6ket/pluginokat tudom aj\u00e1nlani:
      • VSCode: yzhang.markdown-all-in-one
      • Android Studio: Markdown Editor
      "}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 7d7f7e8..2b78d2f 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,92 +2,92 @@ None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily None - 2024-11-10 + 2024-11-11 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index d9cd1bf..0e89380 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ