A házi feladat kötelező része a tárgynak. Összesen 60 pontot lehet szerezni. Két módon lehetséges a teljesítése:
Lehetőség van egyéni feladat elkészítésére, melyben ti egy saját ötletet valósítottok meg, és azt adjátok be elkészült feladatnak. Mielőtt belekezdenétek, mindenképp egyeztessétek le (személyesen vagy Teamsen keresztül), hogy milyen alkalmazást akartok megvalósítani. Alapvetően egyéni feladatmegoldást várunk el, de van lehetőség csapatban elkészített alkalmazásra is, ameddig jól elkülöníthető, hogy ki melyik részein dolgozott az alkalamzásnak. Pár példa alkalmazásötletet találhattok az alábbi linkeken:
https://www.figma.com/file/Bs9PgPtAeRSkwCs7Q8ByrH/BME-Flutter-Kurzus
Minden feladat egyénileg kerül értékelésre. A szempontok közé tartozik a feladat komplexitása, a megvalósított alkalmazás kinézete, illetve a kód minősége. Igény esetén van lehetőség javítani is az eredményen. A leadási határidő a 14. hét vége, tehát december 6.. A Moodle rendszerén keresztül kell feltölteni az elkészült megoldást ZIP archívumban. A feltöltendő archívum nevében szerepeljen az egyeni szó is!
A másik lehetőségben egy megadott alkalmazást kell elkészítenetek. Ehhez egy kiinduló projektből kell egy, az alább megadott alkalmazást elkészítenetek. Az ellenőrzés tesztek segítségével fog megtörténni, mely része a kiinduló projektnek, így ti is nyomon tudjátok követni, hogy hány pontot kapnátok az aktuális állapotában. A kiinduló projektet innen tudjátok letölteni
A feladatban egy egyszerű, felhasználok megtekintését megvalósító alkalmazást kell elkészítenetek. Két fő oldalból fog állni: egy bejelentkező, illetve egy listázó felületből. Az alkalmazásban a dio könyvtárat kell használni a hálózati kérések indításáért, ezeket azonban egy Interceptoron alapuló megoldással még a Dart kódból lekezelésre kerülnek, tehát nincs szükség egy külső szerverre való csatlakozáshoz. A projektben az állapotkezelésnél választhattok a provider vagy bloc alapú megoldások között. Elég egy állapotkezelési megoldással elkészíteni az alkalmazást.
Mivel a kiértékelés a tesztek alapján történik, fontos, hogy azok sikeresen le tudjanak futni. Ehhez szükséges betartani pár alapvető szabályt:
- A
main.dart
fájlban találhatóDO NOT MODIFY
kommenttel ellátott függvényeket, illetve paramétereket nem szabad módosítani. AconfigureDependencies()
metódusba vehettek fel tetszőleges függőséget aget_it
könyvtárhoz, illetve a MaterialApp egyéb paramétereit módosíthatjátok. - Minden, a projektben mellékelt osztálynál TILOS az osztálynevek, megadott függvények vagy mezők módosítása.
- bloc esetében nem érdemes új állapotokat felvenni, mert azok a tesztelés során hibához vezethetnek.
- Bármelyik állapotkezelési megoldást választva az ahhoz szükséges
Provider
osztályt (ChangeNotifierProvider
vagyBlocProvider
) az adott oldal elé vegyük fel. Ezt úgy tudjuk a legkönyebben elérni, ha a MaterialApp-on belül az oldal létrehozásánál wrappeljük az oldalt a megfelelő widgettel. TILOS az adott oldalon belül létrehozni az adott állapotkezelést végző objektumot (ilyenkor az oldalnak a tesztek nem tudnák átadni a mockolt objektumokat). - Új fájlokat, osztályokat nyugodtan fel lehet venni.
- A projekt során szükséges lesz használni a
Dio
, illetve aSharedPreferences
osztályokat. Ezeket a kód tetszőleges helyéről aGetIt.I<Dio>()
ésGetIt.I<SharedPreferences>()
hívásokkal tudjuk elérni. TILOS ezeket az objektumokat magunknak létrehozni.
A Dio
objetkum úgy lett konfigurálva, hogy minden kommunikációt kiírjon a konzolba, így könnyen tudjuk ellenőrizni, hogy milyen kérések indulnak el, illetve milyen válaszokat kapunk.
Ha kiválasztottuk az állapotkezelési megoldást, amit használni szeretnénk, akkor elég csak az adott mappára (lib/ui/bloc vagy lib/ui/provider) fókuszálni.
A fő irányelv, hogy az alkalmazásnak teljesítenie kell a teszteket Értékelés fejezetben leírt módon. Ha eltérés található a szöveges specifikáció és a tesztek között, akkor ezt kérlek jelezzétek, illetve a tesztek élveznek elsőbbséget. Az alkalmazás készítése során nyugodtan lehet a teszteket is vizsgálni, de ne csak ezek alapján készüljön el az.
Az alkalmazás egy bejelentkezési felülettel kell induljon. Itt található egy email és egy jelszó beviteli mező, egy 'Jegyezz meg' checkbox, illetve egy bejelentkezési gomb. Ezek mellett az oldal tetszőlegesen egyedivé tehető. A bejelentkezés gomb megnyomására az alábbi folyamatnak kell megtörténnie:
- Ellenőrizzük, hogy érvényesek-e a beírt értékek. Az email mezőnek megfelelő formátumban kell lennie, a jelszónak pedig legalább 6 karakter hosszúnak kell lennie (ezek ellenőrzésére használható például a
validators
könyvtár). Ha valamelyik nem felel meg, akkor az adott mező alá ki kell írni egy hibaüzenetet (InputDecoration.errorText
). Amint megváltozik valamelyik mező tartalma, akkor a mezőhöz tartozó hibaüzenetet egyből törölni kell. - Ha érvényesek a mezők, el kell indítani a bejelentkezési folyamatot. provider esetében a
login()
metódust kell meghívni, illetve az ezáltal visszaadott Future eredményét megfelelően lekezelni. bloc esetében egyLoginSubmitEvent
eseményt kell elküldeni a BLoC felé. Ezeknek csak akkor legyen hatása, ha még nem lett elindítva bejelentkezési folyamat. - A bejelentkezés során töltőállapotba kell jutni. provider esetén ezt az
isLoading
változó jelzi, míg bloc-nál aLoginLoading
állapotot kell kiadni. Töltőállapotban minden bemeneti mezőt le kell tiltani (szöveges mezők esetén azenabled
változó, a checkbox és gomb esetén pedig a megfelelő callback paramétereketnull
értékre állításával). - Ha a hálózati kérés hibával zárul (pl. hibás felhasználónév/jelszót ad meg a felhasználó), akkor a hiba szövegét egy Snackbar-ban kell megjeleníteni. Ehhez provider esetén a
login()
metódusban egyLoginException
hibát dobjunk, amit a login oldalon el tudunk kapni (ekkor alogin()
által visszaadottFuture
hibával fog végződni). bloc esetében egyLoginError
állapotot küldjünk ki, amit egy BlocListener vagy BlocConsumer segítségével tudunk megfigyelni és lekezelni. - Ha sikeres a kérés, akkor kapni fogunk egy tokent, amivel tudjuk magunkat azonosítani a szerver felé. Ha a felhasználó bepipálta a 'Jegyezz meg' checkboxot, akkor ezt a tokent mentsük el tetszőleges kulccsal a
SharedPreferences
objektumunkba. Erre a tokenre a későbbiekben még szükség lesz. - Sikeres kérés esetén lépjünk át a lista oldalra. Ehhez nevesített útvonalat használjunk, a
/list
útvonalra navigáljunk át. provider esetében ezt alogin()
által visszaadott Future hiba nélküli lefutásával tudjuk detektálni, míg bloc esetén egyLoginSuccess
állapotot kell kiküldeni. - Végül ne felejtsük el a töltőállapotot megszüntetni az
isLoading
hamisra állításával, vagy aLoginForm
állapot elküldésével.
A bejelentkezéshez a Dio objektumon egy POST
kérést kell indítani a /login
útvonalra. A kérés body részében (data
változó) egy Map objektumot kell küldeni, melyben az email kulccsal a felhasználó által beírt email címet, a password kulccsal pedig a jelszót küldjük.
!!!FONTOS!!! A tesztek sikeres működése érdekében a kérésben mindenképp egy Map objektumot kell átadni, nem működik, hogy egy JSON sorosítható objektumot küldünk át. Ha külön osztályt hozunk létre a kommunikáció elősegítéséhez, akkor manuálisan hívjuk meg a toJson()
metódust az objektumon, mellyel egy Map objektumot fogunk megkapni. !!!FONTOS!!!
A beépített Interceptor csak akkor fogja elfogadni a kérést, ha az email cím [email protected], a jelszó pedig password. Ekkor egy Map objektumot kapunk vissza, amiben a token kulccsal találjuk meg a tokent. Egyéb esetben hibával végződik a kérés, a válaszban pedig egy Map-et találunk, melyben message kulccsal találjuk meg a hibaüzenetet, amit meg kell jeleníteni.
Ha a felhasználó valamikor már bejelentkezett a 'Jegyezz meg' funkcióval, akkor az alkalmazás indulásakor egyből be kell léptetni. Ehhez az oldal indulásakor (initState()
metóduson belül) provider esetében mindig hívjuk meg a tryAutoLogin()
metódust, mely igazzal tér vissza, ha sikerült a bejelentkeztetés. bloc esetében ugyanezet egy LoginAutoLoginEvent
esemény kiküldésével, illetve LoginSuccess
állapottal tudjuk elérni. Ilyenkor a SharedPreferences
objektumon ellenőrizzük, hogy van-e mentett token, és ha igen, jelentkeztessük be a felhasználót és navigáljunk át a lista oldalra.
Sikeres bejelentkezés esetén egy listázó oldalra kell elnavigálni. A navigáció során figyeljünk arra, hogy a login oldal kikerüljön a navigációs előzményekből (pushReplacementNamed()
használatával). A lista oldal tetején egy AppBar legyen, mely tartalmaz egy IconButton gombot. Erre kattintva jelentkeztessük ki a felhasználót (navigáljunk vissza a bejelentkező oldalra, illetve töröljük a tokent mindenhonnan)!
Az oldal fő részén jelenítsük meg a betöltött felhasználókat egy függőlegesen tekerhető felületen. Jelenítsük meg a listában a felhasználók profilképét, illetve nevét.
A betöltés folyamata az alábbiak alapján működjön:
- Az oldalra indulásakor egyből indítsuk el a töltést a
loadUsers()
meghívásával vagyListLoadEvent
kiküldésével. - A kérés közben a loginhoz hasonlóan töltőállapotban legyen az oldalunk (jelenítsünk meg egy
ProgressIndicator
-t az oldalon közben). - Hiba esetén a loginhoz hasonlóan egy Snackbar-ban jelenítsük meg a hiba okát.
- Sikeres válasz esetén provider esetében a
users
listában, míg bloc esetében egy ListLoaded állapotban adjuk vissza a betöltött felhasználókat.
A felhasználói adatok betöltéséhez egy GET
kérést kell a /users
útvonalra küldeni. Az Interceptor csak akkor fog válaszolni, ha egy bejelentkezett felhasználó küldi a kérést, ehhez a headerben el kell küldeni Authorization
kulccsal a Bearer <LOGIN TOKEN>
sztringet. Ennek hiányában hibával válaszol a szerver.
Sikeres kérés esetén egy felhasználói listát fogunk visszakapni, ami Map objektumokat fog tartalmazni, melyekben az avatarUrl kulccsal érjük el a felhasználó profilképének URL címét, illetve name kullcsal kapjuk a megjelenítendő felhasználó nevet.
A pontozást az automatizált tesztek fogják végezni. A test
mappán belül található az összes teszt. Minden teszt tartalmaz egy rövid leírást, illetve a leírás végén lévő [x]
jelöli, hogy hány pontot ér az adott teszt sikeres teljesítése. A tesztek a következő részekből állnak:
common
: Itt találhatóak az általános tesztek, melyeknek állapotkezelési megoldástól függetlenül teljesíteni kell. Összesen 25 pont szerezhető ezekből.bloc
/provider
: Itt találhatóak az állapotkezelési megoldásoktól függő tesztek. Ezek közül a választott állapotkezelési könyvtár tesztjeit kell csak futtatni. Ezekből a tesztekből is összesen 25 pont szerezhető.screenshots
: Egy speciális teszt, mely bejárja a teljes alkalmazást, és közben összesen 8 képernyőképet készít. A sikeres futtatás 2 pontot ér, míg minden jó képernyőkép 1 pontot, összesen 10 pontért. Ahhoz, hogy elkészüljenek a képek, a tesztelés során az--update-goldens
argumentumot meg kell adni.
A képernyőképek készítését én Windows platformon tudtam kipróbálni. Ha valakinek bármilyen problémája adódna ezzel a résszel, keress meg Teams-en.
Android Studio esetében a tesztek helyesen felkonfigurálva megtalálhatóak a konfigurációk között.
!!FIGYELEM!! A tesztek módosítása szigorúan TILOS. A kiértékelés során felül lesznek írva az eredeti tesztekkel, ezért ezen belül bármi nemű módosítás el fog veszni, és az eredeti kiértékelés szerint lesz a pontozás.
A kiértékelés automatizálásához szükséges még egy segéd fájl kitöltése, mely a projekt gyökerében megtalálható TEST_INFO.txt
fájlban kell megtenni. Ebben soronként a következő információkat kell beírni
- Neptun kód
- Választott állapotkezelési megoldás (
bloc
vagyprovider
) - Opcionális: Szöveges értékelés az automatizált házi feladatról
Az elkészült projektet becsomagolva a Moodle-ön keresztül kell beadni (elég csak a lib
mappát és pubspec.yaml
fájlt becsomagolni). A házi feladat sikeres teljesítéséhez legalább 24 pontot el kell érni. A beadási határidő a 14. hét vége, tehát december 6...
A beadás a Moodle felületén keresztül történik. Ide az elkészült projektet kell feltölteni becsomagolt verzióban (.zip kiterjesztéssel).
Figyeljetek rá, hogy az archívum ne legyen túl nagy méretű (nagyobb, mint 10 MB).
Ehhez érdemes lefuttatni a flutter clean
utasítást a projektmappán belül az archívum létrehozása előtt.
Az automatizált tesztek miatt van pár rész, amire érdemes még odafigyelni a megoldás során, illetve pár hiba, amit nem könnyű elsőre értelmezni.
type 'Null' is not a subtype of type '***'
: Ilyen hibákat tipikusan akkor dob a teszt, ha nem találja a mockolt objektum azt a függvényhívást, amit a kód hívna. Ilyenkor érdemes megnézni a tesztet, hogy pontosan milyen hívások vannak értelmezve a mockolt objektumon (ezeket awhen()
hívásokon belül láthatjátok). Tipikus problémát szokott okozni, hogy a lista lekérésénél az autentikációs token a Dioget()
hívásának az options paraméterében kerül átadásra. Ez bár működő megoldás, de minden kérésben meg kellene adni, ezért inkább egy interceptort szoktak használni. A mostani projektben arra kérnélek titeket, hogy a Dio objektum options változójában lévő headert állítsátok, az itt megadott értékek minden kéréshez hozzá fognak adódni.type 'List<dynamic>' is not a subtype of type 'List<***>'
: Ilyen hibákat akkor szokott dobni, ha a hálózati kommunikáció során olyan típust feltételeztek egy gyűjteményről, amit a Dart futás közben nem lát. Figyeljetek arra, hogy például egy.map()
hívás nem fogja mindig átkasztolni a gyűjteményt az új típusra, ilyenkor az explicitcast()
függvényhívással tudtok olyan típusú gyűjteményt készíteni, amire szükségetek van (tehát pl.result.map(...).cast<UserItem>.toList()
).- Hiányzó dependenciák: Előfordulhat, hogy bizonyos tesztek hibát dobnak arra, hogy nincsen a GetIt-be beregisztrálva objektum. Ilyenkor ellenőrizzétek, hogy minden saját osztályt a
configureCustomDependencies()
vettetek fel. Ezek mellett a*_page_*_test
teszteknél nincsen szükség a GetIt-re a sikeres lefutáshoz. Ha mégis hibát dobna, az valószínűleg azért történik, mert az oldal létrehozásakor egyből megpróbáljátok kiolvasni valamelyik függőséget. Mivel magának az oldalnak nincsen szüksége közvetlenül az adott függőségekre, érdemes azokat azokba az eseménykezelő függvényekbe mozgatni, ahol tényleg aktuálisan szükség van rá.