Separation av funktioner i Flutter-applikationer
Engineering at ClickUp

Separation av funktioner i Flutter-applikationer

Nyligen var jag tvungen att implementera introduktionsguider för nykomlingar på ClickUp! Det var en väldigt viktig uppgift, eftersom många nya användare skulle upptäcka plattformen tack vare den otroligt roliga reklamfilmen som vi premiärvisade under Super Bowl! ✨

via ClickUp

Genomgången gör det möjligt för våra många nya användare, som kanske inte känner till ClickUp ännu, att snabbt förstå hur man använder flera funktioner i applikationen. Det är ett pågående arbete, precis som den nya resursen ClickUp University som vi arbetar med! 🚀

Lyckligtvis gjorde programvaruarkitekturen bakom ClickUp Flutter-mobilapplikationen det möjligt för mig att implementera denna funktionalitet ganska snabbt, till och med genom att återanvända de riktiga widgets från applikationen! Detta innebär att genomgången är dynamisk, responsiv och exakt matchar appens riktiga applikationsskärmar – och kommer att fortsätta att göra det, även när widgets utvecklas.

Jag kunde också implementera funktionaliteten tack vare rätt uppdelning av ansvarsområden.

Låt oss se vad jag menar här. 🤔

Separation av ansvarsområden

Att utforma en mjukvaruarkitektur är ett av de mest komplexa ämnena för ingenjörsteam. Bland alla ansvarsområden är det alltid svårt att förutse framtida mjukvaruutvecklingar. Därför kan det vara till stor hjälp för dig och dina teammedlemmar att skapa en välstrukturerad och avkopplad arkitektur!

Den största fördelen med att skapa små, frikopplade system är utan tvekan testbarheten! Och det var detta som hjälpte mig att skapa en demoalternativ till de befintliga skärmarna från appen!

En steg-för-steg-guide

Hur kan vi nu tillämpa dessa principer på en Flutter-applikation?

Vi kommer att dela med oss av några tekniker som vi använder för att bygga ClickUp med ett enkelt genomgångsexempel.

Exemplet är så enkelt att det kanske inte belyser alla fördelar med tekniken, men tro mig, det kommer att hjälpa dig att skapa mycket mer underhållbara Flutter-applikationer med komplexa kodbaser. 💡

Applikationen

Som exempel kommer vi att skapa en applikation som visar USA:s befolkning för varje år.

via ClickUp

Vi har två skärmar här:

  • HomeScreen: visar helt enkelt alla år från 2000 till idag. När användarna trycker på en årskakel navigerar de till DetailScreen med ett navigeringsargument inställt på det valda året.
  • DetailScreen : hämtar året från navigeringsargumentet, anropar datausa.io API för detta år och analyserar JSON-data för att extrahera det associerade befolkningsvärdet. Om data är tillgängliga visas en etikett med befolkningsantalet.

Vi kommer att fokusera på implementeringen av DetailScreen, eftersom den är mest intressant med sitt asynkrona anrop.

Steg 1. Naiv approach

Naiv approach Stateful Widget
via ClickUp

Den mest uppenbara implementeringen för vår app är att använda en enda StatefulWidget för hela logiken.

Åtkomst till argumentet för årsnavigering

För att komma åt det begärda året läser vi RouteSettings från den ärvda widgeten ModalRoute.

HTTP-anrop

I det här exemplet anropas funktionen get från http-paketet för att hämta data från datausa.io API, den resulterande JSON-koden analyseras med metoden jsonDecode från biblioteket dart:convert och Future behålls som en del av tillståndet med en egenskap som heter _future.

Rendering

För att skapa widget-trädet använder vi FutureBuilder, som återuppbygger sig själv utifrån det aktuella läget för vårt framtida asynkrona anrop.

Granska

Okej, implementeringen är kort och använder endast inbyggda widgets, men tänk nu på vår ursprungliga avsikt: att bygga demoalternativ (eller tester) för den här skärmen. Det är mycket svårt att kontrollera resultatet av HTTP-anropet för att tvinga applikationen att rendera i ett visst tillstånd.

Det är här begreppet inversion av kontroll kommer att hjälpa oss. 🧐

Steg 2. Inversion av kontroll

Inversion av kontroll till leverantör och API-klient
via ClickUp

Denna princip kan vara svår att förstå för nya utvecklare (och också svår att förklara), men den övergripande idén är att extrahera problemen utanför våra komponenter – så att de inte ansvarar för att välja beteendet – och istället delegera det.

I en mer vanlig situation består det helt enkelt av att skapa abstraktioner och infoga implementationer i våra komponenter så att deras implementation kan ändras senare om det behövs.

Men oroa dig inte, det kommer att bli tydligare efter vårt nästa exempel! 👀

Skapa ett API-klientobjekt

För att kontrollera HTTP-anropet till vårt API har vi isolerat vår implementering i en dedikerad DataUsaApiClient-klass. Vi har också skapat en Measure-klass för att göra det enklare att hantera och underhålla data.

Tillhandahålla en API-klient

I vårt exempel använder vi det välkända leverantörspaketet för att injicera en DataUsaApiClient-instans i roten av vårt träd.

Använda API-klienten

Leverantören tillåter alla underordnade widgetar (som vår DetailScreen) att läsa den närmaste DataUsaApiClient-överordnade i trädet. Vi kan sedan använda dess getMeasure-metod för att starta vår Future, istället för den faktiska HTTP-implementeringen.

Demo API-klient

Nu kan vi dra nytta av detta!

Om du inte visste det: alla klasser i Dart definierar också implicit ett associerat gränssnitt. Detta gör det möjligt för oss att tillhandahålla en alternativ implementering av DataUsaApiClient som alltid returnerar samma instans från dess getMeasure-metodanrop.

Denna metod

Denna metod

Visa en demosida

Nu har vi alla nycklar för att visa en demoversion av DetailPage!

Vi åsidosätter helt enkelt den nuvarande DataUsaApiClient-instansen genom att istället omsluta vår DetailScreen i en leverantör som skapar en DemoDataUsaApiClient-instans!

Och det är allt – vår DetailScreen läser istället denna demoinstans och använder våra demoMeasure-data istället för ett HTTP-anrop.

Granska

Detta är ett utmärkt exempel på inversion av kontroll. Vår DetailScreen-widget ansvarar inte längre för logiken för att hämta data, utan delegerar den istället till ett dedikerat klientobjekt. Och nu kan vi skapa demoinstanser av skärmen eller implementera widget-tester för vår skärm! Fantastiskt! 👏

Men vi kan göra ännu bättre!

Eftersom vi inte kan simulera ett laddningsläge har vi till exempel inte full kontroll över några lägesförändringar på widgetnivå.

Steg 3. Tillståndshantering

Statementshantering i Flutter-applikationer
via ClickUp

Detta är ett hett ämne inom Flutter!

Du har säkert redan läst långa trådar där människor försöker välja den bästa lösningen för tillståndshantering för Flutter. Och för att vara tydlig, det är inte vad vi kommer att göra i den här artikeln. Enligt vår mening är det tillräckligt att separera din affärslogik från din visuella logik! Att skapa dessa lager är verkligen viktigt för underhållbarheten. Vårt exempel är enkelt, men i verkliga applikationer kan logiken snabbt bli komplex och en sådan separation gör det mycket lättare att hitta dina rena logikalgoritmer. Detta ämne sammanfattas ofta som tillståndshantering.

I det här exemplet använder vi en grundläggande ValueNotifier tillsammans med en Provider. Men vi kunde också ha använt flutter_bloc eller riverpod (eller en annan lösning), och det hade också fungerat utmärkt. Principerna förblir desamma, och så länge du separerar dina tillstånd och din logik är det till och med möjligt att porta din kodbas från en av de andra lösningarna.

Denna separering hjälper oss också att kontrollera alla tillstånd i våra widgets så att vi kan simulera dem på alla möjliga sätt!

Skapa ett dedikerat tillstånd

Istället för att förlita oss på AsyncSnapshot från ramverket representerar vi nu vårt skärmläge som ett DetailState-objekt.

Det är också viktigt att implementera metoderna hashCode och operator == för att göra vårt objekt jämförbart efter värde. Detta gör det möjligt för oss att upptäcka om två instanser ska betraktas som olika.

💡 De jämförbara eller frysta paketen är utmärkta alternativ som hjälper dig att implementera hashCode- och operator ==-metoderna!

💡 De jämförbara eller frysta paketen är utmärkta alternativ som hjälper dig att implementera hashCode- och operator ==-metoderna!

Vårt tillstånd är alltid kopplat till ett år, men vi har också fyra olika möjliga tillstånd när det gäller vad vi vill visa för användaren:

  • NotLoadedDetailState: datauppdateringen har ännu inte påbörjats
  • LoadingDetailState: data laddas för närvarande
  • LoadedDetailState: data har laddats framgångsrikt med en associerad måttstock.
  • NoDataDetailState: data har laddats, men det finns inga tillgängliga data
  • UnknownErrorDetailState: operationen misslyckades på grund av ett okänt fel

Dessa tillstånd är tydligare än en AsyncSnapshot, eftersom de verkligen representerar våra användningsfall. Och återigen, detta gör vår kod mer underhållbar!

💡 Vi rekommenderar starkt Union-typerna från freezed-paketet för att representera dina logiska tillstånd! Det lägger till många verktyg som copyWith- eller map-metoderna.

💡 Vi rekommenderar starkt Union-typerna från freezed-paketet för att representera dina logiska tillstånd! Det tillför många användbara funktioner, såsom copyWith- eller map-metoderna.

Placera logiken i en Notifier

Nu när vi har en representation av vårt tillstånd måste vi lagra en instans av det någonstans – det är syftet med DetailNotifier. Den kommer att behålla den aktuella DetailState-instansen i sin värdeegenskap och tillhandahålla metoder för att uppdatera tillståndet.

Vi tillhandahåller ett initialt tillstånd NotLoadedDetailState och en uppdateringsmetod för att ladda data från API:et och uppdatera det aktuella värdet.

Ange ett tillstånd för vyn

För att instansiera och observera skärmens status förlitar vi oss också på leverantören och dess ChangeNotifierProvider. Denna typ av leverantör söker automatiskt efter alla ChangeListener som skapats och utlöser en ombyggnad från konsumenten varje gång den meddelas om en förändring (när vårt meddelandevärde skiljer sig från det föregående).

Granska

Bra! Vår applikationsarkitektur börjar se riktigt bra ut. Allt är uppdelat i väldefinierade lager med specifika funktioner! 🤗

En sak saknas dock fortfarande för testbarhet: vi vill definiera det aktuella DetailState för att kontrollera tillståndet för vår associerade DetailScreen.

Steg 4. Visuell dedikerad layoutwidget

Visuella dedikerade layoutwidgets i Flutter-applikationer
via ClickUp

I det sista steget gav vi vår DetailScreen-widget lite för mycket ansvar: den var ansvarig för att instansiera DetailNotifier. Och precis som vi har sett tidigare försöker vi undvika alla logiska ansvarsområden i visningslagret!

Vi kan enkelt lösa detta genom att skapa ett nytt lager för vår skärmwidget: vi delar upp vår DetailScreen-widget i två delar:

  • DetailScreen ansvarar för att ställa in de olika beroendena för vår skärm utifrån den aktuella applikationens status (navigering, aviseringar, status, tjänster, ...).
  • DetailLayout konverterar helt enkelt en DetailState till ett dedikerat träd av widgets.

Genom att kombinera de två kan vi enkelt skapa DetailLayout-demo-/testinstanser, men ha DetailScreen för det verkliga användningsfallet i vår applikation.

Dedikerad layout

För att uppnå en bättre åtskillnad mellan olika funktioner flyttade vi allt under Consumer-widgeten till en dedikerad DetailLayout-widget. Denna nya widget konsumerar endast data och ansvarar inte för någon instansiering. Den konverterar bara lässtatusen till ett specifikt widget-träd.

ModalRoute. av call och ChangeNotifierProvider-instansen förblir i DetailScreen, och denna widget returnerar helt enkelt DetailLayout med ett förkonfigurerat beroendeträd!

Denna mindre förbättring är specifik för leverantörsanvändning, men du kommer att märka att vi också har lagt till en ProxyProvider så att alla underordnade widgetar direkt kan använda en DetailState. Detta gör det enklare att simulera data.

Extrahera widgets som dedikerade klasser

Tveka aldrig att extrahera ett widget-träd till en dedikerad klass! Det förbättrar prestandan och gör koden lättare att underhålla.

I vårt exempel skapade vi en visuell layoutwidget för var och en av de associerade statustyperna:

Demofall

Nu har vi full kontroll över vad vi kan simulera och visa på vår skärm!

Vi behöver bara omsluta en DetailLayout med en Provider- för att simulera layoutens tillstånd.

Slutsats

Det är definitivt inte lätt att skapa en underhållbar mjukvaruarkitektur! Att förutse framtida scenarier kan kräva mycket arbete, men jag hoppas att de tips jag har delat med mig av kommer att hjälpa dig i framtiden!

Exemplen kan verka enkla – det kan till och med se ut som om vi överkonstruerar – men när din app blir mer komplex kommer dessa standarder att vara till stor hjälp! 💪

Ha kul med Flutter och följ bloggen för att få fler tekniska artiklar som denna! Håll dig uppdaterad!

ClickUp Logo

En app som ersätter alla andra