Működő Instagram klón létrehozása a Flutter és a Firebase használatával

Ez egy áttekintés arról, hogy a Flutter és a Firebase miként használták fotómegosztó alkalmazást.

Ó, nem, egy másik Instagram-klón?

A legtöbb klón, amivel találkoztam, vagy csupán felhasználói felület kihívásokkal bír, vagy hiányzik a szolgáltatásból. Ez a projekt azonban teljesebb Instagram-élmény a hírcsatornákkal, megjegyzésekkel, történetekkel, közvetlen üzenetküldéssel, push értesítésekkel, törlés után, felhasználói jelentésekkel, a fiók adatvédelmével és egyebekkel. Ez is letölthető iOS és Android rendszeren.

Az alkalmazást itt töltheti le.

Csak az alapvető témákra fogok összpontosítani, és olyan témákat átugorok, mint például a Firebase Auth, a Cloud Storage és a Firebase Cloud Messaging, mivel ezekről már számos cikk és oktatóanyag található.

Nem fogom megvitatni a felhasználói felület legtöbb elemét, kivéve azokat, amelyeket a legnagyobb kihívásnak tartom.

Mindegyik szakaszban megpróbálom kiemelni a fő elvihető eseményeket.

Projekt építészet

Ez a szakasz csak egy rövid, magas szintű képet ad a projektről.

A projekt architektúrája egyszerű és két fő mappából áll: ui és szolgáltatások.

A projekt felépítése

Az ui mappa három részre bontható: képernyők, kütyü és megosztott.

A képernyők olyan felső szintű kütyü, amelyek az eszköz összes képernyőjén megjelenítik az összes felhasználói felületet.

Időnként a képernyők sok kóddal rendelkező widgeteket tartalmaznak, így ezeket a widgeteket kivonják a megfelelő fájlokba a widget mappában.

Néhány kütyü több képernyőn is felhasználásra kerül, például a betöltési jelző vagy az egyéni görgetési nézet, és ezeket a kütyüket a megosztott mappába helyezik.

A szolgáltatások mappája olyan fájlokat tartalmaz, amelyek kezelik a Firebase szolgáltatásokat, például a Firestore, a Cloud Storage és a Firebase Auth.

Ezenkívül a tárházat is tartalmazza, amely egy absztrakciós réteg az alkalmazás felhasználói felületének eléréséhez. Az alkalmazás felhasználói felületén szükséges bármely szolgáltatásfájl minden funkciója számára a lerakatban van egy funkció, amely hivatkozik erre a funkcióra.

Az alkalmazás felhasználói felületéhez tartozó fájlokban (képernyők és kütyü), amelyeknek felhőszolgáltatásokat kell használniuk, csak a lerakat kerül importálásra.

A kütyü nem tud a szolgáltatások mappájában található többi fájlról.

Adatmodellezés

A modellek mappája, amely tartalmazza az adatobjektumokat: felhasználó, üzenet, megjegyzés stb.

A Firestore-ból beolvasásra kerülő adatobjektumoknak általában van helpergyártó-konstruktora, amely paraméterként egy DocumentSnapshot-t vesz fel:

gyári Post.fromDoc (DocumentSnapshot doc) {return Post (id: doc.documentID, ..., időbélyeg: doc ['időbélyeg'], metaadat: doc ['data'] ?? {}, felirat: doc ['caption '] ??',); }

Ilyen módon beolvashatjuk az objektumokat a tűzoltó dokumentumokból, például:

/// Mock függvény végleges QuerySnapshot snap = várja megosztott gyűjtést ('hozzászólások'). Get ();
végleges lista posts = snap.docs.map ((doc) => Post.from (doc)). toList ();

Adatbázis-tervezés

A projekthez használt adatbázis a Firestore. Miért? Mivel…

  1. A Firestore a valós idejű adatbázis újabb unokatestvére, jobb skálázhatósággal és adatmodellezéssel. További információ itt.
  2. A Firebase a Flutter korai támogatója, és plugin-jüket a kezdetektől elérhetővé tették a pub.dev oldalon.
  3. Olvas> Írás.

A fő megközelítés az adatbázis felépítése oly módon, hogy a szükséges adatokat könnyen be lehessen tölteni. A leküzdendő legnagyobb probléma a Firestore korlátozott lekérdezési képessége volt.

Más adatbázis-megoldásokkal ellentétben nem könnyű lekérdezni valamit, például a „hírközlésem által követett felhasználók legutóbbi hozzászólásainak beolvasása” elemet a hírcsatorna képernyőjén.

A-ha! A pillanat az volt, amikor rájöttem, hogy a képernyőnek csak az adatokat kell gyűjtenie egyetlen adat “vödörből”. Ebben az esetben a lekérdezés egyszerűen arra utal, hogy „dokumentumokat tölt be a hírcsatorna-gyűjteményből”.

Egy olyan közösségi média alkalmazásban, mint az Instagram, a következő> írja. A felhasználó több száz dokumentumot olvashat el, mielőtt egyetlen írást elvégezne (hozzászólás, megjegyzés, követés / követés).

Ezért érdemes minden kemény munkát az írási műveleteken belül elvégezni, és az olvasási műveleteket egyszerűnek tartani.

Amikor a felhasználó feltölt egy bejegyzést, a hozzászólást minden követő hírcsatornájába írja. Más szavakkal, az adatok sokszorosítva vannak.

Igen, ez azt jelenti, hogy egy millió követővel rendelkező felhasználó sokkal többet fog fizetni, mint a tipikus felhasználó. Egy millió követő hírcsatornájának írása megközelítőleg 1,80 dollárba kerül, de valószínűleg sokkal nagyobb az az érték, amelyet valaki összegyűjtött, amelyet sok követő bevisz a szociális platformra.

felhasználók / {userId} / takarmány / {postId}

Ilyen módon könnyen letöltheti a dokumentumokat egyetlen gyűjteményből a feltöltési dátum szerint megrendelve, és szükség szerint meg is ragaszthat.

Ennek az adatbázis-struktúrának az a hátránya, hogy az adatok sokszorosítása miatt további lépésekre van szükség, ha mondjuk, hogy egy felhasználó módosít egy dokumentumot.

Mi lenne, ha a felhasználó megváltoztatja egy bejegyzés feliratát, vagy ami még rosszabb, megváltoztatja profilképét vagy felhasználónevét? Hogyan tükröződnek ezek a változások a követõik hírcsatornájának korábbi bejegyzéseiben? Hogyan írsz üzenetet egy követő hírcsatornájához?

Adja meg a felhőfunkciókat.

Felhőfunkciók

A fő ötlet egy megbízható szerver használata a kód telepítéséhez és az ügyféloldali kód írásának elkerülése, amikor csak lehetséges.

Hogyan írna mindenki hírcsatornájába, ha új üzenet kerül feltöltésre?

Előfordulhat, hogy egy felhasználónak több ezer követője és több ezer hozzászólása van. Ha a felhasználó megváltoztatja profilját vagy bármely hozzászólását, akkor ezeket a változásokat minden követő hírcsatornájába is el kell terjesztenünk.

Ezt a fajta műveletet fan-out műveletnek nevezik, ahol egy dokumentumot az adatbázis több csomópontján (hivatkozáson) másolnak.

Hogyan lehet rajzolni egy új bejegyzést a követők hírcsatornájához:

  1. Szerezd meg a feltöltő követőit.
  2. Minden követő számára hozzon létre egy dokumentum hivatkozást a dokumentum azonosítójának postai azonosítójával.
  3. Írja be a postai adatokat erre a hivatkozásra
  4. Opcionális - írja be az új bejegyzést saját hírcsatornájába

Bármely rajongói műveletnél érdemes a kötegelt írásokat használni. Mindegyik tételnél azonban legfeljebb 500 művelet végezhető, így több tételre lesz szüksége egy rajongó-kimeneti művelethez, amely ennél is többet igényel.

A felhő funkció másik felhasználása az, hogy frissítse a bejegyzés hasonló számlálóját.

Egy hozzászóláshoz hasonló beszámítással való visszaélés támadhat, ha ügyféloldali kóddal irányítják. Ehelyett egy felhőfunkciót használunk, amely meghallgatja a létrehozandó vagy törölt dokumentumot a „kedveli” algyűjteményben, és ennek megfelelően megváltoztatja a bejegyzés hasonló számlálóját a FieldValue.increment használatával:

A felhő funkció kritikus jelentése push értesítések küldése a firebase felhő üzenetküldés (FCM) útján. Ezt a sendFCM függvényt meghívjuk minden releváns exportált függvényben (tetszik, kommentáljuk, követjük az eseményt, megjegyzéssel válaszolunk, közvetlen üzenet):

A háttérkép egy alkalmazás gerince. Az alkalmazás lekérdezési igényei mellett meg kell terveznie és strukturálnia kell az adatbázist is. Mielőtt szépítené alkalmazását, működőképessé tegye.

Térjünk tovább a dolgok felhasználói felületére. Az UI szakasz tartalmaz néhány adatbázis-tervezési dolgot, amelyeket korábban nem fedtem le.

Root PageView és honlap

A gyökér widget egy PageView, az első oldal a szerkesztő, a második a fő honlap az összes navigációs füllel, a harmadik pedig a közvetlen üzenetküldő képernyő.

1. szerkesztő 2. otthoni 3. közvetlen üzenetküldés

Válthat az oldalak között úgy, hogy elcsúsztatja vagy megnyomja a legtöbb navigációs gombot. Állítsa az oldalnézet kezdeti oldalát 1-re, a kezdőlapra.

Ha utánozni szeretné a Instagram iOS alkalmazásban észlelt navigációs viselkedést, akkor a CupertinoTabScaffold és a CupertinoTabView alkalmazást kell használni a kezdőlap minden lapján. Minden lap nézet a saját navigációs veremét kezeli, ami fontos, ha egyszerre több lapot szeretne böngészni.

A CupertinoTabView használatakor azonban furcsa hibával találkoztam, amikor a szerkesztő képernyőjén a szövegmezőre koncentráltam, tehát Andrea Bizzotto egyedi navigációs megoldását használtam, amely megszabadult a hibától.

A navigációs kötegnek a kezdőoldal legalacsonyabb útvonalához való eljuttatásához minden laplap nézethez létre kell hoznia egy globális navigációs kulcsot:

Térkép > _navigatorKeys = {TabItem.feed: GlobalKey (), TabItem.search: GlobalKey (), TabItem.create: GlobalKey (), TabItem.activity: GlobalKey (), TabItem.profile: GlobalKey (),};

A lapnézet hozzárendelése egy navigációs billentyűvel. Ezt akkor is meg kell tennie, ha CupertinoTabView alkalmazást használ.

/// Kezdőlap (Hírcsatorna) lap
Navigátor (kulcs: _navigatorKeys [TabItem.feed], ...)
/// Keresés fül
Navigátor (kulcs: _navigatorKeys [TabItem.search], ...)

Ezután használja a BottomNavigationBar onTap (index) visszahívását, hogy kiválassza, melyik veremet szeretné megjeleníteni:

/// Győződjön meg arról, hogy a megnyitott lap az aktuális lap, ha (tab == currentTab)
/// Előugrás az elsőig _navigatorKeys [tab] .currentState .popUntil ((route) => route.isFirst);

Szeretne egy képernyőt gördíteni a tetejére? Minden laphoz létre kell hoznia egy görgetővezérlőt:

final feedScrollController = ScrollController (); .... végleges profilScrollController = ScrollController ();

Rendeljen hozzá egy fő görgetési nézet widget a görgetővezérlőhöz (ne hagyja figyelmen kívül az InitRoute és az onGenerateRoute programot, ha CupertinoTabView alkalmazást használ):

/// Saját profil képernyőnavigátor (kulcs: _navigatorKeys [TabItem.profile], InitRoute: '/', onGenerateRoute: (routeSettings) {return MaterialPageRoute (builder: (context) => MyProfileScreen (scrollController: profileScrollController));); ,),

A BottomNavigationBar onTap (index) visszahívásában kiválaszthatja, hogy melyik vezérlőt kívánja görgetni a tetejére:

if (tab == currentTab) {
kapcsoló (fül) {eset TabItem.home: controller = feedScrollController; szünet; ... eset TabItem.profile: controller = profileScrollController; szünet; } /// Görgessen a tetejére, ha (controller.hasClients) controller.animateTo (0, időtartam: scrollDuration, görbe: scrollCurve); }

takarmány

A fő widget a post list list widget:

  1. Fejléc
  2. Photo PageView
  3. Eljegyzési sáv (Tetszik, Megjegyzés és Megosztás gombok)
  4. Felirat (nem látható)
  5. Mint a gróf bár
  6. Megjegyzés számláló sáv
  7. Legfontosabb megjegyzések
  8. Időbélyeg

Ha kíváncsi: A pezsgőfürdőnek tűnő gomb lehetővé teszi, hogy doodle-ra tegyen egy üzenetet. Láthatja, amit mások rajzoltak.

A fejléc, a fotóoldal nézetének, a feliratnak és az időbélyegnek az adatai közvetlenül a hírcsatorna-gyűjteményben található postai dokumentumból beolvashatók: felhasználók / {userId} / feed / {postId}.

Az eljegyzési sáv trükkös a hasonló gomb miatt. A Like gomb színe megváltozik attól függően, hogy tetszett-e a hozzászólás, vagy sem.

Először hozzon létre egy olyan funkciót, amely visszaadja a DocumentSnapshot adatfolyamát a firestore pillanatképek () tulajdonság használatával:

/// Ellenőrizze, hogy az aktuális felhasználó tetszett-e egy hozzászólást /// Olyan adatfolyamot ad vissza, hogy didLike = snapshot.data.exists ///auth.uid a bejelentkezett felhasználó felhasználói azonosítójára utal
Folyam myPostLikeStream (Hozzászólás) {végleges ref = postRef (post.id) .collection ('kedveli'). dokumentum (auth.uid); visszatér ref.snapshots (); }

Használja ezt a patakot a StreamBuilder alkalmazáson belül egy reaktív felhasználói felület megjelenítéséhez, amely megfelelően tükrözi, hogy a hasonló gombot megnyomták-e vagy sem:

StreamBuilder (stream: Repo.myPostLikeStream (post), készítő: (kontextus, pillanatkép) {if (! snapshot.hasData) return SizedBox ();
      /// Ha a dokumentum létezik, akkor az aktuális /// bejelentkezett felhasználó tetszett neki a hozzászólást
final didLike = snapshot.data.exists; vissza LikeButton (onTap: () {return didLike? Repo.unlikePost (post): Repo.likePost (post);},
        /// Gomb megjelenése
ikon: nem tetszett? FontAwesome.heart: FontAwesome.heart_o, szín: didLike? Colors.red: Colors.black,); }),

Ennek szépsége az, hogy még ha ugyanazt a hozzászólást több képernyőn vagy eszközön nézi, a több képernyőn lévő hasonló gombok állapota helyesen tükröződik.

Ezenkívül hozzáadott bónuszként a tűzoltó offline lehetőségei miatt a Like gomb továbbra is reagál a felhasználói megnyomásokra, még akkor is, ha offline állapotban van!

Ugyanezt az elvet alkalmazhatja a hasonló kütyüre, mint például a profiloldalon található Követés / Követés gombra.

Előfordulhat, hogy a felhasználói felület egyes részei eltérőnek tűnnek, attól függően, hogy a felhasználót megtekintik. Ezekben az esetekben próbáljon meg egy StreamBuilder alkalmazást használni a reaktív élmény érdekében.

Ha a felhasználói felület elsősorban statikus, és ugyanúgy néz ki, mint bárki (például képaláírás vagy fénykép), akkor az adatokat a szokásos módon lehet letölteni.

A hozzászólások statisztikáit (például a számot, a hozzászólások számát) máshol tárolják, és külön kell letölteni. Ez a bejegyzésgyűjtemény külön gyökér szintű gyűjteményként létezik, és nem a felhasználói gyűjtemény algyűjteménye.

Jövő getPostStats (karakterlánc postId) async {végleges ref = shared.collection ('hozzászólások'). dokumentum (postId); végleges doc = várjon ref.get (); visszatérés! doc.exists? PostStats.empty (postId): PostStats.fromDoc (doc); }

Miért nem tárolja a statisztikákat abban a helyen, ahol a postai dokumentum található?

Mivel a statisztikák nagyon hajlamosak a változásokra, ezért nem szabad megismételni őket.

El tudnád képzelni, hogy minden követője hírcsatornáját frissíti minden alkalommal, amikor egy üzenet kedvel, vagy valaki megjegyzést fűz hozzá?

A legfontosabb megjegyzésekhez a hozzászólások megjegyzésének algyűjteményében kell megkeresnie a hozzászólásokat:

Jövő > getPostTopComments (karakterlánc postId, {int limit}) async {végleges ref = megosztott .collection ('hozzászólások') .document (postId) .collection ('kommentárok') .orderBy ('like_count', csökkenő: true) .limit ( határérték 2); végleges pillanat = várjon ref.getDocuments (); return snap.documents.map ((doc) => Comment.fromDoc (doc)). toList (); }

Igen, megpróbálhatja a felső megjegyzéseket tömbként tárolni ugyanarra a postai dokumentumra, de ehhez összetett felhőfunkciót kell írni, amely meghallgatja a létrehozott vagy törölt megjegyzéseket, valamint meghallgatja a hasonló számukban bekövetkező változásokat, és végül szükség szerint frissíti / rendezi a bejegyzés legfelső megjegyzését. Ráadásul frissítenie kell az összes követő hírcsatornáját is.

Ez azt jelenti, hogy több üzenet olvasására van szükség az egyetlen üzenet letöltéséhez. Ez teljesen rendben van, figyelembe véve az alternatívát, ha az összes adatot egy dokumentumba helyezzük, és potenciálisan több ezer dokumentumot kell frissíteni minden egyes elemhez, megjegyzéshez vagy változtatáshoz. Ne felejtse el, hogy nem minden követője fogja olvasni / letölteni az új üzenetét, de minden rajongói műveletet minden követőjének el kell terjesztenie.

Az olvasási műveletek optimalizálása érdekében előfordulhat, hogy sokkal nehezebbé és költségesebbé teszi a dolgokat, mint amilyennek lennie kellene.

A post-widgetre úgy kell gondolnia, hogy nem egyetlen widgetre, hanem több widgetre kombinálva, mindegyik különféle forrásokból származó adatokból vagy „vödrökből” származik. Ez az adatmodellezésben is segít, mivel egyetlen túlzottan felfúvódott üzenetobjektum helyett több adatobjektumot használ.

Állami menedzsment

Megpróbáltam BloC és Szolgáltató csomagokat használni az alkalmazás állapotának fenntartására. Megállapítottam azonban, hogy a StreamBuilder használata a projektnél egyszerűbb, különös tekintettel arra, hogy a firestore már rendelkezik a DocumentSnapshot (egyetlen dokumentum) és a QuerySnapshot (több dokumentum) adatfolyamokkal.

Egyes esetekben hasznosnak találtam az EventBus használatát, különösen akkor, ha pirítós üzeneteket kell mutatni vagy frissíteni kell az UI-t sikeres utáni feltöltés vagy törlés után.

A legtöbb kütyüben, ahol csak egyszer kell adatokat töltenie, és nem kell meghallgatnia a dokumentum változásait, egyszerűen használhatja a setState () fájlt.

Vegyük például azt a modult, amely egy másik felhasználó hozzászólásait jeleníti meg a profiloldalukon. Az initState () -ben hívjon meg egy függvényt, amely beolvassa a hozzászólásokat:

@ override initState () {_getPosts ();
super.initState (); }

A felhasználói felület automatikusan frissíti és megjeleníti a hozzászólásokat, amikor a setState () hívja:

_getPosts () async {setState (() {isLoadingPosts = true;});
  végleges PostCursor eredmény = várja meg a Repo.getPostsForUser felhasználót (uid: uid, limit: 8,);
if (szerelt) setState (() {isLoadingPosts = false; hozzászólások = eredmény.posts; startAfter = result.startAfter;}); }

A PostCursor objektum egy segítő osztály, amelyet a pagináláshoz használnak, és később vissza fogok térni. Egyszerűen tartalmazza a hozzászólások listáját és az utolsó beolvasott dokumentum DocumentSnapshot-ja.

Az isLoadingPosts változó csak egy zászló, amely jelzi a felhasználói felületnek, hogy mikor kell megjeleníteni a betöltési indikátort.

Az adatoknak az initState () -ben való letöltésének és a felhasználói felület frissített adatainak frissítésének ez a mintája számos más képernyőn megtalálható.

Mindig jó gyakorlat, hogy a setState () meghívása előtt ellenőrizzük, hogy a (szerelt) tulajdonság meg van-e hívva, és ha nem akarjuk, hogy többször is felhívjuk, ha (szerelt), egyszerűen írjuk felül a StatefulWidget setState () -ját:

@ override void setState (fn) {if (szerelt) super.setState (fn); }

Időnként a setState () hívása nem elegendő. Egy kapcsolódó példa a jelenlegi bejelentkezett felhasználó profilképernyőjén lehet, ahol nemcsak üzeneteket kell letöltenie, hanem frissítenünk kell az UI-t, amikor egy üzenet feltöltésre kerül.

Ehhez használhatunk egy aStreamBuilder alkalmazást, azonban nehéz megbirkózni a StreamBuilderrel. Nem akarja megbénítani adatait? Mi történik, ha a jelenlegi bejelentkezett felhasználónak több ezer üzenet van? A profil képernyő minden egyes betöltésekor az összes üzenet egyszerre betöltődik a patakon keresztül. Ez számlázási és sávszélesség szempontjából egyaránt költséges.

A megoldás? Használjon egy patak és a setState () kombinációját;

Az előző példához hasonlóan, először beküldünk néhány hozzászólást betöltésekor. Ezenkívül egy adatfolyam segítségével hallgassa meg az adatbázis összes új üzenetét, és vegye fel a hozzászólást az felhasználói felületbe.

Hozza létre az adatfolyamot, és csatolja hozzá a hallgatót az initState () -ben; új üzenet feltöltésekor frissítse az felhasználói felületet:

final postStream = Repo.myPostStream ();
Lista hozzászólások = [];
@ override void initState () {_getPosts (); postStream.listen ((adatok) {data.documents.forEach ((doc) {if (InitPostsLoaded)) {végleges post = Post.fromDoc (doc); if (post == null) visszatérés; setState (() {posts = [ post] + hozzászólások;});}});}); eventBus.on () .listen ((esemény) {setState (()) {hozzászólások = Lista .fromból (hozzászólások) ..removeWhere ((p) => p.id == event.postId); }); }); super.initState (); }

A patak csak a bejegyzésgyűjtemény legújabb dokumentumát hallgatja meg:

Folyam myPostStream () {végleges ref = userRef (auth.uid) .collection ('hozzászólások') .orderBy ('időbélyeg', csökkenő: true) .limit (1); visszatér ref.snapshots (); }

A hozzászólások törlésekor az Event Bus figyelőt választom. Amikor a bejegyzés törlése befejeződött, a felhasználói felület keres egy azonosítóval rendelkező bejegyzést, és eltávolítja azt a nézetből.

Az állam kezelésének számos módja van. Vannak olyan népszerű államigazgatási megoldások, amelyeket még nem próbáltam, mint például az RxDart. Válasszon egyet, amely elvégzi azt, amit akar, anélkül, hogy túl bonyolult lenne. Ne használja a BloC-ot egy egyszerű számláló alkalmazáshoz - legtöbbször a setState () fogja csinálni a trükköt. De gondoljon arra, hogy a választott megoldás továbbra is kezelhető lesz-e a jövőben.

Lapszámozás

Létrehoztam egy egyszerű segítő osztályt, hogy segítsen a hozzászólásokkal kapcsolatos lapozásban.

osztály PostCursor {végleges lista hozzászólások; végleges DocumentSnapshot startAfter; végleges DocumentSnapshot endAt; PostCursor (this.posts, this.startAfter, this.endAt); }

Ezt az osztályt a következő szolgáltatások szolgáltatásfájljaiban használhatjuk:

Jövő getFeed ({DocumentSnapshot startAfter}) async {final uid = Auth.prof.uid; végleges lekérdezés = startAfter == null? userRef (uid) .collection ('feed') .orderBy ('létrehozott_at', csökkenő: true) .limit (8): userRef (uid) .collection ('feed') .orderBy ('létrehozott_at', csökkenő: true) .limit (14) .startAfterDocument (startAfter); végleges dokumentumok = várjunk query.getDocuments () -ot; végleges hozzászólások = docs.documents.map ((doc) => Post.fromDoc (doc)). toList (); visszatérés docs.documents.isNotEmpty? PostCursor (hozzászólások, docs.documents.last, docs.documents.first): PostCursor (hozzászólások, startAfter, null); }

Ilyen módon a képernyőknek csak egyetlen adatobjektummal kell foglalkozniuk, amely tartalmazza a kütyü megjelenítéséhez és a pagináláshoz szükséges összes adatot. Minimalizálja az üzleti logikát a kütyüben.

Azoknak a képernyőknek a számára, amelyeknek frissítési vonze van, és további funkciók is betöltődnek, létrehoztam egy egyedi görgetési nézetet, amely reagál a túlcsúszásra. Más könyvtárakat is felhasználhat ugyanazon eredmény elérésére.

A lényeg az, hogy egy CustomListView alkalmazást használnak, és deklarálják annak réseit.

Egy CupertinoSliverRefreshControl eszközt használtam egy iOS megjelenés és érzés érdekében, hogy frissítsem a funkciót.

Helyezze a ListView-t vagy bármilyen modult, amely kiterjeszti a ScrollView-t a SliverToBoxAdapterbe.

Végül helyezze be a betöltési jelzőt egy másik SliverToBoxAdapterbe, és csak akkor jelenítse meg, ha a képernyő több adatot tölt be.

Mivel az Android alapértelmezett ClampingScrollPhysics () viselkedése nem a kívánt, akkor meg kell adnunk a BouncingScrollPhysics () beállítást, hogy az onRefresh és onLoadMore visszahívások meghívhatók legyenek a túlcsúszásból.

Ennek negatív hátránya, hogy minden alkalommal, amikor a CustomScrollView alkalmazásba helyez egy Lista-nézetet, be kell állítania a shrinkWrap: true beállítást, amely csökkentette a teljesítményt. Ezenkívül állítsa a ListView fizikáját NeverScrollablePhysics () -re, ha nem akarja, hogy a szülőtől függetlenül görgessen.

Közvetlen üzenetküldő képernyő

A csevegési képernyő önmagában is elegendő lenne, ha a hozzáféréshez csak a felhasználói profil üzenet gombjának megnyomásával lehet hozzáférni. Ugyanakkor, akárcsak az Instagram-ban, közvetlen üzenetküldő (DM) képernyőt akarunk, amely minden aktív csevegést megjelenít.

A DM adatvödör nem tartalmazza a tényleges üzeneteket, hanem olyan dokumentumokat tart fenn, amelyek olyan felhasználókat tartalmaznak, akikkel a múltban csevegtek.

Ezenkívül minden dokumentum számos olyan mezőt fenntart, amelyek fontos információkat tartalmaznak.

Az utoljára ellenőrzött mező tartalmazza a beszélgetésben elküldött utolsó üzenetet.

A last_seen_timestamp arra utal, amikor a felhasználó utoljára megnyitotta a beszélgetést.

Mivel a DM-képernyőnek reagálnia kell az új üzenetekre, egy adatfolyamot és egy StreamBuilder-et használunk az adatok adatainak listázáshoz. Az adatokat a last_checked_timestamp újratelepítésével is rendezzük, hogy a legújabb beszélgetések tetején jelenjenek meg.

StreamBuilder (adatfolyam: Repo.DMStream (), készítő: (kontextus, pillanatkép) {if (! snapshot.hasData) {return LoadingIndicator ();} egyéb {final docs = snapshot.data.documents ..sort ((a, b) {final Timestamp aTime = a.data ['last_checked_timestamp']; final Timestamp bTime = b.data ['last_checked_timestamp']; return bTime.millisecondsSinceEpoch .compareTo (aTime.millisecondsSinceEpoch);});
visszatérés docs.isEmpty? EmptyIndicator ('Nincs megjeleníthető beszélgetés'): ListView.builder (shrinkWrap: igaz, fizika: NeverScrollableScrollPhysics (), itemBuilder: (összefüggés, index) {végleges doc = docs [index]; ...

Ha meg szeretnénk jelölni egy beszélgetést olvasatlan üzenetekkel a ListView-ban, ellenőriznénk, hogy az last_checked_timestamp nagyobb-e, mint a last_seen_timestamp.

final Timestamp lastCheckedTimestamp = doc ['last_checked_timestamp'];
final Timestamp lastSeenTimestamp = doc ['last_seen_timestamp'];
///auth.uid a bejelentkezett felhasználó azonosítójára utal
final hasUnread = (lastSeenTimestamp == null) // Ha nem létezik last_seen_timestamp, akkor új beszélgetésnek kell lennie? lastCheckedSenderId! = auth.uid // ha nem küldtem el az üzenetet, ellenőrizze, láttam-e a legfrissebb // ellenőrzött üzenetet: (lastSeenTimestamp.seconds 

A last_seen_timestamp minden alkalommal frissül, amikor a csevegőképernyőt megnyitják, és új üzenet érkezik a másik felhasználótól.

Most már van minden szükséges információnk a beszélgetések megjelenítéséhez, késlekedés és rendezetlen üzenetek szerint rendezve:

Az olvasatlan beszélgetéseket kék kijelző jelöli, vastag betűvel

Végül a flutter_slidable csomagot használtam annak lehetővé tételére, hogy a felhasználók a lista nézet elem csúsztatásával töröljék a beszélgetéseket.

Az is_persisted mező jelzi, hogy a beszélgetést törölték-e a felhasználó DM képernyőjén. A DM-képernyőt meghajtó adatfolyam lehívja azokat a dokumentumokat, amelyeknek a mezője igazra van állítva.

Folyam DMStream () {return userRef (auth.uid) .collection ('beszélgetések') .hová ('is_persisted', isEqualTo: true) .snapshots (); }

Amikor a felhasználó törli a beszélgetést, az is_persisted mezőt hamis értékre állítja, és új end_at mezőt ad hozzá, amelynek értéke Timestamp.now ().

Future deleteChatWithUser (karakterlánc userId) async {final selfId = auth.uid;
végleges selfRef = userRef (selfId) .collection ('beszélgetések'). dokumentum (userId);
végső hasznos teher = {'is_persisted': false, 'end_at': Timestamp.now (),};
return selfRef.setData (hasznos teher, egyesítés: igaz); }

Ez az új mező lehetővé teszi, hogy csak bizonyos dátumig töltsünk be üzeneteket, amikor a felhasználó úgy dönt, hogy újból megnyitja a beszélgetést, miután a végét törölték.

Vegye figyelembe, hogy a tényleges üzenetek nem törlődnek, ehelyett csak a csevegési munkamenetet távolítják el a felhasználó DM képernyőjén, és a felhasználó soha nem fog látni üzeneteket, mint a törlés dátuma. Ez utánozza az Instagramban észlelt viselkedést.

Csevegő képernyő

Amikor a felhasználó megérint egy elemet a DM képernyőn, akkor a csevegő képernyőre irányítja, ahol az aktuális üzenetek jelennek meg.

Az üzenetek betöltése előtt az első lépés a chat azonosító beolvasása. A chat azonosító két felhasználói azonosító kombinációja. Ez azért van, hogy könnyen utalhassunk egy beszélgetésre anélkül, hogy további lekérdezést kellene készíteniük. Az initState () -ben mindkét felhasználói azonosítót elválasztjuk, sorba rendezzük és egy hypen használatával csatlakozunk:

chatId = (uid.hashCode <= peerId.hashCode)? '$ uid- $ peerId': '$ peerId- $ uid';

Ezután megvizsgáljuk, hogy a beszélgetést korábban töröltük-e, és ellenőrizzük, van-e end_at érték. Amint megkapjuk a chatId és az end_at fájlt, végre be tudjuk tölteni az üzeneteket:

Jövő getInitialMessages () async {végpont = várjon a Repo.chatEndAtForUser (peer.uid);
final InitMessages = várja meg a Repo.getMessages (chatId: chatId, endAt: end); endAt = end; if (InitMessages.isNotEmpty) {startAt = InitMessages.last.timestamp; } setState (() {messages.addAll (InitMessages); InitMessagedFinishedLoading = true;}); Visszatérés; }

Ez a getMessages funkció - a hozzászólásokkal ellentétben a Firestore lekérdezést időbélyegzővel, a DocumentSnapshot helyett pagináljuk:

Jövő > getMessages (karakterlánc chatId, Timestamp endAt, {Timestamp startAt}) async {final ref = shared.collection ('chat'). document (chatId); végső határ = 20; Lekérdezés; query = endAt == null? lekérdezés = ref .gyűjtés ('üzenetek') .orderBy ('időbélyeg', csökkenő: igaz) .limit (limit): startAt == null? ref .gyűjtés ('üzenetek') .orderBy ('időbélyeg', csökkenő: igaz) .endAt ([endAt]). limit (limit): ref. gyűjtemény ('üzenetek') .orderBy ('időbélyeg', csökkenő: igaz ) .startAfter ([startAt]). endAt ([endAt]). limit (limit); final snap = várjon query.getDocuments (); return snap.documents.map ((doc) => ChatItem.fromDoc (doc)). toList (); }

A csevegőgyűjtemény gyökér szintjén található, és az üzenetek algyűjteményét tartalmazza, amely tartalmazza az összes üzenetet és azok tartalmát:

Csevegő képernyő adatvödör

Miután megkaptuk az üzeneteket, végre megjeleníthetjük azokat a csevegő képernyőn:

A csevegéslistaView minden elemét a Firestore-ből beolvasott ChatItem-adatelem táplálja. A kapcsoló utasítás használatával konvertáljuk a ChatItem-t a megfelelő felhasználói felület elemre annak típusa alapján, amelyet aListView kitöltésére használunk. Ez magában foglalja a fehér peer szövegbuborékot, a kék felhasználói szövegbuborékot, az időbélyegzőt és a gépelés jelzőjét.

A chatListView egyedülálló, mivel az üzeneteket alulról tölti be, és további üzeneteket paginál / tölt be, amikor a felhasználó felfelé görget. Lényegében a ListView meg van fordítva. Szerencsére a ListView rendelkezik egy praktikus tulajdonsággal, amelyet igazra állítottunk ennek a viselkedésnek a figyelembevétele érdekében.

Meg kell találnunk a megfelelő helyet a társavatár és az időbélyeg hozzáadásához. A ListView itemBuilder elemében:

itemBuilder: (kontextus, index) {végső üzenet = üzenetek [index]; végső isPeer = message.senderId! = auth.uid; /// Ne feledje: a lista nézete megfordul, végleges isLast = index <1;
final lastMessageIsMine = isLast &&! isPeer; final nextBubbleIsMine = (!! isLast && üzenetek [index - 1] .senderId == auth.uid); végső showPeerAvatar = (isLast && message.senderId == peer.uid) || nextBubbleIsMine; /// Az üzenet dátumának megjelenítése, ha az előző üzenet /// több mint egy órával ezelőtt küldött végső isFirst = index == üzenetek.hossz - 1; final currentMessage = üzenetek [index]; final previousMessage = az első? null: üzenetek [index + 1];
/// Időbélyegző megjelenítése bool showDate; if (previousMessage == null) {showDate = true; } else if (currentMessage .timestamp == null || previousMessage.timestamp == null) {showDate = true; } else {showDate = previousMessage .timestamp.seconds 

Miután meghatározta a showPeerAvatar és a showDate logikai tulajdonságokat, befecskendezheti azokat a widgetbe, amelyet a ListView feltöltéséhez használ.

Szeretné megmutatni, hogy a felhasználó gépel-e? Először egy TextEditingControllert hallgatunk, amelyet a TextField widgethez csatolunk. Hívja ezt a funkciót az initState-ben:

/// A jelenlegi bejelentkezett felhasználó uid karakterlánca uid = FirestoreService.ath.uid;
/// Hallgassa meg, hogy társa gépelte-e a listenToTypingEvent () {textController.addListener (() {if (textController.text.isNotEmpty) {/// Jelzőt, hogy biztosan ne hívja ezt többször.
if (! selfIsTyping) {print ('gépel'); Repo.isTyping (chatId, uid, true); } selfIsTyping = true; } else {selfIsTyping = false; print ('nem gépelés!'); Repo.isTyping (chatId, uid, false); }}); }

Ne felejtsd el abbahagyni a gépelési tevékenység megjelenítését, amikor a csevegő képernyő bezárult. A dispose () módszert felülbíráljuk:

@ override void dispose () {print ('dispose chat screen'); Repo.isTyping (chatId, uid, false); super.dispose (); }

Az isTyping függvény a csevegés is_typing algyűjteményébe ír - nem kell semmilyen adatot megadnunk, csak ellenőriznünk kell, hogy a dokumentum létrehozása vagy törlése a felhasználói azonosítóval történik:

isTyping (String chatId, String uid, bool isTyping) {final ref = shared.collection ('chat'). document (chatId) .collection ('is_typing'); visszatérés gépelés? ref.document (uid) .setData ({}): ref.document (uid) .delete (); }

Ez lehetővé teszi egy olyan adatfolyam létrehozását, amely meghallgatja, hogy melyik csevegő résztvevője írja be:

Folyam isTypingStream (karakterlánc chatId) {vissza megosztott .collection ('csevegés') .document (chatId) .collection ('is_typing') .snapshots (); }

Csevegési képernyőn hallgatjuk ezt a patakot, amely beilleszt vagy eltávolít egy gépelési mutatót:

_isTypingStream = Repo.isTypingStream (chatId); _isTypingStream.listen ((adatok) {if (! szerelt) visszatérés; végleges uids = data.documents.map ((doc) => doc.documentID) .toList (); print (uids); if (uids.contains (peer) .uid)) {/// Győződjön meg arról, hogy csak egy gépelési mutató látható, ha (! peerIsTyping) {peerIsTyping = true; setState (() {messages.insert (0, ChatItem (típus: Bubbles.isTyping,),);}) );}} else {print ('el kell távolítani a társakat gépelésből'); peerIsTyping = false; _removeMessageOfType (Bubbles.isTyping);}});

Amikor a felhasználó üzenetet tölt fel, három írás történik - a csevegési adat-vödörbe, a felhasználó DM-vödörbe és a társak DM-vödörébe:

Jövő uploadMessage ({String content, User peer,}) async {final timestamp = Timestamp.now ();
  végső messageRef = megosztott.gyűjtés ('csevegés'). dokumentum (chatId) .collection ('üzenetek') .document ();
végleges selfRef = userRef (auth.uid) .collection ('beszélgetések'). dokumentum (peer.uid); final peerRef = userRef (peer.uid) .collection ('chat'). dokumentum (auth.uid); végleges selfMap = auth.user.toMap (); végleges peerMap = peer.toMap (); végső hasznos teher = {'sender_id': auth.uid, 'timestamp': időbélyeg, 'content': content, 'type': 'text',}; végső tétel = shared.batch (); /// Csevegőüzenet ref batch.setData (messageRef, payload); /// Saját DM chat ref batch.setData (selfRef, {'is_persisted': true, 'last_checked': payload, 'last_checked_timestamp': timestamp, 'user': peerMap,}, merge: true); /// Peer DM chat ref batch.setData (peerRef, {'is_persisted': true, 'last_checked': payload, 'last_checked_timestamp': timestamp, 'user': selfMap,}, merge: true); return batch.commit (); }

A biztonsági szabályok beállításának módjától függően előfordulhat, hogy a harmadik írást felhőfunkcióval kell elvégeznie. Ezeknek az írásoknak valójában további előnye van - a DM adatcsomag felhasználói adatait minden üzenet küldésekor automatikusan frissítjük. Ezért valószínűleg nem kell írnunk egy rajongói felhőműveletet, amely frissíti a felhasználói adatokat minden alkalommal, amikor valaki profilja megváltozik.

történetek

A történetek valószínűleg a legnagyobb kihívást jelentő tulajdonság a megvalósításhoz. Az igazán összetett felhasználói felületen kívül szükségünk van arra is, hogy nyomon követhessük a felhasználó által látott történeteket. Ezenkívül számos alapvető különbség van a hozzászólások és a pillanatok között, amelyeket később tárgyalok.

3 fő modul működik:

Beépített történetek - ez a widget, amelyet a hírcsatorna képernyőjén látsz. Fő célja annak megmutatása, hogy a követett felhasználók feltöltöttek-e legalább egy történetet az elmúlt 24 órában.

A történeteket hasonló módon írják a felhasználó hírcsatornájába, egy fan-out megközelítést alkalmazva. Amikor egy felhasználó feltölt egy történetet, felhőfunkció aktiválódik, és minden követője hírcsatornájába írja a legfrissebb történetadatokat:

/// Ez elindítja a kiugró felhőfunkciót Future uploadStory ({@required DocumentReference storyRef, @required String url}) async {return waitit storyRef.setData ({'timestamp': Timestamp.now (), 'url: url , 'feltöltő': auth.user.toMap (),}); }

A feltöltő felhasználói azonosítója lesz a dokumentum azonosítója. Ez azt jelenti, hogy a gyűjtemény csak egy feltöltővel kapcsolatos dokumentumot tartalmazhat.

A történetek adattáblázatot adnak

A dokumentum tartalmazza a feltöltő legfrissebb történetének időbélyegét, amelyet a widget adatainak lekérdezéséhez használunk.

Jövő > getStoriesOfFollowings () async {végleges most = Timestamp.now (); final todayInSeconds = now.seconds; final todayInNanoSeconds = most.nanoseconds; /// 24 órája azóta a végső cutOff = Időbélyegző (todayInSeconds - 86400, todayInNanoSeconds); végleges lekérdezés = myProfileRef .collection ('story_feed') .hová ('timestamp', isGreaterThanOrEqualTo: cutOff); final snap = várjon query.getDocuments (); return snap.documents.map ((doc) => UserStory.fromDoc (doc)). toList (); }

A getDocuments () felhasználásával anélkül, hogy korlátozást szabnánk az összes felhasználó letöltésére, akik a közelmúltban közzétettek egy sztorit. Ez az első nagy különbség - ellentétben a hozzászólásokkal, nem hagyjuk el a történetek lekérdezését.

A visszaélések elkerülése érdekében ajánlott, hogy szigorúan korlátozzuk a követhető felhasználók számát, például az Instagram-ban.

Most, hogy megvannak a legutóbbi felhasználói történetek, meg kell jelölnünk, melyik tartalmaz olyan történeteket, amelyeket a felhasználó még nem látott, egy gyűrű segítségével a feltöltő avatárja körül. Ha nem akarja használni a gradienst ehhez az indikátorgyűrűhöz, akkor egyszerűen használhat egy Stack-ot egy CircularProgressIndicator-nal, kissé nagyobb sugárral, mint az avatár:

CircularProgressIndicator (valueColor: AlwaysStoppedAnimation (az Ön színe),),

Folytatnia kell az elmúlt 24 órában megtekintett felhasználói történetek streamét:

Folyam seenStoriesStream () => myProfileRef .collection ('seen_stories') .document ('list') .snapshots ();

Ezt az adatfolyamot egyetlen dokumentumként tartják fenn, ahol minden mező egy feltöltőnek felel meg, és az érték a feltöltő legfrissebb történetének időbélyegzője, amelyet a felhasználó megtekintett:

Vegye figyelembe, hogy minden tűzoltó dokumentumnak 1 MB-os méretkorlátja van. Annak érdekében azonban, hogy elérje ezt a határértéket, a felhasználónak egy nap alatt több tízezer különböző felhasználó történeteit kellett látnia, ami rendkívül valószínűtlen. Ennek oka az, hogy minden alkalommal, amikor a dokumentum frissítésre kerül, egy kis javítást végezünk tavasszal az egy naposnál régebbi adatok eltávolítása érdekében.

Jövőbeli frissítésSeenStories (Térkép adatok) async {végleges most = Timestamp.now (); final todayInSeconds = now.seconds; final todayInNanoSeconds = most.nanoseconds; /// 24 órája azóta a végső cutOff = Időbélyegző (todayInSeconds - 86400, todayInNanoSeconds); végleges ref = myProfileRef.collection ('seen_stories'). dokumentum ('lista'); végleges doc = várjon ref.get (); záró történetek = doc.data ?? {}; /// Távolítsa el a régi adatsorokat.removeWhere ((k, v) => v.seconds 

A patak segítségével egy adott UserStory StoryState-jét kapjuk, ahol az állapot sem lehet, sem látható, sem láthatatlan. Injektálja az állapotot egy StoryAvatar widgetbe, hogy láthatóvá váljon egy színes / gradiens gyűrű, vagy ha látott, egy vékony szürke kört jelenítsen meg.

/// Az Inline Stories widgetben található ListView vízszintes görgetési irányban van
vissza a StreamBuilder (adatfolyam: Repo.seenStoriesStream (), készítő: (kontextus, pillanatkép) {if (! snapshot.hasData) return LoadingIndicator (); final seenStories = snapshot.data.data ?? {}; return ListView.builder (...
scrollDirection: Axis.horizontal, itemCount: widget.userStories? .length ?? 0, itemBuilder: (kontextus, index) {final userStory = widget.userStories [index]; final Timestamp seenStoryTimestamp = seenStories [userStory.uploader.uid]; final storyState = userStory.lastTimestamp == null? StoryState.none: seenStoryTimestamp == null? StoryState.unseen: seenStoryTimestamp.seconds 

Ezt az updateSeenStories függvényt a következő widgetben hívják meg, amelyre tovább lépünk.

Story PageView - Ez a widget nyílik meg, amikor a felhasználó megérinti a felhasználót az Inline Stories widgetben. A Stories élményének nagy része ebben a widgetben található.

Egy másik nagy különbség az, hogy a történeteket csak akkor töltik be, amikor szükség van rájuk. A mindig látható üzenetekkel ellentétben a történetek csak akkor jelennek meg, amikor a felhasználó megnyitja a felhasználó történetét, vagy elcsúszik egy másik felhasználó történetére. Ez a viselkedés megfigyelhető az Instagram-on, ahol a történetek betöltése előtt megjelenik az előrehaladás mutatója.

A történet oldalnézete modálisan jelenik meg, mint az Instagramban. A showModalBottomSheet módszert hívjuk, és a következő mezőket állítjuk be a teljes képernyő kitöltéséhez:

isScrollControlled: true, useRootNavigator: true,

Ily módon el lehet utasítani a StoryPageView-t úgy, hogy elcsúsztatjuk a BottomSheetben található beépített viselkedésnek köszönhetően.

Úgy találtam azonban, hogy bármit is csinálok, az nem tartja tiszteletben a SafeArea-t. Ezért manuálisan kellett megadnom a felső párnázatot, hogy a történet fejléce ne kerüljön átfedésbe a rendszer felhasználói felületének átfedéseivel.

A fájl a következő módszereket tartalmazza a PageView böngészéshez:

Az previousPage () és a nextPage () lehetővé teszi a PageView'sPageController számára, hogy animációkkal váltson az oldalak között. A vezérlők kezdeti oldalát az initState () -ben állítottuk be, amely alapján a felhasználói avatárt megnyomták az InlineStories widgetben.

Az _pop () hívásra kerül, amikor a felhasználó megnyomja a jobb felső sarokban található visszavonási gombot, vagy automatikusan, amikor a PageView utolsó oldalának utolsó története lejátszása befejeződött. Egyszerűen meghívja a Navigator.of (context) .pop () fájlt, amely bezárja az Alsó lapot.

Mielőtt elutasítanánk a StoryPageView-t, frissítenünk kell a seen_stories tűzoltó dokumentumot, ha vannak olyan új történetek, amelyeket a felhasználó látott. Ezt a Repo.updateSeenStories (Térkép adatok) a dispose () metódusban. A StoryView onMomentChanged (int) visszahívásából megkapjuk a feltöltéshez szükséges adatokat.

Az PageView minden oldala StoryView, amely a történetek megjelenítéséért, a felhasználó által megtekintett új történetek nyomon követéséért, a lejátszott első történetek meghatározásáról és arról szól, mikor válthat át egy másik felhasználó történetére.

Maga a StoryView egy Stack widget, amely egy másik PageView-t tartalmaz, amely a MomentView használatával végigjárja a történet képeit.

A Stack widget a felhasználói fejléc, az előrehaladási sáv jelzője és a láthatatlan gesztusdetektorok fedésére szolgál az PageView tetején.

A folyamatjelző sáv nyomon követi az aktuális történet előrehaladását, és megmutatja, hogy hány darab történetet töltött fel az egyes felhasználók. Mindegyik darabot Pillanatnak hívják, és tartalmazza a média URL-jét, a feltöltés dátumát és a megjelenítés időtartamát.

Minden StoryView tartalmaz egy animációs vezérlőt, amely kezeli a történetek automatikus lejátszását. A vezérlő időtartamát az aktuális pillanat időtartamára állítottuk az initState-ben és folytatjuk a _play () fájlban.

vezérlő = AnimationController (vsync: ez, időtartam: story.moments [momentIndex] .duration,) .. addStatusListener ((status) {if (status == AnimationStatus.completed) {switchToNextOrFinish ();}}); _játék();

A _play funkció csak akkor folytatja az animációs vezérlőt, ha a pillanat képe be lett töltve:

/// Folytatja az animációs vezérlőt ///, feltéve, hogy az aktuális pillanat betöltődött void _play () {if (story == null || widget.story == null) return; if (story.moments.isEmpty) visszatér; /// ha a momentIndex nincs a tartományban (törlés miatt), ha (momentIndex> történet.momentok.hossz - 1) visszatér; if (story.moments [momentIndex] .isLoaded) controller.forward (); }

A verem legfelső widgetje a felhasználói gesztusokat észlelő widget, amelyet a történetek szüneteltetésére, folytatására és váltására használunk:

Pozicionált (felső: 120, bal: 0, jobb: 0, gyermek: GestureDetector (viselkedés: HitTestBehavior.opaque, onTapDown: onTapDown, onTapUp: onTapUp, onLongPress: onLongPress, onLongPressUp: onLongPressEnd,),),

Az onTapDown leállítja az animációs vezérlőt, míg az onTapUp a felhasználói gesztus vízszintes pozíciója alapján dönt arról, hogy a következő vagy az előző történetet folytatja-e. Az onLongPress leállítja az animációs vezérlőt is, miközben a StoryView-t teljes képernyős módba fordítja az átfedések (folyamatjelző sáv, felhasználói fejléc stb.) elrejtésével. Az átfedések egy AnimatedOpacity widgetbe vannak csomagolva, amely meghallgatja az isInFullscreenMode értékét. Az onLongPressEnd visszaállítja az isInFullscreenMode hibás értékét és folytatja az animációs vezérlőt:

/// Beállítja a képernyő bal és jobb oldali csappantyú részének arányát ///: balra a visszakapcsoláshoz, jobbra az előreváltáshoz az utolsó dupla momentSwitcherFraction = 0,26;
onTapDown (TapDownDetails részletek) {controller.stop (); } onTapUp (TapUpDetails részletek) {végleges szélesség = MediaQuery.of (kontextus) .size.width; if (details.localPosition.dx  isInFullscreenMode = true); } onLongPressEnd () {print ('onlongpress end'); setState (() => isInFullscreenMode = false); _játék(); }

A switchToNextOrFinish alkalmazásban ellenőrizzük, hogy az aktuális pillanat az utolsó-e. Ha igen, felhívjuk a widget.onFlashForward-ot, amely aktiválja a StoryPageView-t, hogy teljesen új StoryView-t töltsön be, amely tartalmazza a következő felhasználó történetét. Ha nem ez az utolsó pillanat, akkor mondjuk a StoryView oldalvezérlőjének, hogy ugorjon a következő képre. Alaphelyzetbe állítjuk az animációs vezérlőt, és a következő kép betöltésekor folytatjuk. A switchToPrevOrFinish ugyanazt az ötletet követi.

switchToNextOrFinish () {controller.stop (); if (momentIndex + 1> = story.moments.length) {widget.onFlashForward (); } else {controller.reset (); setState (() {momentIndex + = 1; _momentPageController.jumpToPage (momentIndex);}); controller.duration = story.moments [momentIndex] .duration; _játék(); widget.onMomentChanged (momentIndex); }} switchToPrevOrFinish () {controller.stop (); if (momentIndex - 1 <0) {widget.isFirstStory? onReset (): widget.onFlashBack (); } else {controller.reset (); setState (() {momentIndex - = 1; _momentPageController.jumpToPage (momentIndex);}); controller.duration = story.moments [momentIndex] .duration; _játék(); widget.onMomentChanged (momentIndex); }}

Kiindulási pontként a flutter_instagram_stories csomagot használtam, és erősen átdolgoztam az alkalmazást. A teljes történetek tapasztalata egy PageView (StoryView) egy PageView (StoryPageView) részében a ListView (Inline Stories) belül.

Sok olyan dologról van szó, amelyekről még nem beszéltem, mint például a tevékenységi képernyő, a szerkesztő képernyő, a felfedező képernyő, a megjegyzés képernyő, a fiók adatvédelme, a megemlítések, a regex, az egyedi felhasználónév használata a hitelesítéshez és még sok más.

Azt is szeretném rámutatni, hogy a Firebase nem tökéletes megoldás, ha teljes Instagram élményt akar létrehozni. Ez visszatér a Firestore korlátozott lekérdezési képességeihez, és még nem kellett kitalálnom a szuper összetett lekérdezéseket, például „a fiókok letöltése, amelyek hasonlóak a követett fiókokhoz”.