Solvro Talks - Flutter, Dart i dynamiczna generacja kodu, czyli jak piszemy kod "z przyszłości"
Odkryjcie z nami fascynujący świat Fluttera i Darta w najnowszym Solvro Talk! Przedstawiamy dynamiczną generację kodu, czyli jak pisać aplikacje "z przyszłości". W naszym artykule przybliżamy te technologie na przykładzie projektu ToPWR – innowacyjnego przewodnika po Politechnice Wrocławskiej. Odkryjcie, jak dzięki Flutterowi tworzymy aplikacje multiplatformowe z pojedynczej bazy kodu oraz jak Dart zapewnia bezpieczne i wydajne programowanie. Przekonajcie się, dlaczego rozwijając aplikacje w Solvro, piszemy kod, który nie tylko spełnia obecne potrzeby, ale także przyszłe wyzwania.
Nasz projekt: ToPWR
Poniższy Solvro Talk jest prezentacją od zespołu projektowego, który tworzy aplikację mobilną ToPWR, czyli mega super cool podręczny przewodnik po Politechnice Wrocławskiej. Niezbędnik każdego studenta i nie tylko :). Aplikacja aktualnie jest rozwijana w pełni open-source i zbliża się do wypuszczenia dla całej społeczności PWr (najpóźniej w zakresie 2-3 miesięcy - na kolejny semestr na pewno będzie gotowa do zainstalowania). Z chęcią prezentuję kilka ciekawych technologii, które (między innymi!) wykorzystujemy w naszym projekcie, a dla każdego zainteresowanego odsyłam do naszego repozytorium, żeby zostawił nam 🌟 gwiazdkę 🌟 na GitHubie!
Czym jest Flutter?
Framework open-source
Flutter to w pełni open-source framework, tworzony przez Google od 2017 roku.
Multiplatform, single codebase
Pozwala na tworzenie aplikacji multiplatformowych z pojedynczej bazy kodu.
Natywnie kompilowany
Aplikacje kompilują się do kodu maszynowego, natywnego dla środowiska docelowego - przez co jest szybki ⚡ niczym Zygzak Mcqueen.
Multiplatform
Framework mobilny
Flutter zaczął jako framework czysto mobilny. Idea była prosta: Piszesz pojedynczy kod i kompilujesz go na aplikację zarówno na Androida i iOS. Dzięki temu nie ma potrzeby utrzymywania dwóch osobnych zespołów programistycznych piszących dokładnie ten sam produkt, ale na dwie platformy. W ten sposób, używając podobnych zasobów możemy wypuścić aplikację dwa razy szybciej.
Nie tylko mobile
"Mobilki" to nadal obszar w którym flutter czuje się najlepiej. Jednak na ten moment pisząc jeden kod źródłowy we Flutterze możemy skompilować aplikację na prawie każdą platformę:
-
- Aplikacje webowe
- Aplikacje desktopowe, zarówno na Windowsa, Linuxa i MacOS.
- A nawet systemy wbudowane (embedded systems), np. do wbudowanych komputerów Toyoty.
W momencie pisania tego artykułu wszystkie te platformy mają już stabilne wsparcie na kanale dystrybucyjny `stable`. Jak zaczynałem przygodę z tą technologią, stabilne wsparcie było tylko dla aplikacji mobilnych, a wsparcie webowe wchodziło na kanał `beta`. Sensowne wsparcie dla aplikacji desktopowych było jeszcze pieśnią przyszłości.
Jednak w ciągu tych paru lat, Flutter bardzo wyewoluował i dojrzał, wspierając stabilnie wszystkie popularne platformy. Widać tutaj też moc open-source, mimo że Flutter jest rozwijany głównie przez wielką korporację, jaką jest bez wątpienia, Google, przyjmuje też kontrybutorów zewnętrzych. Możesz to być nawet Ty, ale też firmy, często czysto konkurencyjne do Firmy z Mountain View. Najlepszym przykładem jest Microsoft, który oficjalnie i aktywnie kolaborował przy tworzeniu wsparcia dla Windowsa.
Flutter octopus
Jeśli chcesz zobaczyć wieloplatformowość Fluttera w akcji, mam dla Ciebie jedno-minutowy klip z Flutter Product Keynote 2019 "Flutter octopus". Na klipie możesz zobaczyć Flutter Counter App (taki Hello World dla Fluttera), który jest debugowany symultanicznie na 7 różnych urządzeniach o różnych platformach i systemach operacyjnych z pojedynczej bazy kodu.
Pod koniec klipu Zoe zmienia kolor w aplikacji z niebieskiego na czerwony. I co? Aplikacje przebudowują się, reflektując zmianę, ale nie tracą stanu - licznik nadal ma taką samą wartość jak wcześniej. W ten sposób przechodzimy płynnie do Hot Reload - przez niektórych podawany nawet jako największa zaleta Fluttera.
Statefull Hot Reload
Kolejny, 20 sekundowy klip o hot reload:
W filmie przedstawiono krótko zasadę działania hot reload, gdzie względnie proste zmiany są wstrzykiwane na bieżąco do działającej Dart VM - maszyny wirtulanej Darta. Dart to język programowania w którym piszemy aplikacje we Flutterze. Ale hola hola?! Jaka maszyna wirtualna? Przecież wspominałem, że aplikacje są kompilowane natywnie i mają szybkie binarki.
Natywnie szybki
Rozwiązanie tej zagadki jest bardzo proste. Dart i Flutter mają dwa rodzaje kompilacji: Just-in-time dla buildów debug - czyli rozwijając naszą aplikację, używamy developerskiego zestawu narzędzi i możemy korzystać z hot reload i innych wygodnych funkcji. Gdy jednak chcemy wypuścić swoje dzieło na świat, przerzucamy się na builda release, który jest kompilowany Ahead-of-time do kodu natywnego dla danego procesora i optymalizowany czysto pod kątem wydajności i szybkości.
Jak widzimy na obrazku, na wszystkie platformy poza Webem, kompilują się normalnie bezpośrednio do binarek asemblerowych. Jednak z powodu ograniczeń technologicznych, aplikacje webowe kompilują się do javascripta, tworząc dość "tłustą" frontendową aplikacje jedno-stronnicową. Jest to przez to najwolniej działająca platforma docelowa dla Fluttera.
Jednak promykiem nadziei jest wsparcie kompilacji do WebAsembly, które w poprzednim miesiącu weszło do stabilnej wersji Fluttera. Jednak skompilowana tak aplikacja wymaga na ten moment dość nowej wersji przeglądarki opartej na chromium: https://docs.flutter.dev/platform-integration/web/wasm.
Jednakże podobno tak skompilowane aplikacje są szybsze i może wyeliminują ten ostatni wydajnościowy bottleneck Fluttera, jakim trochę są aplikacje webowe.
Koniec zbędnego powtarzania
Załączam "wyznanie" z oficjalnej strony Fluttera i w formie lekkiego żartu, swoją wersję pod którą mogę się podpisać jako Techlead w zepole ToPWR.
Jednakże nie jest to przesadzone. Kod pisze się jeden, stawiając najprostszego `if`-a jeśli chcesz trochę zmienić zachowanie dla danej platformy. Dodajemy też różne widgety, które dbają o responsywność i np. obsługę skrótów klawiszowych na desktopie.
W przypadku funkcjonalności "blisko platformy", możesz napisać pewne fragmenty logiki lub widoki w technologii natywnej, np. natywnym Androidzie (Java/Kotlin), natywnym iOS (Objective-C/Swift), Javascriptcie na Webie i w C++ dla aplikacji desktopowych. Takie natywne fragmenty możesz "opiąć" w Dart-owe API i wypuścić jako plugin na pub.dev. A prawdopodobnie w większości przypadków ktoś już to zrobił przed Tobą :)
Everything’s a widget!
W Flutterze prawie każdy element UI jest widgetem, które można dowolnie w sobie zagnieżdzać i łączyć. Jest też wiele gotowych (oficjalnych) widgetów, implementujących znane elementów z popularnych systemów designowych np. znany z Androida Material Design, znane z iOSa widgety Cupertino, czy Microsoftowe Fluent UI
Bardzo prosto tworzymy też swoje własne widgety, które łatwo się parametryzuje i używa w wielu miejscach w aplikacji, albo nawet w wielu aplikacjach! Na każdej platformie. Żaden problem!
Przykładowy widget, jeszcze do niego parę razy wrócimy:
Dart i jego sound null safety
Dart to język programowania, w którym napisany (częściowo) jest Flutter i z którego korzystamy tworząc aplikacje we Flutterze. Na potrzeby tej prezentacji opowiem tu o pewnej własności jego systemu typowego, jakim jest null-safety. Jest to koncept obecny w conajmniej kilku nowoczesnych językach programowania, ale może jeszcze o tym nie słyszałeś.
Non-nullable types
Zmienne domyślnie nie mogą mieć wartości `null`. Jeśli kiedykolwiek w kodzie wykonamy operację, która spróbuje przypisać wartość nawet tylko potencjalnie nullową, to cała aplikacja się nawet nie skompiluje. Możemy przypisać taką wartość, tylko jeśli ówcześnie się upewnimy, że nie zawiera ona nulla. System typów w Dart jest "sound", czyli jak coś ma typ `int` to znaczy że zawsze będzie liczbą całkowitą i nie ma innej możliwości.
Nullable types
Jeśli chcemy przechowywać gdzieś wartości null, to musimy na to jawnie pozwolić - dodając do typu znak zapytania. Ale potem jeśli chcemy gdzieś użyć tej zmiennej, gdzie null nie jest dopuszczalny, to musimy wpierw sprawdzić czy nie jest nullem - inaczej aplikacja się nawet nie skompiluje. Używając null safety, nigdy więcej nie spotkasz błędu w czasie wykonania, związanego z wartością null w miejscu, gdzie go nie powinno być.
Null safety - przykład
Wracamy do naszego widgeta. Jest to zwykła klasa w Darcie, która rozszerza `StatelessWidget`. Na zaznaczonym fragmencie mamy zdefiniowane pola klasy z typami i konstruktor, który je przyjmuje. Jak widzimy `title` musi być zawsze stringiem, natomiast `actionTitle` może być stringiem lub nullem, bo czasami możemy go nie chcieć i po prostu nie wyświetlamy wtedy fragmentu UI z nim związanego.
I kiedy używamy naszego widgeta i do naszego `title` próbujemy wstawić coś co potencjalnie może być nullem - otrzymamy natymiastowy błąd.
Problemem jest `context.localize`, która ma nullable typ, czyli może być nullem. Używając operatora `?.` wyciągamy z niego atrybut departments, tylko jeśli nie jest nullem. Jednak nadal całe wyrażenie może być nullem, a nasz tytuł nie.
Czas to naprawić! Dodajmy wartość domyślną, którą wstawi się zamiast nulla w tym miejscu używając operatora: `??`:
System typów w Dart - zalety
Bezpieczeństwo czasu kompilacji
Całkowita eliminacja błędów związanych z typami w czasie wykonania.
Łatwiejsze zmiany w kodzie
Przy zmianie, system typów pilnuje prawie wszystkich innych miejsc wymagających zmian.
Szybsze binarki
Bardziej efektywna kompilacja ahead of time.
Dynamiczna generacja kodu
Według definicji jest to "zdolność środowiska wykonawczego lub frameworka developerskiego do automatycznego generowania i aktualizowania kodu źródłowego przed jego kompilacją/wykonaniem". Co to znaczy w praktyce? Mam dla ciebie aż 5 przykładów!
Przykład 1 - UnmodifableEnumMap
Załóżmy, że mamy prostą enumerację, która definiuje nam zakładki w TabView:
Nasz kod
I chcemy z nią powiązać widgety, reprezentujące odpowiednie widoki. Możemy zrobić to w bardzo bezpieczny sposób, używając annotacji `@unmodifableEnumMap`.
Następnie musimy puścić nasz generator build_runner za pomocą prostej komendy w terminalu:
build_runner
Odpala to proces, który znajduje wszystkie z elementami oznaczonymi przez różne generacyjne biblioteki i generuje potrzebne fragmenty kodu w plikach obok z rozszerzeniem `.g.dart`.
Jeśli chcemy żeby build_runner puszczał się automatycznie po każdej zmianie w takim pliku, użyj opcji `watch` zamiast `build`:
Kod wygenerowany
Użycie
Generuje to nam nową implementację `Map`, która pilnuje żeby każda wartość enumeracji miała przypisaną wartość. Np. każda zakładka miała swój widget/widok:
Co jeśli dodamy nowego taba?
Na przykład nową zakładkę parkingi? (TAK - BĘDZIE TAKA W TOPWR)
Błąd kompilacji!!!
Jesteśmy uratowani!
Generator nie pozwolił nam zbudować apki z brakującym widgetem parkingów, możemy to prędko naprawić:
Przykład 2 - freezed
Kolejnym przykładem jest annotacja @freezed, która z klasy która ma same konstruktory, buduje klasę znaną w wielu językach pod nazwą "dataklasy". Klasa freezed to klasa niemodyfikowalna, która nadpisuje operator równości i hashCode, tak aby dwie klasy tego typu były równe wtedy i tylko wtedy gdy wszystkie jej atrybuty podane w konstruktorze są równe. Dodadkowo freezed dodaje serializację toJson() i fromJson() - pozwalając na serializację bez użycia "magic stringów".
Oficjalny przykład z dokumentacji freezed:
Freezed - boredapi.com
Tutaj mój przykład modelu na aktywność z https://www.boredapi.com/api/activity
Przykład 3 - Riverpod
Riverpod to biblioteka do zarządzania stanem, która korzysta z generacji kodu, aby maksymalne zminimalizować swój boilerplate.
Te dwie linijki + kilku linijkowy model z poprzedniego przykładu pozwala nam na pobranie danych z API, zparsowanie ich na bezpiecznie otypowane modele i dzięki riverpod możemy pobrać te dane bezpośrednio w warstwie prezentacji i wyświetlić. Riverpod za nas zajmie się odświeżaniem, opakowywaniem wartości asynchronicznych w bezpieczne `AsyncValue` - czyli łapie za nas wszelakie błędy i specjalnie je opakowuje. Zwraca też specjalną wartość podczas ładowania i pozwala w jednej linijce zaimplementować pull-to-refresh.
Więcej o riverpod: https://riverpod.dev/
Przykład 4 - Riverpod cd.
Drugi przykład z riverpodem, tym razem klasa kontrollera znajomego taba (NavBarEnum). W paru linijkach mamy zaimplementowany controller z automatycznym odświeżaniem UI, jeśli wartości się zmienią poprzez wywołanie metody `goTo`:
Użycie kontrollera (znajomy kod):
Tak wygląda nasłuchiwanie zmian w warstwie UI, która będzie automatycznie przebudowana, jeśli zmieni się tab:
Przedstawiono KĄŻDĄ LINIJKĘ KODU, którą sami musimy napisać. Resztę wygeneruje build_runner.
Przykład 5 (ostatni) graphql_codegen
W ToPWR korzystamy z GraphQL API, zamiast RESTa. Jest to bardzo wygodne dzięki właśnie tej bibliotece. GraphQL jest z natury otypowany, każdy endpoint zwraca plik typu Graphql Schema Definition Language (SDL), czyli otypowaną definicje wszystkich modeli i dostępnych operacji: https://graphql.org/learn/schema/
Nasza biblioteka korzysta z generacji kodu i tego pobranego pliku, aby automatycznie wygenerować nam bezpiecznie otypowane modele i operacje, korzystając tylko z SDLa i naszego prostego tekstowego zapytania GraphQL:
Dzięki graphql_codegen komunikacja z API jest jeszcze prostsza i bezpieczniejsza w kwestii typów.
Generacja kodu zalety
Podsumowując, generacja kodu zapewnia następujące zalety:
Jeszcze większe bezpieczeństwo czasu kompilacji
Możliwe dodatkowe wymuszenie typów lub innych warunków w niektórych niebezpiecznych przypadkach.
Redukcja boilerplate’u
Krótszy i bardziej sensowny kod, a boilerplate pisze się sam
Mniejsza podatność na błędy ludzkie
Automatyzowanie powtarzalnych czynności
Dzięki za czytanie!
Jeśli spodobał ci się ten artykuł, zostaw gwiazdkę na naszym repo: https://github.com/Solvro/mobile-topwr
Spojrzyj na nasze inne projekty w KN Solvro: https://solvro.pwr.edu.pl/portfolio
Rozważ też dołączenie do naszego koła poprzez kontakt: https://solvro.pwr.edu.pl/kontakt lub czekając do następnej co-semestralnej akcji rekrutacyjnej. Aby niczego nie przegapić zaobserwuj nas na Facebooku: facebook.com/knsolvro lub LinkedIn: linkedin.com/company/knsolvro
Jeśli masz jakieś pytania do Fluttera, nie bój się uderzać na https://solvro.pwr.edu.pl/kontakt, maila kn.solvro@pwr.edu.pl lub bezpośrednio do Szymona Kowalińskiego: https://kowalinski.dev/
Nie mogę się doczekać jakie cudowne aplikacje stworzycie we Flutterze! Kto wie, może będzie to jakaś aplikacja, którą wspólnie stworzymy w KN Solvro!