Professional Documents
Culture Documents
Flutter I Dart 2 Dla Poczatkujacych Przewodnik Dla Tworcow Aplikacji Mobilnych Alessandro Biessek Helion
Flutter I Dart 2 Dla Poczatkujacych Przewodnik Dla Tworcow Aplikacji Mobilnych Alessandro Biessek Helion
i Dart 2
dla początkujących
Przewodnik dla twórców aplikacji mobilnych
Alessandro Biessek
Tytuł oryginału: Flutter for Beginners: An introductory guide to building cross-platform
mobile applications with Flutter and Dart 2
ISBN: 978-83-283-7826-1
Copyright © Packt Publishing 2019. First published in the English language under
the title ‘Flutter for Beginners – (9781788996082)’.
All rights reserved. No part of this book may be reproduced or transmitted in any
form or by any means, electronic or mechanical, including photocopying, recording
or by any information storage retrieval system, without permission from the Publisher.
Autor oraz wydawca dołożyli wszelkich starań, by zawarte w tej książce informacje
były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich
wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych
lub autorskich. Autor oraz wydawca nie ponoszą również żadnej odpowiedzialności
za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce.
Helion S.A.
ul. Kościuszki 1c, 44-100 Gliwice
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/flutte_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
d0765ad53fb82babda2278a311da7afb
d
Dla mojej matki Antoniny i mojego ojca Euclidesa
za ich poświęcenia
i za przykład pokazujący siłę determinacji
– Alessandro Biessek
d0765ad53fb82babda2278a311da7afb
d
d0765ad53fb82babda2278a311da7afb
d
Spis treści
O autorze 13
O recenzencie 15
Przedmowa 17
d0765ad53fb82babda2278a311da7afb
d
Spis treści
d0765ad53fb82babda2278a311da7afb
d
Spis treści
d0765ad53fb82babda2278a311da7afb
d
Spis treści
d0765ad53fb82babda2278a311da7afb
d
Spis treści
d0765ad53fb82babda2278a311da7afb
d
Spis treści
10
d0765ad53fb82babda2278a311da7afb
d
Spis treści
11
d0765ad53fb82babda2278a311da7afb
d
Spis treści
12
d0765ad53fb82babda2278a311da7afb
d
O autorze
Alessandro Biessek urodził się w 1993 roku w pięknym mieście Chapecó w stanie Santa Catarina
w południowej Brazylii. Obecnie pracuje tam nad rozwojem aplikacji mobilnych na Androida
i iOS. Ma ponad siedmioletnie doświadczenie w programowaniu, od programowania deskto-
powego w Delphi po back-end z wykorzystaniem PHP, Node.js, Golang i programowanie mobilne
z Apache Flex i Java / Kotlin. Zawsze zainteresowany nowymi technologiami, od dłuższego
czasu podąża za frameworkiem Flutter.
Jestem wdzięczny wszystkim tym, z którymi miałem przyjemność pracować podczas tego projektu,
wszystkim recenzentom oraz całemu zespołowi Packt, który pomógł mi w tej pracy.
Na koniec chciałbym podziękować Tobie, Czytelniku. Twoje wsparcie dla książek takich jak ta,
wyrażone zakupem, umożliwia każdemu dzielenie się swoimi doświadczeniami.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
14
d0765ad53fb82babda2278a311da7afb
d
O recenzencie
15
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
16
d0765ad53fb82babda2278a311da7afb
d
X
Przedmowa
Książka Flutter i Dart 2 dla początkujących pomaga wejść do świata frameworka Flutter i two-
rzyć niesamowite aplikacje mobilne. Przejdziemy od wprowadzenia do języka Dart do dogłębnej
eksploracji wszystkich bloków Fluttera potrzebnych do stworzenia aplikacji wysokiego po-
ziomu. Razem stworzymy w pełni funkcjonalną aplikację. Dzięki jasnym przykładom nauczysz się,
jak rozpocząć mały projekt Fluttera, dodać widżety, stosować style i motywy, łączyć się ze
zdalnymi usługami, takimi jak Firebase, uzyskiwać dane wejściowe użytkownika, dodawać
animacje, aby poprawić wrażenia użytkownika i nie tylko. Ponadto dowiesz się, jak dodawać
zaawansowane funkcje, integrować mapy, pracować z kodem specyficznym dla platformy w na-
tywnych językach programowania i tworzyć fantastyczne interfejsy użytkownika ze spersonalizowa-
nymi animacjami. Krótko mówiąc, ta książka przygotuje Cię na przyszłość tworzenia aplikacji
mobilnych dzięki tej niesamowitej platformie.
Co obejmuje ta książka?
Rozdział 1., „Wprowadzenie do języka Dart”, przedstawia podstawy języka Dart.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Rozdział 5., „Obsługa danych wejściowych i gestów użytkownika”, pokazuje, jak obsługiwać
dane wejściowe użytkownika za pomocą widżetów Fluttera.
Rozdział 6., „Motyw i styl”, uczy, jak stosować różne style do widżetów Fluttera.
Rozdział 7., „Routing: nawigacja między ekranami”, przedstawia sposób dodawania nawigacji
do ekranów aplikacji.
Rozdział 8., „Wtyczki Firebase”, omawia sposób korzystania z wtyczek Firebase w aplikacjach
Fluttera.
Rozdział 9., „Tworzenie własnej wtyczki Fluttera”, wyjaśnia, jak tworzyć własne wtyczki
Flutter.
Rozdział 10., Dostęp do funkcji urządzenia z aplikacji Fluttera”, omawia sposób interakcji
z funkcjami urządzenia, takimi jak kamera i listy kontaktów.
Rozdział 11., „Widoki platformy i integracja map”, pokazuje, jak dodawać widoki map do apli-
kacji Fluttera.
Rozdział 12., „Testowanie, debugowanie i wdrażanie”, zagłębia się w narzędzia Fluttera do po-
prawy produktywności.
Rozdział 13., „Poprawianie doświadczenia użytkownika”, bada, jak poprawić wrażenia użytkow-
nika za pomocą takich funkcji jak wykonywanie kodu Darta w tle i internacjonalizacja.
Rozdział 15., „Animacje”, daje wgląd w to, jak dodawać animacje do widżetów Flutter.
18
d0765ad53fb82babda2278a311da7afb
d
Przedmowa
Zastosowane konwencje
W tej książce występuje wiele konwencji tekstowych.
KodWTekście: wskazuje słowa kodu źródłowego w tekście, nazwy tabel bazy danych, nazwy folde-
rów, nazwy plików, rozszerzenia plików, nazwy ścieżek, adresy URL, dane wejściowe użytkownika
i uchwyty Twittera. Oto przykład: „Oblicza i zwraca wartość wyrażenia2: wyrażenie1 ?? wyrażenie2”.
Kiedy chcemy zwrócić Twoją uwagę na określoną część bloku kodu, odpowiednie wiersze lub
elementy są pogrubione:
main() {
var someInt = 1;
print(reflect(someInt).type.reflectedType.toString()); // wyświetla: int
}
Wszelkie dane wejściowe lub wyjściowe wiersza polecenia są zapisywane w następujący sposób:
dart code.dart
Pogrubienie: Oznacza nowy termin, ważne słowo lub słowa, które widzisz na ekranie — na
przykład słowa w menu lub oknach dialogowych. Oto przykład:
„Ponadto pływający przycisk akcji u dołu powinien przekierowywać Cię do ekranu Request
a favor”.
Wskazówki, ostrzeżenia lub ważne uwagi będą się pojawiać w takich ramkach.
19
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
20
d0765ad53fb82babda2278a311da7afb
d
I
Wprowadzenie
do języka Dart
W tej sekcji poznasz framework Fluttera oraz podstawy języka Dart, nauczysz się konfiguro-
wać własne środowisko, a na koniec dowiesz się, jak zacząć z tego wszystkiego korzystać.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
22
d0765ad53fb82babda2278a311da7afb
d
1
Wprowadzenie
do języka Dart
Framework Flutter korzysta z języka Dart. Nowoczesne frameworki, takie jak omawiany w tej
książce, wymagają nowoczesnego języka wysokiego poziomu, aby zadowolić programistów i umoż-
liwić im tworzenie niesamowitych aplikacji mobilnych.
Zrozumienie języka Dart jest podstawą pracy z Flutterem; programiści muszą znać pochodzenie
tego języka — sposób, w jaki pracuje nad nim społeczność, jego mocne strony oraz dlaczego został
wybrany jako język programowania Fluttera. W tym rozdziale zapoznasz się z podstawami
języka Dart i otrzymasz linki do zasobów, które mogą Ci pomóc w nauce Fluttera. Zapoznasz się
z wbudowanymi typami i operatorami Dart oraz dowiesz się, w jaki sposób Dart jest związany
z programowaniem obiektowym (OOP — object oriented programming). Rozumiejąc, co zapewnia
język Dart, będziesz mógł samodzielnie eksperymentować z jego środowiskiem i poszerzać
swoją wiedzę.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
deweloperowi najlepsze funkcje podczas tworzenia aplikacji mobilnych. Zobaczmy więc, co za-
pewnia Dart i jak działa, abyśmy mogli później zastosować to, czego nauczyliśmy się we Flutterze.
Dart łączy korzyści płynące z większości języków wysokiego poziomu z funkcjami języka dojrza-
łego, w tym:
Wydajne narzędzia — obejmuje narzędzia do analizy kodu, wtyczki
do zintegrowanych środowisk programistycznych (IDE) oraz rozbudowany
ekosystem pakietów.
Odśmiecanie pamięci (garbage collection) — zarządza lub zajmuje się zwalnianiem
pamięci (głównie pamięci zajmowanej przez obiekty, które nie są już używane).
Adnotacje (type annotations) — opcjonalnie: jest to przeznaczone dla tych, którzy
chcą zapewnić bezpieczeństwo i spójność danych w aplikacji.
Typowanie statyczne — chociaż adnotacje są opcjonalne, Dart zapewnia
bezpieczeństwo dla różnych rodzajów danych oraz korzysta z mechanizmu
inferencji i analizowania typów w czasie wykonywania. Ta funkcja jest istotna
dla znajdowania błędów w czasie kompilacji.
Przenośność — nie odnosi się tylko do aplikacji webowych (transponowanych
do JavaScriptu), ale również do aplikacji natywnie skompilowanych do kodu
ARM i x86.
Ewolucja Darta
Zaprezentowany w 2011 roku, Dart nieustannie ewoluuje. W 2013 roku doczekał się stabilnej
wersji, z dużymi zmianami wprowadzonymi w wersji Dart 2.0 pod koniec 2018 roku:
Jego koncepcja koncentrowała się na tworzeniu stron internetowych, a głównym
celem było zastąpienie JavaScriptu — teraz jednak Dart koncentruje się na
obszarach rozwoju mobilnego, a także na Flutterze.
Próbował rozwiązać problemy JavaScriptu — JavaScript nie zapewnia takiej
solidności jak wiele skonsolidowanych języków. Tak więc Dart został stworzony
jako dojrzały następca JavaScriptu.
Oferuje najlepszą wydajność i lepsze narzędzia dla projektów na dużą skalę —
Dart ma nowoczesne i stabilne narzędzia dostarczane przez wtyczki IDE. Został
zaprojektowany, aby uzyskać najlepszą możliwą wydajność, zachowując jednocześnie
dynamiczny język.
Jest uformowany tak, aby był solidny i elastyczny — utrzymując adnotacje typu
jako opcjonalne i dodając funkcje OOP, Dart równoważy dwa światy elastyczności
i solidności.
24
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Wykonywanie kodu Dart działa w dwóch trybach — kompilacji Just-In-Time (JIT) lub kompilacji
Ahead-Of-Time (AOT):
Kompilacja JIT polega na tym, że kod źródłowy jest ładowany i kompilowany
w locie do natywnego kodu przez maszynę wirtualną Dart. Służy do uruchamiania
kodu w wierszu poleceń lub podczas tworzenia aplikacji mobilnej w celu korzystania
z takich funkcji jak debugowanie.
Kompilacja AOT ma miejsce wtedy, gdy maszyna wirtualna Dart i kod są wstępnie
skompilowane, a maszyna wirtualna działa bardziej jak system wykonawczy Dart,
udostępniając odśmiecanie pamięci i różne metody natywne z zestawu narzędzi
dla programistów Dart (SDK – software development kit) w aplikacji.
25
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dart przyczynia się do najsłynniejszej funkcji Fluttera, hot reload, która jest oparta na
kompilatorze JIT Darta, umożliwiając podmianę kodu w czasie działania. Zobacz sekcję
„Dlaczego Flutter korzysta z języka Dart”, aby uzyskać szczegółowe informacje.
Dart w praktyce
Na sposób projektowania Fluttera duży wpływ ma język Dart. Dlatego znajomość tego języka
jest kluczowa dla odniesienia sukcesu. Zacznijmy od napisania kodu, aby zrozumieć podstawy
składni i dostępne narzędzia do programowania Dart.
DartPad
Najłatwiejszym sposobem rozpoczęcia kodowania jest skorzystanie z DartPad (https://dartpad.
dartlang.org/). Jest to świetne narzędzie online do nauki i eksperymentowania z funkcjami
językowymi Darta. Obsługuje podstawowe biblioteki Dart, z wyjątkiem bibliotek VM, takich
jak dart:io.
Flutter jest oparty na Dart i możesz rozwijać kod Dart, mając środowisko programi-
styczne Flutter. Aby się dowiedzieć, jak skonfigurować środowisko programistyczne
Flutter, po prostu odwiedź oficjalną witrynę internetową i zapoznaj się z samouczkiem
instalacji (https://dart.dev/tools/sdk#install).
26
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Lint lub linter to narzędzie, które analizuje kod źródłowy w celu oznaczenia błędów,
błędów stylistycznych i podejrzanych konstrukcji.
Na potrzeby tworzenia stron internetowych Dart proponuje kilka innych narzędzi (za pomocą
dodatkowych kroków instalacji — na https://dart.dev/tools):
webdev (https://dart.dev/tools/webdev) i build_runner (https://dart.dev/
tools/webdev) — oba te narzędzia są używane do tworzenia i obsługi aplikacji
internetowych, przy czym build_runner jest używany do testowania lub gdy
wymagana jest większa konfiguracja niż zapewnia webdev.
dartdevc (https://dart.dev/tools/dartdevc) — jest to narzędzie umożliwiające
integrację kompilatora Dart-do-JavaScript z narzędziami Chrome.
27
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Wszystkie wtyczki IDE używają powyższych rozwiązań „pod spodem”, więc możesz skorzy-
stać z pełnego zestawu narzędzi do programowania Dart.
Witaj, świecie
Poniższy kod jest podstawowym skryptem Dart:
main() { // punkt wejścia aplikacji Dart
var a = 'świat'; // deklaracja oraz inicjalizacja zmiennych
print('hello $a'); // wywołanie funkcji w celu wypisania wartości
}
Ten kod zawiera kilka podstawowych funkcji językowych, które wymagają wyróżnienia:
Każda aplikacja w Dart musi mieć punkt wejścia w postaci funkcji najwyższego poziomu
(więcej informacji na temat funkcji najwyższego poziomu można znaleźć w rozdziale 2.), czyli
funkcję main().
Jeśli zdecydujesz się uruchomić ten kod lokalnie na wstępnie skonfigurowanej maszynie
za pomocą zestawu Dart SDK, zapisz zawartość w pliku Dart, a następnie uruchom go
za pomocą narzędzia Dart w terminalu, na przykład dart hello_world.dart. Spowoduje
to wykonanie głównej funkcji skryptu Dart.
Jak widzieliśmy wcześniej, chociaż Dart jest bezpieczny dla typów, adnotacje typów są opcjonalne.
Tutaj deklarujemy zmienną bez typu i przypisujemy do niej literał typu String.
Literał typu String można ująć w pojedyncze lub podwójne cudzysłowy,
na przykład ‘witaj świecie’ lub „witaj świecie”.
Aby wyświetlić dane wyjściowe na konsoli, możesz użyć funkcji print()
(która jest kolejną funkcją najwyższego poziomu).
W przypadku techniki interpolacji ciągów wyrażenie $a wewnątrz literału String
rozwiązuje wartość zmiennej a. Dart wywołuje metodę toString() obiektu.
O interpolacji ciągów dowiemy się więcej w dalszej części tego rozdziału, w sekcji Typy
i zmienne Darta, kiedy będziemy mówić o typie ciągów.
Weź pod uwagę zwracany typ funkcji main, ponieważ zostało to pominięte w przykładzie. Może
zostać zwrócony typ dynamic, który omówimy później.
28
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Jak widzieliśmy wcześniej w sekcji „Pierwsze kroki z Dart”, Dart jest dojrzały i współpracuje
z wieloma narzędziami, co przyczynia się do sukcesu Fluttera. Spójrzmy, dlaczego Dart był
idealnym wyborem dla frameworka Flutter.
Zwiększanie produktywności
Dart to nie tylko język, przynajmniej nie według swojej koncepcji. SDK Darta zawiera zestaw na-
rzędzi (opisanych w poprzedniej sekcji poświęconej narzędziom programistycznym Dart), z któ-
rych korzysta Flutter, pomagając w wykonywaniu typowych zadań w fazie developmentu. Są to:
kompilatory Dart JIT i AOT,
profilowanie, debugowanie i logowanie za pomocą Dart DevTools
oraz Observatory (więcej w rozdziale 12.),
statyczna analiza kodu za pomocą wbudowanego analizatora
(https://dart.dev/guides/language/analysis-options).
Podczas budowania ostatecznej wersji aplikacji kod zostanie skompilowany za pomocą AOT,
a Twoja aplikacja zostanie dostarczona z niewielką wersją maszyny wirtualnej Dart (która bar-
dziej przypomina bibliotekę uruchomieniową) z funkcjami Dart SDK, takimi jak biblioteki
podstawowe oraz odśmiecanie pamięci.
Ta różnica na pierwszy rzut oka nie wydaje się istotna z punktu widzenia programisty, ponie-
waż chcemy po prostu napisać i uruchomić aplikację, prawda? Jednak jeśli chodzi o produktyw-
ność, staje się to jedną z fundamentalnych zalet Darta używanego przez Flutter.
Hot reload Fluttera jest jedną z jego najbardziej znanych funkcji i pokazuje obiecaną produk-
tywność w akcji. Opiera się na kompilacji JIT, aby dokonać wymiany kodu Dart na żywo pod-
czas uruchamiania aplikacji, dzięki czemu możemy zmienić kod naszej aplikacji i zobaczyć
wynik prawie w czasie rzeczywistym. Za sprawą wtyczek IDE staje się to jeszcze szybsze,
ponieważ po zapisaniu zmiany wtyczka przeładowuje kod, a wynik jest szybko widoczny.
29
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W rozdziale 3., „Wprowadzenie do Fluttera”, dokładniej przyjrzymy się tej i wielu innym
funkcjom.
Niezwykle trudno jest opisać potencjał tej niesamowitej funkcji. Dlatego po zapoznaniu się z roz-
działem 3. proponuję uruchomić projekt startowy Fluttera, aby mieć pierwszy kontakt z tym
niesamowitym rozwiązaniem.
To narzędzie pomaga określić potencjalne problemy z typami oraz składnią przed uruchomie-
niem kodu.
Łatwa nauka
Dart to dla wielu programistów nowy język, a nauka nowego frameworku i nowego języka
w tym samym czasie może być wyzwaniem. Jednak Dart upraszcza to zadanie, nie wymyślając
na nowo koncepcji, tylko po prostu ją dostosowując i starając się, aby była jak najbardziej
efektywna w wyznaczonych zadaniach.
Dart jest inspirowany wieloma nowoczesnymi i dojrzałymi językami, takimi jak Java, JavaScript,
C #, Swift i Kotlin, jak widać poniżej:
30
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Mając to na uwadze, czytanie kodu Dart, nawet bez dogłębnej znajomości języka, jest moż-
liwe. Spójrz również na oficjalną stronę startową dokumentacji:
31
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dojrzałość
Pomimo tego, że jest stosunkowo nowym językiem, Dart nie jest ubogi ani nie brakuje mu
zasobów. Wręcz przeciwnie, w wersji 2 ma już różne nowoczesne zasoby językowe, które po-
magają programiście pisać skuteczny kod na wysokim poziomie.
Dzięki niej programiści mogą pisać nieblokujące wywołania o bardzo prostej składni, a apli-
kacja może kontynuować renderowanie bez żadnych przeszkód.
Operator kolekcji if, widoczny na poprzednim zrzucie ekranu, jest świetnym przykładem no-
wej funkcji, która jest łatwa do zrozumienia, nawet jeśli Dart jest dla Ciebie nowy.
Dart ewoluuje wraz z Flutterem, a to tylko niektóre z ważnych korzyści, jakie język zapewnia
platformie. Dopóki zdasz sobie sprawę, że Dart jest łatwy do nauczenia i przyczynia się do
zwiększenia mocy Fluttera, wyzwanie związane z nauką nowego języka wraz z nową strukturą
staje się łatwiejsze, a nawet przyjemne.
32
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
W tej książce nie będziemy zbytnio się zagłębiać w szczegóły składni Dart. Możesz sprawdzić
kod źródłowy tego rozdziału na GitHubie, aby znaleźć przykłady składni i użyć go jako prze-
wodnika do języka. Później możesz odkrywać określoną składnię lub funkcje — w miarę po-
stępów w nauce frameworka Flutter.
Jeśli znasz już język Dart, możesz użyć tej sekcji jako przeglądu składni; w przeciwnym razie
możesz zapoznać się z tym wprowadzeniem: https://dart.dev/guides/language/language-tour.
Operatory
W Dart operatory to nic innego jak metody zdefiniowane w klasach o specjalnej składni. Gdy więc
używasz operatorów, takich jak x == y, wygląda to tak, jakbyś wywoływał metodę x. == (y)
w celu porównania równości.
33
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ta koncepcja oznacza, że operatory można zastąpić, aby dało się napisać dla nich własną lo-
gikę. Ponownie, jeśli masz doświadczenie w Javie, C #, JavaScript lub podobnych językach,
możesz pominąć większość operatorów, ponieważ są one bardzo podobne.
W tej książce nie będziemy zagłębiać się w każdy szczegół składni Darta. Możesz odwo-
łać się do kodu źródłowego w serwisie GitHub, aby zapoznać się z wieloma przykładami
składni tego języka.
Operatory arytmetyczne
Dart jest dostarczany z wieloma typowymi operatorami, które działają podobnie jak w wielu
innych językach:
+ — dodawanie liczb;
- — odejmowanie;
* — mnożenie;
/ — dzielenie;
~ / — dotyczy dzielenia liczb całkowitych. W Dart każde proste dzielenie za
pomocą / daje podwójną wartość. Aby otrzymać tylko część całkowitą, musiałbyś
dokonać jakiejś transformacji (czyli rzutowania typu) w innych językach
programowania; jednakże tutaj zadanie to wykonuje operator dzielenia liczb
całkowitych;
% — operacja modulo (reszta z dzielenia liczb całkowitych);
-expression — negacja (która odwraca znak wyrażenia).
34
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Niektóre operatory zachowują się inaczej w zależności od typu lewego operandu; na przykład
operator + może służyć do sumowania zmiennych typu num, ale także do łączenia łańcuchów.
Dzieje się tak, ponieważ zostały one zaimplementowane inaczej w odpowiednich klasach, jak
wskazano wcześniej.
Operatory inkrementacji i dekrementacji Dart nie różnią się niczym od typowych języków.
Dobrym zastosowaniem operatorów inkrementacji i dekrementacji jest zliczanie liczby ope-
racji w pętlach.
W Dart, w przeciwieństwie do Javy i wielu innych języków, operator == nie porównuje odwo-
łań do pamięci, ale raczej zawartość zmiennej.
Wynik działania tego kodu będzie różny w zależności od kontekstu wykonania. W DartPad
wynik jest prawdą dla sprawdzenia typu double; wynika to ze sposobu, w jaki JavaScript traktuje
35
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
liczby i, jak już wiesz, Dart dla aplikacji webowej jest wstępnie skompilowany do JavaScriptu
w celu wykonania w przeglądarkach internetowych.
Istnieje również słowo kluczowe as, które jest używane do typowania z nadtypu do podtypu,
na przykład konwersji num na int.
Słowo kluczowe as jest również używane do określania przedrostka dla bibliotek w czasie ich
importu (więcej na ten temat można przeczytać w rozdziale 2., „Średnio zaawansowane pro-
gramowanie w Dart”).
Operatory logiczne
Operatory logiczne w Dart są typowymi operatorami stosowanymi do operandów przyjmują-
cych wartości prawda/fałsz; mogą to być zmienne, wyrażenia lub warunki. Dodatkowo można
je stosować ze złożonymi wyrażeniami, łącząc ich wyniki. Występują następujące operatory
logiczne:
!wyrażenie — aby zanegować wynik wyrażenia, to znaczy true na false i false na true;
|| — aby zastosować logiczne OR między dwoma wyrażeniami;
&& — aby zastosować logiczne AND między dwoma wyrażeniami.
Manipulacja bitami
Dart udostępnia operatory bitowe do manipulowania pojedynczymi bitami liczb, zwykle z ty-
pem num. Są one następujące:
& — aby zastosować logiczne AND do operandów, sprawdzając, czy oba
odpowiadające im bity to 1;
| — aby zastosować logiczne OR do operandów, sprawdzając, czy co najmniej
jeden z odpowiednich bitów ma wartość 1;
^ — aby zastosować logiczny XOR do operandów, sprawdzając, czy tylko jeden
(ale nie oba) z odpowiednich bitów ma wartość 1;
~operand — aby odwrócić bity operandu, na przykład 1 stają się 0, a 0 stają się 1;
<< — aby przesunąć lewy operand o x bitów w lewo (wstawiając 0 od prawej);
>> — aby przesunąć lewy operand o x bitów w prawo (odrzucając bity z lewej).
Podobnie jak operatory arytmetyczne, operatory bitowe również mają wersje skrócone i dzia-
łają dokładnie tak samo, jak poprzednio przedstawione; są to <<=, >>=, &=, ^= i |=.
Obliczanie działa w następujący sposób: jeśli wyrażenie1 ma wartość różną od null, zwraca ją;
w przeciwnym razie zwraca wartość wyrażenie2: wyrażenie1 ?? wyrażenie2.
36
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
final i const
Dzięki metodom final i const zmienna nigdy nie będzie mogła zmieniać swojej wartości po
jej przypisaniu:
final value = 1;
Podobnie jak w przypadku słowa kluczowego final, zmienna value nie może zostać zmie-
niona po zainicjowaniu, a jej inicjalizacja musi nastąpić wraz z deklaracją.
Oprócz tego słowo kluczowe const definiuje stałą w czasie kompilacji — wartości const są
znane w czasie kompilacji. Można ich również użyć do uczynienia instancji obiektów lub list
niezmiennymi w następujący sposób:
const list = const [1, 2, 3]
// oraz
const point = const Point(1,2)
Wbudowane typy
Dart jest językiem programowania bezpiecznym dla typów, więc typy są obowiązkowe dla
zmiennych. Chociaż typy są obowiązkowe, adnotacje typu są opcjonalne, co oznacza, że nie
trzeba określać typu zmiennej podczas jej deklarowania. Dart przeprowadza analizę typu, a do-
kładniej omówimy to w sekcji „Inferencja typów — wprowadzenie dynamiki do pokazu”.
Oto wbudowane typy danych w Dart:
liczby (takie jak num, int i double);
wartości logiczne (takie jak bool);
kolekcje (takie jak listy, tablice i mapy);
ciągi i runy (do wyrażania znaków Unicode w ciągu).
37
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Liczby
W Dart liczby reprezentowane są na dwa sposoby:
Int — 64-bitowe nieułamkowe liczby całkowite ze znakiem, takie jak od -263 do 263-1;
Double — Dart reprezentuje ułamkowe wartości liczbowe 64-bitowe
zmiennoprzecinkowe o podwójnej precyzji.
Oba rozszerzają typ num. Dodatkowo w bibliotece dart:math mamy wiele przydatnych funkcji,
które pomagają w obliczeniach.
BigInt
Dart ma również typ BigInt do reprezentowania liczb całkowitych o dowolnej dokładności,
co oznacza, że limit rozmiaru to pamięć RAM działającego komputera. Ten typ może być bardzo
przydatny w zależności od kontekstu; jednak nie ma takiej samej wydajności jak typy num i po-
winieneś wziąć to pod uwagę, decydując się na jego użycie.
JavaScript zawiera koncepcję bezpiecznych liczb całkowitych, którą Dart stosuje podczas
transpilacji. Jednakże, ponieważ JavaScript używa podwójnej precyzji do reprezentowania
parzystych liczb całkowitych, nie mamy do czynienia z przepełnieniem podczas wykonywania
(maxInt * 2).
Teraz możesz rozważyć umieszczenie BigInt wszędzie, gdzie używałbyś liczb całkowitych,
aby uniknąć przepełnień, ale pamiętaj, że BigInt nie ma takiej samej wydajności jak typy int,
co czyni go nieodpowiednim dla wszystkich kontekstów.
Dodatkowo, jeśli chcesz wiedzieć, w jaki sposób Dart VM obsługuje liczby wewnętrznie,
zapoznaj się z sekcją „Dalsza lektura” na końcu tego rozdziału.
Wartości logiczne
Dart udostępnia dwie dobrze znane wartości literałów dla typu bool: true i false.
Typy boolowskie to proste wartości wskazujące na prawdę lub fałsz, które mogą być przydatne
w dowolnej logice. Jedna rzecz, którą być może zauważyłeś, ale którą chcę podkreślić, dotyczy
wyrażeń.
Wiemy już, że operatory, takie jak > lub ==, to nic innego jak metody o specjalnej składni
zdefiniowanej w klasach i oczywiście mają wartość zwracaną, którą można ocenić za pomocą
38
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
warunków. Typem zwracanym wszystkich tych wyrażeń jest więc bool i, jak już wiesz, wyra-
żenia boolowskie są ważne w każdym języku programowania.
Kolekcje
W Dart listy są uważane za takie same jak tablice w innych językach programowania, z kilkoma
przydatnymi metodami do manipulowania elementami.
Listy mają operator [indeks] umożliwiający dostęp do elementów pod danym indeksem, a dodat-
kowo operator + może być użyty do połączenia dwóch list przez zwrócenie nowej listy z lewym
operandem, po którym następuje prawy.
Kolejną ważną rzeczą dotyczącą list Darta jest ograniczenie długości. Listy rosną zgodnie z na-
szymi potrzebami za pomocą metody add, która dołącza element.
Innym sposobem zdefiniowania listy jest ustawienie jej długości podczas tworzenia. Listy o stałym
rozmiarze nie można rozszerzyć, więc to programista musi wiedzieć, gdzie i kiedy używać list
o stałym rozmiarze. Jeśli spróbujesz coś dołączyć do listy lub uzyskać dostęp do nieprawidło-
wych elementów, generowany jest wyjątek.
Ciągi znaków
W Dart ciągi znaków to sekwencja znaków (kod UTF-16), które są używane głównie do re-
prezentowania tekstu. Ciągi mogą być pojedynczymi lub wieloma liniami. Możesz stosować
pojedyncze lub podwójne cudzysłowy (zwykle w przypadku pojedynczych wierszy) oraz po-
trójne cudzysłowy dla ciągów wielowierszowych.
Aby połączyć ciągi, możemy użyć operatora +. Typ string implementuje przydatne operatory
inne niż plus (+). Implementuje on również operator mnożnika (*), w którym ciąg jest powta-
rzany określoną liczbę razy, a operator [indeks] pobiera znak z określonej pozycji indeksu.
Interpolacja ciągów
Dart ma przydatną składnię do interpolacji ciągów znaków: ${}, która działa w następujący
sposób:
main() {
String someString = "To jest ciąg znaków";
print("Wartość ciągu: $someString ");
// wyświetelenie: Wartość ciągu: To jest ciąg znaków
39
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak być może zauważyłeś, kiedy wstawiamy tylko zmienną, a nie wartość wyrażenia, możemy
pominąć nawiasy i bezpośrednio podać identyfikator $.
Dart ma również koncepcję run do reprezentowania bitów UTF-32. Aby uzyskać więcej
informacji, zapoznaj się z przewodnikiem po języku Dart: https://dart.dev/guides/language/
language-tour.
Literały
Możesz użyć składni [] i {} do inicjalizacji zmiennych, takich jak listy i mapy. Oto kilka przykładów
literałów dostarczonych przez język Dart do tworzenia obiektów dla typów wbudowanych:
Teraz możesz się więc zastanawiać, skąd Dart wie, jaki to typ zmiennej, jeśli nie określisz go
w deklaracji.
„Analizator może wywnioskować typy dla pól, metod, zmiennych lokalnych i większości ar-
gumentów typu ogólnego. Gdy analizator nie ma wystarczających informacji, aby wywniosko-
wać określony typ, używa typu dynamicznego”.
40
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Oto przykład:
import 'dart:mirrors';
main() {
var someInt = 1;
print(reflect(someInt).type.reflectedType.toString()); // wyświetla: int
}
Jak widać, w tym przykładzie mamy tylko słowo kluczowe var. Nie określiliśmy żadnego typu,
ale ponieważ użyliśmy literału int (1), narzędzie analizatora mogło pomyślnie go wywnioskować.
Zmienne lokalne pobierają typ wywnioskowany przez analizator podczas inicjalizacji. W poprzed-
nim przykładzie próba przypisania wartości typu String do someInt zakończy się niepowodzeniem.
Jak być może zauważyłeś, a jest typu String oraz typu dynamic. Typ dynamic jest typem specjalnym
i może przyjąć dowolny typ w czasie wykonywania; dlatego też dowolna wartość może być
rzutowana na dynamiczną.
Dart może wywnioskować typy dla pól, zwracanych metod i argumentów typu ogólnego; omó-
wimy każdy z nich bardziej szczegółowo w odpowiednich rozdziałach tej książki.
Analizator Dart działa również na kolekcjach i szablonach; w przykładach map i list w tym
rozdziale użyliśmy inicjalizatora literału dla obu, więc ich typy zostały wywnioskowane.
Dart zapewnia pewną składnię przepływu sterowania, która jest bardzo podobna do innych
języków programowania; wygląda to następująco:
if-else,
switch/case,
41
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Składnia Dart dla tych przepływów sterowania nie ma żadnych ważnych szczegółów, które wy-
magają szczegółowego przeglądu. Aby uzyskać dokładne informacje, zapoznaj się z oficjalnym
przewodnikiem: https://dart.dev/guides/language/language-tour#control-flow-statements.
Funkcje
W Dart Function jest typem, tak jak String lub num. Oznacza to, że można go również przypi-
sywać do pól lub zmiennych lokalnych bądź przekazywać jako parametry do innych funkcji;
rozważ następujący przykład:
String sayHello() {
return "Witaj świecie!";
}
void main() {
var sayHelloFunction = sayHello; // Przypisanie funkcji
// do zmiennej
print(sayHelloFunction()); // wyświetla Witaj świecie!
}
W tym przykładzie zmienna sayHelloFunction przechowuje samą funkcję sayHello i nie wywo-
łuje jej. Później możemy ją wywołać, dodając () do nazwy zmiennej, tak jakby była to funkcja.
Zwracany typ funkcji można również pominąć, analizator Dart wnioskuje o typie na podstawie
instrukcji return. Jeśli nie podano instrukcji return, przyjmuje się, że zwracana wartość to
null. Jeśli chcesz zaznaczyć, że funkcja nic nie zwraca, zastosuj słowo kluczowe void:
sayHello() { // Zwracana wartość jest typu String
return "Hello world!";
}
Innym sposobem zapisania tej funkcji jest użycie skróconej składni () => wyrażenie;, które
jest również nazywane funkcją Arrow lub funkcją Lambda:
sayHello() => "Witaj świecie!";
Nie możesz pisać instrukcji zamiast wyrażenia, ale możesz użyć już znanych wyrażeń warun-
kowych (czyli?: lub ??).
42
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
W tym przykładzie funkcja sayHello jest funkcją najwyższego poziomu. Innymi słowy,
nie potrzebuje klasy, aby istnieć. Chociaż Dart jest językiem zorientowanym obiektowo,
nie jest konieczne pisanie klas w celu hermetyzacji funkcji.
Parametry funkcji
Funkcja może mieć dwa typy parametrów: opcjonalne i wymagane. Ponadto, podobnie jak w przy-
padku większości współczesnych języków programowania, parametry te można odpowiednio
nazwać, aby uczynić kod bardziej czytelnym.
Nie trzeba określać typu parametru; w tym przypadku parametr przyjmuje typ dynamiczny:
Wymagane parametry — tę prostą definicję funkcji z parametrami uzyskuje się,
po prostu definiując je w taki sam sposób, jak w większości innych języków.
W poniższej funkcji zarówno name, jak i additionalMessage są parametrami
wymaganymi, więc wywołujący musi je przekazać:
sayHello(String name, String additionalMessage) => "Witaj $name.
$additionalMessage";
Parametry opcjonalne pozycyjne — czasami nie wszystkie parametry muszą być
obowiązkowe dla funkcji, można więc zdefiniować również parametry opcjonalne.
Opcjonalna definicja parametru pozycyjnego jest wykonywana przy użyciu składni [].
Opcjonalne parametry pozycyjne muszą występować za wszystkimi wymaganymi
parametrami w następujący sposób:
sayHello(String name, [String additionalMessage]) => "Witaj $ name.
$additionalMessage";
Jeśli uruchomisz powyższy kod bez przekazywania wartości dla additionalMessage, na końcu
zwróconego ciągu zostanie wyświetlona wartość null. Jeśli opcjonalny parametr nie jest określony,
domyślną wartością jest null, chyba że określisz dla niego wartości domyślne:
void main() {
print(sayHello('mój przyjacielu')); // Witaj mój przyjacielu. null
print(sayHello('mój przyjacielu', "Jak się masz?"));
// Witaj mój przyjacielu. Jak się masz?
}
43
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Parametry nazwane nie występują wyłącznie dla parametrów opcjonalnych; aby parametr na-
zwany był parametrem wymaganym, możesz oznaczyć go jako @required:
sayHello(String name, {@required String additionalMessage}) => "Witaj $name. $
additionalMessage";
Funkcja anonimowa to funkcja, która nie ma nazwy; jest również nazywana lambda lub do-
mknięcie (closure). Funkcja forEach() jest tego dobrym przykładem; musimy przekazać jej
funkcję, która będzie wykonywana na każdym z elementów kolekcji listy:
void main () {
var list = [1, 2, 3, 4];
list.forEach((number) => print('witaj $number'));
}
Nasza funkcja anonimowa otrzymuje element, ale nie określa typu; następnie po prostu wy-
pisuje wartość otrzymaną przez parametr.
Zasięg leksykalny — zasięg w Dart jest określany przez layout kodu przy użyciu
nawiasów klamrowych, jak w wielu językach programowania; funkcje wewnętrzne
mają dostęp do zmiennych aż do poziomu globalnego:
globalFunction() {
print("funkcja globalna/najwyższego poziomu");
44
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
}
simpleFunction() {
print("prosta funkcja");
globalFunction() {
print("Nie do końca globalna");
}
globalFunction();
}
main() {
simpleFunction();
globalFunction();
}
W funkcji main natomiast używana jest wersja globalna funkcji globalFunction, ponieważ
w tym zakresie wewnętrzna funkcja globalFunction z simpleFunction nie jest zdefiniowana.
Typy ogólne
Składnia <..> służy do określenia typu obsługiwanego przez kolekcję. Jeśli spojrzysz na po-
przednie przykłady list i map, zauważysz, że nie określiliśmy żadnego typu. Dzieje się tak,
ponieważ są one opcjonalne, a Dart może wywnioskować typ na podstawie elementów podczas
inicjowania kolekcji.
Sprawdź kod źródłowy tego rozdziału w serwisie GitHub, aby zapoznać się z przykła-
dami dotyczącymi kolekcji i typów ogólnych. Pamiętaj, że jeśli narzędzie analizatora Dart
nie może wywnioskować typu, przyjmuje typ dynamiczny.
45
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jeśli jednak określimy typ String dla listy, ten kod nie będzie się kompilował:
main() {
List<String> avengerNames = ["Hulk", "Captain America"];
avengerNames.add(1);
// Teraz, funkcja add() oczekuje typu 'int' więc się nie kompiluje
print("nazwy Avenger: $avengerNames");
}
Określenie typu listy w tym przypadku wydaje się zbędne, ponieważ analizator Dart wywnio-
skuje typ ciągu na podstawie dostarczonych przez nas literałów. Jednak w niektórych przypadkach
jest to ważne, na przykład podczas inicjowania pustej kolekcji, jak w poniższym przykładzie:
var emptyStringArray = <String>[];
Jeśli nie określimy typu pustej kolekcji, mogłaby ona zawierać dowolny typ danych, ponieważ
nie wywnioskowałaby, jaki typ ogólny należy przyjąć.
Aby dowiedzieć się, jak Dart radzi sobie z typami ogólnymi i dodatkowymi strukturami da-
nych dostarczonymi przez język, możesz zapoznać się ze szczegółami w oficjalnym przewodniku:
https://dart.dev/guides/language/language-tour#generics.
46
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Nazywa się je funkcjami pierwszej klasy, ponieważ są one traktowane tak samo jak inne typy.
Kolejną ważną kwestią, na którą należy zwrócić uwagę, jest to, że Dart obsługuje pojedyncze
dziedziczenie w klasie, podobnie jak w Javie i większości innych języków, co oznacza, że klasa
może dziedziczyć bezpośrednio tylko z jednej klasy naraz.
Klasa może implementować wiele interfejsów i rozszerzać wiele klas za pomocą domie-
szek (mixins), które omówimy w dalszej części tego rozdziału.
Oto główne artefakty OOP, które są prezentowane w języku Dart (w tym rozdziale zagłębimy
się w każdy z nich):
Klasa — to plan tworzenia obiektu.
Interfejs — jest to definicja konfiguracji z zestawem metod dostępnych w obiekcie.
Chociaż w Dart nie ma jawnego typu interfejsu, możemy osiągnąć cel interfejsu
za pomocą klas abstrakcyjnych.
Klasa wyliczeniowa — jest to specjalny rodzaj klasy, który definiuje zestaw
wspólnych wartości stałych.
Domieszka — jest to sposób ponownego wykorzystania kodu klasy w wielu
hierarchiach klas.
Właściwości OOP
Każdy język programowania może zapewnić paradygmat OOP na swój sposób, z częściowym
lub pełnym wsparciem, stosując niektóre lub wszystkie z następujących zasad — zobacz ry-
sunek na następnej stronie.
Dart stosuje wiele zasad z wieloma szczegółami. Zastosujmy dostępne techniki i struktury OOP,
aby używać tego paradygmatu w języku Dart.
47
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Wskazane tutaj tematy mogą być dla Ciebie nowe. Bardziej szczegółowo omówiono je
w następnych sekcjach tego rozdziału. Jeśli uznasz to za pomocne, wróć do tej sekcji
później.
Obiekty i klasy
Punktem wyjścia OOP, czyli obiektów, są instancje zdefiniowanych klas. W Dart, jak już zo-
stało powiedziane, wszystko jest obiektem, to znaczy każda wartość, którą możemy przecho-
wywać w zmiennej, jest instancją klasy. Poza tym wszystkie obiekty rozszerzają klasę Object,
bezpośrednio lub pośrednio:
Klasy Dart mogą mieć zarówno elementy należące do instancji (metody i pola),
jak i elementy należące do klasy (metody i pola statyczne).
Klasy Dart nie obsługują przeciążania konstruktora, ale można użyć elastycznej
specyfikacji argumentów funkcji (opcjonalnych, pozycyjnych i nazwanych),
aby zapewnić różne sposoby tworzenia wystąpienia klasy. Możesz także skorzystać
z nazwanych konstruktorów.
Hermetyzacja
Dart nie zawiera jawnych ograniczeń dostępu — inaczej niż w Javie, gdzie występują słowa
kluczowe protected, private i public. W Dart hermetyzacja zachodzi na poziomie biblioteki
zamiast na poziomie klasy (zostanie to omówione w następnym rozdziale).
48
d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart
Dziedziczenie i kompozycja
Dziedziczenie pozwala nam rozszerzyć obiekt do wyspecjalizowanych wersji pewnego typu
abstrakcyjnego. W Dart, po prostu deklarując klasę, już niejawnie rozszerzamy typ Object.
Ponadto:
Dart zezwala na pojedyncze bezpośrednie dziedziczenie.
Dart ma specjalne wsparcie dla domieszek, które mogą być używane do rozszerzania
funkcjonalności klas bez bezpośredniego dziedziczenia, symulowania
wielokrotnego dziedziczenia i ponownego wykorzystywania kodu.
Dart nie zawiera dyrektywy final class, tak jak inne języki; to znaczy,
klasa może być zawsze rozszerzona (mieć dzieci).
Abstrakcja
Po dziedziczeniu, abstrakcja jest procesem, w którym definiujemy typ i jego podstawowe
cechy, opierając się na konkretnych typach rodzica. Ponadto:
Dart zawiera klasy abstrakcyjne, które pozwalają zdefiniować, co coś robi / zapewnia,
bez dbania o to, jak to jest zaimplementowane.
Dart ma potężną, niejawną koncepcję interfejsu, która sprawia, że każda klasa
jest interfejsem, umożliwiając jej implementację przez innych bez jej rozszerzania.
Polimorfizm
Polimorfizm osiąga się przez dziedziczenie i można go traktować jako zdolność obiektu do zacho-
wania się jak inny obiekt; na przykład typ int jest również typem num. Ponadto:
Dart umożliwia zastępowanie metod nadrzędnych w celu zmiany ich oryginalnego
zachowania.
Dart nie pozwala na przeciążenie w sposób, który możesz znać. Nie można
dwukrotnie zdefiniować tej samej metody z różnymi argumentami. Możesz
symulować przeciążenie, używając elastycznych definicji argumentów
(czyli opcjonalnych i pozycyjnych, jak pokazano w poprzedniej sekcji Funkcje)
lub w ogóle go nie używać.
49
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Podsumowanie
Zakończyliśmy nasze wprowadzenie do języka Dart i mam nadzieję, że spodobało Ci się to,
co przeczytałeś do tej pory. W tym pierwszym rozdziale przedstawiliśmy dostępne narzędzia
— niezbędne do rozpoczęcia nauki języka Dart, oraz odkryliśmy, jak wygląda podstawowy
program Dart i poznaliśmy podstawową strukturę kodu Dart.
Pokazaliśmy, jak działa Dart SDK oraz narzędzia, które pomagają w tworzeniu aplikacji Flutter
i sprawiają, że platforma Flutter odniosła sukces w realizacji jej celów.
Dalsza lektura
Oprócz treści tego rozdziału, w celu uzyskania dalszych informacji możesz zapoznać się z nastę-
pującymi materiałami:
Więcej informacji na temat reprezentacji liczb całkowitych w Dart można znaleźć
w następującym artykule, który może pomóc w zrozumieniu, jak język traktuje liczby
wewnętrznie: https://www.dartlang.org/articles/dart-vm/numeric-computation.
Więcej o składni możesz przeczytać tutaj: https://github.com/dart-lang/sdk/
blob/master/pkg/dev_compiler/doc/GENERIC_METHODS.md
50
d0765ad53fb82babda2278a311da7afb
d
2
Średnio zaawansowane
programowanie
w języku Dart
W tym rozdziale poznasz podstawową koncepcję obiektów w Dart, dowiesz się na przykład,
jak tworzyć kod zorientowany obiektowo w Dart, używając elementów takich, jak interfejsy,
interfejsy niejawne, klasy abstrakcyjne, a także domieszki, aby dodać zachowanie w klasie.
Jeśli jesteś doświadczonym programistą lub znasz już Javę bądź podobne języki, możesz po-
minąć niektóre części tego rozdziału, ponieważ zawiera on wiele podobieństw do typowych
koncepcji OOP, takich jak dziedziczenie i hermetyzacja. Ważne jest, abyś zweryfikował niektóre
tematy, nawet jeśli znasz już większość funkcji OOP, takich jak interfejsy niejawne i domieszki,
ponieważ mogą one wprowadzić Cię w nowe koncepcje.
Dowiesz się również, jak korzystać z bibliotek innych firm, aby przyspieszyć rozwój projektu,
zrozumiesz zaawansowane funkcje języka Dart, aby rozpocząć tworzenie aplikacji wielowąt-
kowych za pomocą wywołań zwrotnych (callback) oraz obiektów typu futures. Dowiesz się
także, jak przeprowadzić testy jednostkowe.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
main() {
Person somePerson = new Person();
somePerson.firstName = "Clark";
somePerson.lastName = "Kent";
print(somePerson.getFullName()); // wyświetla Clark Kent
}
Przyjrzyjmy się teraz klasie Person zadeklarowanej w poprzednim kodzie i zwróćmy uwagę
na kilka kwestii:
Aby utworzyć instancję klasy, używamy słowa kluczowego new (opcjonalne),
po którym następuje wywołanie konstruktora. W miarę poznawania treści tej
książki zauważysz, że to słowo kluczowe jest używane rzadziej.
Nie ma jawnie zadeklarowanej klasy nadrzędnej, ale ma jedną, powstałą w wyniku
dziedziczenia niejawnego (jak już wspomniano).
Zawiera dwa pola, firstName i lastName, oraz metodę getFullName(), która łączy
oba pola za pomocą interpolacji ciągów, a następnie zwraca dane.
Nie ma zadeklarowanych metod dostępowych get ani set, więc w jaki sposób
uzyskaliśmy dostęp do firstName i lastName, aby je zmutować? Dla każdego pola
w klasie definiowane są domyślne metody get / set.
Notacja z kropką class.member jest używana w celu uzyskania dostępu do elementu
klasy, cokolwiek to jest — metody lub pola (pobierz / ustaw).
52
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Nie zdefiniowaliśmy konstruktora dla tej klasy, ale, jak pewnie się domyślasz, jest
już dostarczony domyślny pusty konstruktor (bez argumentów).
Zwróć uwagę, że definiujesz tylko nazwy wartości. Typy wyliczeniowe to specjalne typy z zesta-
wem skończonych wartości, które mają właściwość index reprezentującą jego wartość. Zobaczmy
teraz, jak to działa.
Najpierw dodajemy pole do naszej wcześniej zdefiniowanej klasy Person, aby przechowywać
jego typ:
class Person {
...
PersonType type;
...
}
Możesz zobaczyć, że właściwość index ma wartość zero, na podstawie pozycji deklaracji wartości.
Możesz również zobaczyć, że wywołujemy bezpośrednio metodę values dla PersonType. Jest
to statyczny element, należący do typu enum, który po prostu zwraca listę ze wszystkimi jej
wartościami. Wkrótce zbadamy to dalej.
Notacja kaskadowa
Widzieliśmy, że Dart zapewnia notację kropkową, aby uzyskać dostęp do elementu należą-
cego do klasy. Oprócz tego możemy również użyć notacji podwójnej kropki / kaskady, lukru
składniowego (syntactic sugar), który pozwala nam skorzystać z kilku sekwencji operacji na
tym samym obiekcie:
53
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
main() {
Person somePerson = new Person()
..firstName = "Clark"
..lastName = "Kent";
print(somePerson.getFullName()); // wyświetla Clark Kent
}
Rezultat jest taki sam, jak przy zastosowaniu typowego podejścia. To po prostu dobry sposób
na napisanie zwięzłego i czytelnego kodu.
Następnie zagłębimy się w każdy z wymienionych wcześniej składników klasy, aby zrozumieć,
w jaki sposób można je wykorzystać do rozszerzenia klasy w odpowiedzi na wszystkie nasze
potrzeby.
Konstruktory
Aby utworzyć instancję klasy, używamy słowa kluczowego new, a po nim odpowiedniego kon-
struktora z parametrami, jeśli jest to wymagane. Teraz zmieńmy klasę Person i zdefiniujmy
konstruktor z parametrami:
class Person {
String firstName;
String lastName;
main() {
// Person somePerson = new Person(); to by się nie skompilowało
// ponieważ zdefiniowaliśmy wymagane parametry w konstruktorze
Person somePerson = new Person("Clark", "Kent");
print(somePerson.getFullName());
}
Konstruktor jest także funkcją w Dart i jego rolą jest prawidłowe zainicjowanie instancji klasy.
Jako funkcja może mieć wiele cech typowych dla funkcji Dart, takich jak argumenty — wymagane
lub opcjonalne oraz nazwane lub pozycyjne. W poprzednim przykładzie konstruktor ma dwa
obowiązkowe argumenty.
54
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Jeśli zajrzysz do treści naszego konstruktora, zobaczysz, że używa słowa kluczowego this.
Ponadto nazwy parametrów konstruktora są takie same jak nazwy pól, co może powodować
niejednoznaczność. Aby tego uniknąć, poprzedzamy pola instancji obiektu słowem kluczo-
wym this w kroku przypisywania wartości.
Dart zapewnia inny sposób tworzenia konstruktora, takiego jak ten podany w przykładzie,
przy użyciu składni skrótu:
// ... definicja pól klas
// składnia inicjalizacji skrótu
Person(this.firstName, this.lastName);
Możemy pominąć treść konstruktora, ponieważ ustawia ona tylko wartości pól klasy.
Konstruktory nazwane
W przeciwieństwie do Javy i wielu innych języków Dart nie ma przeciążenia przez redefini-
cję, więc aby zdefiniować alternatywne konstruktory dla klasy, musisz użyć konstruktorów
nazwanych:
// ... definicja pól klas
// inne konstruktory
Person.anonymous() {}
Wyłączną różnicą w porównaniu z prostą metodą jest to, że konstruktory nie mają instrukcji
return, ponieważ jedyne, co muszą zrobić, to poprawnie zainicjować instancję obiektu.
Konstruktory nazwane zobaczymy w akcji w rozdziałach poświęconych Flutterowi, po-
nieważ framework używa ich często do inicjowania definicji widżetów.
Konstruktory factory
Inną przydatną składnią w Dart jest konstruktor factory, który pomaga zastosować wzorzec
factory, technikę tworzenia, umożliwiającą tworzenie instancji klas bez określania dokład-
nego typu obiektu wynikowego. Załóżmy, że mamy następujących potomków klasy Person:
class Student extends Person {
Student(firstName, lastName): super(firstName, lastName);
}
Jak widać, klasy potomne są nadal prawie takie same jak klasa Person, ponieważ nie dodają
jeszcze żadnych konkretnych funkcji.
55
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Możemy zdefiniować konstruktor factory w klasie Person, aby utworzyć wystąpienie odpowiedniej
klasy na podstawie wymaganego argumentu type:
class Person {
String firstName;
String lastName;
Person([this.firstName, this.lastName]);
Konstruktor factory jest określany przez dodanie słowa kluczowego factory, po którym na-
stępuje definicja konstruktora, zwykle w klasie bazowej lub klasie abstrakcyjnej. W naszym
przypadku klasa Person definiuje konstruktor factory nazwany na podstawie PersonType okre-
ślonego w argumencie. Jeśli żaden typ nie zostanie przekazany, utworzona zostanie prosta
klasa Person — przy użyciu jej domyślnego konstruktora.
Inną ważną rzeczą, na którą należy zwrócić uwagę, jest to, że konstruktor fabric nie zastępuje
domyślnego konstruktora klas. W związku z tym nadal można utworzyć instancję bezpośred-
nio lub z jego potomków.
Możemy zmodyfikować naszą klasę Person, aby zastąpić starą metodę getFullName() i dodać
ją jako metodę pobierającą, jak pokazano w poniższym bloku kodu:
class Person {
String firstName;
String lastName;
Person(this.firstName, this.lastName);
56
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Person.anonymous() {}
main() {
Person somePerson = new Person("clark", "kent");
Moglibyśmy również stworzyć metodę ustawiającą dla fullName i zdefiniować logikę, która
odpowiada za ustawianie wartości firstName i lastName:
class Person {
// ... definicja pól klas
set fullName (String fullName) {
var parts = fullName.split ("");
this.firstName = parts.first;
this.lastName = parts.last;
}
}
W ten sposób ktoś mógłby zainicjować imię osoby, ustawiając fullName, a wynik byłby taki
sam. (Oczywiście nie przeprowadziliśmy żadnych kontroli w celu ustalenia, czy wartość prze-
kazana jako fullName jest prawidłowa, to znaczy czy nie jest pusta, składa się z dwóch lub
więcej elementów itd.).
57
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
class Person {
// ... definicja pól klas
static String personLabel = "Imię osoby:";
String get fullName => "$personLabel $firstName $lastName";
// zmodyfikowano w celu wydrukowania nowego pola statycznego „personLabel”
}
Person.personLabel = "imię:";
Pola statyczne są skojarzone z klasą, a nie z jakąkolwiek instancją obiektu. To samo dotyczy
definicji metod statycznych. Możemy dodać statyczną metodę, aby shermetyzować wyświe-
tlanie imienia, jak pokazano w poniższym kodzie:
class Person {
// ... definicja pól klas
static String personLabel = "Imię osoby:";
Następnie możemy użyć tej metody do wyświetlenia danych instancji Person, tak jak robili-
śmy to wcześniej:
main() {
Person somePerson = Person("clark", "kent");
Person anotherPerson = Person("peter", "parker");
Person.personLabel = "imię:";
Moglibyśmy zmodyfikować funkcję pobierającą fullName w klasie Person, aby nie używać pola
statycznego personLabel i uzyskać różne wyniki zgodnie z naszymi wymaganiami:
class Person {
// ... definicja pól klas
58
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
}
main() {
Person somePerson = Person("clark", "kent");
Person anotherPerson = Person("peter", "parker");
Jak widać, statyczne pola i metody pozwalają nam ogólnie dodawać określone zachowania do klas.
Dziedziczenie klas
Oprócz niejawnego dziedziczenia do typu Object, Dart pozwala nam rozszerzać zdefiniowane
klasy za pomocą słowa kluczowego extends, w którym dziedziczone są wszystkie elementy
członkowskie klasy nadrzędnej, z wyjątkiem konstruktorów.
Teraz spójrzmy na następujący przykład, w którym tworzymy klasę potomną dla istniejącej
klasy Person:
class Student extends Person {
String nickName;
@override
String toString() => "$fullName, znany jako $nickName";
}
main() {
Student student = new Student("Clark", "Kent", "Kal-El");
print(student);// to samo, co wywołanie student.toString()
// wypisuje Clark Kent, znany jako Kal-El
}
59
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Metoda toString()
Świetnym typowym przykładem przesłaniania zachowania rodzica jest metoda toString().
Celem tej metody jest zwrócenie reprezentacji String obiektu:
class Student extends Person {
// ... fullName (z klasy Person) i inne pola
@override
String toString () => "$ fullName, znany również jako $ nickName";
}
main() {
Student student = new Student("Clark", "Kent", "Kal-El");
print("To jest student: $student");
// wyświetla: To jest student: Clark Kent, znany również jako Kal-El
// wywołuje również niejawnie toString() studenta
}
Jak widać, dzięki temu kod jest bardziej przejrzysty i zapewniamy dobrą reprezentację tek-
stową obiektu, która może pomóc w zrozumieniu logów, formatowaniu tekstu i nie tylko.
Klasy abstrakcyjne
W OOP klasy abstrakcyjne to klasy, dla których nie można utworzyć instancji.
Na przykład nasza klasa Person może być abstrakcyjna, a w kontekście programu istnieje jako
instancja klasy Student lub inny podtyp:
abstract class Person {
// ... zawartość klasy została ukryta dla zachowania zwięzłości
}
Jedyne, co musimy tutaj zmienić, to początek definicji klasy, oznaczając ją jako abstract:
main() {
Person student = new Student("Clark", "Kent", "Kal-El"); // działa jak
//instancja podtypu
// Person p = new Person();
// dla klasy abstrakcyjnej nie można utworzyć instancji
print(student);
}
60
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Jak widać, nie możemy już utworzyć instancji klasy Person, tylko jej podtyp Student.
Klasa abstrakcyjna może mieć abstrakcyjne elementy członkowskie bez implementacji, co pozwala
na implementację przez typy potomne, które je rozszerzają:
abstract class Person {
String firstName;
String lastName;
Person(this.firstName, this.lastName);
Funkcja pobierająca fullName z poprzedniej klasy Person jest teraz abstrakcyjna, ponieważ nie
ma implementacji. Obowiązkiem dziecka jest jej implementacja:
class Student extends Person {
//... other class members
@override
String get fullName => "$firstName $lastName";
}
Klasa Student implementuje metodę pobierającą fullName, ponieważ w przeciwnym razie nie
bylibyśmy w stanie skompilować kodu.
Interfejsy
Dart nie ma słowa kluczowego interface, ale pozwala Ci używać interfejsów w nieco inny
sposób niż to, do czego jesteś przyzwyczajony. Wszystkie deklaracje klas są same w sobie in-
terfejsami. Oznacza to, że definiując klasę w Dart, definiujesz również interfejs, który można
zaimplementować, a nie tylko rozszerzyć o inne klasy. W świecie Darta nazywa się go inter-
fejsem niejawnym.
Na tej podstawie nasza poprzednia klasa Person jest również interfejsem Person, który mógłby
być zaimplementowany zamiast rozszerzony przez klasę Student:
class Student implements Person {
String nickName;
@override
String firstName;
@override
String lastName;
@override
String get fullName => "$firstName $lastName";
61
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
@override
String toString() => "$fullName, znany również jako $nickName";
}
Zauważ, że generalnie kod nie zmienia się zbytnio, z wyjątkiem tego, że członkowie są teraz zde-
finiowani w klasie Student. Klasa Person to tylko kontrakt, który klasa Student przyjęła i musi
wdrożyć.
Jeśli chcesz zadeklarować jawny interfejs, wystarczy utworzyć klasę abstrakcyjną bez
żadnej implementacji, tylko z definicjami członków, i będzie to czysty interfejs, gotowy
do implementacji.
Bez względu na to, jak zadeklarujesz domieszkę, może ona być również używana jako
interfejs, ponieważ ujawnia członków.
Teraz sprawdźmy przykład deklarowania funkcjonalności, którą mogłaby mieć nasza poprzed-
nia klasa Person.
Zastanówmy się, jakie zawody może wykonywać dana osoba — niektórzy ludzie mogą mieć
określone i wspólne umiejętności. Domieszki mogą być w tym przypadku idealne, ponieważ
możemy dodać umiejętności do zawodu bez konieczności rozszerzania wspólnej, bardziej
ogólnej klasy lub implementowania interfejsu w każdej z nich. Ponieważ implementacja byłaby
prawdopodobnie taka sama, spowodowałoby to powielanie kodu:
// Definicja klasy osoby
class ProgrammingSkills {
coding() {
62
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
print("pisanie kodu...");
}
}
class ManagementSkills {
manage() {
print("zarządzanie projektem...");
}
}
Obie klasy będą miały metodę coding() bez konieczności implementowania jej w każdej klasie,
ponieważ jest już zaimplementowana w domieszce ProgrammingSkills.
mixin ManagementSkills {
63
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
manage() {
print("zarządzanie projektem ...");
}
}
Inną rzeczą, którą możemy zrobić, jest ograniczenie klas, które mogą używać określonej do-
mieszki. Aby to zrobić, musimy określić wymaganą nadklasę za pomocą słowa kluczowego on:
mixin ProgrammingSkills on Developer {
coding() {
print ("pisanie kodu ...");
}
}
Domieszki ograniczone przez słowo kluczowe on wymagają, aby klasa docelowa miała
konstruktora bez argumentów.
Klasy wywoływane
Podobnie jak funkcje Dart są niczym więcej niż obiektami, klasy Dart mogą również zacho-
wywać się jak funkcje, to znaczy mogą być wywoływane, pobierają argumenty i zwracają wynik.
Składnia emulowania funkcji w klasie jest następująca:
class ShouldWriteAProgram { // to jest prosta klasa
String language;
String platform;
ShouldWriteAProgram(this.language, this.platform);
// ta specjalna metoda o nazwie 'call' sprawia, że klasa zachowuje się jak funkcja
bool call(String category) {
if(language == "Dart" && platform == "Flutter") {
return category != "to-do";
}
64
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
return false;
}
}
main() {
var shouldWrite = ShouldWriteAProgram("Dart", "Flutter");
Jak widać, zmienna shouldWrite jest obiektem, instancją klasy ShouldWriteAProgram, ale można ją
również wywołać jako normalną funkcję z parametrem i wartością zwracaną. Jest to możliwe
dzięki istnieniu metody call() zdefiniowanej w klasie.
Metoda call() jest specjalną metodą w Dart. Każda klasa, która ją definiuje, może zachowy-
wać się jak normalna funkcja Dart.
Jeśli przypiszesz wywoływalną klasę do zmiennej typu funkcja, zostanie ona niejawnie
przekonwertowana na typ funkcji i będzie zachowywać się jak normalna funkcja.
Najwyższy poziom pisania funkcji jest już znany z rozdziału 1., w którym napisaliśmy najsłyn-
niejszą funkcję Darta: punkt wejścia każdej aplikacji, main(). W przypadku zmiennych sposób
deklarowania jest taki sam. Po prostu zostawiamy ją poza zakresem funkcji, aby była dostępna
globalnie w aplikacji / pakiecie:
var globalNumber = 100;
final globalFinalNumber = 1000;
void printHello() {
print("""Dart z zakresu globalnego.
To jest wartość najwyższego poziomu: $globalNumber
To jest ostateczna wartość najwyższego poziomu: $globalFinalNumber
""");
}
main() {
// najsłynniejsza funkcja najwyższego poziomu Dart
printHello(); // wyświetla domyślną wartość
globalNumber = 0;
// globalFinalNumber = 0; // nie kompiluje się, ponieważ jest to zmienna typu final
printHello(); // wyświetla nową wartość
}
65
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, zmienne i funkcje nie muszą być powiązane z klasą, aby istnieć. To elastyczność
proponowana przez język Dart, która daje programiście możliwość pisania prostego i spójnego
kodu, nie zapominając o wzorcach i funkcjach współczesnych języków.
Zanim przejdziemy do omawiania pakietów Darta, musimy zrozumieć, jak działa najmniejsza
jednostka, z której składa się biblioteka. Najpierw zbadajmy, jak w naszym pakiecie korzystać
z biblioteki, a następnie nauczmy się definiować bibliotekę w Dart.
class Person {
String firstName;
String lastName;
PersonType type;
Person([this.firstName, this.lastName]);
66
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
void main() {
Ponieważ pliki znajdują się w tym samym katalogu, ścieżka importu to tylko nazwa pliku. Po
dodaniu instrukcji import możemy użyć dowolnego dostępnego z niej kodu — w taki sam
sposób, jak zrobiliśmy to z klasami Person i Student.
Możemy również określić identyfikatory, których jawnie nie chcemy importować, używając
słowa kluczowego hide. W tym przypadku zaimportujemy wszystkie identyfikatory z biblioteki poza
tymi po słowie kluczowym hide:
// import ‘person_lib.dart’ hide Pracownik;
67
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Na szczęście Dart ma sposób, aby to obejść. Można skorzystać ze słowa kluczowego as, które można
dodać po instrukcji import, aby ustawić przedrostek dla wszystkich identyfikatorów z impor-
towanej biblioteki:
import 'a.dart' as libraryA;
import 'b.dart' as libraryB;
void main() {
libraryA.Person personA = libraryA.Person("Clark", "Kent");
print("Osoba A: ${personA.fullName}");
Jak widać, bez tego przedrostka nie mamy możliwości zidentyfikowania, której klasy Person
użyć. To samo dotyczy każdego identyfikatora biblioteki publicznej, takiego jak funkcja lub
zmienna. Po określeniu prefiksu musimy dodawać go do każdego wywołania członka tej biblioteki,
nie tylko do wywołań powodujących konflikt.
Rodzaje importu
W poprzednich przykładach zaimportowaliśmy lokalną bibliotekę plików, która znajduje się
w tym samym katalogu co aplikacja, więc podaliśmy tylko nazwę pliku.
68
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Jednakże, w przypadku korzystania z pakietów Dart innych firm, tak nie jest. W takim przy-
padku pliki nie będą istniały w tym samym katalogu, więc przyjrzyjmy się, jak możemy zaim-
portować zewnętrzną bibliotekę pakietu Dart.
Istnieje kilka sposobów określania ścieżek do bibliotek w instrukcji import, a my już używali-
śmy dwóch z nich: względnego importu plików i importowania z pakietu. Przyjrzyjmy się
temu teraz bardziej szczegółowo.
Załóżmy, że mamy katalog zawierający mały pakiet foo z dwoma plikami: a.dart i b.dart.
Aby je zaimportować, możemy użyć wielu podejść:
Względnej ścieżki do pliku — jest ona podobna do metody, której użyliśmy
w poprzednim przykładzie, ponieważ biblioteki znajdowały się w tym samym
folderze. Możemy po prostu umieścić względną ścieżkę do pliku biblioteki, który
chcemy zaimportować, w następujący sposób:
import 'foo/a.dart';
import 'foo/b.dart';
Bezwzględnej ścieżki do pliku — możemy dodać bezwzględną ścieżkę do pliku
biblioteki, dodając przedrostek file:// do ścieżki importu:
import "file:///c:/dart_package/foo/a.dart";
import "file:///c:/dart_package/foo/b.dart";
Chociaż jest to możliwe, import bezwzględny nie jest zalecany i jest to zły sposób im-
portowania bibliotek, ponieważ w rozproszonych środowiskach programistycznych
prawdopodobnie spowoduje problemy podczas lokalizowania plików.
Adres URL w internecie — w taki sam sposób, jak przy użyciu bezwzględnej
ścieżki do pliku, możemy dodać adres URL witryny internetowej zawierającej
kod źródłowy biblioteki — za pomocą protokołu http://:
import „http://dartpackage.com/dart_package/foo/a.dart”;
Pakiet — to najpowszechniejszy sposób importowania biblioteki. Tutaj określamy
ścieżkę do biblioteki z katalogu głównego pakietu. W dalszej części tego rozdziału
zbadamy definicję pakietów; w przypadku importu lokalnej biblioteki przechodzi
ona od katalogu głównego pakietu, w dół drzewa źródłowego, aż do pliku biblioteki:
import 'package:my_package/foo/a.dart';
import 'package:my_package/foo/b.dart';
Ta metoda jest zalecanym sposobem importowania bibliotek, ponieważ działa dobrze z biblio-
tekami lokalnymi (czyli lokalnymi plikami i bibliotekami projektu), i jest sposobem na użycie
dostarczonych bibliotek z pakietów innych firm.
Zapraszam do ponownego przyjrzenia się temu przykładowi, gdy dowiesz się, czym jest
pakiet w kontekście Darta. Kod źródłowy tego rozdziału można znaleźć w serwisie GitHub.
69
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Decyzja o podziale jest ważna nie tylko ze względu na hermetyzację, ale także ze względu na
to, jak odbiorcy bibliotek będą je importować i ich używać. Załóżmy na przykład, że mamy
dwie ściśle powiązane klasy, które muszą być razem, aby mogły pracować. Dzielenie ich na
różne biblioteki zmusi klientów do zaimportowania obu. Nie jest to najbardziej praktyczny
sposób, dlatego bardzo ważne jest, aby podczas tworzenia bibliotek open source uważać na
ich dzielenie.
W Dart każdy identyfikator jest domyślnie dostępny z dowolnego miejsca, wewnątrz i na zewnątrz
biblioteki, z wyjątkiem sytuacji, gdy jest poprzedzony znakiem _ (podkreślenie). Oznacza to, że
staje się prywatny dla deklarujących bibliotek, uniemożliwiając dostęp do niego z zewnątrz.
Spójrz na następny przykład, w którym użyliśmy przedrostka _.
70
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Definicja biblioteki
Dart ma słowo kluczowe definiujące bibliotekę, jest to library — jak można się spodziewać.
Chociaż opcjonalne, to słowo kluczowe jest bardzo przydatne podczas tworzenia wielu biblio-
tek plików lub tworzenia dokumentacji dla bibliotek przed opublikowaniem ich jako interfej-
sów API.
Dart ma narzędzie dartdoc do generowania dokumentacji HTML dla pakietów Dart. Aby
skorzystać z tego narzędzia, musimy w określony sposób pisać komentarze. Omówimy
to dalej na przykładach.
Przyjrzyjmy się, jak zdefiniować bibliotekę za pomocą tego słowa kluczowego, a także różnym
podejściom, które można zastosować podczas tworzenia bibliotek, aby uzyskać poprawną her-
metyzację i uczynić korzystanie z biblioteki bardziej zwięzłym.
Biblioteka jednoplikowa
Najbardziej uproszczonym sposobem definiowania biblioteki jest dodanie całego powiąza-
nego kodu, czyli klas, funkcji najwyższego poziomu i zmiennych w jednym pliku. Na przykład
nasza poprzednia biblioteka Person wygląda następująco:
class Person {
String firstName;
String lastName;
PersonType _type;
Person({this.firstName, this.lastName});
71
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
// nie możemy uzyskać dostępu do właściwości _type, ponieważ jest ona prywatna
// dla biblioteki programmer._type = PersonType.employee;
print(programmer);
}
Chociaż kuszące jest zdefiniowanie całego powiązanego kodu w jednym pliku, może to być trud-
niejsze do utrzymania, ponieważ kod i jego złożoność rosną w czasie. Skorzystaj z tej funkcjo-
nalności tylko w przypadku prostych typów definicji, które prawdopodobnie nie zmienią się
w czasie.
Aby zdefiniować bibliotekę wieloplikową, możemy użyć kombinacji instrukcji part, part of
oraz library:
part — pozwala bibliotece określić, że składa się z małych części;
part of — określa, z których bibliotek się składa;
library — korzysta z powyższych instrukcji part, ponieważ musimy powiązać
cząstkowe pliki (part) z główną częścią biblioteki (main).
Przyjrzyjmy się, jak będzie wyglądał poprzedni przykład, gdy użyjemy instrukcji part:
// „główna” część biblioteki, person_library.dart
// zdefiniowana za pomocą słów kluczowych library i part
library person;
part 'person_types.dart';
part 'student.dart';
part 'programmer.dart';
72
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
class Person {
String firstName;
String lastName;
PersonType _type;
Person({this.firstName, this.lastName});
String toString() => "($_type): $firstName $lastName";
}
Sama implementacja niczego nie zmienia; jedyną różnicą jest wyrażenie part of na po-
czątku pliku.
Ponadto, jak widać, właściwość _type jest również dostępna w plikach part, ponieważ jest
prywatna dla biblioteki person, a wszystkie pliki znajdują się w tej samej bibliotece.
73
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Gdyby pliki part miały jakieś pola, klasy lub funkcje najwyższego poziomu i zmienne
z prefiksem _, byłyby one dostępne dla głównego pliku biblioteki (main) i innych części;
pamiętaj, wszystkie są w tej samej bibliotece.
main() {
// dostęp do klasy Programmer jest dozwolony, część biblioteki person_library
Programmer programmer = Programmer(firstName: "Dean", lastName: "Pugh");
// nie ma dostępu do właściwości _type, jest ona prywatna dla biblioteki person
// programmer._type = PersonType.employee;
print(programmer);
}
Spójrz na powyższy kod; biblioteka person nie musi niczego zmieniać, gdyż wprowadzone
przez nas modyfikacje są w wewnętrznej strukturze biblioteki.
Możemy nie tworzyć części bibliotecznych i po prostu podzielić bibliotekę na małe pojedyn-
cze biblioteki. W przypadku poprzednich przykładów spowodowałoby to kilka ważnych
zmian podczas implementacji.
Mamy poprzednie części jako trzy indywidualne biblioteki: person_library, programmer i student.
Chociaż są ze sobą powiązane, zachowują się jak pojedyncze biblioteki i nie znają niczego
poza publicznymi członkami:
// biblioteka person zdefiniowana w person_library.dart
class Person {
String firstName;
String lastName;
final PersonType type;
74
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Biblioteka programmer importuje bibliotekę person, aby uzyskać dostęp do jej klasy Person:
// biblioteka programmer zdefiniowana w programmer.dart
import 'person_library.dart';
import 'person_library.dart';
main() {
// mamy dostęp do klasy Programmer, ponieważ jest ona częścią biblioteki
// person_library
Programmer programmer = Programmer(firstName: "Dean", lastName: "Pugh");
Student student = Student(firstName: "Dilo", lastName: "Pugh");
75
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
print(programmer);
print(student);
}
Biblioteka person będzie miała niewielką zmianę, ponieważ teraz jest podzielona na wiele
części, przez co będziemy musieli zaimportować każdą bibliotekę, z której chcemy korzystać
indywidualnie.
Nie jest to wielka sprawa, gdy mówimy o małych bibliotekach, ale spróbuj pomyśleć o bar-
dziej złożonej strukturze bibliotek, w której importowanie wszystkich wzajemnie powiąza-
nych bibliotek z osobna utrudniłoby ich użycie.
Tutaj pojawia się instrukcja export. Możemy wybrać główny plik biblioteki i stamtąd wyeks-
portować wszystkie mniejsze biblioteki z nim powiązane. W ten sposób aplikacja musi zaim-
portować tylko jedną bibliotekę, a wszystkie mniejsze biblioteki będą dostępne obok niej.
main() {
// możemy uzyskać dostęp do klasy Programmer i Student podczas ich eksportowania
// z biblioteki person_library
Programmer programmer = Programmer(firstName: "Dean", lastName: "Pugh");
Student student = Student(firstName: "Dilo", lastName: "Pugh");
print(programmer);
print(student);
}
Zwróć uwagę, że zmienia się tylko instrukcja import. Możemy normalnie używać klas z małych
bibliotek, ponieważ są one eksportowane z biblioteki person_library.
Teraz, po zrozumieniu koncepcji biblioteki Dart, możemy zbadać, jak połączyć te fragmenty
kodu w coś, co można udostępniać i wielokrotnie używać: pakiet Dart.
Pakiety Darta
Pakiet Dart jest punktem wyjścia każdego projektu Dart. W poprzednich przykładach nie
przejmowaliśmy się tym, ponieważ używaliśmy przykładów składni jednoplikowych; jednak
w praktyce zawsze będziemy pracować z pakietami:
76
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Główną zaletą używania i tworzenia pakietów jest to, że kod można ponownie wykorzystać
i udostępnić.
W ekosystemie Dart odbywa się to za pomocą narzędzia pub, które pozwala nam pobierać
i wysyłać zależności do witryny pub.dartlang.org i repozytorium.
Ogólnie rzecz biorąc, istnieją dwa rodzaje pakietów Dart: pakiety aplikacji i pakiety bibliotek.
Z drugiej strony pakiety bibliotek to te zawierające przydatny kod, który może być pomocny
w wielu projektach. Te pakiety mogą być używane jako zależność i mają również inne zależności.
Mówiąc prościej, zalecana struktura pakietu Dart nie różni się zbytnio między aplikacją a pakie-
tem biblioteki — różnią się od siebie ich przeznaczenie i zastosowanie.
Struktury pakietów
Pierwszą ważną rzeczą, na którą należy zwrócić uwagę w przypadku struktury projektu pa-
kietu Dart, jest to, że jej ważność jest potwierdzona przez obecność pliku pubspec.yaml; to
77
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
znaczy, że jeśli w Twojej strukturze znajduje się plik pubspec.yaml, to jest to pakiet, który zawiera
opis w tym pliku — bez tego pliku nie może istnieć pakiet. Tak wygląda typowy pakiet:
Ten przykładowy pakiet został wygenerowany przy użyciu narzędzia Stagehand. Więcej
informacji można znaleźć w poniższej sekcji.
W przypadku pakietów aplikacji nie ma wymaganego układu projektu (ponieważ nie jest on
przeznaczony do publikacji w repozytorium pub); jednakże w przypadku jego rozbudowy ist-
nieje już kilka zalecanych sposobów i konwencji, których należy przestrzegać. Przyjrzyjmy się
wspólnej strukturze ogólnego pakietu Dart. Większość struktury jest konwencjonalna i zależy od
złożoności projektu i tego, czy chcesz w jakiś sposób udostępniać jego kod.
Przyjrzyjmy się roli każdego pliku i katalogu w typowej strukturze pakietu Dart:
pubspec.yaml — jak już wspomniano, jest to podstawowy plik pakietu i opisuje go
w repozytorium. Pełną strukturę tego pliku poznamy szczegółowo później.
Katalogi lib/ i lib/src/ — są to miejsca, w których znajduje się kod źródłowy
biblioteki pakietów. Jak już wiesz, prosty plik .dart to mała biblioteka, więc wszystko,
co umieścisz w katalogu lib, jest publicznie dostępne dla innych pakietów i znane
jako publiczne API pakietu. Podkatalog src zawiera, zgodnie z konwencją, cały
wewnętrzny kod pakietu, to znaczy jego prywatny kod źródłowy, który nie jest
przeznaczony do bezpośredniego importu przez innych.
78
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Aby zrozumieć, jak pisać testy jednostkowe, możesz zapoznać się z sekcją Wprowadzenie
do testów jednostkowych Darta.
79
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ogólnie rzecz biorąc, większość plików i ich struktura nie zmienia się z pakietu na pakiet,
więc tworzenie za każdym razem całej struktury pakietu Dart może być żmudne. Właśnie
dlatego stworzono narzędzie Stagehand — do generowania szablonów projektów Dart.
Narzędzie pub jest obecne w Dart SDK. Jeśli masz gotowe środowisko Dart lub Flutter,
możesz skorzystać z tego narzędzia. W przeciwnym razie zajrzyj ponownie do rozdziału 1.
Aby uruchomić Stagehand lub inne narzędzie do obsługi pakietów z wiersza poleceń, możesz
skorzystać z jednego z dwóch sposobów:
pierwszy polega na wprowadzeniu polecenia (przed innymi):
pub run global
drugi polega na dodaniu katalogu pamięci podręcznej pakietów globalnych Dart
do ścieżki systemu operacyjnego.
Zapoznaj się z opisem pola name w sekcji Plik pubspec, aby zrozumieć, jak poprawnie
nazwać swój pakiet.
Alternatywnie, jeśli masz poprawnie skonfigurowaną ścieżkę, możesz użyć polecenia stagehand
<template>, gdzie <template> jest żądanym szablonem Stagehand.
80
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Możesz sprawdzić dostępne szablony projektów na stronie projektu w witrynie Darta pod
adresem https://pub.dartlang.org/packages/stagehand.
Plik pubspec
Plik pubspec znajduje się w sercu pakietu Dart i aby zrozumieć, jak prawidłowo opisać pakiet,
musimy zrozumieć, jaka jest struktura tego pliku. Ten plik jest oparty na składni yaml, po-
wszechnie używanym formacie plików konfiguracyjnych, a jego struktura jest łatwa do odczy-
tania i analizowania. Plik pubspec wygląda następująco:
name: simple_package_structure
description: Przykład prostego pakietu
version: 1.0.0
homepage: https://www.example.com
author: Alessandro Biessek <alessandrobiessek@gmail.com>
environment:
sdk: '>=2.0.0 <3.0.0' # sprawdź poniższą sekcję dependencies (zależności)
# aby zrozumieć wersjonowanie
dependencies:
json_serializable: ^2.0.1
dev_dependencies:
test: ^1.0.0
Projekty Flutter zawierają również plik pubspec z określonymi dostępnymi polami. Więcej
informacji można znaleźć w rozdziale 3., „Wprowadzenie do Fluttera”.
Plik określa informacje o metadanych pakietu, co jest przydatne, jeśli chcesz opublikować
pakiet. Definiuje również zależności pakietu od innych firm i wersję Dart SDK. Przyjrzyjmy
się bardziej szczegółowo polom pliku pubspec:
name — to jest identyfikator pakietu. Jest wymagany i powinien zawierać tylko
małe litery i cyfry oraz znak _; dodatkowo powinien to być prawidłowy identyfikator
Dart (czyli nie może zaczynać się cyframi i nie może być słowem zastrzeżonym).
Jest to bardzo ważna właściwość, jeśli chcesz opublikować pakiet w repozytorium
publikacji, dobrze jest sprawdzić istniejące nazwy pakietów, aby uniknąć powielania.
description — chociaż jest to pole opcjonalne, jest wymagane, jeśli zamierzasz
opublikować pakiet, opisując prostymi słowami jego przeznaczenie.
version — jest to również opcjonalne pole dla pakietów osobistych, ale jest
wymagane do publikacji w repozytorium pub. Ważne jest, aby zachować spójność
wersji pakietu, z której będzie mogła korzystać społeczność.
homepage — w przypadku pakietów pub będzie to link do strony pakietu w witrynie
internetowej wydawcy. Bardzo ważne jest, aby go podać, jeśli zamierzasz
go opublikować.
81
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
author — chociaż nie jest to pole obowiązkowe, ważne jest, aby podać dane
kontaktowe twórcy lub twórców biblioteki. Ponadto biblioteka może mieć więcej
niż jednego autora; w tym przypadku można użyć składni listy YAML, ustawiając
zamiast tego pole authors (zwróć uwagę na opcjonalne informacje kontaktowe):
authors:
- Alessandro Biessek <alessandrobiessek@gmail.com>
- Alessandro Biessek
dependencies i dev_dependencies — odnoszą się do rzeczywistego przeznaczenia
pliku pubspec, czyli do korzystania z biblioteki i jej rozwoju wymagana jest lista
pakietów firm trzecich.
environment — oprócz zależności innych firm istnieje jeszcze jedna, powiedzmy,
główna zależność pakietu, czyli sam zestaw Dart SDK. W tym polu musisz określić
cel i obsługiwane wersje Dart SDK.
Pole environment określa zależność od SDK; Zaleca się określenie docelowej wersji ze-
stawu Dart SDK przy użyciu składni range, ponieważ zakres semantyczny nie jest zgodny
ze starszymi wersjami (czyli <1.8.3).
Typowa struktura pubspec zawiera pola, które zostały określone wcześniej. Aby uzyskać pełne wy-
jaśnienie pliku pubspec i innych pól, odwiedź witrynę internetową Darta: https://dart.dev/tools/
pub/pubspec.
82
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
To jest minimalny opis pakietu i nie ma określonych zależności, nawet docelowej wersji Dart
SDK. Wykonajmy jednak polecenie pub get w folderze package, ponieważ będzie działać w ten
sam sposób:
pub get
Zwróć uwagę na nowe pliki wygenerowane przez polecenie w folderze .packages; te pliki są
ważne, aby narzędzie pub działało z pakietami zależności:
.packages — odwzorowuje zależności w systemowej pamięci cache pub
(poprzednio wspomniane w sekcji „Stagehand — generator projektów Dart”).
Zamiast wykonywać kopie we wszystkich pakietach, narzędzie pub po prostu
przechowuje mapowanie między pakietem a jego odpowiednią lokalizacją
w systemie. Po zmapowaniu pakietu w tym miejscu będzie można go zaimportować
do kodu Darta. Ten plik nie powinien znajdować się w systemie zarządzania
kodem źródłowym; Dzieje się tak, ponieważ jest generowany i zarządzany przez
narzędzie pub.
pubspec.lock — jest to plik pomocniczy narzędzia pub, który zawiera wszystkie
wykresy zależności pakietu, czyli listę wszystkich zależności bezpośrednich
i przechodnich. Zawiera również dokładne wersje i inne informacje o metadanych
dotyczące wszystkich zależności. Zaleca się dołączenie tego pliku do systemu
zarządzania źródłami tylko wtedy, gdy jest to pakiet aplikacji; pomaga to na
przykład zespołowi programistów pracować z dokładnie taką samą konfiguracją
zależności. Jeśli używasz pakietu biblioteki, zwykle nie jest on dołączany, ponieważ
oczekuje się, że będzie działał z dużym zakresem zależności, to znaczy nie powinien
być blokowany do określonych wersji.
Pamiętaj, że powyższe pliki generowane są przez narzędzie pub, więc nie powinieneś
ich edytować.
83
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Określanie zależności
Teraz, gdy już wiesz, jak narzędzie pub obsługuje pakiety wewnątrz projektu, przyjrzyjmy się,
jak dodać do niego zależności.
Zależności są określane w polu dependencies pliku pubspec. Jest to pole w formacie listy
YAML, więc możesz określić ich dowolną liczbę. Załóżmy, że w naszym projekcie potrzebu-
jemy pakietu json_serializable. Możemy to określić, po prostu dodając go do listy w nastę-
pujący sposób:
name: adding_dependencies
dependencies:
json_serializable:
# poniżej inne pakiety
Tutaj dodajesz nazwę pakietu (<package>), a następnie pola <constraints>: wersja i źródło.
W tym przypadku nie określiliśmy żadnego ograniczenia (constraint), więc zakłada on dowolną
dostępną wersję i domyślne źródło (pub.dartlang.org).
Zauważ, że dwukropek: po nazwie pakietu nie jest opcjonalny; lista zależności oczekuje,
że każda zależność będzie wartością mapy YAML. Aby uzyskać więcej informacji, możesz
zapoznać się z dokumentacją YAML pod adresem https://docs.ansible.com/ansible/latest/
reference_appendices/YAMLSyntax.html.
Ograniczenie wersji
Ograniczeniem wersji może być konkretny numer wersji, zakres albo ograniczenie minimum
lub maksimum. Przyjrzyjmy się, jak to wygląda w każdej sytuacji:
Dowolne / puste — podobnie jak w poprzednim przykładzie, możemy nie stosować
ograniczenia wersji, na przykład json_serializable: lub json_serializable: any.
Konkretna wersja — możemy dodać konkretny numer wersji, z którym chcemy
pracować, na przykład json_serializable: 2.0.1.
Minimalne ograniczenie — tutaj możemy dodać minimalną akceptowalną wersję
pakietu. Możemy to zrobić na dwa sposoby: json_serializable: '> 1.0.0', gdzie
akceptujemy dowolną wersję późniejszą niż określona wersja (z wyłączeniem
określonej), lub json_serializable: '> = 1.0.0', gdzie akceptujemy dowolną
wersję wyższą lub równą podanej wersji.
84
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Ograniczenie źródła
Narzędzie pub nie szuka tylko pakietów w repozytorium publikacji; Jeśli korzystałeś już z in-
nego systemu zarządzania pakietami, wiesz, że czasami warto hostować swoje pakiety w in-
nych miejscach niż repozytorium publiczne, takich jak prywatne pakiety firmowe lub osobi-
ste. W przypadku źródłowej części specyfikacji pakietu mamy cztery możliwości zmiany
miejsca, w którym narzędzie pub ma go szukać:
Hostowane źródło: jest to domyślne repozytorium pub lub inny alternatywny
serwer http, który implementuje api pub. Rozważmy następujący blok kodu:
dependencies:
json_serializable:
hosted:
name: json_serializable
url: http://pub-packages-private-server.com # zmiana serwera
Jak widać, musimy określić pole hosted tylko wtedy, gdy nie używamy repozytorium publikacji,
czyli domyślnego źródła.
Ścieżka źródła — tutaj możesz dodać zależność pakietu z własnego systemu:
dependencies:
json_serializable:
path: /Users/biessek/json_serializable
Chociaż nie możesz udostępniać pakietu z tego rodzaju zależnościami, może to być przydatne
na etapach rozwoju.
Źródło Git — tutaj możesz określić pakiet z repozytorium Git:
85
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
dependencies:
json_serializable:
git:
url: git://github.com/dart-lang/json_serializable.git
path: path/to/json_serializable # jeśli katalogiem głównym pakietu nie jest
# katalog główny
# repozytorium
ref: master # zależy od konkretnego zatwierdzenia, tagu, gałęzi
Może to być przydatne na etapach rozwoju lub jeśli opublikowany kod źródłowy pakietu nie jest
jeszcze obecny w repozytorium pub.
Źródło SDK — SDK może mieć własne pakiety, których można używać jako
zależności:
dependencies:
flutter_localizations: # zależność dostępna we sdk fluttera
sdk: flutter
Do tej pory ten sposób określania ograniczeń źródła był używany tylko w przypadku zależno-
ści SDK Fluttera.
Wprowadzenie do programowania
asynchronicznego z wykorzystaniem
obiektów Future i Isolate
Dart to jednowątkowy język programowania, co oznacza, że cały kod aplikacji działa w tym samym
wątku. Mówiąc prościej, chodzi o to, że każdy kod może blokować wykonanie wątku, powodując
długotrwałe operacje, takie jak żądania we/wy lub żądania http.
Chociaż Dart jest jednowątkowy, może wykonywać operacje asynchroniczne za pomocą obiektów
Future. Ponadto, aby przedstawić wynik tych operacji asynchronicznych, Dart używa obiektu
Future w połączeniu ze słowami kluczowymi async i await. Opiszemy te ważne pojęcia, aby
opracować responsywną aplikację.
Obiekty Future
Obiekt Future<T> w Dart reprezentuje wartość, która zostanie dostarczona kiedyś w przyszło-
ści. Może być wykorzystana do oznaczenia metody, na przykład z przyszłym wynikiem; ozna-
cza to, że metoda zwracająca obiekt Future<T> nie zwróci poprawnej wartości natychmiast,
ale zamiast tego zrobi to po pewnych obliczeniach w późniejszym czasie.
86
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Rozważmy następujący kod, w którym mamy funkcję main, wywołującą długotrwałą operację:
import 'dart:io';
void longRunningOperation() {
for (int i = 0; i < 5; i++) {
sleep(Duration(seconds: 1));
print("index: $i");
}
}
main() {
print("start długiej operacji");
longRunningOperation();
Jeśli wykonasz powyższy kod, zauważysz, że zatrzymuje on wykonywanie funkcji main podczas
działania funkcji longRunningOperation(). Jest to działanie synchroniczne i prawdopodobnie nie
będzie dobrze pasować we wszystkich przypadkach użycia.
87
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jeśli wykonasz powyższy kod, możesz zauważyć coś dziwnego; dane wyjściowe są następujące:
start długiej operacji
dalszy ciąg funkcji main
index z funkcji main: 10
index z funkcji main: 11
index z funkcji main: 12
index z funkcji main: 13
index z funkcji main: 14
koniec funkcji main
index: 0
index: 1
index: 2
index: 3
index: 4
Nie jest to współbieżny kod, w którym jeden jego fragment jest wykonywany po drugim, tak
jak poprzednio; tutaj zmienia się kolejność. W poprzednim przykładzie zmiana występuje,
kiedy funkcja longRunningOperation() wywołuje await w innej funkcji asynchronicznej (async).
W tym przypadku funkcja zostaje zawieszona i będzie wznowiona dopiero po upływie 1 sekundy.
Jednak po opóźnieniu funkcja main jest już uruchomiona ponownie, ponieważ nie oczekuje
na zakończenie długiej operacji, w związku z czym kod longRunningOperation() zostanie wyko-
nany dopiero po jej zakończeniu.
Jedną z rzeczy, którą możemy zrobić, jest przekształcenie funkcji main() w funkcję asynchro-
niczną i oczekiwanie na wykonanie longRunningOperation(). W ten sposób funkcja main()
zostanie zawieszona zaraz po wywołaniu await longRunningOperation() i będzie wznowiona do-
piero po jej wykonaniu. Zachowuje się jak normalny kod synchroniczny, w następujący sposób:
main() async {
print("start długiej operacji");
await longRunningOperation();
Jak być może zauważyłeś, poprzednie funkcje nigdy nie działają naprawdę asynchronicznie.
Dzieje się tak, ponieważ czekamy na wykonanie metody longRunningOperation() przed wykona-
niem reszty kodu. Aby działały asynchronicznie, powinniśmy pominąć słowo kluczowe await
w następujący sposób:
main() async {
print("start długiej operacji");
longRunningOperation();
88
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Dart wykonuje obie metody async w tym samym wątku. Obie funkcje działają w tym przy-
padku asynchronicznie, ale nie oznacza to, że są wykonywane równolegle.
Dart wykonuje jedną operację naraz; dopóki wykonywana jest jedna operacja, nie może
zostać przerwana przez żaden inny kod Dart.
To wykonanie jest kontrolowane przez pętlę Event Darta, która działa jak menedżer dla obiek-
tów Future i kodu asynchronicznego.
Możesz zapoznać się z oficjalną dokumentacją Darta na temat pętli zdarzeń Event, aby
zrozumieć, jak działa: https://dart.dev/articles/archive/event-loop.
Aby wykonać kod Dart równolegle (to znaczy w tym samym czasie), używamy obiektów Isolate.
Obiekty Isolate
Być może zastanawiałeś się, jak wykonać prawdziwie równoległy kod i poprawić wydajność i szyb-
kość reakcji? Do tego służą obiekty Isolate. Każda aplikacja Dart składa się z co najmniej
jednej instancji Isolate, instancji main Isolate, w której działa cały kod aplikacji. Aby więc utwo-
rzyć kod wykonywania równoległego, musimy utworzyć nową instancję Isolate, która może
działać równolegle z main Isolate:
89
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Obiekty Isolate można uznać za rodzaj wątku, ale jak sama nazwa wskazuje, niczego między
sobą nie dzielą. Oznacza to, że nie współużytkują pamięci, więc nie musimy tutaj używać blokad
i innych technik synchronizacji wątków.
Aby komunikować się między tymi obiektami, czyli wysyłać i odbierać dane między nimi, musimy
wymieniać wiadomości. Dart zapewnia odpowiednie rozwiązanie.
main() {
print("start długiej operacji");
Isolate.spawn(longRunningOperation, "Hello");
90
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
Teraz kod obu tych funkcji działa niezależnie po wykonaniu spawn na obiekcie Isolate.
Testy jednostkowe to jedna z rzeczy, które mogą nam pomóc w pisaniu modularnego, wydajnego
i wolnego od błędów kodu. Test jednostkowy nie jest oczywiście jedynym sposobem testowa-
nia kodu, ale jest kluczową częścią testowania małych fragmentów oprogramowania w sposób,
który izoluje je od innych części, pomagając nam skupić się na konkretnych rzeczach.
Pokrycie całego kodu aplikacji testami jednostkowymi nie gwarantuje, że jest on w 100%
wolny od błędów; pomaga nam jednak w stopniowym uzyskiwaniu dojrzałego kodu i jest to
jeden z kroków zapewniających dobry cykl rozwoju, z okresowymi wydaniami stabilnymi.
Dart zapewnia również przydatne narzędzia do pracy z testami. Rzućmy okiem na punkt wyjścia
dla testów jednostkowych kodu Dart: pakiet test Darta.
91
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dzięki temu możemy używać bibliotek pakietu test do pisania testów jednostkowych.
Możemy napisać test jednostkowy oceniający implementację tej metody przy użyciu pakietu test:
import 'package:test/test.dart';
import 'package:unit_tests/calculator.dart';
void main() {
Calculator calculator;
setUp(() {
calculator = Calculator();
});
92
d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart
package:test_api expect
test\calculator_tests.dart 12:7 main.<fn>
Masz także możliwość tworzenia grup testów, ponieważ możesz pomyśleć, że tylko jeden przypa-
dek testowy nie wystarczy do skutecznego przetestowania jednostki kodu. Załóżmy, że zmienimy
nasz zestaw testów, aby mieć grupę (group) testów badających sumę (o nazwie sum tests):
void main() {
Calculator calculator;
setUp(() {
calculator = Calculator();
});
group("sum tests", () {
test('calculator sumTwoNumbers() sum the both numbers', () {
expect(calculator.sumTwoNumbers(1, 2), 3);
});
test('calculator sumTwoNumbers() sum null as it was 0', () {
expect(calculator.sumTwoNumbers(1, null), 1);
});
});
}
93
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Receiver: null
Tried calling: _addFromInteger(1)
dart:core int.+
package:unit_tests/src/calculator_base.dart 3:14 Calculator.sumTwoNumbers
test\calculator_tests.dart 15:25 main.<fn>.<fn>
00:01 +1 -1: Some tests failed.
Wystąpił jeden udany test (+1) i jeden błąd (-1) — opisany tuż pod opisem testu, który zakończył
się niepowodzeniem. Mając to na uwadze, możemy zmienić implementację sumTwoNumbers()
tak, aby akceptowała wartość null jako wartość 0, i ponownie uruchomić test:
00:01 +2: All tests passed!
Jak widać, testy mogą nam pomóc w zapobieganiu błędom logicznym w produkcji; Oczywiście
zawsze możemy mieć pewne błędy, ale testy mogą pomóc nam zapobiec ich jak największej liczbie.
Podsumowanie
W tym rozdziale zobaczyliśmy, jak język Dart jest zorganizowany pod względem paradygmatu
OOP. Widzieliśmy, że język udostępnienia programiście wszystkie funkcje związane z OOP,
ale także pewne szczegóły, które mają na celu rozszerzenie możliwości, takie jak domieszki
— do odkrywania korzyści płynących z dziedziczenia, oraz interfejsy niejawne, które pozwa-
lają na implementację dowolnej klasy przez dowolną inną klasę, wywoływane klasy dodające
zachowanie funkcji do prostych obiektów oraz funkcje i zmienne najwyższego poziomu, które
nie muszą być powiązane z żadną klasą. Jest to bardzo przydatne w przypadku funkcji narzę-
dziowych, które nie zależą od kontekstu.
Zbadaliśmy, jak zbudowane są pakiety Dart, jak korzystać z narzędzia pub, aby dodawać za-
leżności do projektu, i jak używać pakietów innych firm. Sprawdziliśmy wiele sposobów two-
rzenia struktury biblioteki oraz sposób, w jaki tworzony jest pakiet Dart. Ponadto nauczyliśmy się,
jak poprawnie opisać pakiet w pliku pubspec, aby utworzyć pakiety, które można udostępniać.
94
d0765ad53fb82babda2278a311da7afb
d
3
Wprowadzenie
do Fluttera
W tym rozdziale poznasz historię frameworka Flutter, dowiesz się, jak i dlaczego został stworzony
oraz jaka była jego dotychczasowa ewolucja. Wyjaśnię, w jaki sposób jego społeczność się do niej
przyczynia oraz jak i dlaczego się szybko rozrosła w ciągu ostatnich kilku miesięcy. Zostaniesz
wprowadzony w główne funkcje Fluttera, z krótkimi porównaniami do innych frameworków.
Zobaczysz także, jak stworzyć podstawowy projekt za pomocą Fluttera. Aby to osiągnąć, będziemy
potrzebować odpowiedniej maszyny skonfigurowanej z Flutterem i jego różnymi wymaganiami
wstępnymi.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Istnieje wiele platform programistycznych dla urządzeń mobilnych, które mają wspólny cel:
tworzenie natywnych aplikacji mobilnych na Androida i iOS z pojedynczą bazą kodu. Niektóre
z tych frameworków są powszechnie przyjmowane przez społeczność i zapewniają podobne roz-
wiązania problemów, na które rzekomo mają odpowiedzieć. Wiedząc o tym, możemy zapytać:
Dlaczego powstał Flutter?
Czy naprawdę tego potrzebujemy?
W jakim stopniu jest lepszy niż konkurencyjne frameworki?
Sprawdźmy, jak działa Flutter, i odpowiedzmy na niektóre z tych pytań, zanim się nim zajmiemy.
96
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Możesz więc pomyśleć, że nowym frameworkom trudno jest znaleźć swoje miejsce na pełnym
rynku, ale tak nie jest. Flutter ma zalety, dzięki którym jest na tym samym poziomie co na-
tywne frameworki:
wysoka wydajność,
pełna kontrola nad interfejsem użytkownika,
język Dart,
wsparcie przez Google,
framework typu open source,
zasoby i narzędzia dla programistów.
Wysoka wydajność
W tej chwili trudno powiedzieć, że w praktyce wydajność Fluttera jest zawsze lepsza niż
wszystkich innych frameworków, ale można śmiało stwierdzić, że tak powinno być. Na przy-
kład jego warstwa renderująca została opracowana z myślą o dużej liczbie klatek na sekundę.
Jak zobaczymy w sekcji dotyczącej renderowania Flutter, niektóre z istniejących framewor-
ków opierają się na JavaScripcie i renderowaniu HTML, co może powodować narzuty wydaj-
ności, ponieważ wszystko jest rysowane w widoku WebView (komponent wizualny, taki jak
przeglądarka internetowa). Niektóre używają widżetów producenta oryginalnego sprzętu (OEM
— Original Equipment Manufacturer), stosujących żądania interfejsu API systemu operacyj-
nego w celu renderowania komponentów, co tworzy wąskie gardło w aplikacji, ponieważ wy-
maga dodatkowego kroku w celu renderowania interfejsu użytkownika (UI – user interface).
97
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Zobacz sekcję „Renderowanie Fluttera”, aby dowiedzieć się więcej o podejściu do ren-
derowania Fluttera w porównaniu z innymi rozwiązaniami.
Zobaczmy:
Kontrola nad wszystkimi pikselami na urządzeniu — frameworki ograniczone
przez widżety OEM będą odtwarzać co najwyżej to, co natywnie opracowana
aplikacja, ponieważ wykorzystują tylko dostępne komponenty platformy. Z drugiej
strony frameworki oparte na technologiach webowych mogą odtwarzać więcej niż
komponenty specyficzne dla platformy, ale mogą być również ograniczone przez
mobilny silnik sieciowy dostępny na urządzeniu. Uzyskując kontrolę nad
renderowaniem interfejsu użytkownika, Flutter umożliwia programistom tworzenie
interfejsu użytkownika na swój sposób, udostępniając rozszerzalny i bogaty
interfejs API widżetów, zapewniający narzędzia, których można użyć do stworzenia
unikalnego interfejsu użytkownika bez wad w wydajności i bez ograniczeń
w projektowaniu.
Zestawy UI platformy — nie używając widżetów OEM, Flutter może zepsuć
projekt platformy, ale tak nie jest. Flutter jest wyposażony w pakiety, które
zapewniają widżety do projektowania platform, Material w systemie Android
i Cupertino w systemie iOS.
98
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Elementy wizualne nazywamy widżetami. Tak też nazywa je Flutter. Więcej na ten temat
powiemy w sekcji „Wprowadzenie do widżetów” w tym rozdziale.
Dart
Od samego początku jednym z głównych celów Fluttera było bycie wydajną alternatywą dla
istniejących frameworków wieloplatformowych. Kluczowym punktem projektu była również
łatwość programowania rozwiązań mobilnych.
Kompilacje Dart Just in Time (JIT) i Ahead of Time (AOT) są wprowadzane, gdy ma
miejsce faza kompilacji. W AOT kod jest kompilowany przed uruchomieniem. W JIT kod
jest kompilowany podczas działania. (Sprawdź sekcję „Wprowadzenie do Darta” w pierw-
szym rozdziale).
Wysoka wydajność — dzięki wsparciu dla kompilacji AOT Flutter nie wymaga
powolnego pomostu między środowiskami (na przykład od nienatywnych
do natywnych), co sprawia, że aplikacje Flutter uruchamiają się znacznie szybciej.
Ponadto Flutter wykorzystuje przepływ stylów funkcjonalnych z krótkotrwałymi
obiektami, co oznacza wiele krótkotrwałych alokacji. Odśmiecanie pamięci Dart
działa bez blokad, pomagając w szybkiej alokacji.
99
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dart i Flutter zostały opracowane przez Google i — jak zobaczymy — jest to ważne.
Wsparcie Google’a
Flutter to zupełnie nowy framework, a to oznacza, że nie zajął jeszcze wysokiej pozycji na
rynku programowania mobilnego. Jednak to się zmienia, a perspektywy na najbliższych kilka
lat są bardzo pozytywne.
Fuchsia OS i Flutter
Nie jest już tajemnicą, że Google pracuje nad swoim nowym systemem operacyjnym Fuchsia
jako zamiennikiem systemu operacyjnego Android. Warto zwrócić uwagę na to, że Fuchsia
OS może być uniwersalnym systemem Google działającym nie tylko na telefonach komórko-
wych, co bezpośrednio wpływa na adopcję Fluttera. Dzieje się tak dlatego, że Flutter będzie
100
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
pierwszą metodą tworzenia aplikacji mobilnych dla nowego systemu operacyjnego Fuchsia
(za pomocą Fluttera rozwijany jest również interfejs użytkownika). Ponieważ system jest ukie-
runkowany na więcej urządzeń niż tylko smartfony, Flutter z pewnością będzie miał wiele
ulepszeń.
Rozwój frameworka jest bezpośrednio związany z nowym systemem operacyjnym Fuchsia. W miarę
zbliżania się premiery tego systemu ważne jest, aby firma Google posiadała już aplikacje mo-
bilne dla niego przeznaczone. Google ogłosiło na przykład, że aplikacje na Androida będą kompa-
tybilne z nowym systemem operacyjnym, co znacznie ułatwi przejście na Fluttera.
Ponieważ Flutter to oprogramowanie typu open source, społeczność i Google mogą współpra-
cować, aby:
pomóc w usuwaniu błędów i tworzeniu dokumentacji kodu,
tworzyć nowe treści edukacyjne dotyczące frameworka,
tworzyć dokumentację i wsparcie dla użytkowników,
podejmować decyzje dotyczące ulepszeń na podstawie prawdziwych informacji
zwrotnych od użytkowników.
Wsparcie dla programistów jest jednym z głównych celów frameworka. Dlatego oprócz bycia
blisko społeczności framework zapewnia świetne narzędzia i zasoby. Zobaczmy je.
101
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Łatwy start — Flutter jest dostarczany z rozwiązaniem flutter doctor, które jest
narzędziem wiersza poleceń. Prowadzi programistę przez konfigurację systemu,
wskazując, co jest potrzebne, aby być gotowym do skonfigurowania środowiska
frameworka. Wygląda to tak jak na poniższym zrzucie ekranu.
Jak widać, polecenie flutter doctor identyfikuje podłączone urządzenia oraz wykrywa, czy
są dostępne aktualizacje.
Hot reload — jest to funkcja, na której skupiano się podczas prezentacji na temat
frameworka. Łącząc możliwości języka Dart (takie jak kompilacja JIT) i moc Fluttera,
deweloper może natychmiast zobaczyć zmiany projektu wprowadzone w kodzie
w symulatorze lub urządzeniu. We Flutterze nie ma specjalnego narzędzia do
podglądu layoutu. Funkcja hot reload sprawia, że jest to niepotrzebne.
Teraz, gdy dowiedzieliśmy się więcej o zaletach Fluttera, przyjrzyjmy się kompilacjom
oprogramowania.
102
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Jak już zauważyliśmy, Flutter opiera się na kompilacji Darta AOT dla wersji release i kompi-
lacji JIT w trybie programowania / debugowania. Dart to jeden z niewielu języków, który
można skompilować zarówno do AOT, jak i JIT, a dla Fluttera jest to świetne rozwiązanie.
Obsługiwane platformy
Obecnie Flutter obsługuje urządzenia z architekturą ARM dla Androida, działające co naj-
mniej w wersji Jelly Bean 4.1.x, oraz urządzenia iOS z iPhone 4S lub nowsze. Oczywiście
aplikacje Flutter można normalnie uruchamiać na symulatorach.
Google zamierza przenieść środowisko wykonawcze Fluttera do sieci WWW, korzystając z możli-
wości Darta: kompilacji do JavaScriptu. Projekt początkowo nosił nazwę Hummingbird, a obec-
nie jest znany jako „Flutter for web”.
103
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Renderowanie Fluttera
Jeden z głównych aspektów, który sprawia, że Flutter jest wyjątkowy, to sposób, w jaki rysuje
elementy wizualne na ekranie. Duża różnica w porównaniu z innymi frameworkami polega
na tym, jak aplikacja komunikuje się z SDK platformy, o co prosi SDK i co robi sama:
Platformę SDK można postrzegać jako interfejs między aplikacjami a systemem operacyjnym
i usługami. Każdy system zapewnia własny SDK z własnymi możliwościami i jest oparty na
języku programowania (to znaczy Kotlin / Java dla Android SDK i Swift / Objective C dla iOS
SDK). Wspomnieliśmy wcześniej o niektórych podejściach renderowania używanych przez
różne frameworki; przyjrzyjmy się im teraz bardziej szczegółowo.
Technologie webowe
Widzieliśmy już frameworki, które używają elementów WebView do odtwarzania interfejsu
użytkownika poprzez połączenie HTML i CSS. Pod względem wykorzystania platformy wy-
glądałoby to tak jak na rysunku na następnej stronie.
Aplikacja nie wie, jak platforma wykonuje renderowanie; jedyne, czego potrzebuje, to widżet
WebView, na którym będzie renderował kod HTML i CSS.
Oprócz części renderującej należy zwrócić uwagę na to że aby uzyskać dostęp do syste-
mowych interfejsów API, kod JavaScript potrzebuje pośrednika do wywoływania kodu
natywnego, co powoduje niewielki narzut wydajności.
104
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
W tym trybie renderowania praca jest wykonywana przez SDK tak jak w normalnej aplikacji
natywnej, ale przed tym layout jest definiowany przez dodatkowy krok w języku frameworku.
Każda zmiana w interfejsie użytkownika powoduje komunikację między kodem aplikacji a kodem
natywnym, który jest odpowiedzialny za wywołanie zestawu SDK platformy, działając jak
105
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
pośrednik. Podobnie jak w przypadku poprzedniej techniki, może występować niewielkie obcią-
żenie aplikacji, być może trochę większe niż poprzednio, ponieważ renderowanie występuje
często, a zatem także i komunikacja.
Wprowadzenie do widżetów
Zrozumienie widżetów Fluttera jest niezbędne, jeśli chcesz z nimi pracować. Wiesz, że Flutter
przejmuje kontrolę nad renderowaniem i robi to z myślą o rozszerzalności i dostosowywaniu,
mając na celu zwiększenie kontroli dla programisty. Zobaczmy, jak Flutter stosuje pomysł na
widżety w przypadku aplikacji, aby tworzyć niesamowite interfejsy użytkownika.
Widżety można rozumieć jako wizualną (ale nie tylko) reprezentację części aplikacji. Wiele z nich
tworzy interfejs użytkownika aplikacji. Wyobraź sobie to jako układankę, w której definiujesz
elementy.
106
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Celem widżetów jest zapewnienie, aby aplikacja była modułowa, skalowalna i wyrazista, zawierała
mniej kodu i nie narzucała ograniczeń. Głównymi cechami interfejsu użytkownika opartego
na widżetach we Flutterze są kompatybilność i niezmienność.
Kompatybilność
Flutter wybiera kompozycję zamiast dziedziczenia, mając na celu zachowanie prostoty każ-
dego widżetu i dobrze zdefiniowanego celu. Elastyczność, która jest jednym z celów frameworka,
pozwala deweloperowi na tworzenie wielu kombinacji, aby osiągnąć niesamowite rezultaty.
Niezmienność
Flutter opiera się na reaktywnym stylu programowania, w którym instancje widżetów są krótko-
trwałe i zmieniają swoje opisy (wizualnie lub nie) w oparciu o zmiany w konfiguracji, więc reaguje
on na zmiany i propaguje je do swoich widżetów tworzących i tak dalej.
Z widżetem Fluttera może być skojarzony stan, a gdy skojarzony stan ulegnie zmianie, można go
przebudować, aby pasował do reprezentacji.
Widżety to podstawowe elementy składowe interfejsu. Aby poprawnie zbudować UI, Flutter
organizuje widżety w drzewie widżetów.
107
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Drzewo widżetów
To kolejna ważna koncepcja layoutów Fluttera. To tutaj ożywają widżety. Drzewo widżetów
stanowi logiczną reprezentację wszystkich widżetów UI. Powstaje w czasie tworzenia layoutu
(obliczenia i informacje strukturalne) i jest używane podczas renderowania (ramki ekranu)
i wykrywania położeń kursora (hit testing — interakcje dotykowe), czyli podczas czynności,
które Flutter robi najlepiej. Używając wielu algorytmów optymalizacyjnych, stara się jak najmniej
manipulować drzewem, zmniejszając całkowitą ilość pracy poświęconej na renderowanie, dążąc
do większej wydajności:
Widżety są reprezentowane w drzewie jako węzły. Może ono mieć z nim skojarzony stan; każda
zmiana jego stanu powoduje odbudowanie widżetu i związanego z nim dziecka.
Jak widać, struktura potomna drzewa nie jest statyczna i definiuje ją opis widżetów. Relacje
dzieci w widżetach są tym, co tworzy drzewo interfejsu; istnieją ze względu na kompozycję, więc
często widzi się wbudowane widżety Fluttera, które ujawniają właściwości dziecka (child) lub
dzieci (children), w zależności od przeznaczenia widżetu.
Drzewo widżetów nie działa samodzielnie w ramach frameworku. Korzysta z drzewa elemen-
tów; które jest powiązane z drzewem widżetów, reprezentując zbudowany widżet na ekranie.
W związku z tym każdy widżet będzie miał odpowiadający mu element w drzewie elementów
po jego zbudowaniu.
108
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Hello Flutter
Czas zająć się programowaniem. Po skonfigurowaniu środowiska programistycznego Fluttera
możemy zacząć korzystać z jego poleceń. Typowym sposobem uruchomienia projektu Fluttera
jest wykonanie następującego polecenia:
flutter create <output_directory>
Tutaj output_directory będzie również nazwą projektu Flutter, jeśli nie określisz go jako argumentu.
109
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jeśli myślisz, że wygląda to podobnie jak w przypadku pakietów Dart, możesz mieć rację. Projekty
Flutter są rodzajem pakietu Dart, oczywiście z pewnymi osobliwościami. Wymieniając podsta-
wowe elementy konstrukcji, otrzymujemy:
android / ios — zawiera kody specyficzne dla platformy. Jeśli znasz już strukturę
projektu Androida z Android Studio, nie ma tu niespodzianki. To samo dotyczy
projektów XCode iOS.
hello_flutter.iml — to jest typowy plik projektu IntelliJ, który zawiera informacje
JAVA_MODULE używane przez IDE.
Katalog lib — jest to główny folder aplikacji Fluttera; wygenerowany projekt
powinien zawierać przynajmniej plik main.dart, nad którym można rozpocząć
pracę. W kilku krokach szczegółowo sprawdzimy ten plik.
pubspec.yaml i pubspec.lock — jak być może pamiętasz z rozdziału 2., plik
pubspec.yaml jest tym, co definiuje pakiet Dart. Jest to jeden z głównych plików
projektu, w którym wymieniasz zależności aplikacji, a w przypadku Fluttera
nawet coś więcej. Przyjrzymy się temu zagadnieniu dokładniej w rozdziale 4.
README.md — ten plik zwykle zawiera opis projektu i jest bardzo powszechny
w projektach open source.
Katalog test — zawiera wszystkie pliki projektu związane z testami. Tutaj
możemy dodać testy jednostkowe, jak widzieliśmy wcześniej, a także testy
widżetów przy użyciu pakietów specyficznych dla Fluttera.
110
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Dla większości przykładów zawartych w tej książce używamy narzędzi wiersza poleceń
bezpośrednio z terminala. Ponadto do celów informacyjnych używanym IDE jest Visual Studio
Code. Pamiętaj, że IDE używają tych narzędzi „pod spodem” do interakcji z projektem.
Plik pubspec
Plik pubspec we Flutterze jest podobny do prostego pakietu Darta. Poza tym zawiera dodatkową
sekcję dotyczącą konfiguracji specyficznych dla Fluttera. Zobaczmy szczegółowo zawartość
pliku pubspec.yaml:
name: hello_flutter
description: Nowy projekt Fluttera.
version: 1.0.0+1
Początek pliku jest prosty. Jak już wiemy, właściwość name jest definiowana, gdy wykonujemy
polecenie pub create, po którym następuje domyślny opis (description) projektu.
Możesz określić opis podczas wykonywania polecenia flutter create, używając argu-
mentu --description.
Właściwość version jest zgodna z konwencjami pakietu Dart: numer wersji plus opcjonalny
numer wersji kompilacji oddzielony znakiem +. Oprócz tego Flutter pozwala na zmianę tych
wartości podczas kompilacji. Bardziej szczegółowo przyjrzymy się temu w rozdziale 12., w sekcji
„Przygotowywanie aplikacji do wdrożenia”.
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
111
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dart SDK jest wbudowany we Flutter SDK, więc nie musisz ich instalować osobno.
Sekcja flutter pozwala nam skonfigurować zasoby zawarte w aplikacji, które mają być używane
w czasie wykonywania, takie jak obrazy (images), czcionki (fonts) i plik JSON, zwykle każdy
plik niezwierający kodu źródłowego, który pomaga w budowie aplikacji:
uses-material-design — w następnym rozdziale zobaczymy widżety Material
dostarczone przez Fluttera. Oprócz nich możemy również skorzystać z ikon Material
Design (https://material.io/tools/icons/?style= baseline), które mają niestandardowy
format czcionki. Aby to działało poprawnie, musimy aktywować tę właściwość
(ustawić ją na true), dzięki czemu ikony będą zawarte w aplikacji.
asssets — ta właściwość pobiera ścieżki zasobów, które zostaną dołączone do
końcowej aplikacji. Sprawdź poniższy kod, aby uzyskać więcej informacji o tym,
jak go używać. Pliki zasobów można organizować w dowolny sposób; dla Fluttera
ważna jest ścieżka do plików. Określ ścieżkę do pliku względem katalogu głównego
projektu. Jest to używane później w kodzie Darta, gdy trzeba odwołać się do pliku
zasobów. Oto przykład:
assets:
- images/home_background.jpeg
112
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Aby dodać obraz do późniejszego wykorzystania, dodajemy ścieżkę do listy zasobów; jeśli chcemy
dodać wszystkie pliki wewnątrz katalogu, po prostu określamy do niego ścieżkę.
assets:
- images/
Będziemy sprawdzać, jak załadować różne zasoby, kiedy zajdzie taka potrzeba.
Możesz również przeczytać więcej na temat specyfikacji zasobów w witrynie Fluttera:
https://flutter.io/docs/development/ui/assets-and-images.
Plik lib/main.dart
Głównym plikiem wygenerowanego projektu jest punkt wejścia aplikacji Fluttera:
void main() => runApp(MyApp());
Funkcja main jest punktem wejścia Dart aplikacji. To, co sprawia, że aplikacja Flutter zajmuje
scenę, to funkcja runApp wywoływana przez przekazanie widżetu jako parametru, który będzie
głównym widżetem aplikacji (samą aplikacją).
Flutter run
Aby uruchomić aplikację Fluttera, musimy mieć podłączone urządzenie lub symulator.
Sprawdzanie odbywa się za pomocą znanych już narzędzi flutter doctor i flutter emulators.
Poniższe polecenie pozwala poznać istniejące emulatory systemu Android i iOS, których
można użyć do uruchomienia projektu:
flutter emulators
113
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, to polecenie uruchamia debuger i udostępnia funkcję hot reload. Pierwsze uru-
chomienie aplikacji może zająć trochę więcej czasu niż kolejne:
114
d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera
Aplikacja jest uruchomiona; można zobaczyć znak debugowania w prawym górnym rogu.
Oznacza to, że nie jest to uruchomiona wersja release, jak już wiesz; jest to wersja rozwojowa
aplikacji z funkcjami hot reload oraz debugowania.
Poprzedni przykład został uruchomiony na symulatorze iPhone’a 6s. Ten sam wynik zostałby
osiągnięty przy użyciu emulatora systemu Android lub urządzenia wirtualnego z syste-
mem Android (AVD — Android Virtual Device).
Podsumowanie
W tym rozdziale w końcu zaczęliśmy bawić się frameworkiem Fluttera. Najpierw poznaliśmy
kilka ważnych pojęć dotyczących Fluttera, głównie koncepcję widżetów. Widzieliśmy, że widżety
są centralną częścią świata Fluttera, w którym jego zespół nieustannie pracuje nad ulepsza-
niem istniejących widżetów i dodawaniem nowych. Dzieje się tak, ponieważ koncepcja widżetów
jest wszędzie, od wydajności renderowania po ostateczny wynik na ekranie.
115
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W następnym rozdziale zagłębimy się w rodzaje widżetów, takie jak stanowe i bezstanowe,
oraz dowiemy się, jak i kiedy można ich używać. Przeczytamy również o wbudowanych widżetach
i rozpoczniemy projekt aplikacji Flutter, który będziemy śledzić do końca książki. Skumulujemy
w nim wiedzę zdobytą w każdym rozdziale.
116
d0765ad53fb82babda2278a311da7afb
d
II
Interfejs użytkownika
Fluttera — wszystko
jest widżetem
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
118
d0765ad53fb82babda2278a311da7afb
d
4
Widżety: tworzenie
layoutów Fluttera
W tym rozdziale poznasz główne koncepcje widżetów, różnice między widżetami bezstanowymi
i stanowymi, najpopularniejsze widżety we Flutterze oraz dowiesz się, jak dodać je do swojej
aplikacji i jak tworzyć pełne interfejsy za pomocą wbudowanych lub niestandardowych wi-
dżetów opracowanych przez Ciebie.
Interfejsy użytkownika prawie nigdy nie są statyczne; jak wiesz, często się zmieniają. Chociaż
z definicji niezmienne, widżety nie mają być ostateczne — w końcu mamy do czynienia z UI,
a UI z pewnością ulegnie zmianie w trakcie cyklu życia każdej aplikacji. Dlatego Flutter zapewnia
nam dwa rodzaje widżetów: bezstanowe i stanowe.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Duża różnica między nimi polega na sposobie budowania widżetu. Do obowiązków programisty
należy wybór rodzaju widżetu, który ma być używany w każdej sytuacji podczas tworzenia
interfejsu użytkownika, aby maksymalnie wykorzystać możliwości warstwy renderującej wi-
dżety Fluttera.
Widżety bezstanowe
Typowy interfejs użytkownika będzie składał się z wielu widżetów, a niektóre z nich nigdy nie
zmienią swoich właściwości po utworzeniu. Nie mają stanu; to znaczy, że nie zmieniają się
same przez jakieś wewnętrzne działanie lub zachowanie. Zamiast tego są zmieniane przez zdarze-
nia zewnętrzne w widżetach nadrzędnych w drzewie widżetów. Można więc śmiało powie-
dzieć, że widżety bezstanowe zapewniają kontrolę nad tym, jak są powiązane z jakimś widże-
tem nadrzędnym w drzewie. Poniżej przedstawiono reprezentację widżetu bezstanowego:
Tak więc widżet podrzędny otrzyma swój opis od widżetu nadrzędnego i sam go nie zmieni.
Jeśli chodzi o kod, oznacza to, że widżety bezstanowe mają tylko właściwości final zdefinio-
wane podczas konstrukcji, i to jedyna rzecz, którą należy zbudować na ekranie urządzenia.
120
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Widżety stanowe
W przeciwieństwie do widżetów bezstanowych, które otrzymują opis od rodziców, utrzymu-
jący się przez cały okres ich istnienia, widżety stanowe mają dynamicznie zmieniać opisy w trakcie
swojego życia. Z definicji widżety stanowe są również niezmienne, ale mają firmową klasę State,
która reprezentuje ich bieżący stan. Przedstawia to poniższy schemat:
Trzymając stan widżetu w osobnym obiekcie State, framework może go w razie potrzeby od-
budować bez utraty bieżącego skojarzonego stanu. Element w drzewie elementów zawiera
odniesienie do odpowiedniego widżetu, a także skojarzony z nim obiekt State, który powiadomi
o konieczności przebudowania widżetu, a następnie spowoduje również aktualizację w drze-
wie elementów.
Ten projekt został utworzony z domyślnymi argumentami z domyślnego szablonu Fluttera i repre-
zentuje małą aplikację z licznikiem, który pokazuje, ile razy został naciśnięty przycisk plus (+):
121
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, klasa MyApp rozszerza statelessWidget i zastępuje metodę build(BuildContext). Ta me-
toda opisuje część interfejsu użytkownika; to znaczy, że tworzy pod nim poddrzewo widżetów.
122
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
W opisywanym przykładzie MyApp jest elementem głównym (root) drzewa widżetów i dlatego
tworzy wszystkie widżety w drzewie. W tym przypadku jego bezpośrednim dzieckiem jest
MaterialApp. Zgodnie z dokumentacją jest to określone w następujący sposób:
BuildContext to argument dostarczany do metody build jako przydatny sposób interakcji z drzewem
widżetów. Umożliwia dostęp do ważnych informacji o przodkach, które pomagają opisać bu-
dowany widżet. Pamiętaj, opis zależy tylko od tych informacji kontekstowych i właściwości
widżetu, które są zdefiniowane w konstruktorze.
Widżetom Material Design przyjrzymy się szczegółowo, gdy będziemy badać dostępne
wbudowane widżety, a także w rozdziale 6.
Oprócz innych właściwości MaterialApp zawiera właściwość home, która określa pierwszy widżet
wyświetlany jako strona główna aplikacji. Tutaj home reprezentuje widżet MyHomePage, który jest
w tym przykładzie widżetem stanowym.
@override
_MyHomePageState createState() => _MyHomePageState();
}
Rozszerzając statefulWidget, MyHomePage musi zwrócić prawidłowy obiekt State w swojej meto-
dzie createState(). W naszym przykładzie zwraca instancję _MyHomePageState.
Zwykle widżety stanowe definiują odpowiadające im klasy State w tym samym pliku.
Ponadto stan jest zazwyczaj prywatny dla biblioteki widżetów, ponieważ klienci zewnętrzni
nie muszą bezpośrednio z nią współpracować.
123
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // Ten końcowy przecinek sprawia, że automatyczne formatowanie jest
), // przyjemniejsze.
);
}
}
Prawidłowy stan widżetu to klasa, która rozszerza klasę frameworku State zdefiniowaną w doku-
mentacji w następujący sposób:
Logika i stan wewnętrzny dla StatefulWidget.
Stan widżetu MyHomePage jest definiowany przez pojedynczą właściwość _counter. Właściwość
_counter zachowuje liczbę naciśnięć przycisku w prawym dolnym rogu ekranu. Tym razem za
zbudowanie widżetu odpowiada klasa potomna widżetu State. Składa się z widżetu Text,
który wyświetla wartość _counter.
124
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Widżet stanowy ma zmieniać wygląd w trakcie swojego życia — to znaczy, że to, co go definiuje,
zmieni się — dlatego należy go przebudować, aby odzwierciedlał takie zmiany. Tutaj zmiana
następuje w metodzie _incrementCounter(), która jest wywoływana za każdym razem, gdy
naciśnięty zostanie przycisk.
Skąd framework wie, kiedy coś w widżecie się zmienia i musi go przebudować? Odpowiedzią
jest setState. Ta metoda otrzymuje funkcję jako parametr, w którym należy zaktualizować odpo-
wiedni widżet nawiązujący do State (czyli metodę _incrementCounter). Wywołując setState,
framework jest powiadamiany, że musi przebudować widżet. W naszym przykładzie jest wy-
woływana w celu odzwierciedlenia nowej wartości właściwości _counter.
125
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Widżety dziedziczone
Oprócz statelessWidget i statefulWidget we frameworku Fluttera istnieje jeszcze jeden typ
widżetu, InheritedWidget. Czasami jeden widżet może potrzebować dostępu do danych z góry
drzewa i w takim przypadku musielibyśmy replikować informacje w dół do interesującego wi-
dżetu. Ten proces przedstawiono na poniższym schemacie:
Załóżmy, że niektóre widżety w drzewie muszą uzyskać dostęp do właściwości title z po-
ziomu widżetu głównego. Aby to zrobić, za pomocą statelessWidget lub statefulWidget mu-
sielibyśmy zreplikować właściwość w odpowiednich widżetach i przekazać ją przez konstruk-
tor. Replikowanie właściwości we wszystkich widżetach podrzędnych może być denerwujące.
Aby rozwiązać ten problem, Flutter udostępnia klasę InheritedWidget, pomocniczy rodzaj wi-
dżetu, który pomaga propagować informacje w dół drzewa, jak pokazano na diagramie na
następnej stronie.
Jeśli dodamy InheritedWidget do drzewa, każdy widżet znajdujący się poniżej może uzyskać dostęp
do danych, udostępnionych za pomocą metody inheritFromWidgetOfExactType(InheritedWidget)
klasy BuildContext, która otrzymuje typ InheritedWidget jako parametr i używa drzewa do zna-
lezienia pierwszego przodka widżetu żądanego typu.
126
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
127
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
ma stan, wymaga, aby odpowiedni stan został z nim przeniesiony. Krótko mówiąc, to właśnie
klucz pomaga frameworkowi to robić. Trzymając wartość klucza, dany element będzie znał odpo-
wiedni stan widżetu.
W dalszej części książki będziemy używać w naszej aplikacji kluczy. Jeśli chcesz
teraz znaleźć więcej szczegółów na temat wpływu key na widżet i dostępnych typów
kluczy, zapoznaj się z oficjalnym wprowadzeniem do kluczy w dokumentacji:
https://flutter.io/docs/development/ui/widgets-intro#keys.
Widżety wbudowane
Flutter kładzie duży nacisk na interfejs użytkownika, dlatego zawiera duży katalog widżetów,
które umożliwiają tworzenie niestandardowych interfejsów zgodnie z Twoimi potrzebami.
Dostępne widżety Fluttera obejmują zarówno proste elementy, widżet Text (przykład aplika-
cji licznika), jak i złożone widżety, które pomagają projektować dynamiczny interfejs użyt-
kownika z animacjami i obsługą wielu gestów.
Widżety podstawowe
Widżety podstawowe we Flutterze są dobrym punktem wyjścia, nie tylko ze względu na ła-
twość użycia, ale także dlatego, że demonstrują moc i elastyczność frameworka, nawet w prostych
przypadkach.
Nie będziemy studiować wszystkich dostępnych widżetów, ponieważ zniweczyłoby to cel tej
książki, dlatego wymienimy tylko niektóre z nich dla Twojej wiedzy, a część z nich będziemy wy-
korzystywać w praktyce, abyś mógł nauczyć się podstaw w celu dalszego poszerzania wiedzy.
Widżet Text
Text wyświetla ciąg tekstu w dowolnym stylu:
Text(
"To jest tekst",
)
128
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Aby zobaczyć wszystkie dostępne właściwości widżetu Text, przejdź na oficjalną stronę
z dokumentacją widżetu: https://docs.flutter.io/flutter/widgets/Text-class.html.
Widżet Image
Image wyświetla obraz z różnych źródeł i formatów. Obsługiwane w dokumentach formaty
obrazu to JPEG, PNG, GIF, animowany GIF, WebP, animowany WebP, BMP i WBMP:
Image(
image: AssetImage(
"assets/dart_logo.jpg"
),
)
Właściwość Image widżetu określa ImageProvider. Wyświetlany obraz może pochodzić z różnych
źródeł. Klasa Image zawiera różne konstruktory dla różnych sposobów ładowania obrazów:
Image (https://api.flutter.dev/flutter/widgets/Image/Image.html), do uzyskania obrazu
z ImageProvider (https://api.flutter.dev/flutter/painting/ImageProvider-class.html),
jak w poprzednim przykładzie.
Image.asset (https://api.flutter.dev/flutter/widgets/Image/Image.asset.html) tworzy
AssetImage , który służy do uzyskania obrazu z AssetBundle
(https://api.flutter.dev/flutter/ services /AssetBundle-class.html) przy użyciu
klucza zasobu. Przykład jest następujący.
Image.asset(
'assets/dart_logo.jpg',
)
Image.network (https://api.flutter.dev/flutter/widgets/Image/Image.network.html)
tworzy NetworkImage w celu uzyskania obrazu z adresu URL.
Image.network(
'https://picsum.photos/250?image=9',
)
Image.file (https://api.flutter.dev/flutter/widgets/Image/Image.file.html)
tworzy FileImage w celu uzyskania obrazu z pliku
(https://api.flutter.dev/flutter/dart- io/File-class.html).
Image.file(
File(file_path)
)
Image.memory (https://api.flutter.dev/flutter/widgets/Image/Image.memory.html)
tworzy MemoryImage w celu uzyskania obrazu z Uint8List
(https://api.flutter.dev/flutter/dart-typed_data/Uint8List-class.html).
129
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Image.memory(
Uint8List(image_bytes)
)
Aby zobaczyć wszystkie dostępne właściwości widżetu Image, przejdź do oficjalnej strony
dokumentacji widżetu obrazka: https://docs.flutter.io/flutter/widgets/Image-class.html.
Jeśli nie znasz wytycznych Material Design lub iOS Cupertino, to dobry czas, aby je poznać:
Material Design: https://material.io/guidelines/material-design/introduction.html;
iOS Cupertino: https://developer.apple.com/design/human-interface-guidelines/.
Na przykład Flutter nie ma widżetu Button; zamiast tego zapewnia alternatywne implemen-
tacje przycisków za pomocą wytycznych Google Material Design i iOS Cupertino.
Nie zamierzamy zagłębiać się w każdą właściwość lub zachowanie widżetu, ponieważ
można je łatwo przestudiować, uruchamiając przykłady lub zaglądając do dokumentacji.
Możesz również sprawdzić aplikację Flutter Gallery w Google Play (https://play.google.com/
store/apps/details?id=io.flutter.demo.gallery), aby znaleźć krótką i fajną demonstrację do-
stępnych widżetów.
Buttony
Po stronie Material Design Flutter implementuje następujące komponenty przycisków:
RaisedButton — wypukły przycisk Material Design. Wypukły przycisk składa się
z prostokątnego kawałka materiału, który unosi się nad interfejsem.
FloatingActionButton — pływający przycisk akcji to okrągły przycisk z ikoną,
który znajduje się nad zawartością w celu promowania podstawowej akcji w aplikacji.
130
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Ze względu na wytyczne Material Design, elewację, efekty ink i efekty świetlne, widżety
Material Design są nieco droższe niż widżety Cupertino. Nie jest to duży problem, ale
warto mieć tego świadomość.
Scaffold
Scaffold implementuje podstawową strukturę układu wizualnego Material Design lub iOS
Cupertino. W przypadku zastosowania Material Design widżet Scaffold może zawierać wiele
komponentów Material Design:
body — podstawowa zawartość scaffold. Jest wyświetlany poniżej paska AppBar,
jeśli istnieje.
AppBar — pasek aplikacji składa się z paska narzędzi i potencjalnie innych
widżetów.
TabBar — widżet Material Design, który wyświetla poziomy rząd zakładek. Jest to
zwykle używane jako część AppBar.
TabBarView — widok strony, który wyświetla widżet odpowiadający aktualnie wybranej
karcie. Zwykle używany w połączeniu z TabBar i używany jako widżet body.
BottomNavigationBar — dolne paski nawigacyjne ułatwiają przeglądanie
i przełączanie się między widokami najwyższego poziomu za pomocą jednego
dotknięcia.
Drawer — panel Material Design, który przesuwa się poziomo od krawędzi
scaffold, aby wyświetlić łącza nawigacyjne w aplikacji.
131
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dialogi
Flutter implementuje zarówno okna dialogowe Material Design, jak i Cupertino. Po stronie
Material Design są to SimpleDialog i AlertDialog; po stronie Cupertino są to CupertinoDialog
i CupertinoAlertDialog.
Pola tekstowe
Pola tekstowe są również zaimplementowane w obu wytycznych, przez widżet TextField
w Material Design oraz przez widżet CupertinoTextField w iOS Cupertino. Oba wyświetlają
klawiaturę do wprowadzania danych przez użytkownika. Niektóre z ich wspólnych właściwo-
ści są następujące:
autofocus — określa, czy pole TextField powinno być ustawiane automatycznie
(jeśli nic innego nie jest już ustawione);
enabled — pozwala ustawić pole jako edytowalne lub nie;
keyboardType — pozwala zmienić typ klawiatury wyświetlanej użytkownikowi
podczas edycji.
Widżety wyboru
W Material Design dostępne są następujące widżety wyboru:
Checkbox umożliwia wybór wielu opcji na liście.
Radio umożliwia pojedynczy wybór z listy opcji.
Switch umożliwia przełączanie (włączanie / wyłączanie) pojedynczej opcji.
Slider umożliwia wybór wartości w zakresie poprzez przesuwanie suwaka.
W przypadku iOS Cupertino niektóre z tych funkcji widżetów nie istnieją; są jednak dostępne
alternatywy:
CupertinoActionSheet — modalny arkusz akcji w stylu iOS umożliwiający wybór
opcji spośród wielu.
CupertinoPicker — również kontrolka selektora. Służy do wybierania pozycji
z krótkiej listy.
CupertinoSegmentedControl — zachowuje się jak przycisk opcji, w którym wybór
jest pojedynczą pozycją z listy opcji.
132
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Inne składniki
Istnieją również komponenty specyficzne dla projektu, które są unikalne dla każdej platformy.
Na przykład Material Design obejmuje koncepcję Kart, która w dokumentacji jest zdefinio-
wana w następujący sposób:
Arkusz używany do przedstawienia pewnych powiązanych informacji.
Z drugiej strony widżety specyficzne dla Cupertino mogą mieć unikalne przejścia obecne
w świecie iOS.
Wprowadzenie do wbudowanych
widżetów layoutu
Wydaje się, że niektóre widżety nie pojawiają się na ekranie dla użytkownika, ale jeśli znaj-
dują się w drzewie widżetów, w jakiś sposób tam będą, wpływając na wygląd widżetu pod-
rzędnego (na przykład na jego położenie lub styl).
Aby na przykład umieścić przycisk w dolnym rogu ekranu, moglibyśmy określić pozycję zwią-
zaną z ekranem, ale jak być może zauważyłeś, przyciski i inne widżety nie mają właściwości
Position. Możesz więc zadawać sobie pytanie: „Jak są zorganizowane widżety na ekranie?”.
Odpowiedzią są znowu widżety. Zgadza się! Flutter dostarcza widżety do komponowania samego
layoutu, z pozycjonowaniem, skalowaniem, stylizacją i tak dalej.
Kontenery
Wyświetlanie pojedynczego widżetu na ekranie nie jest dobrym pomysłem na organizację
interfejsu użytkownika. Zwykle tworzymy listę widżetów, które są zorganizowane w określony
sposób; w tym celu używamy kontenerów widżetów.
133
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Innym szeroko stosowanym widżetem jest widżet Stack, organizujący dzieci w warstwy, w których
jedno może częściowo lub całkowicie nakładać się na drugie.
Jeśli wcześniej tworzyłeś jakąś aplikację mobilną, być może korzystałeś już z list i siatek.
Flutter zapewnia klasy dla obu z nich: mianowicie widżety ListView i GridView. Dostępne są
również inne, mniej typowe, ale mimo to ważne widżety kontenerów, takie jak Table, który orga-
nizuje elementy podrzędne w układzie tabelarycznym.
Stylizacja i pozycjonowanie
Zadanie pozycjonowania widżetu podrzędnego w kontenerze, na przykład widżetu Stack, jest
wykonywane przy użyciu innych widżetów. Flutter zapewnia widżety do bardzo konkretnych
zadań. Wyśrodkowanie widżetu w kontenerze odbywa się poprzez umieszczenie go w widżecie
Center. Wyrównanie widżetu podrzędnego względem elementu nadrzędnego można wykonać za
pomocą widżetu Align, w którym żądane położenie można określić za pomocą jego właściwo-
ści aligment. Kolejnym przydatnym widżetem jest Padding, dzięki któremu możemy określić
przestrzeń wokół danego dziecka. Funkcjonalności tych widżetów są zagregowane w widżecie
Container, który łączy te wspólne widżety pozycjonowania i stylizacji, aby zastosować je bezpo-
średnio do dziecka, dzięki czemu kod jest znacznie czystszy i krótszy.
Nie jesteśmy w stanie zbadać wszystkich dostępnych widżetów i wszystkich ich możliwych
kombinacji. Swoją podróż zaczniemy od opracowania małej aplikacji w następnej sekcji, w której
zbadamy niektóre z dostępnych widżetów we wszystkich kategoriach, abyś mógł sobie zwizu-
alizować, jak korzystać z niektórych z nich. Co najważniejsze, nauczysz się podstaw tworzenia
layoutów we Flutterze. Gdy to zrobisz, poznanie nowych i konkretnych widżetów będzie łatwym
zadaniem.
Podczas pisania tej książki Flutter rozwija kolejną wspaniałą funkcję, widok platformy
(platform view), która pozwala nam wykorzystywać wszelkie natywne interfejsy, dostępne
już w iOS i Androidzie. Przeczytasz o tym więcej w rozdziale 11., Widoki platformy oraz
integracja mapy, w sekcji Wyświetlanie mapy.
134
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Ekrany aplikacji
Aplikacja Friend Favors będzie się składać z dwóch ekranów. W obu z nich będziemy korzy-
stać z komponentów Material Design dostarczonych przez Fluttera. Na pierwszym ekranie
pojawi się lista przysług, a na drugim formularz proszenia znajomego o przysługę. Na razie
będziemy używać list w pamięci; oznacza to, że informacje nie będą przechowywane w żadnym
innym miejscu niż aplikacja.
Kod aplikacji
Kod aplikacji nie jest jeszcze w pełni funkcjonalny. Jest wystarczająco mały, aby stworzyć
layout. Tworzy instancję widżetu MaterialApp, która ustawia ekran główny na stronę z listą
przysług o nazwie FavorsPage:
class MyApp extends statelessWidget {
// na razie używając pozorowanych wartości z pliku mock_favors
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: FavorsPage(
pendingAnswerFavors: mockPendingFavors,
completedFavors: mockCompletedFavors,
refusedFavors: mockRefusedFavors,
acceptedFavors: mockDoingFavors,
),
);
}
}
135
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
MaterialApp to widżet, który udostępnia przydatne narzędzia dla całej aplikacji. Jednym z nich
jest widżet Theme, który pozwala nam zmieniać style i kolory naszych aplikacji zgodnie z wytycz-
nymi Material Design. Innym przydatnym narzędziem jest widżet Navigator, który zarządza
zestawem widżetów aplikacji w sposób podobny do stosu nawigacyjnego, na którym możemy
nawigować do ekranu lub wstecz. W aplikacji będziemy używać obu widżetów. Navigator
zastosowaliśmy już, gdy ustawialiśmymy właściwość home widżetu MaterialApp. Navigator działa
na zasadzie ścieżek (route) do widżetu. Oznacza to, że istnieje kilka sposobów definiowania okre-
ślonych ścieżek wskazujących na określone widżety, a kiedy nawigujemy do określonej ścieżki,
Navigator będzie mógł nawigować do odpowiedniego widżetu. Ustawiając właściwość home w ja-
kimś widżecie, mówimy, że Navigator używa tego widżetu za pomocą ścieżki ‘/’.
Jak widać, widżet FavorsPage ma wypełnione niektóre parametry konstruktora. Aby zoba-
czyć, czym one są, czytaj dalej.
Na pierwszym etapie przyjrzymy się początkowej strukturze układu aplikacji, która będzie
ewoluować do końca książki wraz z nowymi stylami i widżetami. W następnym rozdziale do-
wiesz się, jak dodać niektóre metody wprowadzania danych przez użytkownika za pomocą
dotknięć i pól formularzy. Później, w rozdziale 6., zobaczymy, jak dostosować wygląd aplikacji
za pomocą motywu. Zacznijmy więc od przyjrzenia się layoutom ekranu.
136
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
137
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Lista będzie zawierać wszystkie przysługi, rozdzielone według kategorii. U góry layoutu mamy
instancję TabBar, która posłuży do zmiany zakładki na żądaną listę. Następnie na każdej zakładce
mamy listę elementów Card, które zawierają akcje odpowiadające jej kategorii.
Stworzyliśmy klasy Friend i Favor reprezentujące dane aplikacji. Możesz przyjrzeć się
temu bliżej w kodzie źródłowym rozdziału (katalog hands_on_layouts) tej książki. Tutaj
są to proste klasy danych, które nie zawierają żadnej zaawansowanej logiki biznesowej.
Ponadto pływający przycisk akcji na dole ekranu powinien przekierowywać do ekranu Request
a favor, gdzie użytkownik będzie mógł poprosić znajomych o przysługę.
Kod layoutu
Przede wszystkim zdefiniujemy naszą stronę główną jako instancję statelessWidget, ponie-
waż teraz zależy nam tylko na layoucie i nie mamy do wykonania żadnych działań, które spo-
wodowałyby zmianę stanu. Dlatego widżet nadrzędny MyApp przekazuje wartości do zdefinio-
wanych pól listy.
Pamiętaj, że gdy widżet jest bezstanowy, jego opis jest definiowany przez widżet nadrzędny
podczas jego tworzenia. Pokazuje to poniższy kod:
class FavorsPage extends statelessWidget {
// na razie używając pozorowanych wartości z pliku mock_favors
final List<Favor> pendingAnswerFavors;
final List<Favor> acceptedFavors;
final List<Favor> completedFavors;
final List<Favor> refusedFavors;
FavorsPage({
Key key,
this.pendingAnswerFavors,
this.acceptedFavors,
this.completedFfavors,
this.refusedFavors,
}) : super(key: key);
@override
Widget build(BuildContext context) {...} // dla zwięzłości
}
Jak pokazano w poprzednim kodzie, widżet jest definiowany przez listy specyficzne dla przy-
sług. Zwróć także uwagę na parametr key. Chociaż nie jest to tutaj naprawdę potrzebne, dobrą
praktyką jest zdefiniowanie parametru.
Rzućmy okiem na metodę build(), aby zobaczyć, jak zbudowany jest widżet:
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
138
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
child: Scaffold(
appBar: AppBar(
title: Text("Your favors"),
bottom: TabBar(
isScrollable: true,
tabs: [
_buildCategoryTab("Requests"),
_buildCategoryTab("Doing"),
_buildCategoryTab("Completed"),
_buildCategoryTab("Refused"),
],
),
),
body: TabBarView(
children: [
_favorsList("Pending Requests", pendingAnswerFavors),
_favorsList("Doing", acceptedFavors),
_favorsList("Completed", completedFavors),
_favorsList("Refused", refusedFavors),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Ask a favor',
child: Icon(Icons.add),
),
),
);
}
139
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, funkcja tworzy element zakładek dla kategorii, po prostu budując poddrzewo Tab
> Text, gdzie title jest identyfikatorem elementu.
W ten sam sposób każda sekcja listy przysług jest definiowana w swojej metodzie _favorsList():
class FavorsPage extends statelessWidget {
// ... pola, metody budowania i inne
140
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Widżet sekcji przysług jest reprezentowany przez widżet Column, który ma dwa widżety podrzędne:
widżet Text (z elementem nadrzędnym Padding) zawierający tytuł sekcji, jak
poprzednio;
instancję ListView, które będzie zawierać każdy z elementów przysług.
Ta lista jest zbudowana w inny sposób niż poprzednie. Tutaj użyliśmy konstruktora nazwanego
ListView.builder(). Oczekuje on instancji itemCount i itemBuilder, które definiujemy za po-
mocą listy przekazanej jako argument w wywołaniu funkcji _favorsList():
itemCount to po prostu rozmiar listy;
itemBuilder musi być funkcją, która zwraca widżet odpowiadający elementowi
w określonej pozycji. Ta funkcja otrzymuje BuildContext, podobnie jak metoda
build() widżetu, a także pozycję indeksu (tutaj użyliśmy argumentu index, aby
uzyskać odpowiednią przysługę z listy).
Ta forma tworzenia elementów jest optymalna w przypadku list dużych, takich, które rosną
w trakcie cyklu życia, a nawet tych przewijanych w nieskończoność (które być może już widziałeś
w niektórych aplikacjach), ponieważ buduje przedmioty tylko wtedy, gdy są potrzebne, zapobiegając
marnowaniu zasobów. obliczeniowych.
Wartość funkcji itemBuilder tworzy widżet Card dla każdej przysługi na liście argumentów,
pobierając odpowiednią pozycję za pomocą final favor = favors [index];.
Kiedy mówimy o elementach listy, zawsze będziemy potrzebować wartości key widżetu, przy-
najmniej jeśli dodamy do niego obsługę zdarzenia wyboru. Dzieje się tak, ponieważ listy we
Flutterze mogą obiegać wiele elementów podczas zdarzeń przewijania, a dodając klucz, będziemy
twierdzić, że określony widżet ma powiązany z nim określony stan.
141
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Nowy element, który wystąpił, to właściwość margin widżetu Card, która dodaje margines do
widżetu. W tym przypadku dodajemy 10,0 dip (Density-independent Pixels) dla góry i dołu oraz
25,0 dla lewej i prawej strony. Jego dziecko body jest podzielone na trzy części:
Najpierw jest nagłówek, pokazujący znajomego, który wysłał prośbę o przysługę,
zdefiniowany w funkcji _itemHeader().
Row _itemHeader(Favor favor) {
return Row(
children: <Widget>[
CircleAvatar(
backgroundImage: NetworkImage(
favor.friend.photoURL,
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8.0),
child: Text("${favor.friend.name} asked you to...
")),
)
],
);
}
Nagłówek jest zdefiniowany jako poddrzewo Row> [CircleAvatar, Expanded]. Zaczyna się od de-
finicji Row (działa jak widżet Column, ale na osi poziomej), która ma instancję CircleAvatar, czyli
okrągły obraz reprezentujący użytkownika. Tutaj użyliśmy dostawcy NetworkImage; po prostu
przekazujemy do niego adres URL obrazu i pozwalamy mu się załadować. Pozostała przestrzeń
widżetu Row jest używana przez Text z pewną wartością Padding, która pokazuje imię znajomego.
Po drugie, istnieje treść, która jest po prostu widżetem Text z opisem przysługi.
Na końcu jest stopka, która zawiera dostępne akcje dla prośby o przysługę
w zależności od kategorii przysługi, zdefiniowanej w funkcji _itemFooter().
Widget _itemFooter(Favor favor) {
if (favor.isCompleted) {
final format = DateFormat();
return Container(
margin: EdgeInsets.only(top: 8.0),
alignment: Alignment.centerRight,
child: Chip(
label: Text("Completed at:
${format.format(favor.completed)}"),
),
);
}
if (favor.isRequested) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FlatButton(
child: Text("Refuse"),
onPressed: () {},
142
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
),
FlatButton(
child: Text("Do"),
onPressed: () {},
)
],
);
}
if (favor.isDoing) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FlatButton(
child: Text("give up"),
onPressed: () {},
),
FlatButton(
child: Text("complete"),
onPressed: () {},
)
],
);
}
return Container();
}
Zawsze możesz skorzystać z metod klasy pomocnika EdgeInsets, gdy definiujesz pad-
ding lub marginesy. Ma on przydatne do tego metody. Sprawdź oficjalną stronę doku-
mentacji: https://api.flutter.dev/flutter/painting/EdgeInsets-class.html.
Jak widzieliśmy w implementacji list przysług, istnieją różne widżety tworzące layout. Zwróć jed-
nak uwagę, że nie obsługujemy tutaj żadnych działań użytkownika; tym wszystkim zajmiemy
się w następnym rozdziale. Rzućmy okiem na ekran prośby o przysługę.
143
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Zwróć uwagę na właściwość onPressed dla FlatButton; definiuje akcję, gdy użytkownik
ją wybierze. Przyjrzymy się temu w rozdziale 5., więc kontynuuj lekturę!
Widżet ekranu prośby o przysługę również ma widżet Material Design Scaffold z paskiem
aplikacji, który tym razem zawiera akcje. Treść widżetu Scaffold zawiera pola, które przyjmą
informacje wejściowe od użytkownika w celu utworzenia prośby o przysługę.
144
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Kod layoutu
Widżet RequestFavorPage również jest teraz bezstanowy, ponieważ obecnie zależy nam tylko
na jego layoucie:
class RequestFavorPage extends statelessWidget {
final List<Friend> friends;
@override
Widget build(BuildContext context) {...} // dla zwięzłości
}
Jak widać, jedyną rzeczą w opisie widżetu jest lista znajomych, która musi być dostarczona przez
widżet nadrzędny, ponieważ jest to obecnie bezstanowa (statelessWidget) instancja widżetu.
Aby dowiedzieć się, jak poruszać się między ekranami (czyli od listy przysług do ekranu
Poproś o przysługę), przejdź do rozdziału 7., w którym mówimy o wyznaczaniu ścieżek
i nawigacji.
body Scaffolda definiuje układ w widżecie Column. Zawiera dwie nowe właściwości: pierwsza
to mainAxisSize, która definiuje rozmiar na osi pionowej; tutaj używamy MainAxisSize.min,
więc zajmuje ona tylko tyle miejsca, ile jest konieczne. Druga to crossAxisAlignment, które
definiuje wyrównanie elementów podrzędnych na osi poziomej. Domyślnie Column wyrównuje
145
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
swoje elementy podrzędne poziomo do środka. Korzystając z tej właściwości, możemy zmienić to
zachowanie. W Column znajdują się trzy widżety podrzędne, które przyjmą dane wejściowe
użytkownika:
Widżet DropdownButtonFormField, który po wybraniu wyświetla elementy widżetu
DropdownMenuItem w wyskakującym okienku:
...
DropdownButtonFormField(
items: friends
.map(
(f) => DropdownMenuItem(
child: Text(f.name),
),
)
.toList(),
),
...
Tutaj używamy metody map() z typu Darta Iterable, gdzie każdy element z listy (w tym przy-
padku przyjaciele) jest mapowany na nowy widżet DropdownMenuItem. Tak więc każdy element
z listy znajomych zostanie wyświetlony jako element widżetu na liście rozwijanej.
Widżet TextFormField, który umożliwia wprowadzanie tekstu za pomocą
klawiatury:
TextFormField(
maxLines: 5,
inputFormatters: [LengthLimitingTextInputFormatter(200)],
),
146
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Oprócz pól wejściowych w Column znajdują się również widżety Container i Text, które pomagają
w formatowaniu i projektowaniu ekranu. Spójrz na kod źródłowy rozdziału, aby uzyskać pełny
kod layoutu.
W aplikacji utworzyliśmy już część layotu, a jedynymi niestandardowymi widżetami, które stwo-
rzyliśmy, są widżety FavorsPage i RequestFavorPage.
Być może zauważyłeś również, że ze względu na sposób tworzenia layoutów we Flutterze kod
może stać się ogromny i trudny do utrzymania. Aby rozwiązać ten problem, stworzyliśmy
małe metody, które dzielą tworzenie widżetu na części w celu zbudowania pełnego layoutu.
Dzielenie widżetów na małe metody pomaga zmniejszyć rozmiar kodu, ale nie jest tak dobre
dla Fluttera. W naszym przypadku nie mamy jeszcze złożonego layoutu, więc jest to w porządku,
ale w przypadku złożonego layotu, w którym drzewo widżetów może zmieniać się wiele razy,
posiadanie widżetów jako wbudowanych metod nie pomoże frameworkowi w optymalizacji
procesu renderowania.
Aby pomóc platformie w optymalizacji procesu renderowania, powinniśmy zamiast tego podzielić
nasze metody na małe, celowe widżety. Tak więc operacje na drzewie widżetów | drzewie
elementów zostaną zoptymalizowane. Pamiętaj, że rodzaj widżetu pomaga platformie wiedzieć,
kiedy widżet się zmienia i należy go przebudować, co wpływa na cały proces renderowania.
147
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
@override
Widget build(BuildContext context) {
return Card(
key: ValueKey(favor.uuid),
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
child: Padding(
child: Column(
children: <Widget>[
_itemHeader(favor),
Text(favor.description),
_itemFooter(favor)
],
),
padding: EdgeInsets.all(8.0),
),
);
}
Widget _itemHeader(Favor favor) { ... } // dla zwięzłości
Widget _itemFooter(Favor favor) { ... } // dla zwięzłości
}
Jedyną rzeczą, która się zmienia, jest dodanie nowej klasy z odpowiednimi polami typu final,
które mają znaczenie dla renderowania widżetu; metoda build() jest prawie taka sama jak po-
przednia metoda _buildFavorsList().
Zwróć uwagę, że element karty przysługi nadal zawiera części nagłówka i stopki jako metody, od-
powiednio _itemHeader() i _itemActions(). Dzięki temu są one wystarczająco małe, aby nie szko-
dzić procesowi renderowania. Ale pamiętaj, że podzielenie ich na widżety też nie zaszkodzi.
148
d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera
Podsumowanie
W tym rozdziale widzieliśmy każdy z dostępnych typów widżetów Fluttera oraz ich różnice.
Widżety stateless nie są często odbudowywane przez framework; z drugiej strony, widżety
stateful są odbudowywane za każdym razem, gdy zmienia się skojarzony z nim obiekt State
(co może mieć miejsce, na przykład, gdy używana jest funkcja setState()). Widzieliśmy również,
że Flutter zawiera wiele widżetów, które można łączyć w celu tworzenia unikalnych interfej-
sów użytkownika, i że nie muszą one być elementami wizualnymi na ekranie użytkownika;
mogą to być layouty, style, a nawet widżety danych, takie jak InheritedWidget. Rozpoczęliśmy
tworzenie małej aplikacji, którą będziemy rozwijać w następnych kilku rozdziałach; będziemy
dodawać do niej określone funkcje, przedstawiając nowe ważne koncepcje dotyczące Fluttera.
W następnym rozdziale dowiemy się, jak dodać interakcję użytkownika do aplikacji w wyniku re-
akcji na dotknięcia użytkownika i wprowadzane dane, które później będą przechowywane w bazie
Firebase.
149
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
150
d0765ad53fb82babda2278a311da7afb
d
5
Obsługa danych
wejściowych i gestów
użytkownika
Dzięki widżetom można stworzyć interfejs bogaty w zasoby wizualne, które umożliwiają również
interakcję użytkownika za pomocą gestów i wprowadzania danych. W tym rozdziale dowiesz się
o widżetach używanych do obsługi gestów użytkownika, odbierania i potwierdzania jego danych
wejściowych, a także o tym, jak tworzyć własną obsługę niestandardowych danych wejściowych.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Wskaźniki
Flutter rozpoczyna obsługę zdarzeń w warstwie niskiego poziomu (warstwach wskaźników),
gdzie możesz obsłużyć każde zdarzenie wskaźnika i zdecydować, jak nim sterować, na przy-
kład za pomocą przeciągnięcia lub jednego dotknięcia. Struktura Flutter implementuje wy-
syłanie zdarzeń w drzewie widżetów, wykonując sekwencję zdarzeń:
PointerDownEvent to miejsce, w którym rozpoczyna się interakcja — wskaźnik
wchodzi w kontakt z określonym miejscem na ekranie urządzenia. W tym przypadku
platforma przeszukuje drzewo widżetów pod kątem widżetu, który istnieje w miejscu
wskaźnika na ekranie. Ta akcja nazywa się testem trafień.
Każde kolejne zdarzenie jest wysyłane do najbardziej wewnętrznego widżetu,
który pasuje do lokalizacji, a następnie wywoływane jest drzewo widżetów
z widżetów nadrzędnych do katalogu głównego. Tej propagacji akcji zdarzeń nie
można przerwać. Zdarzeniem może być PointerMoveEvent, w którym zmieniana
jest lokalizacja wskaźnika. Może to być również PointerUpEvent lub
PointerCancelEvent.
Interakcja może zakończyć się zdarzeniem pointerUpEvent lub PointerCancelEvent.
W pierwszym przypadku wskaźnik przestaje być w kontakcie z ekranem, podczas
gdy drugi oznacza, że aplikacja nie otrzymuje już żadnych zdarzeń dotyczących
wskaźnika (zdarzenie nie jest kompletne).
Flutter udostępnia klasę Listener, która może być używana do wykrywania omówionych wcze-
śniej zdarzeń interakcji wskaźnika. Widżet ten może otoczyć drzewo widżetów, aby obsługiwać
zdarzenia wskaźnika w jego poddrzewie.
Gesty
Chociaż jest to możliwe, samodzielna obsługa zdarzeń wskaźnika za pomocą widżetu Listener
nie zawsze jest praktyczna. Zamiast tego zdarzenia mogą być obsługiwane na drugiej warstwie
systemu gestów Fluttera. Gesty są rozpoznawane na podstawie wielu zdarzeń wskazujących,
a nawet wielu pojedynczych wskaźników (multitouch). Istnieje wiele rodzajów gestów, które można
obsługiwać:
152
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Podobnie jak widżet Listener dla zdarzeń wskaźnika, Flutter udostępnia widżet GestureDe-
tector, który zawiera wywołania zwrotne dla wszystkich poprzednich zdarzeń. Powinniśmy
je stosować w zależności od efektu, jaki chcemy osiągnąć.
Dotknięcie
Zobaczmy, jak zaimplementować zdarzenie dotknięcia (tap), używając wywołania zwrotnego
onTap widżetu GestureDetector:
// część pliku tap_event_example.dart (pełny kod źródłowy w załączonych plikach)
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_counter++;
});
},
child: Container(
color: Colors.grey,
child: Center(
child: Text(
"Tap count: $_counter",
style: Theme.of(context).textTheme.display1,
),
),
),
);
}
}
153
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
To jest implementacja stanu widżetu, który zawiera przykład. Ma pojedynczy licznik pokazujący, ile
dotknięć zostało wykonanych na ekranie. W tym przykładzie właściwość onTap przechowuje
wywołanie zwrotne, które aktualizuje stan widżetu po dotknięciu ekranu, zwiększając
wartość _counter .
Podwójne dotknięcie
Przykładowy kod źródłowy z podwójnym dotknięciem jest bardzo podobny do powyższego:
// część pliku doubletap_event_example.dart (pełny kod źródłowy w załączonych plikach)
GestureDetector(
onDoubleTap: () {
setState(() {
_counter++;
});
},
child: ... // dla zwięzłości
);
Długie naciśnięcie
Ponownie, różnica w stosunku do poprzednich przykładów jest minimalna:
// część pliku press_and_hold_event_example.dart (pełny kod źródłowy
// w załączonych plikach)
GestureDetector(
onLongPress: () {
setState(() {
_counter++;
});
},
child: ... // dla zwięzłości
);
154
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Przeciąganie w poziomie
Zobaczmy, jak wygląda wersja pozioma:
// część pliku drag_event_example.dart (pełny kod źródłowy w załączonych plikach)
GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
setState(() {
_move = Offset.zero;
_dragging = true;
});
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_move += details.delta;
});
},
onHorizontalDragEnd: (DragEndDetails details) {
setState(() {
_dragging = false;
_dragCount++;
});
},
child: ... // dla zwięzłości
)
Tym razem potrzebujemy trochę więcej pracy niż w przypadku zdarzeń typu dotknięcie. W przy-
kładzie mamy trzy właściwości:
_dragging — służy do aktualizowania tekstu wyświetlanego przez użytkownika
podczas przeciągania.
_dragCount — gromadzi całkowitą liczbę zdarzeń przeciągania wykonanych od
początku do końca.
_move — gromadzi offset przeciągania zastosowanego do Text za pomocą
konstruktora translacji widżetu Transform.
Jak widać, wywołania zwrotne przeciągania otrzymują parametry związane z każdym zdarze-
niem — DragStartDetails, DragUpdateDetails i DragEndDetails — zawierają one wartości,
które mogą pomóc na każdym etapie przeciągania.
155
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Przeciąganie w pionie
Pionowa wersja przeciągania jest prawie taka sama jak wersja pozioma. Istotne różnice dotyczą
właściwości wywołania zwrotnego, którymi są onVerticalDragStart, onVerticalDragUpdate
i onVerticalDragEnd.
To, co zmienia się w przypadku wywołań zwrotnych pionowych i poziomych pod wzglę-
dem kodu, to wartość właściwości delta klasy DragUpdateDetails. W przypadku wersji
poziomej będzie ona miała zmienioną tylko poziomą część przesunięcia, a w przypadku
pionowej — będzie odwrotnie.
Przewijanie
Ta wersja jest również bardzo podobna. Znaczące różnice to nowe właściwości wywołania
zwrotnego: onPanStart, onPanUpdate i onPanEnd. W przypadku przeciągania przewijania oceniane
są przesunięcia obu osi; oznacza to, że są obecne obie wartości delta w DragUpdateDetails,
więc przeciąganie nie ma ograniczenia kierunku.
Skalowanie
Ta wersja to nic innego jak przesuwanie na więcej niż jednym wskaźniku. Zobaczmy, jak ona
wygląda:
// część pliku scale_event_example.dart (pełny kod źródłowy w załączonych plikach)
GestureDetector(
onScaleStart: (ScaleStartDetails details) {
setState(() {
_scale = 1.0;
_resizing = true;
});
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_scale = details.scale;
});
},
onScaleEnd: (ScaleEndDetails details) {
setState(() {
_resizing = false;
_scaleCount++;
});
},
child: ... // dla zwięzłości
)
156
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Jak widać, wywołania zwrotne skalowania wyglądają bardzo podobnie do wywołań zwrotnych
przeciągania, ponieważ otrzymują również parametry związane z każdym zdarzeniem — Scale
StartDetails, ScaleUpdateDetails i ScaleEndDetails — zawierają one wartości, które mogą
pomóc na każdym etapie zdarzenia skalowania.
Element potomny Text jest wyświetlany w RaisedButton, a jego naciśnięcie jest obsługiwane
w metodzie onPressed, jak wspomniano wcześniej.
Flutter zapewnia wiele widżetów danych wejściowych, które pomagają programistom uzyskać
różne rodzaje informacji od użytkownika. Niektóre z nich widzieliśmy już w rozdziale 4., w tym
TextField i różne rodzaje widżetów Selector i Picker.
157
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Flutter zapewnia dwa widżety, które pomagają organizować wprowadzanie kodu, sprawdzać go
i szybko dostarczać informacje zwrotne użytkownikowi. To są widżety Form i FormField.
FormField i TextField
Widżet FormField działa jako klasa bazowa do tworzenia własnego pola formularza, używanego do
integracji widżetu Form. Jego funkcje są następujące:
Pomoc w ustawianiu i pobieraniu bieżącej wartości wejściowej.
Walidacja bieżącej wartości wejściowej.
Zapewnienie informacji zwrotnej z walidacji.
Widżet FormField może funkcjonować bez widżetów Form, ale nie jest to typowe zachowanie
— zachodzi tylko wtedy, gdy mamy, powiedzmy, pojedynczy FormField na ekranie.
Korzystanie z kontrolera
Kiedy mamy ograniczony dostęp z Form i korzystamy z widżetu TextField, musimy użyć jego wła-
ściwości kontrolera, aby uzyskać dostęp do jego wartości. Odbywa się to za pomocą klasy
TextEditingController:
final _controller = TextEditingController.fromValue(
TextEditingValue(text: "Initial value"),
);
158
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Aby słuchać zmian, musimy dodać obiekt nasłuchujący (listener) do naszego _controller:
_controller.addListener(_textFieldEvent);
_textFieldEvent musi być funkcją, która będzie wywoływana za każdym razem, gdy zmieni
się widżet TextField.
Możemy dodać klucz do naszego TextFormField, który później może być użyty do uzyskania
dostępu do bieżącego stanu widżetu poprzez pole key.currentState, zawierające zaktualizo-
waną wartość pola.
Specjalny typ key odnosi się do rodzaju danych, z którymi współpracuje pole wejściowe. W po-
przednim przykładzie jest to String, a ponieważ jest to widżet TextField, key zależy od używanego
widżetu.
159
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Form
Posiadanie FormFieldWidget pomaga nam uzyskać dostęp do jego informacji i weryfikować je
indywidualnie. Ale aby rozwiązać problem zbyt wielu pól, możemy użyć widżetu Form. Widżet
Form logicznie grupuje instancje FormFieldWidget, co pozwala nam wykonywać operacje, w tym
uzyskiwać dostęp do informacji o polach i weryfikować je w prostszy sposób.
Widżet Form umożliwia nam łatwe uruchamianie następujących metod na wszystkich polach
podrzędnych:
save() — spowoduje to wywołanie metody save wszystkich instancji FormField i będzie
funkcjonować jak poprzednio. Działa jak zbiorcze zapisywanie wszystkich pól.
validate() — spowoduje to wywołanie metody validate wszystkich instancji
FormField, powodując wyświetlenie wszystkich błędów jednocześnie.
reset() — spowoduje to wywołanie metody reset wszystkich instancji FormField.
Doprowadzi to do przywrócenia całego formularza do stanu początkowego.
Za pomocą klucza
Widżet Form jest używany z towarzyszącym kluczem (key) typu FormState, który zawiera po-
moce do zarządzania wszystkimi elementami podrzędnymi jego instancji FormField:
final _key = GlobalKey<FormFieldState<String>>();
...
Form(
key: _key,
child: Column(
children: <Widget>[
TextFormField(),
TextFormField(),
],
),
);
Następnie możemy użyć klucza, aby pobrać stan skojarzony z Form i wywołać jego walidację
za pomocą _key.currentState.validate(). Przyjrzyjmy się teraz drugiej opcji.
Korzystanie z InheritedWidget
Widżet Form zawiera przydatną klasę, dzięki której nie trzeba dodawać do niego klucza i nadal
można czerpać z tego korzyści.
160
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Każdy widżet Form w drzewie ma powiązany z nim InheritedWidget. Form i wiele innych widżetów
ujawnia to w statycznej metodzie o nazwie of(), gdzie przekazujemy BuildContext i wyszu-
kujemy drzewo, aby znaleźć odpowiedni stan, którego szukamy. Wiedząc to, jeśli potrzebu-
jemy uzyskać dostęp do widżetu Form znajdującego się gdzieś pod nim w drzewie, możemy
użyć Form.of() i uzyskać dostęp do tych samych funkcji, które uzyskalibyśmy, gdybyśmy użyli
właściwości key:
// część przykładu input / main.dart (załączony pełny kod źródłowy)
// build() w klasie InputFormInheritedStateExamplesWidget
Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
validator: (String value) {
return value.isEmpty ? "cannot be empty" : null;
},
),
TextFormField(),
Builder(
builder: (BuildContext context) => RaisedButton(
onPressed: () {
print("Running validation");
final valid = Form.of(context).validate();
print("valid: $valid");
},
child: Text("validate"),
),
)
],
),
);
...
Zwróć szczególną uwagę na widżet Builder używany do renderowania RaisedButton. Jak widzie-
liśmy wcześniej, dziedziczony widżet można przeglądać w drzewie. Rozważ następujące użycie
RaisedButton bezpośrednio w widżecie Column:
Column(
children: [
// ... inne elementy potomne, usunięte w celu zachowania zwięzłości
TextFormField(),
RaisedButton(
onPressed: () {
print("Running validation");
final valid = Form.of(context).validate(); // to nie powinno działać
// (nieprawidłowa wartość context)
print("valid: $valid");
},
child: Text("validate"),
)
],
...
161
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
162
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Widżet będzie prostym widżetem przyjumjącym jako dane wejściowe 6 cyfr. Później stanie się on
widżetem FormField i będzie udostępniać metody save(), reset() i validate().
163
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jedyną ważną właściwością przedstawioną w tym miejscu jest controller. Za chwilę zobaczymy
powód. Najpierw sprawdźmy powiązaną klasę State:
class _VerificationCodeInputState extends State<VerificationCodeInput> {
@override
Widget build(BuildContext context) {
return TextField(
164
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
controller: widget.controller,
inputFormatters: [
WhitelistingTextInputFormatter(RegExp("[0-9]")),
LengthLimitingTextInputFormatter(6),
],
textAlign: TextAlign.center,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: widget.borderSide,
),
),
keyboardType: TextInputType.number,
onChanged: widget.onChanged,
);
}
}
Zwróć uwagę na ważną część tego kodu: controller: widget.controller. Tutaj ustawiamy
kontroler widżetu TextField jako nasz własny kontroler, abyśmy mogli przejąć kontrolę nad
jego wartością.
@override
void initState() {
super.initState();
_controller.addListener(_controllerChanged);
}
165
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Na podstawie powyższego kodu można sprawdzić, czy zawiera on pojedyncze pole _controller,
które reprezentuje kontroler używany przez widżet FormField. Musi znajdować się w State,
więc utrzymuje wartość przy zmianach layoutu. Jak widać, jest inicjowany w funkcji initState().
Wykonywana jest ona przy pierwszym wstawieniu obiektu widżetu do drzewa widżetów. Tutaj
dodajemy do niego listenera, abyśmy mogli wiedzieć, kiedy wartość zostanie zmieniona
w _controllerChanged.
@override
void reset() {
super.reset();
_controller.text = "";
}
@override
void dispose() {
_controller?.removeListener(_controllerChanged);
super.dispose();
}
Istnieją również inne ważne metody, które musimy nadpisać, aby działały poprawnie:
Odwrotnym odpowiednikiem metody initState() jest metoda dispose().
Tutaj zatrzymujemy się, aby nasłuchiwać zmian w kontrolerze.
Metoda reset() jest nadpisywana, więc możemy ustawić element _controller.text
na pusty, co spowoduje ponowne wyczyszczenie pola wejściowego.
Listener _controllerChanged () powiadamia o stanie FormFieldState za pomocą
metody didChange(), dzięki czemu może zaktualizować swój stan (za pośrednictwem
funkcji setState()) i powiadomić o zmianie dowolny widżet Form, który go zawiera.
Przyjrzyjmy się teraz kodowi widżetu FormField, aby zobaczyć, jak to działa:
class VerificationCodeFormField extends FormField<String> {
final TextEditingController controller;
VerificationCodeFormField({
Key key,
FormFieldSetter<String> onSaved,
this.controller,
FormFieldValidator<String> validator,
}) : super(
key: key,
validator: validator,
builder: (FormFieldState<String> field) {
_VerificationCodeFormFieldState state = field;
return VerificationCodeInput(
controller: state.controller,
166
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
);
},
);
@override
Nowa część znajduje się w konstruktorze. Widżet FormField zawiera wywołanie zwrotne buildera,
które powinno tworzyć skojarzony z nim widżet danych wejściowych. Przekazuje aktualny
stan obiektu, dzięki czemu możemy zbudować widżet i zachować bieżące informacje. Jak wi-
dać, używamy tego do przekazania kontrolera skonstruowanego w State, więc utrzymuje się
nawet po odbudowie pola.
W ten sposób utrzymujemy synchronizację widżetu i State, a także integrujemy się z klasą Form.
Pełny kod źródłowy tego niestandardowego widżetu FormField można sprawdzić w pliku
configuration_code_input_widget.dart.
Ekran przysług
Pierwszy ekran aplikacji zawiera listę różnych przysług i ich statusów. Oprócz wyświetlenia
listy, jedyne czynności, które użytkownik może wykonać, to (zobacz rysunek na następnej
stronie):
1. Wybranie sekcji kategorii przysługi. Obsługą zajmuje się widżet
DefaultTabController (istnieje widżet ListView, który wewnętrznie obsługuje
gesty przesuwania / przewijania).
2. Odmówienie (Refuse) lub wyświadczenie (Do) żądanej przysługi. Na przykład
znajomy poprosił o przysługę, a użytkownik może ją przyjąć lub odrzucić.
Zatem dotknięcie jednego z przycisków powoduje zmianę statusu przysługi
na odrzucona (Refused) lub przyjęta do wykonania (Doing).
167
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
168
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Moglibyśmy użyć naszej globalnej listy przysług bezpośrednio w metodzie onPressed elementu
karty, ale oznaczałoby to dystrybucję logiki biznesowej za pośrednictwem widżetów, co wydaje się
teraz w porządku, ale może łatwo zacząć powodować bałagan.
Gdzie więc powinniśmy skutecznie wykonać tę akcję? Moglibyśmy obsłużyć wszystkie te działania
w widżecie FavorsPage, który zawiera wszystkie listy przysług. Zwróć jednak uwagę, że FavorsPage
jest StatelessWidget, listy przysług są ładowane do jego metody konstruktora, a ponieważ są
bezstanowe, będą ładowane przy każdej przebudowie widżetu, tracąc nasze zmiany.
@override
State<StatefulWidget> createState() => FavorsPageState();
}
Pierwszą rzeczą, którą zmieniamy, jest przodek FavorsPage, a teraz jego jedynym zadaniem jest
zwrócenie instancji FavorsPageState w metodzie createState():
class FavorsPageState extends State <FavorsPage> {
// na razie używając pozorowanych wartości z darta mock_favors
List<Favor> pendingAnswerFavors;
List<Favor> acceptedFavors;
List<Favor> completedFavors;
List<Favor> refusedFavors;
169
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
@override
void initState() {
super.initState();
pendingAnswerFavors = List();
acceptedFavors = List();
completedFavors = List();
refusedFavors = List();
loadFavors();
}
void loadFavors() {
pendingAnswerFavors.addAll(mockPendingFavors);
acceptedFavors.addAll(mockDoingFavors);
completedFavors.addAll(mockCompletedFavors);
refusedFavors.addAll(mockRefusedFavors);
}
@override
Widget build(BuildContext context) { ... } // ukryte dla zwięzłości
}
Teraz obiekt State zawiera informacje, które muszą być zachowywane między przebudowami,
a ten obiekt będzie lokalizacją wszystkich działań dla przysług. Chociaż nie jest optymalny,
będzie przynajmniej scentralizowany w jednym miejscu. Powiedziałbym, że potrzebujemy
jakiejś architektury, aby to zrobić poprawnie, np. MVP, MVVM, BloC lub Redux. Jednak dla
uproszczenia zastosujemy podejście, które tutaj przyjęliśmy.
Możesz sprawdzić oficjalny przewodnik zarządzania stanem jako pierwszy krok doty-
czący architektury aplikacji, wraz z niektórymi alternatywami architektury, dostępny na
https://flutter.dev/docs/development/data-and-backend/state-mgmt i https://medium.com/
flutter-community/flutter-apparchitecture-101-vanilla-scoped-model-bloc-7eff7b2baf7e.
Zacznijmy więc od obsługi oczekujących żądań. Zostały zdefiniowane jako Odrzuć lub Zrób.
Aby je obsłużyć, musimy przekazać procedurę obsługi do właściwości onPressed naszych już
zdefiniowanych widżetów FlatButton w FavorCardItem.
Z metody onPressed przycisku musimy w jakiś sposób uzyskać dostęp do FavorsPageState, aby
wykonać te akcje. Można to zrobić za pomocą metody ancestorStateOfType() z klasy BuildContext,
która wyszukuje w drzewie obiekt State danego typu:
// część klasy FavorsPageState
static FavorsPageState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<FavorsPageState>());
}
Typowym wzorem postępowania w celu podpięcia tej funkcji jest dodanie statycznej metody
dla danego typu (of), która spowoduje wywołanie funkcji frameworka. Ma to na celu zapewnienie
skróconego sposobu dostępu do stanu z mniejszą ilością kodu.
170
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
refusedFavors.add(favor.copyWith(
accepted: false
));
});
}
Jak możesz zauważyć, metoda setState() jest tutaj używana do powiadamiania frameworka
o konieczności odbudowania widżetów. Wewnątrz jego wywołania zwrotnego usuwamy przy-
sługę z listy oczekujących i dodajemy jej zmodyfikowaną wersję do listy odrzuconych. Zmo-
dyfikowaną wersję uzyskuje się poprzez wykonanie kopii oryginalnej przysługi i zmianę jej
właściwości accepted. Tak wygląda metoda copyWith z klasy Favor:
Favor copyWith({
String uuid,
String description,
DateTime dueDate,
bool accepted,
DateTime completed,
Friend friend,
}) {
return Favor(
uuid: uuid ?? this.uuid,
description: description ?? this.description,
dueDate: dueDate ?? this.dueDate,
accepted: accepted ?? this.accepted,
completed: completed ?? this.completed,
friend: friend ?? this.friend,
);
}
171
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Zauważ, że aby utworzyć nową instancję Favor z oryginalnymi wartościami (jeśli są ustawione)
lub otrzymanymi jako argumenty, używany jest operator rozpoznający wartość null (??).
Metoda copyWith() jest bardzo popularna w świecie Fluttera, więc spróbuj się do niej
przyzwyczaić. Jest obecna w wielu widżetach i klasach tego frameworka. Nie jest obowiąz-
kowa, ale to dobry wzorzec postępowania.
acceptedFavors.add(favor.copyWith(accepted: true));
});
}
Jak widać, jest prawie taka sama jak metoda refuseToDo(); jedyne różnice dotyczą docelowej listy
i statusu akceptacji.
172
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Robimy to za pomocą widżetu Navigator, który wyświetla nowy widżet na ekranie. Na razie
możesz zobaczyć, że gest był obsługiwany jak inny przycisk. Aby uzyskać więcej informacji
na temat działania tego widżetu, sprawdź rozdział 7.
Przycisk Close
Widżet CloseButton jest zintegrowany z Navigator. Zdejmuje z niego ostatni widżet, powra-
cając do poprzedniego. Nie musimy tutaj implementować gestu. Korzystając z Navigator, aby
pokazać widżet na ekranie, możemy użyć przycisku Close, aby go usunąć.
173
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Przycisk SAVE
Przycisk SAVE będzie odpowiedzialny za zatwierdzanie i zapisywanie nowych próśb o przy-
sługę. Zapisywanie zostanie omówione w rozdziale 8., kiedy będziemy rozmawiać o integracji
z Firebase.
@override
Widget build(BuildContext context) {
// zwraca poddrzewo widżetu opakowane w Form. ukryte dla zwięzłości.
}
}
Wyszukuje drzewo pod kątem odpowiedniego stanu i prosi o zapisanie. Metoda save() wykonuje
ciężką pracę:
void save () {
if (_formKey.currentState.validate()) {
// zapisz prośbę o przysługę w Firebase
174
d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika
Navigator.pop (kontekst);
}
}
OK, w tej chwili metoda ta nic nie robi; wywołuje tylko walidację dla odpowiedniego formu-
larza — przechodzi przez wszystkie pola formularza i je sprawdza — co już wiesz.
Przejrzyj załączone do rozdziału pliki z kodem źródłowym, aby sprawdzić walidację dla
pól formularza.
Podsumowanie
W tym rozdziale widzieliśmy, jak działa obsługa gestów we frameworku Flutter wraz z meto-
dami obsługi gestów, na przykład dotknięcia, podwójnego dotknięcia, przesuwania i powięk-
szenia. Widzieliśmy kilka widżetów, które korzystają z GestureDetector do obsługi gestów.
Widzieliśmy również, jak używać widżetów Form i FormField do poprawnej obsługi danych
wprowadzanych przez użytkownika.
Wreszcie do naszego projektu dołożyliśmy kilka dodatków do obsługi zdarzeń związanych z przy-
sługami, co pomogło nam uczynić aplikację bardziej interaktywną.
W następnym rozdziale dowiemy się, jak do naszych widżetów dodać kolory, korzystać z motywów
i uzyskać dostęp do bardziej praktycznych zastosowań widżetów Material Design i Cupertino,
zwiększając atrakcyjność naszej aplikacji.
175
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
176
d0765ad53fb82babda2278a311da7afb
d
6
Motyw i styl
Każda aplikacja musi mieć własną tożsamość. Na przykład nasza aplikacja Favors musi mieć
własne kolory i style. Znajomość sposobów stosowania stylów, kolorów i niestandardowych czcio-
nek ma zasadnicze znaczenie dla osiągnięcia tego efektu w każdej aplikacji.
Widżety motywu
Tworzenie aplikacji polega nie tylko na tworzeniu samego kodu. Chodzi również o wrażenia
użytkownika, które oferuje aplikacja.
Kompozycja widżetów Fluttera pomaga w tej części rozwoju. Definiując pojedynczy typ wi-
dżetu, możemy zdefiniować motywy i style, które mają zastosowanie do pojedynczego wi-
dżetu, do wszystkich widżetów w poddrzewie lub do całej aplikacji.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Korzystając z widżetu Theme, możemy dostosować cały wygląd i działanie aplikacji za pomocą
niestandardowych kolorów tekstu, komunikatów o błędach, wyróżnień, a także niestandardo-
wych czcionek. Flutter również używa tego widżetu we własnych widżetach. MaterialApp jest
doskonałym przykładem tego, jak zbudowane są wewnętrzne widżety frameworka: wewnętrz-
nie używa widżetu Theme, aby dostosować wygląd widżetów opartych na Material Design, takich
jak AppBars i Buttons. Zobaczmy, jak w praktyce używać widżetów Theme, aby stosować różne
style do innych widżetów Fluttera.
Widżet Theme
We Flutterze wszystko jest widżetem i za pomocą właściwości child i children możemy zbu-
dować interfejs użytkownika, dodając widżety dla każdego widżetu. Widżet Theme zachowuje
się jak każdy inny; określa właściwości i może mieć potomka.
Widżet Theme współpracuje również z techniką InheritedWidget, więc każdy potomny widżet
może uzyskać do niego dostęp za pomocą Theme.of(context), który wewnętrznie wywołuje
metodę pomocniczą inheritFromWidgetOfExactType z klasy BuildContext. W ten sposób wi-
dżety Material Design używają widżetu Theme do stylizacji:
178
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Zatem dane motywu są stosowane do widżetów potomnych, ale można je zastąpić w lokalnych
częściach drzewa widżetów. Na powyższym schemacie motyw z numerem 2 zastąpi motyw z nu-
merem 1 zdefiniowanym na samym początku drzewa. Poddrzewo numer 2 będzie miało inny
motyw niż reszta drzewa.
Ponadto dzięki tej strukturze możliwe jest utworzenie zupełnie nowego motywu dla niektórych
widżetów lub dziedziczenie z motywu podstawowego i zmiana tylko niektórych właściwości,
aby wpłynąć na poddrzewo.
Klasa ThemeData pomaga widżetowi Theme w wykonywaniu zadania dotyczącego stylizacji. Zobaczmy
to szczegółowo.
ThemeData
Widżet Theme zawiera właściwość o nazwie data, która akceptuje wartość ThemeData, zawierającą
wszystkie informacje o stylu, jasności motywu, kolorach, czcionce itd.
Podczas pisania tej książki opracowywane są alternatywy dla wytycznych iOS Cupertino,
które nie są obecne w stabilnej wersji Fluttera. (Kod w tej książce używa stabilnej wersji).
Używając właściwości klasy ThemeData, będziesz mógł dostosować wszystkie style związane z aplika-
cją, takie jak kolory, typografia i określone składniki. Podczas tworzenia motywów możesz postępo-
wać zgodnie z wytycznymi dotyczącymi projektowania materiałów od Google, które są ukierun-
kowane na projektowanie aplikacji na urządzenia mobilne, internetowe i stacjonarne, lub iOS
Cupertino, które są specyficzne dla platformy Apple.
Oba wzorce mają osobliwości ze względu na platformy docelowe. Wybór, czy postępować
zgodnie z wytycznymi Material Design, iOS Cupertino, czy też bez związku z żadnym z nich,
należy do Ciebie. Flutter ma widżety oparte na Theme, przeznaczone dla obu platform, dzięki
czemu możesz dokładnie zastosować wytyczne lub zaprojektować na swój unikalny sposób.
W kolejnych sekcjach będziemy badać wytyczne dotyczące Material Design i iOS Cupertino.
Kolorowanie jest ważnym tematem w tworzeniu motywów widżetów. Na przykład, jeśli zwięk-
szysz kontrast tekstu, aby podkreślić element interfejsu użytkownika, wymagane jest użycie
właściwych kolorów. Jasność to jedna z kluczowych właściwości klasy ThemeData, która pomoże
w manipulowaniu kolorami. Spójrzmy.
179
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jasność
Jedną z ważnych właściwości motywu jest brightness. Definiowanie tej właściwości jest rów-
nie ważne, jak definiowanie kolorów motywu. Jak sama nazwa wskazuje, podkreśla jasność
motywu aplikacji. Dzięki tej właściwości frameworki mogą określać tekst, przyciski i kolory
podświetlenia, aby uzyskać wystarczający kontrast między zawartością tła i pierwszego planu.
Pomaga wprowadzać kontrast między tekstem, przyciskami i tłem elementów (dzięki widżetom
Material Design). Klasa ThemeData ma konstruktor fallback(), który zwraca jasność motywu
za pośrednictwem wartości Brightness.light. Możesz skorzystać także z konstruktorów dark()
i light(), aby wypróbować to samodzielnie.
Wiele innych właściwości ThemeData odnosi się bezpośrednio do stylizacji, dlatego nie bę-
dziemy ich dalej badać. Zapraszamy do sprawdzenia wszystkich właściwości dostępnych
w klasie ThemeData pod adresem https://docs.flutter.io/flutter/material/ThemeData-class.html.
180
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Jak widać, używamy tylko widżetu Container jako naszego widżetu głównego — bez widżetu
Theme. Możemy więc założyć, że nie mamy żadnych stylów zastosowanych do jego potomnych
widżetów. Również właściwość textDirection jest w tym momencie nowa. Gdy korzystamy
z widżetu MaterialApp w naszym layoucie, dostarcza on nam domyślną wartość textDirection.
Więcej na ten temat w następnej sekcji.
Aby zmienić styl widżetu Text, możemy skorzystać z widżetu Theme. Klasa ThemeData zawiera
właściwość textTheme, która z kolei zawiera konfigurację stylu tekstu zgodnie z wytycznymi
Material Design:
Text(
"Simple Text",
textDirection: TextDirection.ltr,
style: Theme.of(context).textTheme.display1,
),
Właściwość style widżetu Text przyjmuje wartość TextStyle, którą można uzyskać z widżetu
Theme. Jednak, jak być może pamiętasz, nie określiliśmy widżetu Theme w naszym drzewie
aplikacji. W poprzednim przykładzie to działa, ponieważ metoda Theme.of zwraca domyślny
widżet ThemeData, gdy nie jest on zdefiniowany. Jeśli wykonasz kod, zobaczysz, że widżet Text
jest wyświetlany z większym rozmiarem czcionki niż domyślny. Dzieje się tak, ponieważ używamy
stylu display1 z Material Design.
W tym przypadku dodajemy widżet Theme bezpośrednio przed widżetem Text i dostosowujemy
go za pomocą metody copyWith:
181
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Spodziewaliśmy się zobaczyć żółty tekst, ale go nie widzimy, prawda? Dzieje się tak, ponieważ
używamy parametru context z poziomu głównego drzewa. Po przeszukaniu drzewa nie znaj-
dzie on instancji Theme, zwracając element domyślny, jak widzieliśmy w naszym pierwszym przy-
kładzie. Aby to zadziałało, możemy użyć widżetu Builder, który deleguje budowanie widżetu Text:
Builder(
builder: (context) => Text(
"Simple Text",
textDirection: TextDirection.ltr,
style: Theme.of(context).textTheme.display1,
),
)
To działa, ponieważ widżet Builder deleguje budowę na niższy poziom drzewa, przekazując in-
stancję context, który odnajdzie właściwą instancję Theme w trakcie przeszukiwania drzewa. Kiedy
więc uruchamiamy powyższy kod, widżet Text jest wyświetlany z poprawnym stylem display1,
który jest prawie taki sam jak domyślny styl tekstu, tylko jego kolor jest inny, teraz żółty.
Ponieważ tworzenie motywu odnosi się do stylu aplikacji, zawsze musimy dbać o platformę,
na której aplikacja jest wykonywana; zobaczmy, jak może w tym pomóc klasa Platform.
Klasa Platform
Podczas opracowywania aplikacji mobilnych na wiele platform konieczne może być wykona-
nie różnych projektów dla różnych celów. Dlatego możemy skorzystać z klasy Platform, która
pomaga nam uzyskać informacje o środowisku, głównie o docelowym systemie operacyjnym,
poprzez metody:
isAndroid,
isFuchsia,
isIOS,
isLinux,
isMacOS,
isWindows.
182
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Dzięki tym metodom możemy sprawić, że całe nasze drzewo widżetów będzie miało specyficzne
implementacje dla każdej platformy. Oto przykład:
// część przykładu theme/lib/main.dart
Jak widać, w oparciu o platformę docelową przełączamy widżet aplikacji (i motyw) na Mate-
rialApp i ThemeData (dla Androida) lub CupertinoApp i CupertinoThemeData dla dowolnej innej
platformy docelowej.
Widzieliśmy, jak używać widżetów Theme i klas pomocniczych, takich jak klasa ThemeData
i Platform, do stosowania stylów do naszych widżetów. Podstawy wytycznych Material Design
i iOS Cupertino są obecne dla wielu widżetów we Flutterze. Zobaczmy je, aby móc efektywnie
przestrzegać tych specyfikacji.
Material Design
Material Design to wytyczne Google’a dotyczące projektowania, które mają pomóc programistom
w tworzeniu wysokiej jakości treści cyfrowych. Są obecne we Flutterze i wciąż ewoluują wraz
z platformą, wprowadzając nowe widżety zgodne ze specyfikacjami komponentów Material
Design.
Znaczenie stylów Material Design dla platformy Flutter jest oczywiste. Istnieje już sekcja Mate-
rial Design poradnika (https://material.io/develop/flutter/).
Główne widżety Material Design we Flutterze to MaterialApp i Scaffold. Oba pomagają progra-
mistom w projektowaniu aplikacji zgodnie z wytycznymi Material Design bez zbytniego nakładu
pracy.
183
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jeśli chcesz się dowiedzieć, na czym dokładnie polega Material Design, sprawdź
https://material.io/.
Widżet MaterialApp
Widżet Theme to nie jedyny sposób na dodanie motywu do aplikacji. Widżet MaterialApp jest jedy-
nym innym widżetem, który również akceptuje wartość ThemeData za pośrednictwem swojej wła-
ściwości theme.
Oprócz Theme MaterialApp dodaje na przykład właściwości pomocnicze dla lokalizacji, a także
nawigację między ekranami, co omówimy w rozdziale 7.
Dodając widżet MaterialApp jako widżet główny aplikacji, deklarujesz zamiar przestrzegania
wytycznych Material Design.
Skoro będziesz postępować zgodnie z wytycznymi Material Design, framework będzie nieco inny
w stosunku do domyślnego motywu. W poniższym kodzie nie określamy stylu naszego tekstu:
class MaterialAppDefaultTheme extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(
color: Colors.white,
child: Center(
child: Text(
"Simple Text",
// textDirection: TextDirection.ltr, nie potrzebujemy
// teraz podziękuj materialapp
),
),
),
);
}
}
184
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Innymi słowy, zawsze powinniśmy umieszczać widżety Text w jakimś widżecie opartym na
Material Design, aby poprawnie zastosować style typografii zaproponowane przez wytyczne.
Zwróć uwagę, że tym razem nie udostępniliśmy również właściwości textDirection widżetu Text.
Korzystając z widżetu MaterialApp, widzieliśmy, jak zainicjować (init) wytyczne Material Design.
Innym ważnym widżetem, który może pomóc w tym zadaniu, jest widżet Scaffold.
185
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Widżet Scaffold
W rozdziale 4. widzieliśmy, że widżet Scaffold ma właściwości, które pomagają w tworzeniu
layoutu o wyglądzie Material Design. Jego cel jest tak samo ważny jak widżet MaterialApp;
pomaga programistom przestrzegać wytycznych Material Design, po prostu dodając odpowiednie
widżety do właściwości. Ekran główny naszej aplikacji Favors jest zgodny z niektórymi aspektami
Material Design.
Zobaczmy:
186
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Teraz, gdy widzieliśmy, jak wygląda domyślny motyw w niektórych widżetach, zobaczmy, jak
zbudować własny niestandardowy motyw z wybranymi przez nas kolorami.
Motyw niestandardowy
Nasza aplikacja Favors do tej pory nie korzystała z żadnych właściwości Theme ani ThemeData.
Czas dostosować styl aplikacji, aby była bardziej atrakcyjna. Tak będzie wyglądać po zmianie
stylów:
187
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Chociaż domyślnym motywem jest jasny (jasne tło / ciemne teksty), ustawiliśmy primaryColor
Brightness na Brightness.dark, aby tekst, który pojawia się na wierzchu tła, był domyślnie biały.
Zwróć również uwagę, że zdefiniowaliśmy motyw w nowym pliku Dart, aby pomóc w organi-
zacji kodu. Musimy więc go zaimportować, aby użyć go w naszej aplikacji:
return MaterialApp (
theme: lightTheme,
home: FavorsPage (),
);
Zmiana kolorów nie wystarczy, aby zmienić wygląd aplikacji. Inną rzeczą, którą możemy zro-
bić, jest zmiana stylów tekstu i użycie stylów Material Design. Jak widzieliśmy wcześniej,
odbywa się to za pomocą właściwości style widżetów Text. Po wprowadzeniu pewnych zmian
nasze zakładki przysług mogą więc podkreślać niektóre części tekstu.
188
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
To samo dotyczy innych widżetów Text w aplikacji. Jak widać w naszym przykładzie aplikacji Favors,
modyfikowanie stylów naszego widżetu jest łatwe dzięki widżetowi Theme i klasom pomocniczym.
Aby uzyskać więcej informacji, możesz sprawdzić kod źródłowy tego rozdziału w serwisie GitHub,
zachęcamy też do eksperymentowania z niektórymi wartościami w celach praktycznych.
Teraz, gdy znasz już podstawy Material Design, dla porównania przedstawimy iOS Cupertino.
iOS Cupertino
We Flutterze ważny jest cel nadania wyglądu aplikacji natywnej. Mając to na uwadze, podjęto
wiele wysiłków, aby doprowadzić stronę frameworka z Cupertino do tego samego poziomu co
strona Material Design. Podczas pisania tej książki do frameworka dodano wiele widżetów
Cupertino.
Chodzi o to, że ich zachowanie jest wierne aplikacjom natywnym, więc nie jest to łatwe zada-
nie. Ważną rolę odgrywa tu społeczność, wykorzystując komponenty i udzielając informacji
zwrotnych.
Alternatywą iOS Cupertino dla widżetu MaterialApp jest widżet CupertinoApp; zobaczmy jego
kluczowe właściwości i porównanie z widżetem MaterialApp.
CupertinoApp
CupertinoApp zachowuje się tak samo dla Cupertino jak MaterialApp dla Material Design.
Dodaje funkcje i udogodnienia dla programisty, aby podążał za wzorcami projektowymi Cupertino.
Na przykład sprawia, że aplikacja domyślnie używa przwijania skokowego (bouncing scroll),
które jest typowe dla iOS, niestandardowej czcionki, która różni się od Androida, i nie tylko.
Działa to tak samo jak w Material Design. Możemy zdecydować się na użycie CupertinoApp
lub nie. Dlatego nadal możemy używać widżetów CupertinoTheme i CupertinoThemeData w taki
sam sposób, jak robilibyśmy to w przypadku Material Design. To, co zmienia się w praktyce,
to dostępne właściwości.
189
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ponieważ omawiany materiał jest bardzo podobny do poprzedniej sekcji; nie będziemy
tutaj wdawać się w szczegóły. Możesz eksperymentować z motywami i przyjrzeć się
załączonemu folderowi cupertino_theme, aby zapoznać się z przykładami.
Chociaż nie jest to zalecane, obie technologie możemy zawrzeć w kodzie, tworząc niektóre
części zgodne z Material Design, a niektóre zgodne z Cupertino. Możemy stworzyć dwie klasy
aplikacji, jedną dla Material Design, a drugą dla Cupertino. Możemy nawet stworzyć ogólną
klasę aplikacji, która zmienia układ widżetów na podstawie platformy (klasa Platform).
Cupertino w praktyce
Nasza aplikacja Favors została zaprojektowana do korzystania z komponentów Material Design,
ale możemy sprawić, by wyglądała bardziej natywnie w iOS, używając widżetów Cupertino.
Można to zrobić za pomocą kombinacji instrukcji warunkowych podczas tworzenia naszych
widżetów przy użyciu klasy Platform.
190
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Jak widać, w wariancie iOS Cupertino pasek nawigacyjny mamy na dole ekranu. OK, nie wygląda
to zbyt dobrze, ale ważny jest pomysł tworzenia niestandardowych układów w oparciu o plat-
formę docelową. Flutter daje Ci narzędzia, Ty zaś musisz ich właściwie używać.
Musimy sprawdzić platformę docelową i w zależności od niej zbudować różne widżety. Może
to być dość skomplikowane, więc alternatywą jest opracowanie oddzielnych klas widżetów dla
każdej platformy i niemieszanie całego kodu. Pomaga to w organizacji.
W naszym przykładzie utworzyliśmy tylko pierwszy ekran, aby zilustrować, w jaki sposób drzewo
może być uwarunkowane na podstawie platformy. Aplikacja Favors będzie miała ten sam styl
na obu platformach. Zobaczmy teraz, jak używać niestandardowych czcionek.
Ponieważ czcionka jest określona w widżecie Theme, możemy dodać ją do głównego motywu
aplikacji, a następnie zastosować ją do całej aplikacji. Jeśli wolisz określić czcionkę dla każdego
widżetu, również jest to możliwe. Pierwszym krokiem do użycia niestandardowej czcionki
w aplikacjach Fluttera jest zaimportowanie plików czcionek do projektu.
Aby to zrobić, możemy umieścić pliki czcionek w podkatalogu projektu, a następnie zadekla-
rować je w pubspec.yaml. W tym przykładzie będziemy używać czcionek Ubuntu znalezionych
w witrynie Google Fonts.
Pierwszym krokiem jest dodanie plików do katalogu projektu. Powszechną praktyką jest umiesz-
czanie plików czcionek w podkatalogu fonts/ lub asset/ projektu Flutter. Tutaj będziemy korzy-
stać z katalogu fonts/:
191
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Następnie musimy zadeklarować zasoby czcionek w pliku pubspec.yaml, aby framework wiedział,
gdzie znaleźć żądaną czcionkę podczas stylizacji tekstu:
// plik pubspec.yaml - pełny kod źródłowy można znaleźć w przykładowym folderze
// hands_on_fonts
// .. ukryte dla zwięzłości
flutter:
uses-material-design: true
fonts:
- family: Ubuntu
fonts:
- asset: fonts/Ubuntu-Regular.ttf
- asset: fonts/Ubuntu-Italic.ttf
style: italic
- asset: fonts/Ubuntu-Medium.ttf
weight: 500
- asset: fonts/Ubuntu-Bold.ttf
weight: 700
Zapoznaj się z dokumentacją, aby dowiedzieć się, jak poprawnie określić właściwości
weight i style oraz standardowe wartości każdego typu: https://api.flutter.dev/flutter/
dart-ui/FontWeight-class.html i https://api.flutter.dev/flutter/dart-ui/FontStyle-class.html.
192
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Nasza aplikacja domyślnie używa rodziny czcionek Ubuntu we wszystkich widżetach zawie-
rających tekst.
Pamiętaj, że to zachowanie można zmienić w małych sekcjach aplikacji, jeśli wolisz, używając
widżetów Theme lub bezpośrednio zmieniając właściwość stylu widżetów Text.
Jak widać, możesz zastosować niestandardową czcionkę do całej aplikacji, po prostu importu-
jąc żądaną czcionkę i deklarując ją w projekcie. Innym ważnym aspektem w tworzeniu moty-
wów i stylizacji jest dostosowanie layoutów do różnych urządzeń. W tym zadaniu mogą pomóc
widżety MediaQuery i LayoutBuilder. Spójrzmy.
Tworzenie programów obsługujących różne rozmiary ekranów jest wyzwaniem, które zawsze
będzie obecne w życiu programisty, dlatego potrzebujemy mechanizmów, pozwolających nam jak
najlepiej się do tego dostosować. Flutter ponownie daje nam narzędzia potrzebne do zrozu-
mienia ekosystemu, w którym działa aplikacja, dzięki czemu możemy na nim działać.
193
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
LayoutBuilder
Widżet LayoutBuilder udostępnia właściwość typu LayoutWidgetBuilder. Chociaż jest po-
dobny do widżetu Builder, LayoutWidgetBuilder zawiera dodatkowe informacje o rozmiarze
widżetu nadrzędnego w wartości BoxConstraints.
Dzięki tym informacjom sposób budowania można zmienić w zależności od dostępnego miej-
sca. Tak więc na różnych urządzeniach będzie dostępna inna ilość miejsca w widżecie głów-
nym drzewa, co może również ograniczać rozmiary jego dzieci. Korzystając z tego widżetu,
możemy dokonać wyboru czy pokazywać niektóre części layoutu.
Ten widżet jest zależny od rozmiaru widżetu nadrzędnego, więc jest przebudowywany za każ-
dym razem, gdy zmienia się rozmiar. Dochodzi do tego na różne sposoby na urządzeniach
mobilnych. Najprostszym przykładem jest zmiana orientacji aplikacji, która następuje, gdy użyt-
kownik obraca telefon.
Zobaczmy, jak zareagować na zmianę rozmiaru na ekranie. W tym przykładzie zmienimy spo-
sób wyświetlania dwóch widżetów na podstawie dostępnego miejsca. Tak więc widżety są
wyświetlane jeden pod drugim, gdy nie ma dla nich wystarczającej ilości miejsca (oceniamy
to za pomocą instancji BoxConstraints udostępnianej przez widżet LayoutBuilder), lub obok
siebie, gdy jest więcej wolnego miejsca:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// budowa layoutu na podstawie wartości ograniczających
}
)
)
}
}
Jak widać, dodaliśmy widżet LayoutBuilder i możemy zbudować layout na podstawie podanych
ograniczeń:
if (constraints.maxWidth <= 500) {
return Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Container(
color: Colors.green,
child: Center(child: Text("1")),
),
),
Expanded(
child: Container(
color: Colors.blue,
child: Center(child: Text("2")),
194
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
),
),
],
);
}
Wyświetlamy widżet Column, gdy dostępna szerokość jest mniejsza niż 500. A gdy mamy wystarcza-
jąco dużo miejsca, zmieniamy widżet:
return Row (
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Container(
color: Colors.yellow,
child: Center(child: Text("1")),
),
),
Expanded(
child: Container(
color: Colors.purple,
child: Center(child: Text("2")),
),
),
],
);
W tym przypadku zwracamy widżet Row, ponieważ mamy wystarczającą ilość miejsca (więk-
szą niż 500).
195
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, zmian w layoucie dokonujemy nie tylko na podstawie orientacji, ale także dostępnej
szerokości. Innym sposobem reagowania na zmiany, w zależności od dostępnego rozmiaru,
jest użycie klasy MediaQuery. Zobaczmy teraz, jak działa to rozwiązanie.
MediaQuery
MediaQuery to element potomny InheritedWidget zawierający informacje o rozmiarze całego
ekranu, a nie tylko widżetu nadrzędnego. Jako widżet InheritedWidget zapewnia również wcze-
śniej wprowadzoną metodę MediaQuery.of, która wyszukuje drzewo dla instancji MediaQuery.
Jego użycie jest uwarunkowane obecnością instancji w kontekście. Można ją łatwo zagwaran-
tować, dodając instancję WidgetsApp jako nasz widżet główny. WidgetsApp nie jest specyficzne
dla platformy, tak jak MaterialApp czy CupertinoApp, które używają tej klasy w swojej wewnętrznej
implementacji.
Przykład MediaQuery
Nasza aplikacja Favors nie reaguje na razie na zmiany rozmiaru ekranu. Wyświetla pionową
listę kart, które wypełniają dostępne miejsce na ekranie. W przypadku typowych smartfonów
wygląda to dobrze, ale tak się to prezentuje na urządzeniach z większym ekranem:
Jak widać, każda zakładka wypełnia każdy wiersz i są one dużo większe niż to konieczne.
Możemy to uwarunkować w zależności od rozmiaru ekranu i sprawić, by lista wyświetlała więcej
elementów, jeśli jest więcej miejsca, niż potrzebujemy do wyświetlenia zakładki.
196
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Używając klasy MediaQuery, wykonaliśmy obliczenia, aby zmienić liczbę zakładek wyświetlanych
w wierszu:
// część pliku hands_on_mediaquery/lib/main.dart
W powyższym kodzie, jeśli opakujemy listę przysług za pomocą widżetu Expanded, cała dostępna
przestrzeń widżetu Column będzie zajęta i pozwolimy logice zmiany rozmiaru MediaQuery na
działanie metody _buildCardsList():
const kFavorCardMaxWidth = 450.0; // maksymalna szerokość zakładki
197
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
return ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: favors.length,
itemBuilder: (BuildContext context, int index) {
final favor = favors[index];
return FavorCardItem(favor: favor);
},
);
}
}
Do tego zadania dostępnych jest kilka innych widżetów Fluttera, więc być może lepszym
podejściem byłoby użycie innego kontenera niż lista — aby wyświetlać zakładki w bardziej
elastyczny sposób.
Inne klasy mogą pomóc w dostosowaniu layoutu. Zobaczmy niektóre z nich w następnej sekcji.
198
d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl
Korzystając ze wszystkich dostępnych klas, możemy dostosować nasze layouty Fluttera. Jesteśmy
w stanie dostosować nasze widżety oraz całą aplikację.
Podsumowanie
Dostosowywanie aplikacji pod względem stylów ma fundamentalne znaczenie dla tworzenia
niepowtarzalnych wrażeń dla użytkownika i osiągnięcia celów aplikacji. Znajomość klas frame-
worka Fluttera, które pomagają w tym zadaniu, jest kluczowa dla rozwoju dowolnej aplikacji,
w tym naszej aplikacji Favors.
W tym rozdziale widzieliśmy kilka sposobów zmiany stylu naszych aplikacji. Korzystając z widże-
tów Theme i ThemeData, możemy określić style, które zmienią wszystkie widżety znajdujące się
pod nimi w drzewie. Ponadto, korzystając z dostępnych klas aplikacji, MaterialApp i CupertinoApp,
w prosty sposób możemy zmienić styl całej aplikacji.
Widzieliśmy, jak dodać do naszej aplikacji niestandardową rodzinę czcionek, abyśmy mogli zmie-
nić domyślny wygląd naszych tekstów i etykiet. Wreszcie, widzieliśmy, że można zmienić wygląd
naszej aplikacji, dopasowując rozmiar lub orientację za pomocą klas MediaQuery i LayoutBuilder.
Takie zmiany możemy wykonać także w zależności od zastosowanej platformy — przy użyciu
klasy Platform.
W następnym rozdziale dowiemy się, jak we Flutterze działa nawigacja między ekranami oraz
jak używać właściwości Navigator, aby zmienić to, co jest widoczne dla użytkownika.
199
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
200
d0765ad53fb82babda2278a311da7afb
d
7
Routing: nawigacja
między ekranami
W niniejszym rozdziale dowiesz się, jak używać widżetu Navigator do zarządzania trasami apli-
kacji i jak dodawać animacje przejść między ekranami.
Ważną klasą w nawigacji między ekranami we Flutterze jest widżet Navigator, który jest odpowie-
dzialny za zarządzanie zmianami ekranu z logicznym zachowaniem historii.
Nowy ekran we Flutterze to po prostu nowy widżet umieszczony nad innym. Zarządza pojęciem
tras (Routes), które definiują możliwą nawigację w aplikacji. Jak być może już zgadłeś, klasa Route
pomaga Flutterowi w nawigacji.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Navigator
Widżet Navigator jest głównym elementem, który przenosi zadania z jednego ekranu na drugi.
Nawigacja we Flutterze jest oparta na strukturze stosu. Struktura stosu jest odpowiednia do tego
zadania, ponieważ jej koncepcja jest bardzo podobna do zachowania ekranu:
Mamy jeden element na górze stosu. W Navigatorze najwyższym elementem
stosu jest aktualnie widoczny ekran aplikacji.
Ostatni wstawiany element jest pierwszym, który ma być wyjęty ze stosu (potocznie
nazywany jako: ostatni wchodzi, pierwszy wychodzi, last in, first out — LIFO).
Ostatni widoczny ekran jest pierwszym, który jest usuwany.
Główne metody widżetu Navigator to push() i pop().
Overlay
W swojej implementacji Navigator wykorzystuje widżet Overlay. Z dokumentacji wynika:
Elementy Overlay pozwalają niezależnym widżetom podrzędnym pojawiać się nad innymi
widżetami, wstawiając je do stosu elementów Overlay.
Overlay umożliwia każdemu z tych widżetów zarządzanie swoim udziałem za pomocą obiektów
OverlayEntry.
Wykonamy kilka kroków, aby sprawdzić, czy najczęstszym sposobem korzystania z Navigator
i jego widżetu Overlay są widżety aplikacji — WidgetsApp, MaterialApp i CupertinoApp — które
zapewniają wiele sposobów zarządzania nawigacją za pomocą widżetu Navigator.
Podsumowując, stos nawigacji to stos ekranów, które weszły na scenę dzięki metodzie push()
widżetu Navigator.
202
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Route
Elementami stosu nawigacyjnego są trasy (Routes) i istnieje wiele sposobów ich definiowania
we Flutterze.
Kiedy chcemy przejść do nowego ekranu, definiujemy dla niego nowy widżet Route, oprócz
niektórych parametrów zdefiniowanych jako instancja RouteSettings.
RouteSettings
Jest to prosta klasa zawierająca informacje o trasie odnoszące się do widżetu Navigator. Główne
właściwości, które zawiera, są następujące:
name — jednoznacznie identyfikuje trasę. Szczegółowo zbadamy to w następnej sekcji.
arguments — dzięki nim możemy przekazać wszystko do trasy docelowej.
MaterialPageRoute i CupertinoPageRoute
Klasa Route to klasa abstrakcyjna wysokiego poziomu do nawigacji. Nie użyjemy jej jednak
bezpośrednio, choć widzieliśmy, że ekran jest trasą we Flutterze. Różne platformy mogą wymagać
zmian ekranu. We Flutterze istnieją alternatywne implementacje dostosowujące się do plat-
formy. Ta praca jest wykonywana za pomocą MaterialPageRoute i CupertinoPageRoute, które
dostosowują się odpowiednio do Androida i iOS. Dlatego podczas opracowywania aplikacji
musimy zdecydować, czy użyć przejścia Material Design, iOS Cupertino, czy obu, w zależności
od kontekstu.
Przykład
Czas sprawdzić, jak w praktyce wykorzystać widżet Navigator. Utwórzmy podstawowy przepływ,
aby przejść do drugiego ekranu i z powrotem. Będzie to wyglądać mniej więcej tak jak rysu-
nek na następnej stronie.
Podstawowy sposób korzystania z widżetu Navigator jest taki sam jak każdy inny — polega
na dodaniu go do drzewa widżetów:
class NavigatorDirectlyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
203
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Został tutaj dodany widżet Directionality, abyśmy mogli wyświetlać widżety Text.
Pamiętaj, WidgetsApp i jego warianty zarządzają za nas tym i innymi rzeczami.
W poprzednim przykładzie widać, że nie użyliśmy argumentu settings; zamiast tego zwróci-
liśmy trasę domyślną. Najczęstszym podejściem byłoby sprawdzenie właściwości name usta-
wień, która działa jako identyfikator trasy. Framework domyślnie używa nazwy „/” jako trasy
początkowej i w momencie inicjalizacji przekaże ją jako argument. Tak więc w poprzednim
przykładzie początkowa trasa będzie kierować do _screen1.
Sprawdź sekcję „Trasy nazwane” w dalszej części tego rozdziału, aby uzyskać więcej
szczegółów i przykłady nazw tras.
204
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Wynikiem wywołania zwrotnego onGenerateRoute jest obiekt Route. Użyliśmy tutaj typu Ma-
terialPageRoute. W swojej najbardziej podstawowej implementacji powinniśmy również
przekazać do niej wywołanie zwrotne onGenerateRoute. Powinno ono zwrócić widżet do wyświe-
tlenia jako Route. Możesz zapytać: Dlaczego nie użyć właściwości potomka, aby bezpośrednio
dodać widżet potomny?. Jego tworzenie zależy od kontekstu, w którym jest zbudowany, ponieważ
widżet Navigator może tworzyć widżet Route w różnych kontekstach.
Jeśli jednak sprawdzisz poniższy kod, zobaczysz, że możemy przechodzić z jednego ekranu
do drugiego, klikając odpowiedni przycisk. Widzimy to w metodzie _screen1, na przykład:
Widget _screen1(BuildContext context) {
return Container(
color: Colors.green,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Screen 1"),
RaisedButton(
child: Text("Go to Screen 2"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return _screen2(context);
},
),
);
},
)
],
),
);
}
W tym miejscu możesz sprawdzić, czy dostęp do widżetu Navigator jest możliwy za pomocą
statycznej metody Navigator.of. Jak możesz się domyślić, w ten sposób uzyskujemy dostęp
do odpowiedniego przodka Navigator z określonego kontekstu i tak, możemy mieć wiele widże-
tów Navigator w drzewie. To świetnie, ponieważ możemy też mieć różne elementy niezależ-
nej nawigacji w podsekcjach aplikacji.
205
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Widżet _screen2 jest prawie taki sam jak _screen1; jedyną różnicą jest to, że sam wychodzi ze
stosu nawigacji i wraca do widżetu _screen1.
Z poprzednim przykładem jest pewien problem. Jeśli naciśniemy przycisk wstecz na Androidzie,
na ekranie 2, powinniśmy w rezultacie wrócić do ekranu 1, ale tak nie jest. Ponieważ sami
dodaliśmy widżet Navigator, system nie jest tego świadomy: sami musimy nim zarządzać.
Wewnątrz metody didPopRoute() musimy pobrać Route z naszego widżetu Navigator. Nie mo-
żemy jednak uzyskać dostępu do Navigator za pomocą metody statycznej of, ponieważ nie
mamy tutaj kontekstu. Alternatywnie możemy dodać klucz do Navigator i uzyskać dostęp do
jego stanu:
// navigation_directly.dart
class _NavigatorDirectlyAppState extends State<NavigatorDirectlyApp> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ... inne pola i metody
// część metody
Navigator(
key: _navigatorKey,
...
)
}
206
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
W tym przypadku skorzystaliśmy z metody pop() ze stanu Navigator, aby pobrać najwyższą
trasę ze stosu nawigacji. Metoda zwraca wartość true, jeśli obserwator był zarządzany za po-
mocą powiadomień o zdejmowaniu trasy, i zwrócona zostanie wartość zdjęta z Navigator.
Gdy nie ma więcej tras w Navigator, zwrócona zostanie wartość domyślna (aplikacja zakończy
działanie).
WidgetsApp
Jak widzieliśmy wcześniej, nie jest to najbardziej praktyczny sposób wykorzystania widżetu
Navigator w naszych aplikacjach: mamy wiele rzeczy do zarządzania, których można by uniknąć.
Typowym sposobem korzystania z widżetu Navigator są widżety aplikacji. Oferują pewne właści-
wości i metody uwzględniające nawigację w aplikacji:
builder — właściwość builder pozwala nam dodać alternatywną ścieżkę do Navigator,
która jest dodawana przez WidgetsApp.
home — pozwala nam określić widżet odpowiadający pierwszej trasie w aplikacji
(zwykle „/”).
initialRoute — pozwala nam zmienić początkową trasę aplikacji (domyślnie „/”).
navigatorKey i navigatorObserver — pozwala nam określić odpowiednie wartości
dla wbudowanego widżetu Navigator.
onGenerateRoute — tworzy widżety na podstawie nazwy ustawień trasy, takiej jak
ta użyta w poprzednim przykładzie. Jest to wywołanie zwrotne służące do tworzenia
tras za pomocą argumentu RouteSettings.
onUnknownRoute — określa wywołanie zwrotne w celu wygenerowania trasy
w przypadku błędu w procesie budowania tras (na przykład: „Nie znaleziono ścieżki”).
pageRouteBuilder — podobny do onGenerateRoute, ale specjalizujący się w typie
PageRoute.
routes — przyjmuje Map<String, WidgetBuilder>, w którym możemy dodać listę tras
naszej aplikacji wraz z odpowiednimi blokami.
Pisanie poprzedniego przykładu jest łatwiejsze, ponieważ możemy pominąć wszystkie implemen-
tacje specyficzne dla Navigator, takie jak obserwator przycisku Wstecz lub klawisz nawigacji:
class NavigatorWidgetsApp extends StatefulWidget {
@override
_NavigatorWidgetsAppState createState() => _NavigatorWidgetsAppState();
}
207
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, poprzednia implementacja jest znacznie prostsza niż pierwsza; po prostu określamy
właściwość home i pageRouteBuilder aplikacji, a reszta działa automatycznie:
W home ustalamy początkową trasę nawigacji. Dodajemy ją do Builder, aby delegować
go do niższych poziomów drzewa. Zatem gdy nastąpi wyszukiwanie Navigator —
to zadziała.
W pageRouteBuilder określamy, jaki rodzaj obiektu PageRoute powinien być
budowany podczas nawigacji między trasami.
Sposób ten może być jeszcze lepszy, gdy użyjemy tras nazwanych. Zobacz następną
sekcję. Aby uzyskać szczegółowe informacje na temat łączenia tych właściwości,
sprawdź również dokumentację WidgetsApp pod adresem: https://api.flutter.dev/flutter/
widgets/WidgetsApp-class.html. To samo dotyczy MaterialApp i CupertinoApp.
Pełny kod źródłowy tych przykładów można znaleźć w projekcie dotyczącym nawigacji
w katalogu przykładów dla rozdziału.
Możemy zdefiniować serię tras ze skojarzonymi z nimi nazwami. Zapewnia to pewien poziom
abstrakcji dla znaczenia trasy i ekranu. Nawiasem mówiąc, można ich używać w strukturze
ścieżki; innymi słowy, mogą być postrzegane jako podtrasy.
Spójrz na właściwość home WidgetsApp. Niejawnie ustawia widżet trasy początkowej dla
widżetu Navigator. Odnosi się do ścieżki „/”.
208
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Sprawdźmy to:
// navigation_widgetsapp_named_routes.dart
class _NavigatorNamedRoutesWidgetsAppState extends
State<NavigatorNamedRoutesWidgetsApp> {
@override
Widget build(BuildContext context) {
return WidgetsApp(
color: Colors.blue,
routes: {
'/': (context) => _screen1(context),
'/2': (context) => _screen2(context),
},
pageRouteBuilder: <Void>(RouteSettings settings, WidgetBuilder
builder) {
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
W poprzednim przykładzie widać, że użyliśmy właściwości routes, aby ustawić tablicę routingu
dla Navigator i wiedzieć, co zbudować dla każdej ścieżki.
Nadal możemy korzystać z właściwości home, jeśli chcemy, jak pokazano w poniższym przykładzie:
WidgetsApp(
home: Builder(
builder: (context) => _screen1(context),
),
routes: {
'/2': (context) => _screen2(context),
},
...
}
Zauważ, że robiąc to, nie powinniśmy dodawać trasy „/” do mapy routes.
Inną zaletą używania tras nazwanych jest tworzenie nowych tras. Możemy użyć metody pushNamed,
gdy chcemy przejść do ekranu 2 z ekranu 1:
Navigator.of(context).pushNamed('/2');
W ten sposób nie musimy tworzyć obiektu Route w każdym wywołaniu; użyjmy naszego wcześniej
zdefiniowanego Builder w mapie tras RoutesWidgetsApp.
209
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Argumenty
Metoda pushNamed przyjmuje również argumenty, które mają zostać przekazane do nowej trasy:
Navigator.of(context).pushNamed('/2', arguments: "Hello from screen 1");
W tym przypadku musimy użyć onGenerateRoute z WidgetsApp, abyśmy mieli dostęp do tych
argumentów poprzez obiekt RouteSettings:
// navigation_widgetsapp_named_routes_arguments.dart
class _NavigatorNamedRoutesArgumentsAppState
extends State<NavigatorNamedRoutesArgumentsApp> {
@override
Widget build(BuildContext context) {
return WidgetsApp(
color: Colors.blue,
onGenerateRoute: (settings) {
if(settings.name == '/') {
return MaterialPageRoute(
builder: (context) => _screen1(context)
);
} else if(settings.name == '/2') {
return MaterialPageRoute(
builder: (context) => _screen2(context, settings.arguments)
);
}
},
);
}
...
}
Następnie używamy argumentu znajdującego się w builder _screen2, aby wyświetlić dodatkową
wiadomość.
Metoda push i jej warianty zwracają Future, a wartość Future jest wynikiem metody pop().
Widzieliśmy, że możemy przekazać argumenty do nowego Route. Ponieważ ścieżka odwrotna jest
również możliwa, zamiast wysyłać wiadomość do drugiego ekranu, możemy odebrać wiadomość,
gdy pojawi się z powrotem.
210
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
RaisedButton (
child: Text ("Go to Screen 2"),
onPressed: () async {
final message = await Navigator.of(context).pushNamed('/2') ??
"Came from back button";
setState(() {
_message = message;
});
},
),
...
}
}
Aby uzyskać pełny przykład, zapoznaj się z kodem źródłowym tego rozdziału w serwisie
GitHub.
Wynikiem metody push jest obiekt Future, który musimy pobrać za pomocą słowa kluczowego
await. Tutaj występuje pod zmienną _message.
Jeśli nie pamiętasz, jak pracować z Future, wróć do rozdziału 2., do sekcji „Future i async”.
211
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
PageRouteBuilder
PageRouteBuilder to definicja tworzenia trasy. Dokumentacja zawiera następujące wyjaśnienie:
Klasa narzędziowa do definiowania jednorazowych tras dla stron pod kątem wywołań
zwrotnych.
Jak być może pamiętasz, WidgetsApp zawiera właściwość pageRouteBuilder, w której definiu-
jemy PageRoute, jaki ma być używany przez naszą aplikację, i gdzie są zwykle definiowane
przejścia.
212
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
213
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Animacje Hero
Nazwa Hero może wyglądać dziwnie, ale każdy, kto korzystał z aplikacji mobilnej, widział już
tego rodzaju animację. Jeśli tworzysz aplikacje na platformy mobilne, być może słyszałeś już
o współdzielonych elementach lub pracowałeś z nimi; chodzi o elementy, które pozostają między
ekranami. To jest definicja Hero.
Flutter zawiera sposoby ułatwiające tworzenie tego rodzaju animacji. Dlatego możemy zobaczyć, jak
działają widżety Hero, zanim jeszcze zagłębimy się w temat samych animacji.
Najważniejszym elementem tym razem jest widżet Hero. Zwykle jest to tylko jeden element
interfejsu użytkownika, w przypadku którego warto przełączyć się z jednego obiektu Route do
drugiego.
Widżet hero
We Flutterze Hero to widżet, który przechodzi między ekranami. Oto przykład:
Hero w rzeczywistości nie jest tym samym obiektem na każdym ekranie. Jednak z perspek-
tywy użytkownika tak jest. Chodzi o to, aby stworzyć widżet, który żyje między ekranami i po
prostu zmienia swój wygląd. Podobnie jak na poprzednim zrzucie ekranu, element skaluje się
i porusza w tym samym czasie, gdy pojawia się nowy ekran. Oto czego dowiadujemy się z trzech
obrazów na powyższym schemacie:
1. Dana sytuacja występuje, gdy wybieramy element listy. Na przykład przejście
rozpoczyna się, gdy wyświetlany jest ekran szczegółowy.
2. Filmik z procesu przejścia. Tutaj widżet Hero zmieni swoją pozycję i rozmiar,
dopóki nie dopasuje się do wyniku końcowego (3).
3. Ostatni ekran, z Hero z kroku 1, w nowym rozmiarze.
214
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Zaczynamy zmianę od dodania widżetu Hero do naszego drzewa. Powinien on opakować widżety
zaangażowane w animację:
class FavorsPageState extends State<FavorsPage> {
// ...
@override
Widget build(BuildContext context) {
// ...
floatingActionButton: FloatingActionButton(
heroTag: "request_favor",
child: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
215
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Najważniejszą rzeczą, na którą należy zwrócić uwagę, jest prostota. Nasz FloatingActionButton za-
wiera właściwość tagu heroTag, która sprawia, że zachowuje się jak widżet Hero, co oznacza, że
może animować przejście do innego ekranu. Na drugim ekranie wystarczy powtórzyć proces:
// część metody budowania RequestFavorPageState
@override
Widget build(BuildContext context) {
return Hero(
tag: "request_favor",
child: Scaffold(
// reszta kodu scaffold
),
);
}
...
Zwróć uwagę na właściwość tag: w tym miejscu pojawia się magia. Poniższy tekst pochodzi
ze strony internetowej Fluttera:
Istotne jest, aby oba widżety Hero były tworzone z tym samym tagiem, zazwyczaj obiek-
tem reprezentującym dane bazowe.
Zaleca się również, aby widżety Hero miały praktycznie identyczne drzewa widżetów, a nawet
lepiej, aby były tym samym widżetem, aby uzyskać najlepsze wyniki animacji.
Spójrzmy na inny przyklad. Załóżmy, że mamy ekran ze szczegółami naszych przysług, a gdy użyt-
kownik wybiera FavorCardItem, wyświetla odpowiednią przysługę na pełnym ekranie, animując to
przejście za pomocą widżetu Hero. Tak będzie wyglądał efekt:
216
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Wiem, że na zrzutach ekranu może to nie wyglądać fajnie, ale spójrz na załączony kod,
aby zobaczyć potencjał widżetu Hero.
Aby awatar i tekst były animowane na nowym ekranie podczas przejścia, musimy stworzyć
dwie animacje Hero, jedną dla obrazu i jedną dla opisu. Oto co zmieniliśmy w widżecie
FavorCardItem:
class FavorCardItem extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
...
_itemHeader(context, favor),
Hero(
tag: "description_${favor.uuid}",
child: Text(
favor.description,
style: bodyStyle,
),
),
_itemFooter(context, favor)
...
}
...
}
217
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W ten sam sposób zmodyfikowaliśmy metodę _itemHeader, aby widżet Hero opakował nasz avatar:
Widget _itemHeader(BuildContext context, Favor favor) {
...
Hero(
tag: "avatar_${favor.uuid}",
child: CircleAvatar(
backgroundImage: NetworkImage(
favor.friend.photoURL,
),
),
),
...
}
Zwróć uwagę na właściwość tag animacji Hero. Określiliśmy ją, wykorzystując wartość uuid przy-
sługi, aby Hero był jednoznacznie identyfikowalny w kontekście.
Aby uruchomić ekran ze szczegółami przysług, potrzebujemy niewielkiej zmiany w naszym wi-
dżecie FavorsList:
class FavorsList extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
...
Expanded(
child: ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: favors.length,
itemBuilder: (BuildContext context, int index) {
final favor = favors[index];
return InkWell(
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
// transitionDuration: Duration(seconds: 3),
// odkomentuj aby zobaczyć wolniejsze przejście
pageBuilder: (_, __, ___) =>
FavorDetailsPage(favor: favor),
),
);
},
child: FavorCardItem(favor: favor),
);
},
),
),
...
}
...
}
218
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Opakowaliśmy nasz FavorCardItem w widżet InkWell, aby obsługiwać jego dotknięcia. Gdy użyt-
kownik go dotknie, nowy Route zostanie przesłany do Navigator w celu wyświetlenia widżetu
FavorDetailsPage.
Ostatnią częścią, której należy się przyjrzeć, jest widżet FavorDetailsPage. Tutaj tworzymy
ostateczny wygląd ekranu ze szczegółami przysługi, a dzięki umieszczeniu awatara i opisu przy-
sługi w widżetach Hero mamy dużo lepsze przejście. Tak wygląda jego metoda build():
// część hands_on_hero/lib/main.dart
class _FavorDetailsPageState extends State<FavorDetailsPage> {
@override
Widget build(BuildContext context) {
final bodyStyle = Theme.of(context).textTheme.display1;
return Scaffold(
body: Card(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_itemHeader(context, widget.favor),
Container(height: 16.0),
Expanded(
child: Center(
child: Hero(
tag: "description_${widget.favor.uuid}",
child: Text(
widget.favor.description,
style: bodyStyle,
),
),
),
),
],
),
),
),
);
}
}
219
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Hero(
tag: "avatar_${favor.uuid}",
child: CircleAvatar(
radius: 60,
backgroundImage: NetworkImage(
favor.friend.photoURL,
),
),
),
Container(height: 16.0),
Text(
"${favor.friend.name} asked you to... ",
style: headerStyle,
),
],
);
}
Jak widać, jest podobny do widżetu FavorCardItem. Uzyskano lepsze przejście przy minimal-
nych różnicach w drzewie. Należy również zauważyć, że główną rzeczą, o którą należy się zatrosz-
czyć, jest właściwość tag animacji Hero, która — aby efekt zadziałał — musi pasować do orygi-
nalnego tagu.
Aby zobaczyć pełny przykład, zapoznaj się z załączonym kodem źródłowym tego rozdziału.
Znaczenie nadal ma tutaj Navigator, podobnie jak działania push lub pop, które uruchamiają
animację Hero (sygnalizując, że trasa się zmienia).
Oprócz właściwości tag widżet Hero zawiera inne właściwości umożliwiające dostosowanie
przejścia:
TransitionOnUserGestures — aby włączyć / wyłączyć animację Hero dla gestów
użytkownika, takich jak efekt powrotu na Androidzie.
createRectTween i flightShuttleBuilder — wywołania zwrotne do zmiany
wyglądu przejścia.
placeholderBuilder — wywołanie zwrotne w celu zwrócenia widżetu, który może
być pokazany w miejscu źródłowego widżetu Hero podczas przejścia.
W rozdziale 15., w miarę poszerzania wiedzy na temat animacji, będziesz mógł praco-
wać z powyższymi właściwościami.
220
d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami
Jak widać, animacje Hero są łatwe do zaimplementowania we Flutterze, a nawet domyślna ani-
macja dostarczona przez framework może wystarczyć, aby stworzyć dobry efekt na niektórych
elementach layoutu.
Podsumowanie
W tym rozdziale widzieliśmy, jak dodać nawigację między naszymi ekranami. Najpierw po-
znaliśmy widżet Navigator — główny element, jeśli chodzi o nawigację we Flutterze. Widzie-
liśmy, jak tworzy stos nawigacji lub historię przy użyciu klasy Overlay.
Poznaliśmy również inny ważny element nawigacji, Route, i sposób jego definiowania, aby wyko-
rzystać go w naszych aplikacjach. Wypróbowaliśmy różne podejścia do implementacji nawigacji,
z najbardziej typowym zastosowaniem widżetu WidgetsApp.
Wreszcie, widzieliśmy, jak dostosować przejścia między ekranami, aby zmienić domyślne ruchy
specyficzne dla platformy w aplikacjach Material i iOS Cupertino, a także jak używać animacji
Hero do udostępniania elementów między przejściami, aby tworzyć ciekawe efekty.
221
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
222
d0765ad53fb82babda2278a311da7afb
d
III
Tworzenie
profesjonalnych
aplikacji
Aby opracować profesjonalną aplikację, programista musi dodać funkcje, które obejmują wiele
zaawansowanych i niestandardowych mechanizmów, wykorzystując w razie potrzeby wtyczki
rozszerzające framework.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
224
d0765ad53fb82babda2278a311da7afb
d
8
Wtyczki Firebase
Deweloperzy często tworzą kody modułowe, których można używać w wielu aplikacjach. Nie ina-
czej jest w świecie Fluttera; społeczność jest bardzo zaangażowana w sukces tego frameworka
i dostępnych jest wiele świetnych wtyczek dla programistów. W tym rozdziale poznasz i nau-
czysz się, jak korzystać z interesujących wtyczek Firebase, takich jak Auth, Cloud Firestore i ML
Kit, do tworzenia w pełni funkcjonalnej aplikacji bez angażowania się w złożony backend.
Omówienie Firebase
Firebase to produkt Google oferujący wiele technologii dla wielu platform. Jeśli jesteś programistą
mobilnym lub internetowym, znasz tę niesamowitą platformę.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Konfigurowanie Firebase
Do naszej wcześniej opracowanej aplikacji Favors dodamy niektóre technologie Firebase, takie
jak uwierzytelnianie Firebase i Cloud Firestore. Kroki są jednak zawsze takie same dla każdej
aplikacji Fluttera.
226
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
227
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
228
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Tutaj konfigurujemy nasze aplikacje projektowe, ponieważ możemy mieć wiele aplikacji na pro-
jekt (czyli po jednej na każdą platformę mobilną), a także sprawdzamy poświadczenia projektu
używane do konfigurowania SDK na Flutterze.
Musimy skonfigurować dwie aplikacje w Firebase — jedną na iOS i jedną na Androida, tak jak-
byśmy tworzyli mobilne aplikacje natywne. Jeśli więc wykonałeś już tę konfigurację dla do-
wolnej aplikacji, następna sekcja może wyglądać prosto.
229
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Tutaj ważnym ustawieniem jest nazwa pakietu sprawdzana w pakiecie SDK Firebase. W przy-
padku uwierzytelniania (auth) jest również ważny certyfikat; wkrótce to omówimy.
230
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Nazwę pakietu aplikacji dla systemu Android można znaleźć w pliku android/app/build.
gradle za pośrednictwem właściwości applicationId.
Po rejestracji generowany jest plik google-services.json, który należy dodać do naszego projektu
aplikacji. W systemie Android powinien się on znajdować w katalogu android/app.
Ostatnim krokiem jest dodanie pakietu SDK Firebase do plików Gradle. W Androidzie Gradle
może być postrzegany jako odpowiednik pubspec Fluttera. Jednym z jego obowiązków jest zarzą-
dzanie zależnościami aplikacji:
1. Najpierw w pokazany poniżej sposób dodajemy zależność google-services
do classpath w pliku android/build.gradle.
buildscript {
repositories {
google() // dodaj jeśli nie ma
...
}
dependencies {
...
classpath 'com.google.gms:google-services:3.2.1' // dodaj tą linię
}
}
// firebase
// Dodaj poniższą linię na końcu pliku:
apply plugin: 'com.google.gms.google-services'
231
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
FlutterFire
Aplikacje Fluttera korzystają z zestawu wtyczek Fluttera, aby uzyskać dostęp do usług Firebase.
Sprawdź stronę wtyczek FlutterFire, aby uzyskać więcej informacji na temat najnowszych
wersji wtyczek Firebase: https://firebaseopensource.com/projects/flutter/plugins/.
Poza tym powinniśmy w razie potrzeby dodać wszelkie zależności Firebase. Należy również dodać
firebase_auth do pracy z uwierzytelnianiem telefonicznym:
# część pubspec.yaml
dependencies:
...
firebase_core: 0.3.4 # Wtyczka bazowa Firebase
232
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Wykonanie polecenia flutter packages get kończy proces instalacji, co oznacza, że możemy
teraz rozpocząć pracę z wtyczkami.
Uwierzytelnianie Firebase
Jak widzieliśmy wcześniej, Firebase zawiera zbiór przydatnych technologii i musimy skonfiguro-
wać każdą z nich, której możemy potrzebować w naszym projekcie. Skonfigurujmy warstwę
uwierzytelniającą naszej aplikacji. Warstwa ta ma dla niej fundamentalne znaczenie; jak być
może pamiętasz, prośby użytkownika o przysługę są kierowane do znajomych, a aby tak się stało,
potrzebujemy, żeby użytkownik był w stanie wysłać prośbę do określonego użytkownika. Do jego
identyfikacji wykorzystujemy numer telefonu. Musimy to zrobić w następujących krokach:
1. Dodaj do projektu wtyczkę Firebase auth.
2. Jak wskazano wcześniej, wystarczy dodać zależność wtyczki firebase_auth
do naszego pubspec, co pokazano w poniższym kodzie.
# część pubspec.yaml
dependencies:
...
firebase_core: 0.3.4 # Firebase Core
firebase_auth: 0.8.4+5 # Firebase Auth // dodaj to
3. Włącz uwierzytelnianie telefoniczne dla naszego projektu Firebase w konsoli
Firebase.
4. Utwórz ekran uwierzytelniania auth.
5. Sprawdź, czy użytkownik jest zalogowany, a jeśli nie, przekieruj do strony logowania.
233
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Po włączeniu uwierzytelniania możemy dodać testowy numer telefonu, używany tylko pod-
czas fazy rozwoju oprogramowania, aby nie wpływać na wykorzystanie zasobów przez innych
użytkowników, jak pokazano tutaj:
234
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Ważne jest, aby skonfigurować testowy numer telefonu i kod weryfikacyjny. Podczas progra-
mowania Twoja aplikacja na Androida jest podpisywana certyfikatem debugowania. W ten sposób
na ekranie logowania, gdy zostaniesz poproszony o wpisanie numeru telefonu, będzie działać tylko
z poprzednio wymienionymi numerami telefonów. Ponadto zamiast oczekiwać kodu weryfikacyj-
nego, po prostu wpisz ten skonfigurowany wcześniej.
Ekran uwierzytelniania
W przypadku tego ekranu nie będziemy omawiać szczegółów layoutu. Jedynym nowym widżetem
jest tutaj Stepper z Material Design. Ogólna idea jest taka, że użytkownik wprowadza swój numer
telefonu, otrzymuje kod weryfikacyjny, a po jego potwierdzeniu zostaje zalogowany. Wykorzysta-
liśmy również nasze dane wejściowe z rozdziału 5:
235
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, layout jest prosty, a widżet Stepper pomaga w procesie logowania, prowadząc nas
krok po kroku przez następujące czynności:
1. Użytkownik wpisuje swój numer telefonu.
2. Użytkownik wpisuje kod weryfikacyjny (otrzymany SMS-em).
3. Użytkownik podaje swoją nazwę i zdjęcie profilowe.
Jeśli sprawdzisz załączony kod źródłowy, zauważysz, że do naszego widżetu Stepper dodali-
śmy następujące dwa kroki:
1. Wyślij kod weryfikacyjny — w pierwszym kroku użytkownik wpisuje swój numer
telefonu w celu uzyskania kodu weryfikacyjnego.
2. Wprowadź pobrany 6-cyfrowy kod weryfikacyjny — aby potwierdzić tożsamość
użytkownika. Następnie użytkownik zostaje zalogowany.
Oprócz właściwości widżetu Stepper skoncentrujmy się na jego polu onStepContinue, które
jest pokazane poniżej:
// część metody budowania LoginPageState. Wywołanie zwrotne Steppera:
onStepContinue: () {
if (_currentStep == 0) {
_sendVerificationCode();
} else if (_currentStep == 1) {
_executeLogin();
} else {
_saveProfile();
}
},
To pole oczekuje wywołania zwrotnego, które jest uruchamiane, gdy użytkownik naciśnie
przycisk Continue w każdym kroku. Ponieważ zachowujemy aktualnie aktywny krok w polu
_currentStep, wiemy, jaką czynność wykonać. Zobaczmy więc, jak jest wykonywana każda akcja.
236
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Odbywa się to za pomocą metody Firebase SDK o nazwie verifyPhoneNumber, która żąda od ser-
wera rozpoczęcia uwierzytelniania telefonicznego, jak pokazano poniżej:
// metoda _sendVerificationCode (LoginPageState) login_page.dart
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: _phoneNumber,
codeSent: codeSent,
verificationCompleted: verificationSuccess,
verificationFailed: verificationFail,
codeAutoRetrievalTimeout: autoRetrievalTimeout,
timeout: Duration(seconds: 0),
);
}
Oto kilka ważnych rzeczy, na które należy zwrócić uwagę w poprzednim kodzie.
FirebaseAuth.instance odzwierciedla pojedyncze wystąpienie pakietu SDK auth
Firebase, które stanowi pomost między Flutterem a natywnymi bibliotekami auth
Firebase.
Istnieje wiele wywołań zwrotnych do zaimplementowania i właściwości do ustawienia
dla wywołań API uwierzytelniania; mianowicie:
237
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Niezwykle ważne jest, aby przejrzeć witrynę FlutterFire, a także dokumentację wtyczki
firebase_auth w celu zrozumienia poprzednich właściwości: https://pub.dev/packages/
firebase_auth.
Ponadto wyłączono automatyczną weryfikację, ponieważ nie działa ona w pełni w momencie
pisania tej książki; możesz zmienić wywołania zwrotne, aby przetestować je samodzielnie.
await FirebaseAuth.instance.signInWithCredential(
PhoneAuthProvider.getCredential(
verificationId: _verificationId, smsCode: _smsCode,
));
FirebaseAuth.instance.currentUser().then((user) {
if (user != null) {
goToProfileStep();
}
});
}
238
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Jak widać, jest to proste wywołanie metody signInWithCredential z wtyczki Firebase auth, która
oczekuje następujących dwóch argumentów:
verificationId — jest to identyfikator całego procesu logowania. Spójrz na
poprzednie wywołania zwrotne, w których go otrzymaliśmy, i przechowaj go tutaj
do późniejszego wykorzystania. Identyfikuje login, dzięki czemu nie musimy
ponownie przesyłać wszystkich informacji (w tym przypadku numeru telefonu).
smsCode — kod wprowadzony przez użytkownika w celu weryfikacji; jeśli oba są
prawidłowe, logowanie się powiedzie.
Jeśli wykonasz jakieś testy, zauważysz, że aplikacja nie wyświetla użytkownikowi komu-
nikatów informujących o błędach logowania (takich jak nieprawidłowy kod weryfika-
cyjny). W rzeczywistej aplikacji nie jest to idealne zachowanie. Przyjrzyj się wywołaniom
zwrotnym i spróbuj je poprawić.
await user.updateProfile(updateInfo);
239
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ten ekran jest pierwszym ekranem naszej aplikacji. Nie powinniśmy jednak za każdym razem
prosić użytkownika o podanie wszystkich informacji. Przede wszystkim musimy sprawdzić,
czy użytkownik jest już zalogowany, a jeśli tak, po prostu go przekierować, tak jak wcześniej.
Możemy to zrobić, ponownie używając metody FirebaseAuth.instance.currentUser().
Świetnym miejscem do sprawdzenia tego faktu jest metoda initState() klasy LoginPageState:
// część login_page.dart
class LoginPageState extends State<LoginPage> {
...
@override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then((user) {
if (user != null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => FavorsPage(),
),
);
}
});
}
...
}
Jak widać, jeśli aktualny użytkownik Firebase nie ma wartości null, wiemy, że możemy przekie-
rować nawigację na następny ekran, tak jak poprzednio.
Jakie byłyby dobre opinie użytkowników, gdyby aktualny użytkownik miał wartość null?
Pomyśl i znajdź odpowiedź.
240
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
W tym rozdziale wprowadzimy pewne zmiany w naszej aplikacji Favors. Zrobimy, co następuje:
Przeniesiemy naszą listę przysług do Firebase.
Zobaczymy, jak dodawać reguły, aby użytkownik nie mógł uzyskać dostępu
do przysług innego użytkownika.
Wyślemy / zapiszemy prośby o przysługę do innego użytkownika / znajomego
w Cloud Firestore.
Włączamy ją jak każdą inną usługę Firebase. Jedną ważną rzeczą dotyczącą danych jest bez-
pieczeństwo. Firebase zapewnia mechanizmy reguł, dzięki którym możemy skonfigurować
poziom dostępu do wszelkich informacji przechowywanych w naszej bazie danych. Jest to jedyna
rzecz, którą konfigurujemy podczas uruchamiania Cloud Firestone (zobacz pierwszy rysunek
na następnej stronie).
W naszej aplikacji dla uproszczenia nie będziemy definiować żadnych zasad; dlatego wybraliśmy
tryb testowy. Zdecydowanie jednak zachęcam do zapoznania się z tymi zasadami, ponieważ
są one bardzo ważne w przypadku rzeczywistych aplikacji: https://firebase.google.com/docs/
firestore/security/rules-structure?authuser=0 (zobacz drugi rysunek na następnej stronie).
241
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Następnie możemy oprogramować zapis i odczyt przysług do / z bazy danych Cloud Firestore.
242
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Dotyczy to również wtyczki Cloud Firestore. Tak więc pierwszym krokiem jest dodanie ich
niezbędnych zależności do naszego pubspec.yaml, jak pokazano tutaj:
dependencies:
cloud_firestore: ^0.9.5 # Cloud Firestore
Po ich uzyskaniu za pomocą flutter packages get jesteśmy gotowi do zmiany naszego zbioru
przysług.
Kolekcje to po prostu grupa dokumentów. W naszej aplikacji mamy jedną kolekcję o nazwie
przysługi, w której są przechowywane wszystkie dokumenty dotyczące przysług z aplikacji.
Dokument to rekord w kolekcji. Jest powszechnie reprezentowany jako obiekt JSON.
@override
void initState() {
super.initState();
...
pendingAnswerFavors = List();
acceptedFavors = List();
completedFavors = List();
refusedFavors = List();
friends = Set();
watchFavorsCollection();
}
....
void watchFavorsCollection() async {
final currentUser = await FirebaseAuth.instance.currentUser();
Firestore.instance
.collection('favors') // 1
.where('to', isEqualTo: currentUser.phoneNumber) // 2
243
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
.snapshots() //3
.listen((snapshot) {}) //4
...
}
}
Typowe zapytanie Firebase może mieć wiele formatów; to wykonuje następujące czynności:
1. Zaczyna się od określenia docelowej kolekcji — przysług.
2. Dodaje warunek where, aby filtrować przysługi, które są wysyłane tylko na numer
telefonu bieżącego użytkownika.
3. snapshots() tworzy strumień migawek.
4. listen((snapshot) {}) to miejsce, w którym nasłuchujemy zmian w migawkach,
czyli subskrybujemy zmiany migawki. Przy każdej zmianie w bazie danych, która
ma wpływ na zapytanie, zostanie wywołana funkcja przekazana do listen().
Kod wywołania zwrotnego do funkcji listen() jest następujący:
// część watchFavorsCollection
void watchFavorsCollection() async {
final currentUser = await FirebaseAuth.instance.currentUser();
Firestore.instance
.collection('favors')
.where('to', isEqualTo: currentUser.phoneNumber)
.snapshots()
.listen((snapshot) {
List<Favor> newCompletedFavors = List();
List<Favor> newRefusedFavors = List();
List<Favor> newAcceptedFavors = List();
List<Favor> newPendingAnswerFavors = List();
Set<Friend> newFriends = Set();
snapshot.documents.forEach((document) {
Favor favor = Favor.fromMap(document.documentID,
document.data);
if (favor.isCompleted) {
newCompletedFavors.add(favor);
} else if (favor.isRefused) {
newRefusedFavors.add(favor);
} else if (favor.isDoing) {
newAcceptedFavors.add(favor);
} else {
newPendingAnswerFavors.add(favor);
}
newFriends.add(favor.friend);
});
244
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
this.pendingAnswerFavors = newPendingAnswerFavors;
this.refusedFavors = newRefusedFavors;
this.acceptedFavors = newAcceptedFavors;
this.friends = newFriends;
});
});
}
Jak widać, za każdym razem, gdy część kolekcji, której szuka nasze zapytanie, zmieni się poprzez
wstawienie, edycję lub usunięcie przysługi, zostanie wywołane wywołanie zwrotne i:
Tworzona jest nowa lista każdego rodzaju przysługi.
Przysługa jest tworzona za pomocą nowego konstruktora zdefiniowanego przez
fromMap, jak pokazano poniżej.
Favor.fromMap(String uid, Map<String, dynamic> data)
: this(
uuid: uid,
description: data['description'],
dueDate: DateTime.fromMillisecondsSinceEpoch
(data['dueDate']),
accepted: data['accepted'],
completed: data['completed'] != null
? DateTime.fromMillisecondsSinceEpoch
(data['completed'])
: null,
friend: Friend.fromMap(data['friend']),
to: data['to'],
);
To samo dotyczy obiektu Friend. Sprawdź klasę Favor dla tego przykładu.
Sprawdź klasę Friend. W celu prawidłowego użycia w kolekcji Set operator równości
(==) i metoda hashCode zostały zastąpione dla poprawnego wyliczenia.
245
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Tworzymy nową metodę, która będzie używana przy każdej zmianie przysługi, _updateFavorOn
Firebase():
void _updateFavorOnFirebase(Favor favor) async {
await Firestore.instance
.collection('favors') // 1
.document(favor.uuid) // 2
.setData(favor.toJson()); // 3
}
Początek wywołania Firestore jest prawie zawsze taki sam; pobieramy instancję Firestore, a następ-
nie wykonujemy następujące kroki:
1. Idziemy do kolekcji przysług.
2. Otrzymujemy odniesienie do dokumentu przysługi, który chcemy zaktualizować.
3. Ostatnim krokiem jest przesłanie danych w formacie JSON, które mają być
zaktualizowane w odpowiednim dokumencie. Metoda toJson() służy do prostej
konwersji danych przechowywanych w Firebase.
Sprawdź załączony kod źródłowy hands_on_firebase, aby uzyskać pełny kod służący do
wymiany danych z Firebase.
await _saveFavorOnFirebase(
246
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Favor(
to: _selectedFriend.number,
description: _description,
dueDate: _dueDate,
friend: Friend(
name: currentUser.displayName,
number: currentUser.phoneNumber,
photoURL: currentUser.photoUrl,
),
),
); //3
Navigator.pop(context); //4
}
}
Może moglibyśmy poradzić sobie z błędami pojawiającymi się w procesie zapisywania, aby
użytkownik mógł spróbować później? Co o tym myślisz? To dobry moment, aby ulepszyć kod.
Dzięki tym zmianom zapisujemy i pobieramy przysługi z Cloud Firestore, jak pokazano na
poniższym zrzucie ekranu:
247
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Nie napisaliśmy tutaj żadnego kodu dla backendu, a jako bonus, zmiany w czasie rzeczywistym
zostały odzwierciedlone w naszej aplikacji, dzięki czemu świetnie sprawdza się to w kontekstach
obejmujących wielu użytkowników.
Usługa magazynu jest włączona z domyślną definicją reguły, zgodnie z którą tylko uwierzytelnione
żądania mogą wykonywać wywołania zapisu i odczytu. To wystarczy dla naszej aplikacji.
248
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Po tym wstępnym kroku możemy dodać biblioteki specyficzne dla Fluttera i rozpocząć etap
programowania.
Po uzyskaniu zależności za pomocą flutter packages get jesteśmy gotowi do użycia Firebase
Storage w naszym projekcie.
249
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Musimy zmienić naszą metodę _saveProfile () na ekranie logowania. Tutaj dodajemy kod po-
trzebny do przesłania wybranego zdjęcia do Firebase Storage, a następnie przechowujemy ad-
res URL w informacjach profilu użytkownika w następujący sposób:
// część login_page.dart
await user.updateProfile(updateInfo);
250
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => FavorsPage(),
),
);
}
Jak widać, jedyną rzeczą konieczną do zrobienia była zmiana w obiekcie updateInfo we właści-
wości photoUrl. Część kodu odpowiedzialna za zapis jest nadal taka sama. uploadPicture() to
część, która nas interesuje:
uploadPicture(String userUid) async {
StorageReference ref = FirebaseStorage.instance
.ref()
.child('profiles')
.child('profile_$userUid'); // 1
Lista plików jest dostępna w konsoli Firebase, jak pokazano rysunku na następnej stronie.
Na stronie przysług nic się nie zmienia. Tak jak poprzednio, zdjęcie profilowe jest ładowane
w CircleAvatar z NetworkImage tylko wtedy, gdy podano właściwość photoURL znajomego (nie null):
// część strony przysług Klasa FavorCardItem
CircleAvatar(
backgroundImage: favor.friend.photoURL != null
? NetworkImage(
favor.friend.photoURL,
)
: AssetImage('assets/default_avatar.png'),
),
251
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, mamy obsłużony przypadek użytkownika bez zdjęcia profilowego. To tyle, jeśli chodzi
o obsługę Storage dla aplikacji Favors. Istnieje wiele możliwości, które nie zostały jeszcze
zbadane.
Możemy łatwo zintegrować AdMob z naszą aplikacją za pomocą wtyczek FlutterFire. Rejestracja
i korzystanie z AdMob są nieco inne niż w przypadku poprzednich wtyczek, które widzieli-
śmy; musimy w tym celu utworzyć inne konto.
Konto AdMob
Prawdę mówiąc, AdMob jest oddzielony od konsoli Firebase. Chociaż w konsoli mamy sekcję
AdMob, nie mamy nic poza linkami do dokumentacji AdMob i strony początkowej — zobacz
rysunek na następnej stronie.
252
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Możesz postępować zgodnie z instrukcjami na stronie, aby utworzyć nowe konto na wzór
— zobacz pierwszy rysunek na następnej stronie.
253
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
254
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Po utworzeniu aplikacji AdMob możemy ją połączyć z portalem Google AdMob, jak pokazano
poniżej:
Postępuj zgodnie z instrukcją w oknie dialogowym i połącz aplikację AdMob na iOS / Androida
z odpowiednią aplikacją Firebase w projekcie, jak pokazano na zrzucie ekranu na następnej
stronie.
Oznacza to, że dane analityczne zebrane w Firebase pomogą Twojemu AdMob. Taki przepływ
danych ulepsza właściwości produktów i generowanie przychodów.
AdMob we Flutterze
Podobnie jak w przypadku poprzednich wtyczek FlutterFire, musimy dodać zależność AdMob
do naszego pubspec.yaml w następujący sposób:
dependencies:
firebase_admob: ^0.8.0+4 # AdMob
255
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Po uzyskaniu zależności za pomocą polecenia flutter packages get jesteśmy gotowi do korzysta-
nia z Firebase AdMob w naszym projekcie.
Jak widać, inicjalizujemy wtyczkę, podając nasz zarejestrowany identyfikator aplikacji (ważny
dla wersji release aplikacji). W poprzednim przykładzie używamy tylko identyfikatorów testo-
wych. Jest to ta sama wartość, która jest obecna we właściwości FirebaseAdMob.testAppId biblioteki.
Nasze banery możemy przetestować na dwa sposoby:
256
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Odbywa się to poprzez dodanie wartości <meta-data> zawierającej ten sam identyfikator aplikacji,
który został wcześniej skonfigurowany.
257
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Odbywa się to poprzez dodanie wpisu do sekcji <dict> zawierającej ten sam identyfikator
aplikacji, który został wcześniej skonfigurowany dla systemu iOS.
Musimy zachować odniesienie do reklam, gdy je pokazujemy, aby móc je później usunąć.
Najpierw dodajemy je więc jako pola w naszej klasie:
// Klasa RequestFavorPageState
InterstitialAd _interstitialAd;
BannerAd _bannerAd;
_interstitialAd = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
)..load();
Podczas definiowania reklam musimy wziąć pod uwagę kilka rzeczy. Zobacz:
adUnitId to główna właściwość reklamy — jak wynika z dokumentacji AdMob:
Jednostka reklamowa to co najmniej jedna reklama Google wyświetlana jako
wynik jednego fragmentu kodu reklamy AdSense.
258
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Funkcja load() to wywołanie startowe reklam; dzięki niej reklama będzie gotowa
do wyświetlenia.
Funkcja show() sprawia, że reklama jest widoczna (czeka, jeśli load nie została
zakończona).
Inną ważną właściwością jest targetingInfo; pomaga nam kierować reklamy.
Sprawdź klasę MobileAdTargetingInfo, aby uzyskać więcej informacji. W tej klasie
możemy również zdefiniować urządzenia testowe (wcześniej wspomniane w sekcji
AdMob we Flutterze).
Jak widać, baner reklamowy wyświetlamy na początku, zaraz po jego załadowaniu. W dalszej
części metody save() wyświetlana jest również reklama pełnoekranowa:
// metoda save
await _interstitialAd.show();
Reklamy są wyświetlane z logo testu; możesz używać prawdziwych reklam, tworząc jednostki
reklamowe i używać urządzeń testowych:
W następnej sekcji omówimy inną technologię, Firebase ML Kit, która pomaga nam integro-
wać narzędzia uczenia maszynowego w naszych aplikacjach.
259
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Uczenie maszynowe
z wykorzystaniem Firebase ML
Firebase ML Kit pomaga dodawać funkcje uczenia maszynowego do naszej aplikacji — nawet
jeżeli nie mamy doświadczenia z uczeniem maszynowym. Aby rozpocząć pracę, nie jest wymagana
głęboka wiedza na temat sieci neuronowych ani optymalizacji modelu.
Narzędzia na urządzeniu to interfejsy API, które działają w trybie offline i szybko przetwarzają
dane. Z drugiej strony interfejsy API oparte na chmurze polegają na Google Cloud Platform,
aby zapewnić wyniki z dużą dokładnością.
Po uzyskaniu zależności za pomocą flutter packages get jesteśmy gotowi do użycia Firebase ML
Kit w naszym projekcie.
260
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
W zależności od usługi, z której chcemy skorzystać, musimy dodać określone biblioteki na pozio-
mie systemu.
W iOS podstawy krok jest taki sam, dodajemy to poprzez skorzystanie z podów (odpowiednik
wtyczek we Flutterze).
W katalogu ios uruchom polecenie pod init, jeśli nie masz w nim pliku Podfile.
261
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W prostym przykładzie będziemy wykrywać etykiety dla obrazu profilu użytkownika. Odbywa się
to poprzez zmianę zachowania przycisku przechwytywania; po przechwyceniu obrazu urucha-
miamy kod _labelImage().
_labelImage() async {
if (_imageFile == null) return;
setState(() {
_labeling = true;
});
setState(() {
_labels = labels;
_labeling = false;
});
}
262
d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase
Jak widać, na obrazie wykryte zostało wiele obiektów z dużą wartością ufności.
To ważna informacja w uczeniu maszynowym; wszystkie obliczone wartości mają wartość ufności.
Podsumowanie
W tym rozdziale omówiliśmy świetne narzędzia Firebase, które pomagają nam tworzyć w pełni
funkcjonalne aplikacje z zaawansowanymi technologiami. Dodaliśmy do naszej aplikacji uwierzy-
telnianie telefoniczne z weryfikacją kodu SMS za pomocą wtyczki Firebase auth. Później
zmieniliśmy listę przysług i sprawiliśmy, że żądania są wysyłane do usługi Cloud Firestore.
Wtyczka Firebase Storage została użyta do wysłania obrazów profili użytkowników do zaplecza
Firebase Storage, gdzie możemy przechowywać dowolne pliki do wykorzystania w naszych
aplikacjach. Jako bonus udostępniliśmy wprowadzenie do usługi AdMob z wykorzystaniem
wtyczki Firebase AdMob oraz do ML Kit za pośrednictwem wtyczki Firebase ML Vision. Widzie-
liśmy, jak konfigurować nasze aplikacje i zarządzać nimi w konsoli Firebase i portalu AdMob.
Możemy również tworzyć własne wtyczki Fluttera i wykorzystać je w naszych aplikacjach. W ko-
lejnym rozdziale przyjrzymy się procesowi tworzenia wtyczki od implementacji do publikacji
w repozytorium pub.
263
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
264
d0765ad53fb82babda2278a311da7afb
d
9
Tworzenie własnej
wtyczki Fluttera
Programista może korzystać z wtyczek opracowanych przez różne społeczności. Może również
sam udostępnić jakiś moduł lub dołączyć go do własnego zestawu narzędzi. W ten sposób
tworzenie i udostępnianie pakietów jest dzięki frameworkowi Flutter bardzo ułatwione. W tym
rozdziale dowiesz się, jak utworzyć mały projekt wtyczki, aby poznać podstawy tego procesu.
Dodasz także dokumentację i opublikujesz ją, czym wesprzesz społeczność.
W tym rozdziale zostaną omówione następujące tematy:
Tworzenie projektu pakietu / wtyczki.
Struktura projektu wtyczki.
Dokumentacja w pakietach.
Publikowanie pakietu.
Zalecenia dotyczące rozwoju wtyczek.
Ekosystemy Flutter i Dart zapewniają narzędzia, które pomagają w realizacji projektu bez żad-
nych trudności. Proces tworzenia i publikowania pakietu odbywa się w środowisku Fluttera.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, nie zawiera typowych folderów android i ios, ponieważ nie potrzebujemy ich do
prostych pakietów Dart.
266
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
Nawet pubspec.yaml nie ma w sobie nic specjalnego, z wyjątkiem zależności dla SDK:
name: simple_package
description: A new Flutter package project.
version: 0.0.1
author:
homepage:
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
...
Gdybyśmy chcieli stworzyć pakiet tak, aby nie był specyficzny dla Fluttera, moglibyśmy usu-
nąć część frameworkową Fluttera i traktować go jak pakiet Darta. Podobnie jak na przykład
zależność flutter_test — nie jest to konieczne w przypadku pakietów zawierających tylko Dart.
Pamiętaj: w przypadku prostych pakietów Dart możesz użyć generatora projektów Dart
(https://github.com/dart-lang/stagehand). Pisząc więc proste pakiety Dart, używalibyśmy
narzędzia stagehand, a w przypadku Fluttera skorzystalibyśmy z narzędzia create.
Nie będziemy wchodzić w szczegóły implementacji tego rodzaju pakietu, ponieważ jest to prosty
pakiet Dart.
267
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Domyślnie szablon plugin wykorzystuje ObjC dla iOS i Java dla Androida.
Pamiętaj: aby zmienić platformę na Swift lub Kotlin, możesz określić język iOS za pomocą
argumentu -i, a język Androida za pomocą -a.
name: hands_on_platform_version
description: A new flutter plugin project.
version: 0.0.1
author:
homepage:
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
268
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
androidPackage: com.example.hands_on_platform_version
pluginClass: HandsOnPlatformVersionPlugin
Jak widać, plik pubspec jest również podobny do prostego pakietu aplikacji Fluttera. Różnica
tkwi w sekcji plugin wewnątrz sekcji flutter. Ta część definiuje pakiet jako pakiet wtyczki
identyfikujący natywny kod, który będzie tworzył rzeczywistą implementację w określonym
kontekście platformy.
MethodChannel
Komunikacja Fluttera między klientem (Flutterem) a aplikacją hosta (natywną) odbywa się za
pośrednictwem kanałów platformy. Klasa MethodChannel jest odpowiedzialna za wysyłanie komu-
nikatów (wywołania metod) po stronie platformy. MethodChannel na Androida (API) i Flutter
MethodChannel na iOS (API) umożliwiają odbieranie wywołań metod i wysyłanie wyniku
z powrotem:
269
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Technika kanału platformy umożliwia oddzielenie kodu interfejsu użytkownika od kodu spe-
cyficznego dla platformy. Host nasłuchuje na kanale platformy i otrzymuje wiadomość. Może
używać interfejsów API platformy do implementacji logiki i odsyła odpowiedź do klienta,
część aplikacji Fluttera.
Aby zrozumieć, w jaki sposób zachodzi wymiana wiadomości, możesz sprawdzić stronę
https://flutter.dev/docs/development/platform-integration/platform-channels. Zawiera ona
przykłady kanałów platformy i typów wiadomości.
Typ MethodChannel sprawdzimy szczegółowo w rozdziale 13., gdzie zobaczymy, jak doda-
wać do projektów aplikacji natywne kody, a nie tylko pakiety wtyczek.
270
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
271
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
API Darta
Teraz, gdy sprawdziliśmy natywną implementację wtyczki, musimy zrozumieć, w jaki sposób
Flutter komunikuje się z nią z kontekstu Darta. Wygenerowany plik API Darta lib/hands_
on_platform_version.dart jest punktem wejścia dla aplikacji konsumenckich.
Pakiety konsumenckie zaimportują tę bibliotekę, aby użyć wtyczki. Sprawdźmy plik API:
// hands_on_platform_version.dart
class HandsOnPlatformVersion {
static const MethodChannel _channel =
const MethodChannel('hands_on_platform_version'); // 1
Jak widać, klasa HandsOnPlatformVersion jest publiczna i zawiera jedną metodę, która ujawnia
implementacje natywne:
1. Na początku tworzona jest MethodChannel — pomost między Dartem a natywnym
kodem platformy.
2. Metoda platformVersion jest udostępniona konsumentom.
3. Metoda MethodChannel — invokeMethod() służy do wywołania określonej metody
według nazwy, w tym przypadku getPlatformVersion. Ta metoda zwraca obiekt
Future z wynikiem z kodu natywnego.
name: hands_on_platform_version_example
description: Pokazuje jak korzystać z wtyczki hands_on_platform_version.
publish_to: 'none'
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
272
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
dev_dependencies:
flutter_test:
sdk: flutter
hands_on_platform_version:
path: ../
flutter:
uses-material-design: true
Jest to typowy plik pubspec.yaml aplikacji Fluttera, z wyjątkiem ostatniej pozycji na liście
dev_dependencies. Istnieje zależność od wtyczki hands_on_platform_version z wariantem path.
Korzystanie z wtyczki
Aby użyć pakietu wtyczek, zaczynamy od zaimportowania go do naszych bibliotek Dart, jak
w przypadku każdej innej wtyczki:
import 'package:hands_on_platform_version/hands_on_platform_version.dart';
Pełny przykład zachowuje wersję platformy w polu _platformVersion i wywołuje kod natywny
w metodzie initPlatformState():
Future<void> initPlatformState() async { // 1
String platformVersion;
try { // 2
platformVersion = await HandsOnPlatformVersion.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return; // 3
setState(() {
_platformVersion = platformVersion; // 4
});
}
273
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Pliki dokumentacji
Jeśli odwiedzisz witrynę repozytorium pub (pub.dev), zobaczysz ważne informacje o pakiecie.
Są one zbierane z określonych plików obecnych w projekcie:
pubspec.yaml — ten plik zawiera szczegółowe informacje o pakiecie.
name: hands_on_platform_version_example
description: Demonstrates how to use the hands_on_platform_version
plugin.
version: 0.0.1
author: Alessandro Biessek <alessandrobiessek@gmail.com>
# homepage: the plugin homepage
....
Informacje te są przydatne, aby klienci biblioteki wiedzieli, kto ją stworzył i do czego służy.
README.md — to jest krótka dokumentacja dotycząca korzystania z pakietu
i innych ważnych rzeczy.
LICENSE — to jest licencja na używanie pakietu.
CHANGELOG.md — rejestruje zmiany w każdej wersji pakietu.
example/ — to jest praktyczny przykład korzystania z pakietu.
Dokumentacja biblioteki
Kolejna ważna część dokumentacji pakietu znajduje się na poziomie Darta. Konsument musi
znać każdą dostępną metodę, jej argumenty i typy zwracane, aby wiedzieć, jak najlepiej wykorzy-
stać bibliotekę.
274
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
Można to również zastosować do elementów składowych biblioteki, takich jak metody, zmienne
i klasy. Nawet prywatni członkowie mogą mieć komentarze na potrzeby dokumentacji, które
mogą być pomocne w zrozumieniu różnych części biblioteki.
Generowanie dokumentacji
Kiedy publikujesz pakiet, dokumentacja API jest generowana automatycznie (o ile używasz
wspomnianego wcześniej typu komentarza) i publikowana na dartdocs.org. W razie potrzeby
możesz lokalnie wygenerować dokumentację API.
Podczas pisania tej książki okazało się, że istnieje nierozwiązany problem dotyczący po-
wyższego polecenia w systemie Windows; aby się z nim zapoznać, proszę zajrzeć tu:
https://github.com/dart-lang/dartdoc/issues/1949.
Domyślnie dokumentacja jest generowana w katalogu doc/api jako statyczne pliki HTML.
Publikowanie pakietu
Opublikowanie pakietu jest ostatnim krokiem prowadzącym do udostępnienia go społeczno-
ści Fluttera. Cała publikacja odbywa się za pośrednictwem narzędzia pub. Polecenie wykona-
nia publikacji jest następujące:
flutter packages pub publish --dry-run
Argument --dry-run działa tak samo jak w prepublikacji, w której narzędzie pub przeprowa-
dza proces walidacji, ale w rzeczywistości nie przesyła pakietu. Gdy wszystko jest w porządku,
możemy usunąć część --dry-run:
flutter packages pub publish
275
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Spowoduje to efektywne opublikowanie pakietu w witrynie pub, dzięki czemu każdy kod źródłowy
zostanie opublikowany w repozytorium publikacji. Tylko pliki ukryte i zignorowane (w przypadku
korzystania z Git) nie są przesyłane.
Napisanie dobrej, ukierunkowanej wtyczki może być dla innych programistów bardzo po-
mocne. Nie wahaj się sprawdzić istniejącego kodu źródłowego wtyczki i dowiedzieć się, jak
tworzyć wspaniałe narzędzia dla społeczności.
Podsumowanie
W tym rozdziale dowiedziałeś się, jak wygląda pakiet wtyczki Fluttera i jak różni się od apli-
kacji i prostych pakietów Darta. Zobaczyłeś, że wtyczki Fluttera współpracują z kodem na-
tywnym za pomocą MethodChannels, które zapewniają dobre mechanizmy do bezpośredniej
współpracy z systemem.
276
d0765ad53fb82babda2278a311da7afb
d
Rozdział 9. • Tworzenie własnej wtyczki Fluttera
Dowiedziałeś się, jak rozpocząć projekt pakietu wtyczki we Flutterze oraz jak go odpowiednio
udokumentować, aby był użyteczny i zrozumiały dla społeczności. Na koniec pokazaliśmy, jak
upublicznić pakiet w repozytorium pub, aby inni programiści mogli go używać.
W następnym rozdziale będziemy dalej zagłębiać się w konkretny kod platformy, integrując
różne funkcje, które są unikalne dla każdego systemu, takie jak import kontaktów, korzystanie
z kamery i zarządzanie uprawnieniami aplikacji.
277
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
278
d0765ad53fb82babda2278a311da7afb
d
10
Dostęp do funkcji
urządzenia
z aplikacji Fluttera
Aplikacje mobilne nie funkcjonują same w kontekście urządzenia i użytkownika i dotyczy to każ-
dego ich poziomu, od prostszych, przeznaczonych do jednego celu, po bardziej złożone. Aplikacja
może potrzebować dostępu do funkcji sprzętowych, takich jak Bluetooth, aparat, import
kontaktów, aby umożliwić użytkownikowi interakcję ze znajomymi lub udostępnić zawartość
innym aplikacjom i użytkownikom. Dlatego musi poinformować o tym zarówno użytkownika,
jak i urządzenie.
W tym rozdziale dowiesz się, jak zintegrować aplikację z kontekstem użytkownika, na przykład
wyświetlać i uruchamiać adres URL, zarządzać uprawnieniami platformy, uruchamiać aparat
w telefonie i importować kontakty.
W tym rozdziale zostaną omówione następujące tematy:
Uruchomienie adresu URL z aplikacji.
Zarządzanie uprawnieniami aplikacji.
Importowanie kontaktów z telefonu.
Integracja aparatu w telefonie.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
nas jak pomost, a aplikacja aparatu jest niezależna od podstawowego systemu, ponieważ nie mu-
simy wiedzieć, jak uruchomić aplikację aparatu i jak pobrać plik obrazu, po prostu prosimy o wy-
konanie tej pracy.
Dobrym zastosowaniem wtyczki jest robienie zdjęcia profilowego, ponieważ w przyszłej wersji
aplikacji moglibyśmy pozwolić użytkownikowi na importowanie obrazu z galerii i używanie
go w ten sam sposób.
Teraz wyobraźmy sobie inny przypadek: użytkownik prosi innego użytkownika o przysługę, która
obejmuje dostęp do adresu URL, aby uzyskać więcej informacji na temat przysługi. Jeśli na
przykład ktoś poprosi Cię o zakup produktu w witrynie e-commerce, dobrze jest udostępnić
link do produktu, aby uniknąć nieporozumień.
Funkcję udostępniania linku można dodać do aplikacji za pomocą wtyczki url_launcher. Chodzi
o to, że w przypadku wielu funkcji naszych aplikacji nie musimy wiedzieć, jak platforma działa
pod spodem, ponieważ dostępnych jest wiele przydatnych wtyczek Fluttera.
Wyświetlanie linku
Przede wszystkim użytkownik musi zidentyfikować łącze z możliwością kliknięcia. W kontek-
ście mobilnym musimy maksymalnie uprościć sprawę, więc jak być może wiesz, nie jest właściwe
dodawanie kolejnego pola do prośby o przysługę, aby dodać link do przysługi. Spójrz na apli-
kację do czatu, której możesz teraz używać; możesz wpisać adres URL, a kiedy wyślesz go do
innego użytkownika, automatycznie pojawi się jako klikalny tekst i nie będziesz musiał wykonywać
żadnej czynności; po prostu piszesz.
W naszej aplikacji możemy zamienić linki URL dodane do opisu przysługi w klikalne linki na
kartach przysług. Być może myślisz o napisaniu kodu z taką funkcjonalnością — nie byłoby
to trudne:
Przeanalizuj opis przysługi, aby znaleźć linki.
Utwórz wiele TextSpan, aby zmienić styl tekstu.
Obsłuż dotknięcie za pomocą gestów Fluttera.
TextSpan może być używany, gdy chcemy zastosować różne style do części tekstu.
Więcej informacji można znaleźć w dokumentacji widżetu TextSpan:
https://api.flutter.dev/flutter/painting/TextSpan-class.html.
280
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
Chociaż jest to proste, zakodowanie powyższej funkcjonalności zajmie trochę czasu. Dlatego
dobrze jest używać wtyczek, gdy tylko jest to możliwe: zwiększa to produktywność.
Wtyczka flutter_linkify
Istnieje oczywiście taka wtyczka, która już odpowiada za stylizowanie linków w tekście,
flutter_linkify. Wykonuje ona zadanie opisane w poprzedniej sekcji i przedstawia wynik za po-
mocą widżetu Linkify. Analizuje tekst w poszukiwaniu linków i używa elementów span do roz-
różnienia prostego tekstu od linków. Dodatkowo udostępnia przydatne funkcje:
Właściwość onOpen jako wartość przyjmuje wywołanie zwrotne, które będzie
obsługiwać kliknięcie odsyłacza.
Właściwość humanizing, która wyświetla łącze bez HTTP / HTTPS.
Zmieniliśmy naszą aplikację Favors tak, aby na kartach przysług były wyświetlane linki z opisu
prośby.
Część żądania nie wymaga żadnych modyfikacji, ponieważ użytkownik wpisuje łącze nor-
malnie w tekście.
281
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Tekst zaczynający się od http:// lub https:// pojawia się jako link i można go kliknąć. Następnym
krokiem jest obsługa kliknięcia, aby otworzyć docelowy adres URL.
Możesz sprawdzić obsługiwane schematy adresów URL dla każdej platformy dla iOS
pod adresem: https://developer.apple.com/library/archive/featuredarticles/iPhoneURL
Scheme_Reference/Introduction/Introduction.html oraz dla Androida pod adresem:
https://developer.android.com/guide/components/intents-common.html.
Ponownie, dzięki pracy społeczności Fluttera, możemy dokonać tego poziomu integracji za
pomocą zaprezentowanej wcześniej wtyczki url_launcher.
282
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
Wtyczka url_launcher
Wtyczka url_launcher działa jako pomost do obsługi linków natywnych dla platformy, dzięki
czemu nie musimy się martwić o szczegóły na poziomie platformy.
Użycie wtyczki ogranicza się do kilku funkcji, z których główną jest launch(url). Funkcja
uruchamiania pobiera adres URL jako argument i dba o uruchomienie, które jest specyficzne
dla każdego systemu.
W systemie Android utworzy intencję do obsługi systemu przez aplikację przeglądarki (lub wy-
świetli komponent WebView dla obsługi webowej, jeśli forceWebView jest ustawiony na true).
W systemie iOS adresy URL są obsługiwane domyślnie w kontrolerze widoku należącym do
aplikacji.
Jak widać, wtyczka robi większość pracy za nas. Wystarczy wywołać jej funkcję z odpowiednim
argumentem:
1. Najpierw sprawdzamy, czy za pomocą funkcji canLaunch urządzenie jest w stanie
uruchomić adres URL. To nas upewni, że na urządzeniu jest zainstalowana
aplikacja, która obsługuje adresy URL.
2. Na koniec, jeśli to możliwe, uruchamiamy adres URL; spowoduje to wysłanie
zamiaru uruchomienia do odpowiedniej platformy.
Aby mieć pojęcie o tym, co jest zaimplementowane pod spodem w każdym systemie,
radzę przyjrzeć się natywnej części kodu źródłowego wtyczki.
283
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W najnowszych wersjach systemu iOS należy dołączyć opis użycia w kluczach pliku ios/Runner/
Info.plist dla typów danych, do których aplikacja musi mieć dostęp, w przeciwnym razie ule-
gnie awarii. Na przykład, aby uzyskać dostęp do kamery, musi ona zawierać NSCameraUsage
Description.
Tak więc kluczowa różnica polega na tym, że w systemie Android dostęp do każdego zasobu
użytkownika oparty jest na uprawnieniach, które trzeba dodać do pliku manifest. Należy także
poprosić o dostęp za pomocą interfejsów API dostarczonych przez system. W iOS musisz dodać
opis w Info.plist do każdego wrażliwego zasobu, aby system wyświetlił monit o akceptację lub
odrzucenie przez użytkownika.
Jak widzieliście w naszej aplikacji Favors, do tej pory nie martwiliśmy się o uprawnienia; jedynym
odniesieniem do nich jest link w pliku AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">
284
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
<uses-permission android:name="android.permission.INTERNET"/>
...
</manifest>
Dzięki społeczności Flutter mamy kilka wtyczek, które pomogą nam w tym zadaniu. Dobrym
przykładem jest permissions_handler.
Repozytorium pub zawiera zestaw wtyczek, które pomagają w tym zadaniu. Oto niektóre z nich:
contact_picker — obsługuje wybieranie numeru telefonu z listy kontaktów w telefonie.
contact_service — zapewnia interfejs API, który pozwala nam wybrać kontakt
i zarządzać nim.
285
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak zapewne pamiętasz, nasza aplikacja Favors pozwala użytkownikowi poprosić innego użytkow-
nika o przysługę, dodając numer telefonu wybranego znajomego. Zaimportowanie kontaktu z listy
kontaktów w telefonie to najlepszy sposób na zrobienie tego.
Pierwszym krokiem jest dołączenie wtyczki jako zależności w pliku pubspec.yaml i uruchomienie
polecenia flutter packages get:
dependencies:
contact_picker: ^0.0.2
Następnie musimy zmienić ekran prośby o przysługę. Dodajemy przycisk Import po prawej
stronie listy rozwijanej dla znajomych:
286
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
Dzięki temu będziemy mogli łatwo zaimportować kontakt. Następnie dodajemy wywołanie
zwrotne onPressed dla przycisku Import contact:
onPressed: () {
_importContact();
},
Ostatnim krokiem jest zapisanie przysługi. W tym kroku musimy uzyskać informacje o znajo-
mym z _importedFriend, tak jak zrobiliśmy z _selectedFriend z rozwijanej listy znajomych:
void save(BuildContext context) async {
...
await _saveFavorOnFirebase(
Favor(
to: _importedFriend?.number ?? _selectedFriend?.number,
...
)
287
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
)
...
}
Jedyną potrzebną modyfikacją było dodanie właściwości ‘to’ dla nowego Favor — będzie
wskazywała na wartość _importedFriend lub _selectedFriend.
Jak pewnie się domyślasz, kontakty telefoniczne są zasobami użytkowników i dlatego są informa-
cjami chronionymi. Użytkownik musi więc zezwolić aplikacji na odczyt lub zapis kontaktów.
Jeśli pamiętasz, każda platforma ma swój sposób obsługi uprawnień i na tej podstawie musimy
wdrażać odpowiednie żądania.
Zachowanie tego rekordu zależy od wersji systemu, w którym jest zainstalowana aplikacja.
Sprawdź to tutaj: https://developer.android.com/training/permissions/requesting.
288
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
<dict>
...
<key>NSContactsUsageDescription</key>
<string>You can import a friend from a list of contacts.</string>
</dict>
Gdy aplikacja próbuje uzyskać dostęp do kontaktów w iOS, system zapyta o zgodę użytkow-
nika, wyświetlając podany opis.
Podsumowując, _checkPermissions otrzyma bieżący status uprawnienia, a jeśli nie zostanie ono
przyznane, zażąda go. Odpowiednim miejscem do wywołania tej funkcji jest przycisk Contact
import, zanim zaimportujemy kontakt:
void _importContact() async {
await _checkPermissions();
...
}
W naszym przypadku wynik funkcji _checkPermissions() jest tylko ilustracyjny, ponieważ nie
potrzebujemy pozwolenia.
289
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak być może pamiętasz, w rozdziale 8. udało nam się wysłać zdjęcie profilowe użytkownika
do Firebase Storage i użyliśmy wtyczki image_picker, aby pobrać plik obrazu z kamery. Przyj-
rzyjmy się więc teraz szczegółowo, jak to działa.
Odbywa się to za pomocą klasy ImagePicker. Używamy jej metody pickImage(), aby uruchomić
aparat i zrobić zdjęcie (wszystko to zarządzane jest przez wtyczkę). Następnie przechwycony
obraz zamieniany jest na plik do naszego użytku.
Kod źródłowy pliku login_page.dart możesz znaleźć w serwisie GitHub, aby zapoznać
się z pełnym przykładem wykorzystania wtyczki image_picker. Ponadto ważne jest, aby
sprawdzić dokumentację wtyczki na https://pub.dev/packages/image_picker, ponieważ
wymaga ona pewnej konfiguracji do działania.
290
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
Gdy aplikacja spróbuje uzyskać dostęp do kamery w iOS, system zapyta o zgodę użytkownika,
wyświetlając podany opis.
291
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Odpowiednim momentem do wywołania tej funkcji jest etap wyboru obrazu profilu, wewnątrz
metody _importImage():
void _importImage() async {
await _checkPermissions();
...
}
Podsumowanie
W tym rozdziale widzieliśmy, jak używać wtyczek, aby korzystać z funkcji telefonu, takich jak
aparat, kontakty i uruchamianie adresu URL. Dowiedzieliśmy się, że społeczność Fluttera
zapewnia zestaw wtyczek dla wszystkich potrzebnych funkcji.
292
d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera
Później wtyczka image_picker została użyta w ten sam sposób do pobrania zdjęcia profilowego
użytkownika przy logowaniu i ponownie skorzystaliśmy z wtyczki permission_handler do spraw-
dzenia i zażądania uprawnień do aparatu.
W rozdziale 11. będziemy nadal integrować wtyczki Flutter. Tym razem zobaczymy, jak przepro-
wadzić integrację map z aplikacjami Fluttera.
293
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
294
d0765ad53fb82babda2278a311da7afb
d
11
Widoki platformy
oraz integracja mapy
Wyświetlanie map, związane z pozycją użytkownika, jest obecnie częstą funkcją aplikacji mobil-
nych. W tym rozdziale dowiesz się, jak zintegrować Google Maps z aplikacjami Fluttera; umożliwi
to dodawanie znaczników i interakcji za pomocą interfejsu Google Places API.
Wyświetlanie mapy
Zacznijmy od stworzenia aplikacji wyświetlającej mapę, a później dodamy do niej funkcje.
Framework Fluttera nie zawiera widżetu mapy w swoim podstawowym SDK; jest to obsługiwane
przez oficjalną wtyczkę google_maps_flutter, której użyjemy do wyświetlenia takiej mapy —
zobacz rysunek na następnej stronie.
W chwili pisania tej książki google_maps_flutter dostępny jest tylko w wersji testowej; oznacza to,
że wtyczka opiera się na nowym mechanizmie Fluttera do osadzania widoków Androida i iOS,
a ponieważ ten mechanizm jest obecnie w fazie testowej, wtyczka również w niej jest.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Widoki platformy
PlatformView firmy Flutter to widżet, który osadza natywny widok systemu Android / iOS
i integruje go w drzewie widżetów Fluttera. Widoki platformy to widżety stanowe, które kon-
trolują zasoby skojarzone z widokiem natywnym platformy. Jeśli chodzi o osadzanie, ten rodzaj wi-
doku jest kosztowny, dlatego należy go używać ostrożnie i tylko wtedy, gdy jest to naprawdę
konieczne. Możesz go użyć do wyświetlania map, ponieważ Flutter nie ma równoważnego
widżetu, który sam wyświetla mapę.
296
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Ta funkcja została zaprezentowana podczas wydania Flutter 1.0 i w chwili pisania tego
artykułu wciąż ewoluuje na platformach Android i iOS, więc śledź jej status w repozy-
torium Fluttera: https://github.com/flutter/flutter/labels/a%3A%20platform-views.
Umożliwi to włączenie funkcjonalności dla aplikacji iOS, dzięki czemu będziemy mogli korzystać
z niej w naszej aplikacji.
Aby uprościć sprawę, tworzymy projekt wtyczki; zobacz rozdział 9., aby zapamiętać, jak utworzyć
projekt wtyczki. W tym projekcie definiujemy nowy widok, HandsOnTextView, który jest natywnym
widokiem wyświetlania tekstu (TextView w systemie Android i UITextView w systemie iOS).
Na początku, po utworzeniu projektu wtyczki, definiujemy Dart API. To jest kod, który tworzy
pomost z Dart do kodu natywnego. Tworzymy widżet HandsOnTextView.
Jak możesz zobaczyć, jego metoda budowania składa się z następujących ważnych części:
W zależności od typu platformy, Theme.of(context).platform, tworzymy instancję
widżetu AndroidView lub UiKitView.
297
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
To wszystko dotyczy strony Darta w widoku platformy. Teraz musimy zdefiniować widok na
odpowiednich platformach.
W rozdziale 13. sprawdzimy, jak dodać kod natywny do aplikacji. Możesz tam również
znaleźć przydatne informacje, które pomogą Ci zrozumieć, jak działa widok platformy.
Rejestrujemy fabrykę widoków, identyfikując ją za pomocą typu / klucza, aby podczas tworzenia
widoku platformy silnik Fluttera mógł znaleźć odpowiednią fabrykę i delegować do niej tworzenie
widoku. Nawiasem mówiąc, fabryka widoków jest odpowiedzialna za tworzenie instancji widoków
z określonych typów. Jak widać, zarejestrowaliśmy fabrykę widoków dla typu com.example.
handson/textview. Otrzymujemy wystąpienie platformViewRegistry za pomocą metody
platformViewRegistry() i za jej pośrednictwem dodaliśmy naszą fabrykę do rejestru, więc gdy
ktoś zapyta o zarejestrowany typ, konstrukcja zostanie delegowana do instancji fabryki HandsOnText
ViewFactory.
298
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
init {
textView.text = text
}
Jak widać, musi implementować interfejs frameworka PlatformView. Interfejs wymaga dwóch
metod, getView i dispose:
getView() musi zwracać widok systemu Android, który ma być osadzony
w kontekście Fluttera.
Metoda dispose() jest wywoływana, gdy widok zostanie odłączony od kontekstu
Fluttera. Możemy jej użyć do wyczyszczenia dowolnego zasobu lub odniesienia,
aby zapobiec wyciekom pamięci.
299
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Następnie sprawiamy, że klasa HandsOnTextViewFactory może zwrócić wersję widoku dla sys-
temu iOS:
public class HandsOnTextViewFactory: NSObject, FlutterPlatformViewFactory {
W naszym przypadku do strony natywnej przekazujemy tylko ciąg znaków. Mogliśmy użyć
StringCodec jako kodeka wiadomości, ale ze względu na nasz przykład użyliśmy standardo-
wego kodeka.
Aby znaleźć wszystkie możliwe typy kodeków, sprawdź dokument z kodekami wiado-
mości, https://docs.flutter.io/flutter/services/MessageCodec-class.html.
300
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Widżet platformy wygląda jak każdy inny widżet — zobacz rysunek na następnej stronie.
Opakowanie widoku platformy w SizedBox ogranicza jego wymiary; w przeciwnym razie zająłby
on całą dostępną przestrzeń. Jednak nie jest to obowiązkowe; klasy AndroidView i UiKitView
są odpowiedzialne za udostępnianie widoków platformy w hierarchii widżetów w innych widżetach.
Podobnie jak funkcja widoków platformy, ta wtyczka jest nadal w fazie aktywnej ewolucji,
więc konieczne może być sprawdzenie zmian na stronie wtyczki: https://pub.dev/packages/
google_maps_flutter.
301
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Wtyczka eksponuje widżet GoogleMap i to wszystko. Poza tym widżet udostępnia typowe funkcje
map, które są ważne, aby był w pełni konfigurowalny i interaktywny. Najważniejsze z nich to:
mapType — służy do zmiany stylu wyświetlanych fragmentów mapy, na przykład
MapType.normal wyświetla informacje o ruchu i terenie, a MapType.Satellite
wyświetla zdjęcia lotnicze.
markery — pozwalają na dodawanie znaczników na górze mapy (zobacz sekcję
„Dodawanie znaczników do mapy”).
myLocationEnabled — włącza na mapie warstwę Moja lokalizacja. Umożliwia
wyświetlenie wskaźnika w bieżącej lokalizacji urządzenia, a także przycisku
Moja lokalizacja, aby użytkownik mógł w miarę możliwości przenieść się
do aktualnej znanej lokalizacji.
302
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Włączenie funkcji Moja lokalizacja wymaga od nas również dodania uprawnień doty-
czących lokalizacji do obu natywnych platform naszej aplikacji. Zapoznaj się z sekcją
„Zarządzanie uprawnieniami aplikacji” z poprzedniego rozdziału, aby przypomnieć sobie,
jak to zrobić.
Wtyczka udostępnia również niektóre wywołania zwrotne, abyśmy mogli zareagować na określone
zdarzenia na mapie:
onMapCreated — wywoływane, gdy mapa jest strukturalnie gotowa.
onTap — wywoływane po dotknięciu mapy.
onCameraMoveStarted , onCameraMove i onCameraIdle — wywoływane przy
odpowiednich zdarzeniach kamery.
Pierwszym wymaganym krokiem jest dodanie zależności wtyczki do pliku pubspec.yaml i zainsta-
lowanie go za pomocą polecenia flutter packages get:
dependencies:
...
google_maps_flutter: ^0.5.3
303
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
304
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
305
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dostęp do klucza API można uzyskać później, w eksploratorze interfejsu API w Google
Cloud Console.
Ten klucz służy do inicjowania wtyczki mapy na obu platformach, podobnie jak robiliśmy to
wcześniej w przypadku AdMob i Firebase.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
306
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
[UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR KEY HERE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions:
launchOptions)
}
}
Pamiętaj, że w iOS musimy wyrazić zgodę na wersję testową osadzonych widoków, dodając
określone ustawienie w pliku Info.plist (patrz poprzednia sekcja „Widoki platformy”).
Jedyną obowiązkową właściwością, którą można ustawić w widżecie GoogleMap, jest initial
CameraPosition, która ustawia wizualizację mapy w docelowym położeniu zdefiniowanym
w instancji CameraPosition. Klasa CameraPosition obsługuje również właściwości zoom, tilt
i bearing.
Dzięki tej konfiguracji możemy zobaczyć GoogleMap w akcji — zobacz rysunek na następnej
stronie.
Jak widać ponownie, widżet wypełnia całą dostępną przestrzeń, co jest zachowaniem zdefi-
niowanym przez PlatformView. Ponadto domyślnie włączone są interakcje mapy, takie jak powięk-
szanie i przesuwanie.
307
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Klasa Marker
Marker, jak wspomniano w dokumentacji, po prostu oznacza położenie geograficzne na mapie.
Dodaje na niej informacje kontekstowe, takie jak identyfikacja miejsca, punktu kontrolnego
lub ciekawej lokalizacji.
Znaczniki są zwykle definiowane za pomocą ikony oraz jednej lub wielu akcji w zdarzeniu
event. Do najczęściej używanych podczas dodawania znaczników do mapy należą następujące
właściwości:
position — chociaż nie jest wymagana przez samą wtyczkę, identyfikuje położenie
geograficzne znacznika na mapie — dlatego prawie zawsze jest konieczna.
308
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Uwaga z dokumentacji:
Ikona znacznika jest rysowana w oparciu o ekran urządzenia, a nie powierzchnię mapy,
co oznacza, że niekoniecznie zmieni orientację w wyniku obrócenia, pochylania lub powięk-
szania mapy.
309
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
markers: _markers,
);
}
Jak widzieliście, dodawanie znaczników do widżetu GoogleMap jest tak proste, jak wyświetle-
nie samej mapy, ponieważ jest zgodne z paradygmatem Fluttera polegającym na przebudowie
widżetu z opisem zawartym w jego konstrukcji (czyli markers).
Uwaga dla ciekawskich: znaczniki wyznaczają klika z 17 oszałamiających miejsc wartych od-
wiedzenia na mapie Google’a, można je znaleźć na lifehack.org: https://www.lifehack.org/
articles/lifestyle/17-stunning-places-visit-with-google-maps.html.
310
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Następnie musimy dodać przycisk do layoutu, aby móc dodać znacznik po wstępnej budowie.
Wywołanie zwrotne przycisku onPressed uruchamia _addMarkerOnCameraCenter w następujący
sposób:
void _addMarkerOnCameraCenter() {
setState(() {
_markers.add(Marker(
markerId: MarkerId("${_markers.length + 1}"),
infoWindow: InfoWindow(title: "Added marker"),
icon: BitmapDescriptor.defaultMarker,
position: _cameraCenter,
));
});
}
Jak widać, metoda setState służy do przebudowy widżetu i dodaje Marker do zestawu _markers.
Jedyną nową częścią tutaj jest dodanie position: _cameraCenter do Marker.
Wartość _cameraCenter to właściwość stanu, która śledzi środkowe położenie kamery w widżecie
GoogleMap. Jest pobierana za pomocą wywołania zwrotnego onCameraMove widżetu w następujący
sposób:
GoogleMap(
...
onCameraMove: _cameraMove,
),
W ten sposób za każdym razem, gdy użytkownik naciśnie przycisk, znacznik zostanie dodany
w środkowej lokalizacji na mapie. Chociaż nie jest to przypadek powszechny w świecie rze-
czywistym, stanowi praktyczny punkt wyjścia do interakcji z mapą.
311
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Spójrz na przykład hands_on_maps w serwisie GitHub, aby sprawdzić MapPage jako przy-
kład widżetu pełnostanowego. Zobacz także zmiany w layoucie, wyświetlające przycisk.
GoogleMapController
Kolejny poziom interakcji zapewnia klasa GoogleMapController, która działa w bardzo po-
dobny sposób do znanych kontrolerów, takich jak TextEditingController.
Pobieranie GoogleMapController
W przeciwieństwie do innych kontrolowanych widżetów nie dostarczamy sami kontrolera do
widżetu GoogleMap. Zamiast tego zostanie nam to przekazane za pośrednictwem wcześniejszego
wywołania zwrotnego onMapCreated. Musimy więc tylko zapisać to w następujący sposób:
GoogleMap(
...
onMapCreated: (controller) {
_mapController = controller;
},
),
312
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Jak widać, za pomocą wcześniej pobranej instancji GoogleMapController możemy wysłać animację
kamery w nowe miejsce na mapie.
W tej sekcji będziemy używać interfejsu API, aby uzyskać szczegółowe informacje (czyli na-
zwę) miejsca dodanego przez użytkownika za pomocą naszego wcześniej utworzonego przy-
cisku Place marker.
313
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Sprawdź, czy jesteś we właściwym projekcie, i kliknij przycisk ENABLE. Spowoduje to udo-
stępnienie interfejsu API Google Places za pomocą tego samego klucza API co wcześniej.
Wtyczka udostępnia wywołania jako metody swojej klasy GoogleMapsPlaces. Jedną z oferowa-
nych przez nią metod jest getDetailsByPlaceId, która pobiera szczegóły (details) na temat punktu
końcowego usługi internetowej i opakowuje odpowiedź w klasę PlacesDetailsResponse.
Sprawdź stronę wtyczki, aby dowiedzieć się o wszystkich dostępnych metodach usługi
internetowej: https://pub.dartlang.org/packages/google_maps_webservice.
314
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Następnie możemy zacząć korzystać z wtyczki. Pierwszą rzeczą, jaką musimy zrobić, jest utwo-
rzenie instancji klasy GoogleMapsPlaces, dzięki której będziemy mieli dostęp do dostarczo-
nych metod:
@override
void initState() {
super.initState();
_googleMapsPlaces = GoogleMapsPlaces(
apiKey: ‘API_KEY’,
);
}
Robimy to w metodzie initState, abyśmy mogli z niej skorzystać zaraz po wyświetleniu mapy
użytkownikowi. _googleMapsPlaces to pole stanu widżetu MapPage.
Następnie definiujemy metodę, która zapyta o nazwę miejsca na podstawie pary szerokość
(latitude) / długość (longitude) geograficzna:
Future<PlacesSearchResponse> _queryLatLngNearbyPlaces(LatLng position)
async {
return await _googleMapsPlaces.searchNearbyWithRadius(
Location(position.latitude, position.longitude),
1000,
);
}
setState(() {
_markers.add(Marker(
markerId: MarkerId("${_markers.length + 1}"),
infoWindow: InfoWindow(
title: "Added marker - $firstMatchName"
),
icon: BitmapDescriptor.defaultMarker,
position: _cameraCenter,
));
});
}
315
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, jest kilka modyfikacji w stosunku do poprzedniej wersji. Oto zmiany:
Metoda jest teraz asynchroniczna (async), ponieważ wtyczka zwraca wynik
na przyszłość i nie chcemy na niego czekać.
Otrzymujemy pierwsze dopasowanie zapytania (tylko jego adres), jeśli istnieje.
Dodajemy informacje o nazwie we właściwości tytułu InfoWindow.
Istnieje wiele innych sposobów integracji interfejsu API GooglePlaces z aplikacją: to był po
prostu jeden przykład. Na tym kończymy integrację map w aplikacjach Fluttera. Śledź aktualizacje
wtyczek, ponieważ ta funkcja wciąż ewoluuje wraz z frameworkiem.
Podsumowanie
W tym rozdziale poznaliśmy podstawy korzystania z map we Flutterze za pomocą wtyczki
google_maps_flutter. Widzieliśmy, że jej działanie oparte jest na funkcji widoku platformy, która
umożliwia nam wyświetlanie natywnych widoków w kontekście Fluttera. Widzieliśmy, jak sami
możemy tworzyć te widoki, korzystając ze struktury frameworka.
316
d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy
Widzieliśmy już dostępne właściwości widżetu GoogleMap i to, jak nim manipulować, aby wyświe-
tlać na nim znaczniki i przesuwać kamerę za pomocą klasy GoogleMapController.
Wreszcie, użyliśmy interfejsu API Google Places, aby uzyskać informacje o lokalizacji i wyświetlić
je na znaczniku za pomocą klasy InfoWindow.
317
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
318
d0765ad53fb82babda2278a311da7afb
d
IV
Zaawansowany Flutter
— zasoby dla złożonych
aplikacji
Złożone i unikalne aplikacje obejmują takie funkcje jak pisanie kodu natywnego dla platformy
i dostosowywanie zasobów frameworka zgodnie z ich potrzebami.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
320
d0765ad53fb82babda2278a311da7afb
d
12
Testowanie,
debugowanie
i wdrażanie
Flutter zapewnia doskonałe narzędzia, które pomagają deweloperowi osiągnąć cele na platformie,
od testowego API po narzędzia i wtyczki IDE. W tym rozdziale dowiesz się, jak dodać testy,
aby utworzyć aplikację wolną od błędów, debugować, aby znaleźć i rozwiązać określone pro-
blemy, profilować wydajność aplikacji w celu znalezienia wąskich gardeł i sprawdzać widżety
interfejsu użytkownika. Dowiesz się również, jak przygotować aplikację do wdrożenia w App
Store i Google Play.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dzięki Flutterowi możliwe są dobrze znane testy jednostkowe i integracyjne. Dodatkowo mo-
żemy opracować testy widżetów, aby sprawdzić ich działanie w izolowanym środowisku. Zobaczmy,
jak możemy napisać zarówno testy widżetów, jak i integracyjne, aby upewnić się, że nasze
aplikacje działają poprawnie.
Możesz przejrzeć rozdział 2., ponieważ testy jednostkowe Fluttera to nic innego jak
testy jednostkowe Darta.
Testy widżetów
Testy widżetów służą do walidacji widżetów w środowisku izolowanym. Wyglądają bardzo
podobnie do testów jednostkowych, ale skupiają się właśnie na widżetach.
Ich głównym celem jest sprawdzenie interakcji widżetów i upewnienie się, że widżety wyglądają
zgodnie z oczekiwaniami. Ponieważ widżety znajdują się w drzewie widżetów w kontekście
Fluttera, ich testy wymagają uruchomienia środowiska frameworka. Dlatego Flutter udostęp-
nia narzędzia do pisania takich testów poprzez pakiet flutter_test.
Pakiet flutter_test
Pakiet flutter_test jest dostarczany z Flutter SDK, zbudowany na pakiecie testowym i zawiera
zestaw narzędzi pomagających nam pisać i uruchamiać testy widżetów.
Jak wspomniano wcześniej, testy widżetów muszą być wykonywane w środowisku widżetów,
a Flutter w tym pomaga za pomocą klasy WidgetTester, która zawiera logikę budowania i interakcji
pomiędzy testowanym widżetem a środowiskiem Flutter.
Nie musimy samodzielnie tworzyć instancji tej klasy, ponieważ framework udostępnia funkcję
testWidgets(). Jest ona podobna do funkcji Darta test(), opisanej wcześniej w rozdziale 2.,
sekcji „Wprowadzenie do testów jednostkowych”.
Funkcja testWidgets
Ta funkcja jest punktem wejścia do każdego testu widżetu we Flutterze:
void testWidgets(String description, WidgetTesterCallback callback, { bool
skip: false, Timeout timeout })
322
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Zwróć uwagę, że wersja pakietu nie została określona. Ponadto źródło jest skonfiguro-
wane jako Flutter SDK.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Ten przykładowy test widżetu sprawdza zachowanie znanej nam aplikacji licznika. Test przebiega
następująco:
Test jest definiowany za pomocą opisu i poprzednio poznanej właściwości
WidgetTesterCallback. Zwróć również uwagę, że wywołanie zwrotne ma modyfikator
async, podobnie jak metody WidgetTester, ponieważ zwraca typ Future.
Wszystko zaczyna się od widżetu: await tester.pumpWidget (MyApp ()).
Powoduje to renderowanie interfejsu użytkownika z danego widżetu — w tym
przypadku MyApp.
323
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Podobnie jak w przypadku stałej find, dostępnych jest wiele Matchers; findNothing i findOneWidget
to tylko niektóre z nich.
324
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Flutter zapewnia wiele narzędzi pomocnych w tym zadaniu. Jak widzieliśmy wcześniej w roz-
dziale 1., Dart zawiera zestaw narzędzi pomocnych w pracy programisty.
Nie polecamy konkretnego IDE dla rozwoju Fluttera. Możesz się domyślić, że debugowanie
nie jest bez niego możliwe. Jednak narzędzia Darta również są do tego przygotowane.
Observatory
Debugowanie Flutter jest oparte na narzędziu Dart Observatory, które jest obecne w Dart SDK
i pomaga w profilowaniu i debugowaniu aplikacji Darta, takich jak aplikacje Fluttera.
Gdy aplikacja Fluttera jest uruchamiana w trybie debugowania (pamiętaj o kompilacji JIT z roz-
działu 1.), to narzędzie jest uruchamiane automatycznie, umożliwiając debugowanie i profilo-
wanie w aplikacji. Używając polecenia flutter run, jako część danych wyjściowych po komuni-
kacie Hot Reload, dostaniesz address:port. Ten adres jest adresem interfejsu użytkownika
Observatory; mamy do niego dostęp za pośrednictwem wielu przeglądarek internetowych
i tak to wygląda:
325
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Wyświetlone zostają różne informacje o uruchomionej aplikacji, takie jak wersja Fluttera, uży-
wana pamięć, hierarchia klas i dzienniki. Można również użyć ważnego dodatkowego narzędzia,
do debugowania:
Na tej stronie, jak widać, mamy dostęp do wszystkich funkcji debugowania, takich jak:
Dodawanie i usuwanie punktów przerwania (breakpoints).
Uruchamianie krok po kroku, linia po linii.
Przełączanie i zarządzanie izolatami.
Podczas korzystania z niektórych IDE, takich jak Visual Studio Code lub Android Studio / IntelliJ,
nie będziesz używać bezpośrednio narzędzi, takich jak interfejs użytkownika Observatory.
IDE pod spodem korzysta z Dart Observatory i udostępnia swoje funkcje za pośrednictwem
interfejsu IDE.
326
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
W tym przykładzie punkt przerwania pojawi się tylko wtedy, gdy warunek w parametrze when
ma wartość true, czyli tylko wtedy, gdy argument password ma wartość null. Powiedzmy, że
jest to nieoczekiwana wartość: wstrzymanie wykonywania aplikacji w tym momencie może
pomóc zobaczyć, dlaczego tak się dzieje i jak na to zareagować. Jest to bardzo przydatne do
śledzenia nieoczekiwanych stanów i błędów logicznych.
debugPrint() i print(): print() — to metoda logowania informacji do konsoli
Fluttera. Kiedy używamy polecenia flutter run, wyjście logowania jest
przekierowywane do konsoli i możemy zobaczyć wszystko, co pochodzi z wywołań
print() i debugPrint(). Jedyną różnicą między tymi wywołaniami jest to, że wersja
debugPrint() zapobiega usuwaniu logowania przez jądro Androida (logi Fluttera
opakowują tylko adb logcat).
DevTools
DevTools Darta jest zdefiniowany w dokumentacji w następujący sposób:
Zestaw narzędzi wydajnościowych dla Darta i Fluttera.
To ma być kolejna wersja narzędzi Observatory. Środowiska IDE już integrują ten pakiet
w swoich wewnętrznych elementach i jest on podobny do Observatory, jak widać na rysunku
na następnej stronie.
Jak łatwo zauważyć, pakiet ma kilka narzędzi, które mogą pomóc w analizie wydajności aplikacji
Fluttera, podobnie jak Observatory. Możesz go włączyć / zainstalować, uruchamiając następujące
polecenie w terminalu:
pub global activate devtools
327
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, musimy udostępnić port uruchomionej aplikacji (port Observatory, tak jak poprzednio),
aby DevTool mogło sprawdzić pomiary aplikacji.
Aby uzyskać szczegółowe informacje na temat kroków instalacji dla różnych systemów
operacyjnych i różnych IDE, sprawdź stronę dokumentacji DevTools: https://flutter.github.io/
devtools/.
Należy również pamiętać, że w momencie pisania tej książki pakiet DevTools jest nadal
w wersji wstępnej i może ulec zmianie do czasu, gdy to przeczytasz.
328
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Profiler Observatory
Jak wcześniej widzieliśmy, Observatory udostępnia deweloperowi wiele narzędzi do pomiaru
wydajności aplikacji i zapobiegania ewentualnym problemom z nią związanym. Odbywa się
to za pomocą prezentacji wielu wskaźników, jak widać:
Pamięć, użycie procesora i inne informacje są dostępne za pośrednictwem monitora, dzięki czemu
możemy ocenić różne aspekty aplikacji.
Tryb profilowania
Kiedy uruchamiamy naszą aplikację Fluttera w domyślnym trybie debug za pomocą polecenia
flutter run, nie możemy oczekiwać takiej samej wydajności jak w trybie release. Jak już wiemy,
Flutter uruchamia tryb debugowania za pomocą kompilatora JIT Dart podczas działania apli-
kacji, w przeciwieństwie do trybów release i profilowania, w których kod aplikacji jest wstępnie
kompilowany przy użyciu kompilatora AOT Dart.
329
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Aby dokonać oceny wydajności, musimy upewnić się, że aplikacja działa z maksymalną wydajno-
ścią; dlatego Flutter zapewnia różne metody wykonywania: debugowanie, profilowanie i release.
W trybie profilowania aplikacja jest kompilowana w bardzo podobny sposób do trybu release,
co jest zrozumiałe, ponieważ musimy wiedzieć, jak aplikacja będzie działać w rzeczywistych
warunkach.
Aby uruchomić aplikację w trybie profilowania, powinniśmy dodać flagę --profile do polecenia
uruchomienia (pamiętaj, że jest ona dostępna tylko na prawdziwych urządzeniach):
flutter run --profile
Działając w tym trybie, mamy wszystkie potrzebne informacje do ogólnego sprawdzenia wydaj-
ności aplikacji. Innym użytecznym narzędziem, które umożliwia tryb profilowania, jest nakładka
wydajności (performance overlay).
Nakładka wydajności
Nakładka wydajności to wizualne informacje zwrotne wyświetlane w aplikacji. Oferuje wiele
pomocnych statystyk wydajności. W szczególności wyświetla informacje o czasie renderowania.
Oto przykład wyświetlanej nakładki wydajności — zobacz rysunek na następnej stronie.
Wyświetlane są dwa wykresy przedstawiające czas renderowania ramek przez dwa wątki, inter-
fejsu użytkownika i procesor graficzny. Bieżąca ramka jest wyświetlana na pionowym
zielonym pasku. Dodatkowo możemy zobaczyć ostatnie 300 klatek i oraz krytyczne etapy
renderowania.
Flutter używa wielu wątków do wykonania swojej pracy. Interfejs użytkownika i procesor
graficzny zawierają funkcje wyświetlania frameworka i dlatego oba są wyświetlane w nakładce
wydajności. Wątek interfejsu użytkownika to miejsce, w którym wykonywany jest kod Darta,
gdzie odbywa się budowanie logiki i opisu widżetów, framework tworzy drzewo warstw dla
działania wątku GPU, grafika zostaje ożywiona i działa biblioteka graficzna Skia.
Dodatkowo, oprócz tych wątków, Flutter ma również wątek Platform, w którym działa kod
wtyczki, oraz wątek I/O, w którym uruchamiane są kosztowne zadania I/O. Oba wątki nie poja-
wiają się w nakładce platformy.
330
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Niektóre z możliwych ulepszeń, w których nakładka wydajności może pomóc, możesz spraw-
dzić pod adresem https://flutter.dev/docs/perf/rendering/ui-performance#the-performance-
-overlay.
Oba narzędzia oferują nam metryki, dzięki czemu możemy dokładnie sprawdzić fragmenty
kodu. Ale co z layoutem? Z pewnością możemy mierzyć wydajność klatka po klatce na pod-
stawie czasu renderowania naszego drzewa widżetów, jak widzieliśmy wcześniej na przykła-
dzie nakładki wydajności. Ale co powiesz na sprawdzenie, czy nasze drzewo zajmuje więcej
miejsca, niż potrzeba — czyli ma więcej widżetów, niż to konieczne — lub czy widżet jest
tworzony we właściwym czasie / na odpowiednim poziomie.
331
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
W tym zadaniu może nam pomóc Inspektor Fluttera. Ponownie, dostęp do tej funkcjonalności
możemy uzyskać dzięki świetnym narzędziom dla programistów.
Inspektor widżetów
Inspektor widżetów to kolejny wspaniały zestaw narzędzi, które mogą pomóc programiście
w zadaniach optymalizacyjnych. To narzędzie zapewnia szczegółową wizualizację drzewa
widżetów.
Jak widać, prezentowane jest drzewo widżetów i mamy dostęp do wszystkich szczegółów dotyczą-
cych widżetów. Dla twórców stron internetowych będzie to wyglądać bardzo podobnie do eks-
ploratora elementów w narzędziach dla programistów internetowych, na przykład w Chrome.
Patrząc również na poprzedni zrzut ekranu, dostaliśmy małą wskazówkę, aby włączyć opcję
tracking widget creation. Kiedy pominiemy tę flagę, narzędzie pokaże drzewo głębsze, niż mo-
glibyśmy się spodziewać; dlatego udostępnia widżety pośrednie poza tymi, które definiujemy
w naszej aplikacji.
332
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Dzięki temu mamy drzewo, które bardziej przypomina to zdefiniowane w naszym kodzie, co uła-
twia śledzenie problemów. Dostępne są też szczegóły właściwości widżetów, które również
pomagają w znalezieniu drobnych problemów z layoutem.
Wydanie aplikacji w Google Play Store i App Store wymaga ważnych kont wydawcy. Dlatego
zapoznaj się z dokumentacją obu platform, aby dowiedzieć się, jak publikować w sklepach po utwo-
rzeniu wersji wydania aplikacji.
Google pobiera jednorazową opłatę rejestracyjną w wysokości 25 dolarów, którą należy uiścić przed
przesłaniem aplikacji. Możesz zalogować się na https://play.google.com/apps/publish/signup/.
W sklepie App Store obowiązuje opłata członkowska w wysokości 99 dolarów rocznie. Możesz
znaleźć szczegóły i zalogować się na https://developer.apple.com/support/compare-memberships/.
333
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Aby skompilować aplikację w trybie wydania, wystarczy dodać flagę --release do polecenia
flutter run i podłączyć fizyczne urządzenie. Chociaż możemy to zrobić, zwykle nie używamy
polecenia flutter run z flagą --release. Zamiast tego korzystamy z tej flagi z poleceniem
flutter build, aby mieć wbudowany plik aplikacji w docelowych formatach Androida / iOS dla
dystrybucji.
W chwili pisania tej książki częściowo obsługiwany jest również format pakietu aplikacji
na Androida.
Zacznijmy od przygotowania naszej aplikacji Favors do wydania w Google Play, abyśmy mogli
przejrzeć wszystkie ostatnie kroki do opublikowania aplikacji Fluttera.
AndroidManifest i build.gradle
W systemie Android informacje meta o aplikacji są dostarczane zarówno w plikach Android
Manifest.xml, jak i build.gradle, więc musimy przejrzeć i wprowadzić pewne poprawki w obu.
AndroidManifest — uprawnienia
Jednym z ważnych kroków, które musimy wykonać, jest przejrzenie uprawnień wymaganych
w pliku AndroidManifest.xml. Pytanie tylko o uprawnienia, których potrzebujesz, jest dobrą i za-
lecaną praktyką, ponieważ Twoja aplikacja może zostać przeanalizowana, a publikacja może zostać
odwołana, jeśli poprosisz o więcej uprawnień, niż jest to wymagane.
334
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
...
</manifest>
Oprócz uprawnień istnieje również tag uses-feature (patrz rozdział 10.), który może ograni-
czyć instalację na urządzeniach z dostępną określoną funkcją (nie jest to nasz przypadek), więc
ważne jest, aby to także sprawdzić.
AndroidManifest — metatagi
Kolejnym bardzo ważnym krokiem jest przejrzenie metatagów dodanych do aplikacji pod ką-
tem współpracy z usługami takimi jak AdMob czy Google Maps. W naszej aplikacji Favors
AdMob był jedynym dodanym kluczem, więc możemy sprawdzić jego wartość, aby upewnić się,
że usługa będzie działać również z właściwym kluczem:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">
...
<application>
...
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ADMOB-KEY"/>
</application>
</manifest>
335
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ikona i nazwa są zdefiniowane w tagu manifestu application. Domyślnie ikona odnosi się do
domyślnej ikony Fluttera, jak widać:
<manifest ...>
...
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Hands On: Favors app"
android:icon="@mipmap/ic_launcher">
....
</manifest>
Ikona aplikacji Favors została wygenerowana w narzędziu Android Asset Studio. Pomaga
ono nam w przestrzeganiu wytycznych Androida i generowaniu wielu wariantów ikon:
https://romannurik.github.io/AndroidAssetStudio/index.html.
Sprawdź wytyczne Material Design dotyczące ikon, aby upewnić się, że tworzysz niesamo-
witą ikonę dla swojej aplikacji: https://material.io/design/iconography/.
Po zmianie nazwy i wymianie ikony możemy przejrzeć plik build.gradle, aby wprowadzić osta-
teczne poprawki do wdrożenia.
Pamiętaj, aby wybrać dobrą wartość, ponieważ nie można jej zmienić po przesłaniu aplikacji
do sklepu.
336
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Jak widać, możemy zmienić więcej ustawień, niż tylko applicationId. We Flutterze wersje SDK
są zwykle zmieniane w dwóch przypadkach:
Jeśli wymagania frameworku ulegną zmianie.
Jeśli używamy jakiejś biblioteki, która wymaga wyższej minimalnej wersji SDK.
Z pewnością możemy zmienić tę wartość na naszą, jeśli chcemy, ale pamiętaj, aby przestrze-
gać wymagań frameworku.
337
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Teraz, gdy użyjemy polecenia flutter build apk lub flutter run --release, aplikacja zostanie
podpisana naszym kluczem.
Po wprowadzeniu tych zmian jesteśmy gotowi do tworzenia i dystrybucji naszej aplikacji. Został jesz-
cze tylko ostatni krok: sprawdź wartości versionCode i versionName aplikacji; są wypełniane
automatycznie z pliku pubspec.yaml. Dlatego przejrzenie tego pliku może być również ważne.
Po zbudowaniu pliku .apk za pomocą polecenia flutter build apk możemy go zainstalować na
podłączonym urządzeniu fizycznym za pomocą polecenia flutter install. Również plik, który
ma być opublikowany w Sklepie Play, jest dostępny w: build/app/output/apk/app.apk.
338
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Możesz także popracować nad minifikacją i zaciemnieniem kodu (obfuscation), aby zmniejszyć roz-
miar aplikacji i poprawić ochronę przed inżynierią wsteczną: https://github.com/flutter/flutter/
wiki/Obfuscating-Dart-Code.
Podobnie jak w Android, najpierw musimy przejrzeć pewne informacje o aplikacji w ustawie-
niach projektu Xcode, tak jak zrobiliśmy to w AndroidManifest.xml. Potem będziemy mogli
stworzyć archiwum aplikacji gotowe do publikacji w App Store.
Pamiętaj, że musisz zarejestrować się w programie dla programistów, aby móc publiko-
wać w App Store. (Dotyczy to również rejestracji aplikacji w App Store Connect). Więcej
informacji znajdziesz w oficjalnym przewodniku: https://help.apple.com/app-store-connect/
#/dev2cd126805.
W iOS proces przebiega inaczej. Przesyłanie i publikowanie są zarządzane w Xcode, więc aby prze-
słać aplikację, najpierw tworzymy rekord w App Store Connect, wypełniamy opisy, a następ-
nie w Xcode budujemy i przesyłamy naszą aplikację na iOS. Aby zarejestrować aplikację, wy-
konaj następujące czynności:
1. Każda aplikacja iOS jest powiązana z identyfikatorem pakietu, unikalnym
identyfikatorem zarejestrowanym w Apple. Najpierw tworzymy rekord
w identyfikatorach aplikacji (https://idmsa.apple.com/IDMSWebAuth/
signin?appIdKey=891bd3417a7776362562d2197f89480a8547b108fd934911bcbea
0110d07f757&path=%2Faccount%2Fresources%2F&rv=1), wypełniając
Bundle ID, który jest odpowiednikiem applicationId w Androidzie.
339
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Xcode
W Xcode, aby aplikacja była gotowa do wdrożenia, musimy wprowadzić pewne zmiany. Musimy
zmienić ikonę aplikacji, publiczną nazwę i identyfikator pakietu. Jest to bardzo podobne do
tego, co zrobiliśmy w Androidzie.
Zwróć także uwagę na wartości wersji i kompilacji; są one podobne odpowiednio do nazwy
wersji i kodu wersji w systemie Android. Przy każdym przesyłaniu do App Store, czy to Store,
czy TestFlight, musimy zwiększyć wartość wersji w pliku pubspec.yaml.
W Deployment Target możemy ustawić minimalną wymaganą wersję iOS, domyślnie 8.0 —
minimalną wersję obsługiwaną przez Fluttera.
Xcode — AdMob
W przeciwieństwie do konfiguracji w pliku AndroidManifest.xml, nie musimy aktualizować
naszego identyfikatora AdMob w iOS. W tym przypadku wartość identyfikatora jest pobierana
z wartości przekazanej do inicjalizacji SDK FirebaseAdMob w samym Darcie:
FirebaseAdMob.instance.initialize(
appId: 'YOUR_ADMOB_APP_ID'
);
Po dokonaniu tych ustawienń możemy zbudować wersję aplikacji na iOS, tak jak to zrobiliśmy
dla Androida, za pomocą polecenia flutter build ios. Następnie potrzebujemy ostatniego
kroku w Xcode, aby wydać naszą aplikację:
340
d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie
Podsumowanie
W tym rozdziale zawarliśmy wprowadzenie do testów widżetu Fluttera. Widzieliśmy, jak można
ich używać do testowania poszczególnych widżetów i jak są zbudowane za pomocą klasy
WidgetTester w funkcji testWidgets.
Dowiedzieliśmy się również, jak możemy używać narzędzi Fluttera do szczegółowego bada-
nia wydajności aplikacji oraz dostępnych narzędzi do sprawdzania użycia pamięci i procesora
za pomocą interfejsu użytkownika Observatory i nakładki wydajności. Następnie zapoznaliśmy się
z nowym pakietem DevTools.
Na koniec zbadaliśmy czynności potrzebne do tego, aby przygotować naszą aplikację do wdrożenia,
takie jak sprawdzenie informacji i szczegółów, zmiana ikony aplikacji widocznej dla użytkow-
nika i wykonanie kroków specyficznych dla platformy w celu zbudowania aplikacji gotowej
do publikacji.
W następnym rozdziale omówimy kilka ważnych tematów związanych z natywnym kodem i kana-
łami platformy i sprawdzimy, jak przygotować aplikację do internacjonalizacji.
341
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
342
d0765ad53fb82babda2278a311da7afb
d
13
Poprawa komfortu
użytkowania
Jeśli chcesz, aby Twoja aplikacja osiągnęła wysoki poziom, musisz pozostawić ją otwartą na ciągłą
interakcję z kontekstem użytkownika, nawet jeśli obecnie nie jest uruchomiona. Ponadto two-
rzenie międzynarodowej i w pełni intuicyjnej aplikacji umożliwia jej stopniowy rozwój. W tym roz-
dziale dowiesz się, jak tworzyć procesy wykonywane w tle, jak tłumaczyć aplikację na język
docelowy i dodawać funkcje ułatwień dostępu, które zwiększają jej użyteczność.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Flutter zapewnia różne sposoby zwiększania dostępności aplikacji dzięki komponentom prze-
znaczonym dla użytkowników z pewnymi rodzajami niepełnosprawności.
W systemie Android i iOS możemy włączyć duże czcionki poprzez ustawienia dostępności
w konfiguracjach systemu operacyjnego.
Internacjonalizacja Fluttera
Flutter zapewnia widżety i klasy, które pomagają w internacjonalizacji, a same biblioteki
Fluttera są umiędzynarodowione. Odbywa się to za pomocą trzech pakietów, intl, intl_translation
i flutter_localizations. Sprawdźmy te pakiety i zastanówmy się, jak pomagają w internacjonalizacji.
Pakiet intl
Pakiet Darta, intl, jest podstawą tłumaczeń w Darcie, jak podano na jego stronie w pub:
Ten pakiet zapewnia możliwości internacjonalizacji i lokalizacji, w tym tłumaczenie wiado-
mości, liczbę mnogą i dostosowanie do płci użytkownika, formatowanie i analizowanie
daty / liczb oraz tekst dwukierunkowy.
344
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
W tym pakiecie mamy mechanizmy do ładowania tłumaczeń z plików .arb. Ten format jest
również obsługiwany przez Google Translators Toolkit. Każdy plik .arb zawiera pojedynczą
tabelę JSON, która odwzorowuje identyfikatory zasobów na zlokalizowane wartości.
Pakiet intl_translation
Pakiet intl_translation jest oparty na intl. Jest potrzebny tylko w fazie rozwoju i zawiera
narzędzie do generowania i analizowania tłumaczeń z / do plików .arb. Za pomocą tego pa-
kietu możemy przetłumaczyć nasze wiadomości w formacie .arb, a następnie zaimportować
je do Darta w celu użycia z pakietem intl.
Pakiet flutter_localizations
Pakiet flutter_localizations zapewnia zestaw 52 języków (w momencie pisania tej książki)
do użycia z widżetami Fluttera. Domyślnie widżety Fluttera są dostarczane tylko z angielskimi
lokalizacjami, więc do obsługi innych języków można użyć pakietu flutter_localizations.
Zależności
Pierwszym krokiem jest dodanie zależności lokalizacyjnych do pliku pubspec.yaml i pobranie
ich za pomocą polecenia flutter packages get:
dependencies:
...
flutter_localizations:
sdk: flutter
dev_dependencies:
intl_translation: ^0.17.3
...
Jak wspomniano wcześniej, pierwszą zależnością jest dodatkowy pakiet lokalizacyjny Fluttera
służący do korzystania z jego wbudowanych widżetów, a druga daje nam narzędzia do gene-
rowania kodu Darta z tekstami z plików .arb.
Klasa AppLocalization
Następnym krokiem jest utworzenie klasy, która opakowuje wartości lokalizacyjne aplikacji.
345
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
// część app_localization.dart
import 'l10n/messages_all.dart';
class AppLocalizations {
static Future<AppLocalizations> load(Locale locale) {
final String name =
locale.countryCode == null ? locale.languageCode :
locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((bool _) {
Intl.defaultLocale = localeName;
return new AppLocalizations();
});
}
346
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Oprócz tej klasy musimy utworzyć inną klasę odpowiedzialną za udostępnianie zasobów
AppLocalizations do aplikacji. Oto jak to wygląda:
class AppLocalizationsDelegate extends
LocalizationsDelegate<AppLocalizations> {
const AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return ['en', 'es', 'it'].contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) {
return AppLocalizations.load(locale);
}
@override
bool shouldReload(LocalizationsDelegate<AppLocalizations> old) {
return false;
}
}
Tworzenie każdego z tych plików może być żmudne, więc możemy użyć narzędzia intl_translation
do wygenerowania tych plików. Najpierw tworzymy katalog do przechowywania nowych
plików — w tym przykładzie lib/l10n. Następnie generujemy pliki .arb za pomocą następu-
jącego polecenia:
347
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ostatni parametr odnosi się do pliku zawierającego klasę lokalizacji aplikacji — w na-
szym przypadku lib/app_localization.dart.
To polecenie wygeneruje plik o nazwie intl_messages.arb w lib/i10n, który służy jako szablon
dla naszych tłumaczeń:
{
"@@last_modified": "2019-04-22T21:32:20.153408",
"title": "Hello world App",
"@title": {
"description": "The application title",
"type": "text",
"placeholders": {}
},
"hello": "Hello",
"@hello": {
"type": "text",
"placeholders": {}
}
}
Możemy stworzyć żądane tłumaczenia na podstawie tego pliku, kopiując go, zmieniając jego
nazwę na pliki intl_<kod_języka> i tłumacząc wymagane zasoby:
Sprawdź GitHuba, aby znaleźć kod źródłowy wszystkich plików i pełny przykład.
Teraz mamy wygenerowany kod Darta zawierający przetłumaczone zasoby. Nie będziemy
bezpośrednio dotykać tego kodu, gdy będziemy musieli dodać zasoby; robimy to w plikach
app_localization.dart i .arb.
348
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
W tym kroku nasze zasoby ładowane są do naszej aplikacji. Teraz, aby efektywnie z nich korzystać,
wykorzystujemy metodę of w naszej klasie AppLocalizations:
class MyHomePage extends StatelessWidget {
MyHomePage({Key key}) : super(key: key);
@override
349
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dzięki tej metodzie mamy dostęp do naszej instancji i wszystkich pobranych zasobów, które
zdefiniowaliśmy wcześniej. To wszystko po to, aby aplikacja była zlokalizowana — jak widać,
otrzymujemy różne teksty dla różnych ustawień regionalnych urządzeń:
350
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Wymiana między światem Fluttera a platformą musi być jak najmniej zauważalna, aby nie znie-
chęcała dewelopera do korzystania z frameworka.
Do tej pory używaliśmy niektórych wtyczek do tworzenia funkcji, które zależą od implementacji
platformy. Wtyczki, a nawet sama aplikacja, mogą wymagać jakiejś komunikacji z kodem platformy,
aby całość działała. Wszystkim tym zarządza silnik Fluttera, więc aby komunikować nasz kod
aplikacji Fluttera z natywnym kodem Swift / Objective-C i Kotlin / Java, będziemy korzystać
z kanałów platformy.
W rozdziale 9., w którym zobaczyliśmy, jak opracować własną wtyczkę Flutter, mieliśmy wprowa-
dzenie do kanałów metod. Kanały metod są, z definicji, specjalizacją kanału platformy Fluttera.
Zobaczmy więc szczegółowo, jak to wszystko działa. Kanały metod będą omawiane w kilku na-
stępnych sekcjach.
Kanał platformy
Aplikacje Fluttera są hostowane w typowej aplikacji natywnej, to znaczy, że gdy uruchamiasz
aplikację Fluttera, istnieje natywna aplikacja iOS lub Androida, działająca z delegacjami in-
terfejsu użytkownika do Fluttera. Jak już wiesz, Flutter samodzielnie renderuje cały interfejs
użytkownika, a aby to działało, natywna warstwa Fluttera jest wyposażona w cały kod potrzebny
do skonfigurowania View Androida lub UIViewController iOS, w którym framework może działać.
Niektóre platformy mobilne polegają na generowaniu kodu, aby dokonać konwersji z jakiegoś
ogólnego języka najwyższego poziomu na język natywny, tzn. prawie zawsze piszesz kod tylko
w języku specyficznym dla platformy, który później jest konwertowany na natywny (Kotlin /
Java i Swift / Objective -DO). Utrudnia to platformie utrzymanie aktualnego API w stosunku
351
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
do hostów. Ponieważ Flutter zamierza być obecny na wielu platformach, byłoby mu jeszcze
trudniej to osiągnąć i ewoluować w tym samym czasie.
Aby zaspokoić tę potrzebę, Flutter opiera się na elastycznym stylu przekazywania wiadomo-
ści, zwanym kanałem platformy. Przyjrzyjmy się jego strukturze:
To jest widok architektury kanału platformy Fluttera. Oficjalna strona internetowa to:
https://flutter.dev/docs/development/platform-integration/platform-channels.
Jak widać na tym diagramie, kanały MethodChannels są używane do wysyłania / odbierania komu-
nikatów. Diagram pokazuje, jak ogólnie działają kanały platformy:
Aplikacja Fluttera wysyła wiadomości do hosta / części natywnej (iOS lub
Android) aplikacji przez kanał platformy.
Host / natywna część aplikacji nasłuchuje na kanale platformy, odbiera wiadomość
i przetwarza ją za pomocą własnej implementacji, używając interfejsów API
dostarczonych przez system, a na koniec odsyła wynik do wywołującej części
aplikacji Fluttera.
352
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Kodeki wiadomości
Jak widzieliśmy do tej pory, MethodChannel jest głównym przykładem i najczęściej używanym
kanałem platformy, ponieważ usuwa wiele zawiłości tłumaczenia danych z Darta na natywne
języki programowania i odwrotnie.
Istnieją również inne sposoby komunikacji między językiem natywnym a Flutterem, takie jak
BasicMessageChannel. Więcej informacji znajdziesz w oficjalnym samouczku dotyczącym kana-
łów platformy: https://flutter.dev/docs/development/platform-integration/platform-channels.
353
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Funkcja compute() jest idealna do obliczeń, których wykonanie może zająć więcej niż kilka
milisekund, co może spowodować utratę niektórych ramek. Istnieją również alternatywy dla
obliczeń krótkoterminowych. Przypomnij sobie przypadki użycia Futures z rozdziału 2.
SendPort i ReceivePort
Jak wskazano wcześniej, wiadomość przekazywana do funkcji compute() i zwracana przez nią war-
tość muszą spełniać pewne ograniczenia. Pochodzą one z warstwy komunikacyjnej izolatów.
Izolaty, jak wspomniano wcześniej, komunikują się ze sobą za pośrednictwem wiadomości. Te wia-
domości są wysyłane i odbierane za pośrednictwem instancji SendPort i ReceivePort.
354
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Aby wysłać wiadomość do portu izolatu, musimy najpierw uzyskać odpowiadającą mu instan-
cję ReceivePort. Klasa ReceivePort udostępnia metodę pobierającą sendPort, która jest powiązana
z izolatem, dzięki czemu możemy wysyłać do niego komunikaty. W jaki sposób izolat pobiera
ReceivePort z innego izolatu? Robi to za pośrednictwem klasy IsolateNameServer.
IsolateNameServer
Klasa IsolateNameServer jest globalnym rejestrem izolatów Darta, z którego możemy rejestro-
wać i wyszukiwać SendPorts i ReceivePorts. Mówiąc najprościej, izolat może zarejestrować swój
ReceivePort za pomocą metody IsolateNameServer.registerPortWithName, a inne izolaty mogą
uzyskać odpowiedni SendPort za pomocą metody IsolateNameServer.lookupPortByName().
Przykład compute()
Jak wspomniano wcześniej, aby utworzyć izolat do wykonywania długich procesów, używamy
funkcji compute(). W wywołaniu zwrotnym izolatu możemy mieć dowolną implementację,
która zostanie przekazana do funkcji obliczeniowej. Jedynym wymaganiem jest to, aby była
to funkcja najwyższego poziomu. Spójrz na przykład na następujący kod:
import 'dart:io';
void backgroundCompute(args) {
print('background compute callback');
print('calculating fibonacci from a background process');
int first = 0;
int second = 1;
for (var i = 2; i <= 50; i++) {
var temp = second;
second = first + second;
first = temp;
sleep(Duration(milliseconds: 200));
print("first: $first, second: $second.");
}
Ta metoda oblicza pierwsze 50 liczb Fibonacciego i zapisuje w logach urządzenia. Jak widać,
zawiera wywołanie sleep, które jest blokujące; oznacza to, że żadne operacje asynchroniczne
nie mogą być przetwarzane w izolacie, gdy jest on zablokowany.
Możemy wykonać izolację, aby uruchomić to wywołanie zwrotne w dowolnym miejscu apli-
kacji Fluttera, uruchamiając następujące polecenie:
compute(backgroundCompute, null);
355
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Ważnym aspektem, na który należy jednak zwrócić uwagę, jest to, że nowy izolat jest elemen-
tem podrzędnym głównego izolatu aplikacji Flutter, a więc jeśli aplikacja zostanie zakończona
(to znaczy, gdy użytkownik usunie ją z paska zadań), izolat dziecka również jest zakończony.
W niektórych sytuacjach możemy chcieć wykonać jakiś kod całkowicie niezależny od głównej
aplikacji, jak w poniższych przykładach:
Podczas otrzymywania powiadomień push i aktualizacji informacji. Aplikacja nie musi
być uruchomiona, abyśmy otrzymywali i przetwarzali zdalne powiadomienia push.
Innym przykładem jest nasłuchiwanie zmian lokalizacji użytkownika lub
wchodzenie w strefy geograficzne.
W przypadku pobierania informacji o serwerze.
Wreszcie — podczas przesyłania plików na serwer. W zależności od rozmiaru plików
operacje mogą zająć dużo czasu.
Do momentu napisania tej książki nie ma domyślnego interfejsu API do obsługi tych przypadków
użycia, więc autorzy wtyczek i programiści, którzy potrzebują tego rodzaju funkcji w swoich
aplikacjach, aby utworzyć izolaty pracujące w tle i ustanawiać komunikację między warstwami,
muszą poradzić sobie z niskopoziomowymi założeniami silnika Fluttera.
Aby stworzyć proces w tle, możemy podzielić odpowiedzialność na języki i warstwy aplikacji.
Musimy również sprawdzić, co możemy, a czego nie możemy zrobić z frameworkiem i platformą
bazową. Uporządkujmy to:
1. Najpierw musimy zdefiniować punkt wejścia izolatu pracującego w tle — jest
podobny do funkcji main() naszej aplikacji. Izolat pracujący w tle musi mieć swoją
główną funkcję.
2. Po zdefiniowaniu tego punktu wejścia możemy uruchomić izolat w tle. Z perspektywy
aplikacji wykonujemy następujące czynności:
Wysyłamy żądanie przez wywołanie metody do natywnej strony naszej
aplikacji w celu zainicjowania nowego izolatu.
Po stronie natywnej tworzymy potrzebną strukturę i uruchamiamy nowy izolat
niezależnie od aplikacji, która wysłała żądanie.
356
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Gdy mamy działający izolat, powiadamiamy stronę natywną, aby wiedziała, że może
komunikować się z izolatem.
3. Z aplikacji możemy rozpocząć wysyłanie żądań do strony natywnej, która będzie
przetwarzać rzeczy związane ze strukturą Fluttera i delegować do izolatu w tle.
Ten proces wydaje się o wiele bardziej złożony niż potrzeba i chociaż nie jest prosty, społecz-
ność Fluttera stara się go jak najszybciej ulepszyć, aby uprościć zadanie przetwarzania w tle
w Darcie.
Stwórzmy przykład, używając tego samego algorytmu Fibonacciego co poprzednio. Tym ra-
zem uruchamiamy izolat z aplikacji, tak jak poprzednio, ale jeśli zakończymy aplikację (usu-
wając ją z paska zadań), logi urządzenia nadal będą zapisywane, ponieważ proces wciąż będzie
działał w tle.
Inicjalizacja obliczeń
Z poziomu aplikacji, gdy klikniemy przycisk obliczania, powinna ona zainicjować proces,
który widzieliśmy wcześniej. Pierwszym krokiem jest wywołanie metody za pośrednictwem
kanału metody. Stworzyliśmy przykład w strukturze wtyczki, abyś mógł łatwo go zmienić, a nawet
357
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
class HandsOnBackgroundProcess {
static void calculateInBackgroundProcess() async {
final callbackHandle = PluginUtilities.getCallbackHandle(
backgroundIsolateMain
);
await pluginChannel.invokeMethod(
"initBackgroundProcess",
[callbackHandle.toRawHandle()]
);
}
}
Przyjrzyjmy się najpierw punktowi wejścia izolatu Dart, a następnie sprawdźmy kod potrzebny
do jego prawidłowego działania.
358
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
int first = 0;
int second = 1;
for (var i = 2; i <= 50; i++) {
var temp = second;
second = first + second;
first = temp;
sleep(Duration(milliseconds: 500));
print("first: $first, second: $second.");
}
Po stronie Darta wykonywanie w tle musimy zaimplementować tylko raz. Następnie dla każdej
z platform (Android / iOS) powinniśmy skonfigurować środowisko do działania tego izolatu.
359
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Klasa HandsOnBackgroundProcessPlugin
Pierwszym krokiem jest skonfigurowanie wtyczki, tak jak to zrobiliśmy w rozdziale 9. Rozpo-
czynamy implementacją statycznej metody registerWith, która powiadamia silnik Fluttera
o istnieniu instancji wtyczki:
class HandsOnBackgroundProcessPlugin(
private val context: Context
) : MethodChannel.MethodCallHandler{
companion object {
...
@JvmStatic
fun registerWith(registrar: PluginRegistry.Registrar) {
val channel = MethodChannel(
registrar.messenger(),
"com.example.handson/plugin_channel"
)
val plugin = HandsOnBackgroundProcessPlugin(
registrar.context()
)
channel.setMethodCallHandler(plugin)
}
}
...
}
360
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Wykonanie izolatu pracującego w tle odbywa się w dwóch krokach. Pierwszy to implementacja
executeBackgroundIsolate(), jak poniżej:
...
private fun executeBackgroundIsolate(context: Context, callbackHandle:
Long) {
val preferences = context.getSharedPreferences(
SHARED_PREFERENCES_KEY,
IntentService.MODE_PRIVATE
)
preferences.edit().putLong(ARG_CALLBACK_KEY, callbackHandle).apply()
startBackgroundService(context)
}
...
361
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Klasa BackgroundProcessService
Klasa BackgroundProcessService to usługa systemu Android, która będzie działać podczas wy-
konywania naszego izolatu. Ponieważ działa w tle, aplikacja może zostać zamknięta, a izolat
będzie działał normalnie.
Wykonaniem usługi zarządza system Android; nie mamy nad tym pełnej kontroli, więc musimy re-
agować na zdarzenia dostarczane przez system, aby wykonywać nasz izolat na podstawie stanu
Service.
Wszystko zaczyna się od metody onCreate, kiedy system tworzy naszą metodę Service i możemy
skonfigurować wszystkie zasoby potrzebne do jej działania. Jest to dobre miejsce na uruchomienia
izolatu w tle:
class BackgroundProcessService : Service(), MethodChannel.MethodCallHandler
{
override fun onCreate() {
super.onCreate()
createNotification()
FlutterMain.ensureInitializationComplete(applicationContext, null)
startBackgroundIsolate()
}
...
}
Jak widać, powyższy kod robi więcej niż tylko inicjalizacja naszego izolatu:
1. Najpierw ustawiliśmy powiadomienia za pomocą metody createNotification().
Powiadomienie jest umieszczane na pasku stanu Androida i powoduje, że nasza
usługa działa na pierwszym planie. Zasadniczo usługi (Service) działające w tle są
bardziej narażone na zabicie przez system w przypadku braku zasobów.
Natomiast usługi pierwszego planu mają wyższy priorytet w systemie i w tym
przypadku jest mniej prawdopodobne, że zostaną zakończone.
2. Następnie używamy wywołania FlutterMain.ensureInitializationComplete
(applicationContext, null), które potwierdza, że silnik Fluttera jest
skonfigurowany i możemy korzystać z takich funkcji jak kanały platformy.
3. Na koniec uruchamiamy izolat za pomocą wywołania startBackgroundIsolate().
Wygląda następująco:
private fun startBackgroundIsolate() {
val preferences = applicationContext.getSharedPreferences(
362
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
SHARED_PREFERENCES_KEY,
MODE_PRIVATE
)
val callbackHandle = preferences.getLong(ARG_CALLBACK_KEY, 0L)
if (callbackHandle == 0L) return
val callback =
FlutterCallbackInformation.lookupCallbackInformation(
callbackHandle
) ?: return
sBackgroundFlutterView?.runFromBundle(args)
backgroundChannel = MethodChannel(
sBackgroundFlutterView,
"com.example.handson/background_channel"
)
backgroundChannel?.setMethodCallHandler(this)
sPluginRegistrantCallback?.registerWith(
sBackgroundFlutterView?.pluginRegistry
)
}
Ta metoda inicjuje i rejestruje nową instancję wtyczki pracującą w tle — w silniku Fluttera,
tak jak w normalnych aplikacjach. Proces jest nieco trudniejszy, więc zobaczmy, jak to zrobić:
1. Najpierw otrzymujemy wywołanie zwrotne Darta, które jest punktem wejścia
nowego izolatu pracującego w tle. Aby to osiągnąć, pobieramy uchwyt
z przechowywanych współdzielonych preferencji i korzystamy z metody
FlutterCallbackInformation.lookupCallbackInformation w celu pobrania
informacji zwrotnych potrzebnych do jej uruchomienia.
2. Następnie tworzymy nową instancję metody FlutterNativeView. Ten widok służy
do zapewnienia odpowiedniego środowiska do działania nowego izolatu.
W Androidzie tak działa silnik Fluttera. Pamiętaj, że widok (View) jest przekazywany
do naszej strony Darta, aby aplikacja działała na nim. Zwróć uwagę na drugi
parametr przekazany do konstruktora FlutterNativeView, true — oznacza on,
że widok będzie działał w tle i nie będzie potrzebował powierzchni do rysowania.
3. Aby ostatecznie wykonać izolat, używamy metody runFromBundle() z instancji
FlutterNativeView, którą widzieliśmy wcześniej. Ta metoda wymaga instancji
FlutterRunArguments, aby zidentyfikować, co będzie działać. Nasza zmienna args
przechowuje informacje, które otrzymaliśmy z wywołania zwrotnego, takie jak
callbackName i callbackLibraryPath, aby znaleźć nasz punkt wejścia dla izolatu.
363
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Aby dowiedzieć się więcej o wątkach w systemie Android, zapoznaj się z dokumentacją:
https://flutter.dev/docs/get-started/flutter-for/android-devs#how-do-you-move-work-
to-a-background-thread.
Właściwość PluginRegistrantCallback
W przykładowym projekcie przekazujemy instancję PluginRegistrantCallback do klasy Service.
Tworzymy potomka klasy FlutterApplication, który będzie podawał nasze wywołanie zwrotne do
usługi:
class Application: FlutterApplication(),
PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
Log.w("BACKGROUND", "application")
BackgroundProcessService.setPluginRegistrant(this)
}
Jak widać, przekazujemy instancję aplikacji do instancji Service, aby mogła zarejestrować się
w silniku Fluttera. Aby to zadziałało, musimy również ustawić naszą klasę aplikacji w Android
Manifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hands_on_background_process_example">
<application
android:name=".Application"
android:label="hands_on_background_process_example"
>
...
</manifest>
364
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
Po skonfigurowaniu wtyczki i izolatu tła musimy się z nim komunikować, aby rozpocząć obli-
czenia. Wszystko, co trzeba zrobić, to obsłużyć wywołania metod z kanału metody (z tła), który
zdefiniowaliśmy:
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result?)
{
if (call.method == "backgroundIsolateInitialized") {
backgroundChannel?.invokeMethod("calculate", null)
} else if (call.method == "calculationFinished") {
sBackgroundFlutterView?.destroy()
sBackgroundFlutterView = null
shutdownService()
} else {} // metoda „calculate” z tego kanału, obsługiwana przez izolat Darta.
}
To cała implementacja dla Androida; dzięki temu, nawet jeśli zakończymy naszą aplikację, usuwa-
jąc ją z paska zadań, izolacja będzie działać w tle.
365
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Duża część pracy jest podobna do działaniu na Androidzie, z wyjątkiem części Service. Zacznijmy
więc od definicji wtyczki.
Klasa SwiftHandsOnBackgroundProcessPlugin
Rejestracja i konfiguracja wtyczki odbywa się w podobny sposób jak w przypadku klasy HandsOn
BackgroundProcessPlugin. Tym razem w statycznej funkcji register() mamy:
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.example.handson/plugin_channel",
binaryMessenger: registrar.messenger()
)
let instance = SwiftHandsOnBackgroundProcessPlugin(
registrar: registrar
)
registrar.addMethodCallDelegate(instance, channel: channel)
}
Podobnie jak w wersji na Androida, skonfigurowany zostaje kanał metody o nazwie com.example.
handson/plugin_channel , który jest używany do inicjalizacji obliczeń za pomocą metody
initBackgroundProcess , co widać poniżej:
public func handle(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
) {
if (call.method == "initBackgroundProcess") {
guard let args = call.arguments as? NSArray else {
return
}
guard let handle = args[0] as? Int64 else {
return
}
executeBackgroundIsolate(handle: handle)
}
}
W takim przypadku, ponieważ nie mamy separacji jako usługi, uruchamiamy izolat w tle bezpo-
średnio za pomocą wywołania.
366
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
allowHeadlessExecution: true
)
guard let info = FlutterCallbackCache.lookupCallbackInformation(
handle
) else {
return
}
let entrypoint = info.callbackName
let uri = info.callbackLibraryPath
_backgroundRunner!.run(
withEntrypoint: entrypoint,
libraryURI: uri
)
_backgroundChannel = FlutterMethodChannel(
name: "com.example.handson/background_channel",
binaryMessenger: _backgroundRunner!
)
_registrar.addMethodCallDelegate(
self,
channel: _backgroundChannel!
)
SwiftHandsOnBackgroundProcessPlugin._registerPlugins?(
_backgroundRunner!
)
}
Ostatni krok nie jest naprawdę potrzebny w iOS. Nie ma innego wątku działającego
w tle obok naszej aplikacji. Zostaje przeniesiony do stanu tła, ale wtyczka nadal jest
normalnie rejestrowana. Gdyby nasza aplikacja została wykonana w jakimś kluczu
UIBackgroundMode, jak wspomniano wcześniej, ta rejestracja byłaby nadal ważna.
367
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
if (call.method == "initBackgroundProcess") {
// ... jak wcześniej
} else if (call.method == "backgroundIsolateInitialized") {
self.taskID = UIApplication.shared.beginBackgroundTask {
self.taskID = .invalid
}
_backgroundChannel?.invokeMethod("calculate", arguments: nil)
} else if (call.method == "calculationFinished") {
if(self.taskID != nil && self.taskID != .invalid) {
UIApplication.shared.endBackgroundTask(self.taskID!)
self.taskID = .invalid
}
// koniec zadania w tle
}
}
Ważne jest, abyś zrozumiał, dlaczego i kiedy można tego użyć: https://developer.apple.com/
documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_
background/extending_your_app_s_background_execution_time.
Podobnie jak w systemie Android, rejestrowana jest również wtyczka działająca w tle. Robimy
to za pomocą wywołania zwrotnego _registerPlugins. Jest ono przekazywane do wtyczki za
pośrednictwem funkcji statycznej setPluginRegistrantCallback(), która jest wywoływana
w klasie AppDelegate aplikacji, bardzo podobnej do Androida:
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
368
d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania
GeneratedPluginRegistrant.register(with: self)
SwiftHandsOnBackgroundProcessPlugin.setPluginRegistrantCallback(
registerPlugins: registerPlugins
)
return super.application(
application,
didFinishLaunchingWithOptions: launchOptions
)
}
}
Implementacja trochę różni się od wersji na Androida, funkcja registerPlugins jest funkcją
najwyższego poziomu, jak poniżej:
func registerPlugins(registry: FlutterPluginRegistry) {
GeneratedPluginRegistrant.register(with: registry)
}
Jak widać, jest podobna do funkcji zdefiniowanej w aplikacji na Androida, która służy do rejestracji
wtyczek za pomocą narzędzia GeneratedPluginRegistrant.register.
Nasza aplikacja zachowuje się podobnie do Androida. Odczytaliśmy wszystkie nasze logi, nawet
te z tła:
369
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Podsumowanie
W tym rozdziale omówiliśmy zaawansowane metody, dzięki którym nasza aplikacja jest bardziej
przyjazna dla użytkownika oraz interaktywna. Zaczęliśmy od poznania dostępnych narzędzi skon-
centrowanych na ułatwieniach dostępu dla użytkowników, zapewnianych przez framework
Flutter.
Na koniec przyjrzeliśmy się opcjom przetwarzania w tle za pomocą Fluttera, przechodząc od bardzo
użytecznej funkcji compute() do usługi w tle w systemie Android i trybach pracy w tle w syste-
mie iOS. Widzieliśmy również cechy i ograniczenia każdej platformy w tym aspekcie.
W następnym rozdziale przyjrzymy się manipulacjom graficznym widżetów oraz sposobom prze-
kształcania widżetów i rysowania niestandardowych kształtów za pomocą obiektów Canvas.
370
d0765ad53fb82babda2278a311da7afb
d
14
Operacje graficzne
na widżetach
Domyślne widżety wystarczają do stworzenia ładnie wyglądającej aplikacji Fluttera. Jednak roz-
szerzenie możliwości widżetów o transformacje layoutów, takie jak przezroczystość, obrót i deko-
racje, może jeszcze bardziej poprawić UX. W tym rozdziale dowiesz się, jak dodać te transfor-
macje do widżetu. Dowiesz się również, jak zmodyfikować widżet, dodając do niego transformacje
graficzne za pomocą klasy Transform, i jak użyć kanwy (canvas) do narysowania niestandardo-
wego widżetu.
Transformacje widżetów
za pomocą klasy Transform
Czasami musimy zmienić wygląd widżetu. Abyśmy mogli odpowiedzieć na dane wejściowe
użytkownika lub wykonać ciekawe efekty w layoucie, potrzebne może być przesunięcie widżetu
na ekranie, zmiana jego rozmiaru, a nawet nieznaczne zniekształcenie.
Jeśli kiedykolwiek próbowałeś to zrobić w rodzimych językach programowania, być może napo-
tkałeś pewne trudności. Flutter, jak pamiętasz, bardzo koncentruje się na projektowaniu interfejsu
użytkownika i proponuje ułatwienie życia programistom.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Widżet Transform
Widżet Transform jest jednym z najlepszych przykładów mocy i spójności frameworka Flutter.
Jest to widżet jednofunkcyjny, który po prostu stosuje transformację graficzną do swojego po-
tomka i nic więcej nie robi. Posiadanie widżetów skupionych na jednym celu ma fundamen-
talne znaczenie dla zachowania lepszej struktury layoutu, a Flutter robi to bardzo dobrze.
Widżet Transform, jak sugeruje jego nazwa, wykonuje jedno zadanie: przekształca swojego
potomka. Chociaż jego zadanie jest bardzo złożone, programista dostaje jego uproszczoną
wersję. Przyjrzyjmy się jego konstruktorowi:
const Transform({
Key key,
@required Matrix4 transform,
Offset origin,
AlignmentGeometry alignment,
bool transformHitTests: true,
Widget child
})
Jak widać, poza typową właściwością key widżet nie potrzebuje wielu argumentów, aby wykonać
swoje zadanie. Oto one:
transform — to jedyna obowiązkowa właściwość (adnotacja @required) używana
do opisu transformacji, która zostanie zastosowana do widżetu podrzędnego.
Obiekt Matrix4 to czterowymiarowa (4D) macierz opisująca transformację
w sposób matematyczny. Więcej szczegółów podamy później.
origin — jest to początek układu współrzędnych, w którym należy zastosować
macierz transform. Jest on określany przez typ Offset, reprezentujący w tym
przypadku punkt (x, y) w układzie kartezjańskim, względem lewego górnego rogu
renderowanego widżetu.
alignment — podobnie jak origin, można go użyć do manipulowania pozycją
zastosowanej macierzy transform. Dzięki niemu możemy określić w bardziej
elastyczny sposób origin, który wymaga od nas użycia rzeczywistych wartości
pozycji. Nic nie stoi na przeszkodzie, aby w tym samym czasie używać zarówno
origin, jak i alignment.
transformHitTests — określa, czy testy trafień (czyli dotknięcia) są obliczane
w przekształconej wersji widżetu.
child — to jest potomek widżetu, do którego zostanie zastosowana transformacja.
372
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Aby poznać wszystkie dostępne możliwości, jakie oferuje ta klasa, sprawdź oficjalną
dokumentację Matrix4: https://api.flutter.dev/flutter/vector_math/Matrix4-class.html.
Pamiętaj, że są to podstawy przekształceń zastosowanych za pomocą widżetu Transform.
Rodzaje transformacji
Chociaż widżety Matrix4 i Transform wydają się proste, klasa Transform zapewnia dewelope-
rowi jeszcze więcej udogodnień dzięki konstruktorom fabrycznym. Jest ich wiele dla każdej
z możliwych transformacji, dzięki czemu niezwykle łatwo jest zastosować transformację do wi-
dżetu bez głębszej znajomości obliczeń geometrycznych. Są one następujące:
Transform.rotate() — konstruuje widżet Transform, który obraca swoje dziecko
wokół jego środka.
Transform.scale() — konstruuje widżet Transform, który skaluje swoje dziecko
w jednolity sposób.
Transform.translate() — konstruuje widżet Transform, który transluje swoje
dziecko przez x, z przesunięciem.
Obrót
Transformacja rotacji pojawia się w sytuacjach, w których chcemy po prostu obrócić nasz widżet.
Używając konstruktora Transform.rotate(), możemy uzyskać takie efekty — zobacz rysunek
na następnej stronie.
373
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jak widać, nie różni się to zbytnio od domyślnego konstruktora Transform. Różnice są następujące:
Brak właściwości transform — używamy wariantu rotate(), ponieważ chcemy
zastosować rotację, więc nie musimy określać całej macierzy. Zamiast tego po
prostu używamy właściwości angle.
Angle — określa żądany obrót w radianach, zgodnie z ruchem wskazówek zegara.
Origin — domyślnie obrót jest stosowany względem środka elementu potomka.
Możemy jednak użyć właściwości origin, aby manipulować początkiem obrotu,
tak jakbyśmy translowali środek widżetu o przesunięcie origin, powodując,
że obrót będzie względem innego punktu, jeśli chcemy.
Skalowanie
Skalowanie pojawia się w sytuacjach, w których chcemy po prostu spowodować zmianę rozmiaru
naszego widżetu, zwiększając lub zmniejszając jego skalę. Możemy otrzymać coś takiego —
zobacz rysunek na następnej stronie.
Ten rodzaj transformacji jest zwykle wykonywany przy użyciu konstruktora Transform.scale().
Zobaczmy, jak to wygląda:
Transform.scale({
Key key,
@required double scale,
Offset origin,
AlignmentGeometry alignment: Alignment.center,
bool transformHitTests: true,
Widget child
})
374
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Jak widać, podobnie jak w przypadku konstruktora fabrycznego rotate(), wariant ten nie różni
się zbytnio od domyślnego:
Brak właściwości transform — tutaj ponownie używamy właściwości scale zamiast
całej macierzy transformacji.
Scale — to jest to, czego używamy do określenia żądanej skali w formacie double,
gdzie 1.0 to oryginalny rozmiar widżetu. Reprezentuje wartość skalarną, która ma
zostać zastosowana do każdej osi x i y.
Alignment — domyślnie skala jest stosowana względem środka potomka. Tutaj
możemy użyć właściwości alignment, aby zmienić początkową wartość skali.
Możemy połączyć właściwości alignment oraz origin, aby uzyskać pożądany rezultat.
Translacja
Translacja najprawdopodobniej pojawi się w animacjach (zobacz rozdział 15.). Za pomocą kon-
struktora Transform.translate() przesuwamy widżet po ekranie — zobacz rysunek na następnej
stronie.
375
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Transformacje złożone
Możemy i najprawdopodobniej połączymy szereg wcześniej zaobserwowanych transformacji,
aby uzyskać unikalne efekty, takie jak obracanie w tym samym czasie, gdy przesuwamy i skalujemy
widżet, jak w przykładzie przedstawionym na następnej stronie.
376
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Obracanie widżetów
Jak wspomniano wcześniej, możemy użyć konstruktora Transform.rotate() w celu dodania
widżetu Transform do drzewa widżetów odpowiedzialnego za obracanie jego elementu podrzęd-
nego. Możemy użyć czegoś takiego:
Transform.rotate(
angle: -45 * (math.pi / 180.0),
child: RaisedButton(
child: Text("Rotated button"),
onPressed: () {},
),
);
377
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Dodajemy widżet, który jest obrócony o 315º zgodnie z ruchem wskazówek zegara (lub o -45º
przeciwnie do ruchu wskazówek zegara). Dokładnie taki sam wynik można osiągnąć za pomocą
domyślnego konstruktora widżetu Transform oraz transformacji Matrix4:
Transform(
transform: Matrix4.rotationZ(-45 * (math.pi / 180.0)),
alignment: Alignment.center,
child: RaisedButton(
child: Text("Rotated button"),
onPressed: () {},
),
);
Argumenty, które musimy podać, aby uzyskać ten sam wynik, są następujące:
transform z obrotem wokół osi z.
alignment transformacji.
Skalowanie widżetów
Aby skalować widżety, używamy typowego konstruktora Transform.scale(). W celu skalowania
widżetu możemy go użyć w następujący sposób:
Transform.scale(
scale: 2.0,
child: RaisedButton(
child: Text("scaled up"),
onPressed: () {},
),
);
Aby uzyskać ten sam wynik przy użyciu domyślnego konstruktora Transform, używamy:
Transform(
transform: Matrix4.identity()..scale(2.0, 2.0),
alignment: Alignment.center,
child: RaisedButton(
child: Text("scaled up"),
onPressed: () {},
),
);
Podobnie jak w przypadku rotacji, musimy określić zarówno początek transformacji z właści-
wością alignment, jak i instancję Matrix4 opisującą transformację skali.
Translowanie widżetów
W bardzo podobny sposób używamy konstruktora Transform.translate(), dodając widżet
Transform jako rodzica widżetu, po którym chcemy się poruszać:
378
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Transform.translate(
offset: Offset(100, 300),
child: RaisedButton(
child: Text("translated to bottom"),
onPressed: () {},
),
);
Jak widać, dodajemy widżet Transform jako dziecko do innego widżetu Transform, tworząc
transformację. Chociaż ta metoda jest prostsza do odczytania, ma wadę: do drzewa widżetów
dodajemy więcej widżetów, niż potrzeba.
379
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Tak jak poprzednio, określamy parametr alignment transformacji jako środek widżetu podrzęd-
nego, a następnie instancję Matrix4, aby ją opisać. Jak widać, jest to wersja bardzo podobna
do tej wykorzystującej wiele widżetów Transform, ale bez zagnieżdżonych widżetów pogłę-
biających drzewo widżetów.
Prostota, jaką Flutter wnosi do kompozycji interfejsu użytkownika, nie kończy się jednak na
widżetach. Co powiesz na zmianę wyglądu widżetu? Nie mówię o rozszerzaniu za pomocą
widżetu Transform poprzez jego translację lub obracanie. Możemy stworzyć widżet z własnym,
niepowtarzalnym wyglądem, własnym kształtem i własnymi zachowaniami. Jest to możliwe dzięki
pomocy trzech głównych klas: CustomPaint, CustomPainter i Canvas.
Klasa Canvas
Jeśli kiedykolwiek programowałeś jakiś interfejs użytkownika w jakimkolwiek języku, być może
słyszałeś lub pracowałeś z jakimś rodzajem Canvas. Jak sama nazwa wskazuje, zapewnia ona
różne sposoby malowania. Canvas można postrzegać jako przestrzeń, nad którą pracujemy,
rysując kształty za pomocą naszych zdefiniowanych stylów, takich jak linie, koła i prostokąty.
Canvas Fluttera nie działa jako dosłowne płótno. Zasadniczo jest to tylko interfejs do zapisywania
operacji graficznych, które mają być narysowane w następnej klatce renderowania.
380
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Transformacje Canvas
Wszystkie operacje, które wykonujemy na Canvas, takie jak rysowanie linii lub prostokąta, są
zorientowane w układzie współrzędnych, tak jak każdy inny system rysowania interfejsu użyt-
kownika. Ten układ współrzędnych ma początek. Domyślnie jest to definiowane przez widżet
CustomPaint, do którego należy Canvas. Ważne jest, aby pamiętać, że z powodu tej cechy na
wszystkie operacje, które wykonujemy na Canvas, wpływa jego obecna transformacja. Kiedy
tylko chcemy, możemy przekształcić płótno, aby wpłynąć na kolejne operacje.
Początkowo Canvas nie ma transformacji, to znaczy jego macierz transformacji jest instancją
Matrix4.
ClipRect
Podobnie jak transformacje, Canvas ma bieżący region przycinania, co oznacza, że możemy
przyciąć część płótna do narysowania. Jest to przydatne, gdy chcemy tylko narysować część
złożonego kształtu, nie przejmując się zbytnio obliczeniami.
Metody
Jak wspomniano wcześniej, Canvas działa poprzez zapisywanie operacji rysowania do następnej
klatki. Aby móc to zrobić, mamy udostępnionych wiele metod, które pozwalają nam rysować
różne kształty. Przyjrzyjmy się najczęściej stosowanym:
drawArc() — służy do rysowania zamkniętych łuków lub segmentów okręgu.
drawCircle() — służy do rysowania okręgów o określonym promieniu.
drawImage() — służy do rysowania obrazu na płótnie.
drawLine() — służy do rysowania linii na płótnie.
drawRect() — służy do rysowania prostokątów na płótnie.
rotate() — dodaje transformację rotacji do bieżącej transformacji Canvas.
scale() — dodaje skalowanie do bieżącej transformacji Canvas.
translate() — dodaje tłumaczenie do bieżącej transformacji Canvas.
Aby poznać więcej metod i szczegółów, zapoznaj się z dokumentacją klasy Canvas:
https://docs.flutter.io/flutter/dart-ui/Canvas-class.html.
381
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Obiekt Paint
Obiekt Paint to opis stylu używanego podczas rysowania na Canvas. Pozwala nam definiować
takie szczegóły, jak kolory i szerokość obrysu. Wszystkie metody rysowania na płótnie pobie-
rają obiekt Paint jako parametr. Możemy ponownie użyć tej samej instancji Paint w wielu
wywołaniach rysowania.
Widżet CustomPaint
Obiekt Canvas nie jest dostępny nigdzie we Flutterze; może to spowodować zamieszanie.
Zawsze, gdy chcemy narysować coś ręcznie, musimy użyć widżetu CustomPaint. Głównym
celem tego widżetu jest dostarczenie nam obiektu Canvas, nad którym możemy pracować.
Posiadanie Canvas i widżetu CustomPaint nie wystarczy do rysowania. Celem CustomPaint jest
dostarczenie Canvas i delegowanie obiektu CustomPainter, który będzie odpowiedzialny za
rysowanie na nim.
Aby utworzyć widżet CustomPaint, najpierw dodajemy go do naszego drzewa widżetów, tak jak
robimy to w przypadku innych widżetów. Przyjrzyjmy się najpierw jego konstruktorowi, aby
to zrozumieć:
const CustomPaint({
Key key,
CustomPainter painter,
CustomPainter foregroundPainter,
Size size: Size.zero,
bool isComplex: false,
bool willChange: false,
Widget child
})
Jest kilka właściwości, którym musimy się przyjrzeć, aby zrozumieć, jak to działa:
painter — malarz, który rysuje treść na płótnie.
foregroundPainter — implementacja malarza, która rysuje zawartość na płótnie
po namalowaniu dziecka.
size — jeśli właściwość child nie jest pusta, używany jest rozmiar dziecka i ta wartość
jest ignorowana; w przeciwnym razie określa rozmiar potrzebny do rysowania.
isComplex i willChange — wskazówki dotyczące pamięci podręcznej rastra,
pomagające w analizie kosztów renderowania.
child — dziecko, które ma być poniżej w drzewie widżetów, jak w przypadku
każdego innego widżetu.
382
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Obiekt CustomPainter
Wiemy, jak ważny jest obiekt CustomPainter (lub painter). Jak wspomniano wcześniej, malarz
jest odpowiedzialny za narysowanie czegoś na Canvas. Zawsze, gdy chcemy stworzyć własną,
unikalną logikę rysowania, musimy rozszerzyć klasę CustomPainter i zastąpić dwie podstawowe me-
tody: paint() i shouldRepaint().
Metoda paint
Metoda paint() to miejsce, w którym CustomPainter wykonuje swoje zadanie. Jest wywoły-
wana za każdym razem, gdy widżet jest proszony o ponowne narysowanie. Oto jak wygląda:
void paint (
Canvas canvas,
Size size
)
Operacje malowania powinny pozostać wewnątrz zadanego obszaru. Oto co mówi dokumentacja:
Operacje graficzne poza granicami mogą być dyskretnie ignorowane, przycinane lub nie.
383
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Metoda shouldRepaint
To ważna metoda, szczególnie w przypadku silnika Fluttera. Oto jak wygląda:
bool shouldRepaint (
covariant CustomPainter oldDelegate
)
Praktyczny przykład
Czas zobaczyć, jak możemy użyć widżetów Canvas i CustomPaint do stworzenia widżetu z wła-
snym obrazem. W tym przykładzie utworzymy widżety wykresów — a dokładniej wykres kołowy
i radialny. Wykresy kołowe to przydatny rodzaj grafiki statystycznej, która jest podzielona na
wycinki w celu zilustrowania proporcji liczbowych.
Definiowanie widżetu
Na początek zazwyczaj definiujemy widżet, aby utrzymać minimalny poziom organizacji. Definiu-
jemy widżet PieChart; będzie to element potomny StatelessWidget. Ten widżet powinien opisy-
wać warstwę malowania i udostępniać to, czego potrzebują inne widżety. Tak wyglądają wła-
ściwości PieChart w naszym przypadku:
class PieChart extends StatelessWidget {
final List<int> values;
final List<Color> colors;
...
}
384
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
385
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
To wszystko, co jest potrzebne do widżetu, ponieważ ciężka praca zostanie wykonana przez
klasę PieChartPainter.
Definiowanie CustomPainter
Zdefiniowanie naszej klasy potomnej CustomPainter jest tutaj najważniejszym krokiem. Jak
wspomniano wcześniej, w tym przykładzie zdefiniowaliśmy malarza, który pobiera listę wartości
int i na tej podstawie rysuje wykres przypominający okrąg z proporcjonalnymi plasterkami.
Jak wspomniano wcześniej, aby to zadziałało, musimy nadpisać dwie metody z CustomPainter.
@override
bool shouldRepaint(PieChartPainter oldDelegate) {
return !ListEquality().equals(oldDelegate.values, values) ||
!ListEquality().equals(oldDelegate.colors, colors);
}
@override
void paint(Canvas canvas, Size size) {
var center = Offset(size.width / 2, size.height / 2);
var radius = (size.width * 0.75) / 2;
386
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Przeanalizujmy to:
1. Najpierw musimy zdefiniować wymiary wykresu. Za pomocą podanego
parametru size możemy ustawić jego środek i promień, który stanowi połowę
75 procent dostępnej przestrzeni (var radius = (size.width * 0,75) / 2;),
dzięki temu zachowamy trochę miejsca wokół wykresu.
2. Następnie tworzymy instancję Rect z podanych właściwości center i radius. Ten
prostokąt będzie przydatny, gdy narysujemy łuki każdego wycinka (zobacz później
wyjaśnienie metody _paintCircle).
3. Wartość total, którą otrzymujemy, sumując wszystkie wartości danego wycinka.
Będzie to również przydatne, gdy narysujemy każdy z łuków wycinka.
4. Na koniec możemy narysować wykres kołowy na płótnie.
startAngle += sweepAngle;
}
}
Sekcje wykresu są rysowane sekwencyjnie. Musimy wiedzieć, od jakiej wartości kąta rozpo-
cząć wycinanie i jaka ma być nowa wartość kąta — dochodząc do pełnego obrotu 360 °. Metoda
Canvas — drawArc polega na określeniu kąta początkowego łuku i odpowiadającego mu kąta
rozwarcia. Możemy uzyskać każdy z kątów wycinka, stosując prostą regułę trzech obliczeń na
podstawie poprzednio obliczonej wartości total.
Reguła trzech jest regułą matematyczną, która pozwala nam rozwiązywać problemy
z proporcjami bezpośrednimi i odwrotnymi.
Mając to na uwadze, zobaczmy, jak narysujemy każdy z łuków wycinka i utworzymy cały wykres:
387
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Zobaczmy, jak możemy użyć metody drawArc z klasy Canvas, aby narysować nasze wycinki.
388
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Jak widać, musimy przekonwertować wartości kąta na radiany przed wysłaniem ich do funkcji
drawArc.
Wykres radialny jest bardzo podobny do wykresu kołowego; jedyną różnicą jest to, że w środku
znajduje się etykieta pokazująca sumę wartości.
Definiowanie widżetu
Widżet RadialChart jest bardzo podobny do widżetu PieChart zdefiniowanego wcześniej, z tymi
samymi parametrami i tym samym podstawowym celem. Jedyne, czego potrzebujemy, to jego
metoda build():
389
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: CustomPaint(
painter: RadialChartPainter(
values,
colors,
Theme.of(context).textTheme.display1,
Directionality.of(context),
),
),
),
],
);
}
Jak widać, różnica polega na wartości przekazywanej do właściwości painter widżetu Custom-
Paint. Tutaj używamy nowej klasy RadialChartPainter, która ma własną implementację pa-
int(). Oprócz wartości i kolorów przekazujemy do niego dwa dodatkowe parametry:
TextStyle, który zostanie użyty do narysowania etykiety wartości całkowitej.
Instancja TextDirection potrzebna do rysowania tekstów we właściwej orientacji.
Definiowanie CustomPainter
Klasa RadialChartPainter, podobnie jak widżet RadialChart, różni się w bardzo specyficznych
częściach od PieChartPainter, który został wcześniej zdefiniowany. Na pierwszy rzut oka me-
toda paint() jest prawie taka sama jak w przypadku wykresu kołowego:
// część klasy radial_chart.dart RadialChartPainter
@override
void paint(Canvas canvas, Size size) {
var center = Offset(size.width / 2, size.height / 2);
var radius = size.width * 0.75 / 2;
Jak widać, jedyną różnicą jest dodatkowe wywołanie _paintTotal(canvas, total, chartRect);.
390
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
Zanim sprawdzimy tę nową metodę, zobaczmy najpierw, jakie zmiany zaszły w metodzie
_paintCircle():
// część klasy radial_chart.dart RadialChartPainter
void _paintCircle(Canvas canvas, int total, Rect chartRect) {
Paint sectionPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 30.0;
sectionPaint.color = color;
canvas.drawArc(
chartRect,
(startAngle + 2) * _toRadians,
(sweepAngle - 2)* _toRadians,
false,
sectionPaint,
);
startAngle += sweepAngle;
}
}
Jak widać, prawie wszystko jest takie samo, wystarczy zwrócić uwagę na kilka punktów:
Zmieniliśmy nasz styl sectionPaint na PaintingStyle.stroke; w ten sposób
kształt narysowany za pomocą paint nie zostanie wypełniony — zamiast tego
będzie miał tylko narysowany kontur. Dlatego też ustawiliśmy właściwość
strokeWidth.
Jak mogłeś zauważyć, przed wysłaniem wartości kąta do funkcji drawArc
dodajemy 2° do wartości startAngle i odejmujemy 2° od wartości sweepAngle,
pozostawiając niewielką przestrzeń między wycinkami, aby uzyskać lepszy efekt
wizualny.
Na koniec przekazujemy wartość false do parametru useCenter, aby utworzyć
nie ypełnione koło, ale segment łuku.
391
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
totalPainter.layout(maxWidth: chartRect.width);
totalPainter.paint(
canvas,
chartRect.center.translate(
-totalPainter.width / 2.0,
-totalPainter.height / 2.0,
),
);
}
392
d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach
To wszystko, jeśli chodzi o nasz widżet CustomPaint. Jak być może zauważyłeś, nasze wykresy
wyglądają do siebie bardzo podobnie. Największa różnica dotyczy zdefiniowanego malarza.
Możemy zdefiniować pojedynczy widżet, w którym możemy pobrać żądany typ wykresu i po
prostu zmienić malarza, którego wysyłamy do widżetu CustomPaint.
Podsumowanie
W tym rozdziale dowiedzieliśmy się, jak zmienić wygląd naszych widżetów za pomocą klasy
Transform i jej dostępnych przekształceń, takich jak skalowanie, translacja i obracanie. Widzieli-
śmy również, jak możemy łączyć transformacje, używając bezpośrednio klasy Matrix4.
Dowiedzieliśmy się, jak można użyć klasy Canvas do przejęcia kontroli nad narysowanymi
widżetami i jak możemy to wykorzystać do tworzenia własnych obrazów.
Wreszcie zobaczyliśmy, jak widżet CustomPaint może się przydać do tworzenia własnych widże-
tów, które mają nie tylko unikalne funkcje, ale także niepowtarzalny wygląd zdefiniowany
przez element potomny. CustomPainter.
W ostatnim rozdziale dowiemy się, jak animować widżety, korzystając z poznanych tutaj
transformacji.
393
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
394
d0765ad53fb82babda2278a311da7afb
d
15
Animacje
Wprowadzenie do animacji
We Flutterze animacje są szeroko obsługiwane, a platforma zapewnia wiele sposobów animo-
wania widżetów. Istnieją również animacje wbudowane, gotowe do użycia, które wystarczy
podłączyć do widżetów, aby były animowane. Chociaż Flutter abstrahuje od wielu zawiłości
związanych z animacjami, istnieje kilka ważnych koncepcji, które musimy zrozumieć, zanim
zagłębimy się w ten temat.
Klasa Animation<T>
We Flutterze animacje składają się ze statusu i wartości typu T. Status animacji odpowiada jej
stanowi (czy jest uruchomiona, czy zakończona); jego wartość odpowiada jego aktualnej wartości
i jest przeznaczona do zmiany podczas wykonywania animacji.
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Jednym z najczęstszych rodzajów animacji, z jakimi się spotkasz, jest reprezentacja typu
Animation<double>, ponieważ wartość double dokładniej odzwierciedla współrzędne w przestrzeni.
Klasa Animation udostępnia sposoby uzyskiwania dostępu do jej stanu i wartości podczas urucho-
mionego cyklu. Dzięki odbiornikom statusu (status listeners) wiemy, kiedy animacja zaczyna
się, kończy lub idzie w odwrotnym kierunku. Używając metody addStatusListener(), możemy
na przykład manipulować naszymi widżetami w odpowiedzi na zdarzenia początku lub końca ani-
macji. W ten sam sposób możemy dodać detektory wartości za pomocą metody addListener
(), dzięki czemu otrzymujemy powiadomienie za każdym razem, gdy zmienia się wartość
animacji, i możemy przebudować nasze widżety za pomocą metody setState() {}.
AnimationController
AnimationController jest jedną z najczęściej używanych klas animacji Fluttera. Pochodzi
z klasy Animation<double> i dodaje kilka podstawowych metod manipulowania animacjami.
Klasa Animation jest podstawą animacji we Flutterze; jak wspomniano wcześniej, nie ma żad-
nych metod związanych z kontrolą animacji. AnimationController dodaje do koncepcji ani-
macji na przykład następujące elementy kontroli:
Sterowanie odtwarzaniem i zatrzymywaniem — AnimationController dodaje
możliwość odtwarzania animacji do przodu, do tyłu lub zatrzymania.
Czas trwania — prawdziwe animacje mają ograniczony czas na odtworzenie,
to znaczy odtwarzane są przez chwilę i kończą się lub powtarzają.
Umożliwia ustawienie bieżącej wartości animacji — powoduje zatrzymanie
animacji i powiadamia odbiorniki o stanie i wartości.
Pozwala zdefiniować górną i dolną granicę animacji — dzięki temu możemy
poznać zakładane wartości przed i po odtworzeniu animacji.
396
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
AnimationController({
double value,
Duration duration,
String debugLabel,
double lowerBound: 0.0,
double upperBound: 1.0,
AnimationBehavior animationBehavior: AnimationBehavior.normal,
@required TickerProvider vsync
})
TickerProvider i Ticker
Interfejs TickerProvider opisuje obiekty, które mogą udostępniać obiekty Ticker.
Obiekty Ticker są używane przez każdą klasę, która musi wiedzieć, kiedy zostanie zbudowana
następna klatka. Są powszechnie używane pośrednio przez AnimationControllers. Korzystając
z klasy State, możemy ją rozszerzyć za pomocą TickerProviderStateMixin lub SingleTicker
ProviderStateMixin, aby mieć TickerProvider i używać go z obiektami AnimationController.
CurvedAnimation
Klasa CurvedAnimation służy do definiowania progresji klasy Animation jako krzywej nielinio-
wej. Możemy użyć jej do zmodyfikowania istniejącej animacji poprzez zmianę jej metody in-
terpolacji. Jest to również przydatne, gdy chcemy użyć innej krzywej podczas odtwarzania
397
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Klasa Curves definiuje wiele krzywych gotowych do użycia w naszej animacji, a nie tylko
Curves.linear.
Aby zobaczyć szczegółowo, jak zachowuje się każda z krzywych, sprawdź stronę dokumen-
tacji Curves: https://api.flutter.dev/flutter/animation/Curves-class.html
Tween
Oprócz wszystkich tych klas mamy jedną, która może pomóc w konkretnych zadaniach doty-
czących zakresu animacji. Jak widzieliśmy, domyślnie proste wartości początkowe i końcowe
animacji wynoszą odpowiednio 0.0 i 1.0. Za pomocą Tween możemy zmienić zakres lub typ
AnimationController — bez modyfikowania go. Tween mogą być dowolnego typu, a jeśli chcemy,
możemy również utworzyć własną klasę Tween. Chodzi o to, że Tween zwraca wartości w okre-
sach między początkiem a końcem, które możesz przekazać jako właściwości do wszystkiego,
co animujesz; na przykład możemy zmienić rozmiar widżetu, pozycję, przezroczystość, kolor
itd., używając dla każdej z nich określonych wartości Tween.
Mamy również dostępne inne klasy potomne Tween, takie jak klasa CurveTween, która może
modyfikować krzywą animacji, lub ColorTween, który tworzy interpolację między kolorami.
Korzystanie z animacji
Podczas pracy z animacjami nie zawsze będziemy tworzyć dokładnie te same obiekty anima-
cji, ale możemy znaleźć pewne podobieństwa dla różnych przypadków użycia. Obiekty ani-
macji są przydatne do zmiany typu i zakresu animacji. Przez większość czasu będziemy kom-
ponować animacje za pomocą instancji AnimationController, CurvedAnimation i Tween.
Animacja obrotu
Za pomocą klasy AnimationController możemy uzyskać bardziej płynny obrót przycisku —
zobacz rysunek na następnej stronie.
398
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
W tym przykładzie tworzymy nasz widżet w bardzo podobny sposób jak wcześniej (w roz-
dziale 14.):
_rotationAnimationButton() {
return Transform.rotate(
angle: _angle,
child: RaisedButton(
child: Text(„Rotated button”),
onPressed: () {
if (_animation.status == AnimationStatus.completed) {
_animation.reset();
_animation.forward();
}
},
),
);
}
Zobaczmy teraz, jak jest wykonywana część animacji. Musimy więc wiedzieć, jak utworzyć
nasz obiekt AnimationController i uruchomić go. Przyjrzyjmy się najpierw naszej przykładowej
klasie:
class _RotationAnimationsState extends State<RotationAnimations> with
SingleTickerProviderStateMixin {
double _angle = 0.0;
AnimationController _animation;
...
}
399
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Funkcja initState() z naszej klasy State jest idealnym miejscem do ustawienia i uruchomienia
animacji:
@override
void initState() {
super.initState();
_animation = createRotationAnimation();
_animation.forward();
}
animation.addListener(() {
setState(() {
_angle = (animation.value * 360.0) * _toRadians;
});
});
return animation;
}
400
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
Jak widać, możemy wygenerować nasze pożądane wartości na podstawie wartości animacji,
więc w większości przypadków Animation<double> wystarczy do zabawy z animacjami.
Gdybyśmy chcieli, moglibyśmy dodać inną krzywą do animacji, używając CurveTween, jak widać
w metodzie createBounceInRotationAnimation():
createBounceInRotationAnimation() {
var controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);
animation.addListener(() {
setState(() {
_angle = (animation.value * 360.0) * _toRadians;
});
});
return controller;
}
Tutaj tworzymy kolejną instancję Animation, używając metody drive() kontrolera i przekazując
żądaną krzywą za pomocą obiektu CurveTween. Zwróć uwagę, że zamiast kontrolera do nowego
obiektu animacji dodaliśmy detektory, ponieważ chcemy, aby wartości zależały od krzywej.
Ważną kwestią, na którą należy zwrócić uwagę, jest to, że musimy pozbyć się naszej instancji
klasy AnimationController pod koniec okresu istnienia naszej klasy State, aby zapobiec wyciekom:
@override
void dispose() {
_animation.dispose();
super.dispose();
}
Należy to zrobić dla każdego rodzaju animacji, które robimy, ponieważ zawsze będziemy pra-
cować z AnimationController.
Animacja skalowania
Aby stworzyć animację skalowania i uzyskać lepszy efekt niż bezpośrednia zmiana atrybutu
skali, możemy ponownie użyć klasy AnimationController:
401
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Tym razem, aby zbudować nasz widżet RaisedButton ze skalowaniem, definiujemy widżet
Transform za pomocą dobrze znanego konstruktora Transform.scale:
_scaleAnimationButton() {
return Transform.scale(
scale: _scale,
child: RaisedButton(
child: Text("Scaled button"),
onPressed: () {
if (_animation.status == AnimationStatus.completed) {
_animation.reverse();
} else if (_animation.status == AnimationStatus.dismissed) {
_animation.forward();
}
},
),
);
}
Zauważ, że teraz używamy właściwości _scale. Przyjrzyjmy się zmianie w metodzie onPressed.
Odtwarzamy animację w trybie odwrotnym za pomocą funkcji reverse() klasy AnimationController,
jeśli jest zakończona, i odtwarzamy do przodu, jeśli jest w swoim stanie początkowym (to zna-
czy po jej przewinięciu do tyłu).
Tworzenie obiektu animacji odbywa się w bardzo podobny sposób do animacji rotacji, ale są
drobne modyfikacje w konstrukcji kontrolera:
createScaleAnimation() {
var animation = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 2.0,
debugLabel: "animations demo",
duration: Duration(seconds: 2),
);
animation.addListener(() {
402
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
setState(() {
_scale = animation.value;
});
});
return animation;
}
Jak widać, teraz zmieniamy wartości lowerBound i upperBound kontrolera, ponieważ chcemy, aby
przycisk rósł, aż jego rozmiar będzie dwukrotnie większy, a nie chcemy, aby był mniejszy niż
jego rozmiar początkowy (scale = 1.0). Poza tym zmieniamy nasz detektor wartości animacji,
aby uzyskać wartość z animacji bez żadnych obliczeń.
Animacja translacji
Tak jak poprzednio, możemy uzyskać lepszy wygląd naszej transformacji translacji i uczynić
ją płynniejszą za pomocą AnimationController:
Konstrukcja naszego widżetu jest podobna jak wcześniej; jedynym wyjątkiem jest wywołanie
Transform.translate(). Teraz mamy inny typ wartości niż double. Zobaczmy, co musimy
zmienić, aby wykonać animację przesunięcia (Offset):
createTranslateAnimation() {
var controller = AnimationController(
403
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 2),
);
animation.addListener(() {
setState(() {
_offset = animation.value;
});
});
return controller;
}
Jak widać, aby zmodyfikować przesunięcie naszego widżetu, zastosowaliśmy inne podejście,.
Użyliśmy instancji Tween <Offset>, przekazanej do obiektu AnimationController za pomocą
metody drive(), tak jak to zrobiliśmy wcześniej z CurveTween. Działa to, ponieważ klasa Offset
nadpisuje operatory matematyczne, takie jak odejmowanie i dodawanie:
// część pliku geometry.dart z pakietu dart:ui
class Offset extends OffsetBase {
...
Offset operator -(Offset other) => new Offset(dx - other.dx, dy - other.dy);
Offset operator +(Offset other) => new Offset(dx + other.dx, dy + other.dy);
...
}
Dzięki temu możliwe jest obliczenie przesunięć pośrednich (wartości animacji), a następnie
można uzyskać interpolację między dwiema wartościami przesunięcia.
Aby uzyskać szczegółowe informacje, sprawdź kod źródłowy klasy Offset: https://github.
com/flutter/engine/blob/master/lib/ui/geometry.dart. Zwróć również uwagę, że aby utwo-
rzyć niestandardowe interpolacje, zazwyczaj piszemy niestandardowe klasy Tween; aby
uzyskać więcej informacji, zobacz następny przykład.
404
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
405
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
if (_animation.status == AnimationStatus.completed) {
_animation.reverse();
} else if (_animation.status == AnimationStatus.dismissed) {
_animation.forward();
}
},
),
),
),
);
}
To działa, a w prostych przypadkach najlepiej jest zachować kod w taki sposób, ponieważ mamy
mniej obiektów, którymi musimy się zająć, i jedną animację do odtworzenia.
Aby jednak kod był łatwiejszy w utrzymaniu, lepiej jest oddzielić obliczenie wartości od samej
animacji. W ten sposób możemy użyć Tween; przypomnijmy sobie przykład Offset, w którym
jest on obliczany i po prostu otrzymujemy wartość gotową do użycia.
Niestandardowy Tween
Aby utworzyć niestandardową klasę Tween, najpierw musimy zdefiniować nasz obiekt wartości.
Tutaj zdecydowaliśmy się na grupowanie wartości transformacji:
class ButtonTransformation {
final double scale;
final double angle;
final Offset offset;
@override
lerp(double t) {
return super.lerp(t);
}
}
406
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
Musimy zdefiniować metodę lerp() dla niestandardowego Tween (lerp oznacza interpolację
liniową), która jest odpowiedzialna za zwracanie pośredniej wartości ButtonTransformation
między początkiem (begin) a końcem (end) na podstawie wartości t.
Spoglądając na domyślną implementację lerp() klasy Tween, widzimy, że jest to bardzo proste:
// część klasy tween.dart Tween
@protected
T lerp(double t) {
assert(begin != null);
assert(end != null);
return begin + (end - begin) * t;
}
Powyższy kod oblicza wartość lerp() przy użyciu operatorów +, - i * na obiektach typu T.
Oznacza to, że możemy po prostu zaimplementować te operatory w naszym ButtonTransformation,
a Tween będzie działał tak, jak w przypadku każdego innego typu:
class ButtonTransformation {
...
ButtonTransformation operator -(ButtonTransformation other) =>
ButtonTransformation(
scale: scale - other.scale,
angle: angle - other.angle,
offset: offset - other.offset,
);
ButtonTransformation operator +(ButtonTransformation other) =>
ButtonTransformation(
scale: scale + other.scale,
angle: angle + other.angle,
offset: offset + other.offset,
);
Teraz klasa Tween może również generować pośrednie wartości ButtonTransformation. Następnie
możemy użyć wygenerowanych wartości animacji tak jak poprzednio:
createCustomTweenAnimation() {
var controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);
407
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
angle: 360.0,
offset: Offset(70, 200),
scale: 2.0,
)));
animation.addListener(() {
setState(() {
_buttonTransformation = animation.value;
});
});
return controller;
}
Jak widać, duża różnica polega na wykorzystaniu naszej właściwości CustomTween. Zwróć uwagę,
że zawsze musimy zdefiniować wartości początkowe (begin) i końcowe (end), ponieważ wartości
Tween są oparte na zakresie zdefiniowanym przez odpowiednią interpolację.
Dzięki tym przykładom zobaczyliśmy, jak używać i stosować najważniejsze animacje we Flutte-
rze. W następnych sekcjach zobaczymy alternatywne sposoby stosowania animacji do naszych
widżetów.
Korzystanie z AnimatedBuilder
Patrząc na kod, który napisaliśmy w ostatniej sekcji, widzimy, że nie ma w nim nic złego: nie
jest zbyt skomplikowany ani duży. Jeśli się jednak przyjrzymy uważnie, zauważymy mały pro-
blem — nasza animacja przycisku jest pomieszana z innymi widżetami. Dopóki nasz kod się
nie skaluje i nie staje się bardziej złożony, jest to w porządku, ale wiemy, że przez większość
czasu tak nie jest. Możemy zatem mieć prawdziwy problem.
Klasa AnimatedBuilder może nam pomóc w rozdzieleniu zadań; nasz widżet, niezależnie od tego,
czy jest to RaisedButton, czy cokolwiek innego, nie musi wiedzieć, że jest renderowany w anima-
cji, a rozbicie metody build na widżety, gdzie każdy z nich ponosi jedną odpowiedzialność,
może być postrzegane jako jeden z podstawowych tematów we frameworku Fluttera.
408
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
Klasa AnimatedBuilder
Widżet AnimatedBuilder istnieje, abyśmy mogli tworzyć złożone widżety, które zawierają ani-
mację jako część większej funkcji build. Podobnie jak każdy inny widżet, jest on zawarty w drzewie
widżetów i ma właściwość child. Sprawdźmy jego konstruktor:
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required TransitionBuilder builder,
Widget child
})
Jak widać, oprócz dobrze znanej właściwości key mamy tutaj kilka innych ważnych elementów:
animation — to jest właściwa animacja obiektu Listenable, który przechowuje
listę detektorów, dostających powiadomienia o zmianie obiektu. Jak pewnie się
już domyślasz, AnimatedBuilder będzie wykrywał aktualizację animacji, więc nie
musimy już tego robić ręcznie za pomocą metody addListener().
builder — tutaj modyfikujemy widżet child na podstawie wartości animacji.
child — to jest widżet, który istnieje niezależnie od animacji. Dlatego tworzymy
go tak, jak robilibyśmy to bez animacji.
@override
void initState() {
super.initState();
_animation = createAnimation();
_controller.forward();
}
...
}
409
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
return _controller.drive(CustomTween(
begin: ButtonTransformation.none,
end: ButtonTransformation(
angle: 360.0,
offset: Offset(70, 200),
scale: 2.0,
)));
}
Nie musimy już wykrywać aktualizacji animacji (nie mamy wywołania addListener()), ponieważ
jest to robione bezpośrednio przez widżet AnimatedBuilder.
410
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
Jak widać, animacja jest wyraźnie oddzielona od tworzenia RaisedButton. Tworzymy instancję
i przekazujemy ją do nowego widżetu o nazwie ButtonTransition, razem z naszym obiektem
_animation. Zobaczmy ten zupełnie nowy widżet:
class ButtonTransition extends StatelessWidget {
final Animation<ButtonTransformation> _animation;
final RaisedButton child;
const ButtonTransition({
Key key,
@required Animation<ButtonTransformation> animation,
this.child,
}) : _animation = animation,
super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
child: child,
builder: (context, child) => Transform(
transform: Matrix4.translationValues(
_animation.value.offset.dx,
_animation.value.offset.dy,
0,
)
..rotateZ(_animation.value.angle * _toRadians)
..scale(_animation.value.scale, _animation.value.scale),
child: child,
),
);
}
}
Dokumentacja mówi:
Korzystanie z gotowego elementu child jest całkowicie opcjonalne, ale w niektórych
przypadkach może znacznie poprawić wydajność i dlatego jest dobrą praktyką.
411
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
Chociaż końcowy efekt wizualny jest taki sam, podzielenie rzeczy na małe części z pojedyn-
czymi zadaniami jest ważną koncepcją, która poprawia konserwację kodu i może prowadzić
do lepszej wydajności.
Korzystanie z AnimatedWidget
Oddzielenie naszej animacji od widżetów za pomocą widżetu AnimatedBuilder jest niezwykle
łatwe i, jak widzieliśmy, może przynieść wiele korzyści. Flutter oferuje kolejną interesującą
alternatywę, która robi to samo co widżet AnimatedBuilder — za pomocą prostszej składni.
Jest to powszechne, gdy mamy do czynienia z dobrze zorganizowaną strukturą, taką jak
Flutter; zazwyczaj istnieje więcej niż jeden sposób zrobienia czegoś i nie oznacza to, że istnieją
między nimi znaczące różnice. AnimatedWidget i AnimatedBuilder są tego świetnymi przykładami.
Oba mają na celu oddzielenie części animacji od części tworzącej widżet.
Podczas gdy widżet AnimatedBuilder deleguje tworzenie widżetu do metody builder, Animated
Widget definiuje wszystko, co jest potrzebne w odniesieniu do animacji, i po prostu musimy
nadpisać jego metodę build(), aby odzwierciedlić aktualizacje animacji. W efekcie Animated
Builder jest klasą AnimatedWidget.
Klasa AnimatedWidget
AnimatedWidget jest klasą abstrakcyjną i, jak powiedzieliśmy wcześniej, musimy bezpośrednio
przesłonić jej metodę build(), aby odzwierciedlić zmiany animacji. Jego konstruktor jest zdefinio-
wany w następujący sposób:
const AnimatedWidget({
Key key,
@required Listenable listenable
})
Jak widać, jedyną wymaganą właściwością jest obiekt Listenable, który może nasłuchiwać
aktualizacji animacji. Za całą logikę budowania widżetu odpowiada jego klasa zstępująca.
412
d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje
const AnimatedButton({
Key key,
@required Listenable animation,
this.button,
}) : super(
key: key,
listenable: animation,
);
@override
Widget build(BuildContext context) {
Animation<ButtonTransformation> animation = listenable;
return Transform(
transform: Matrix4.translationValues(
animation.value.offset.dx,
animation.value.offset.dy,
0,
)
..rotateZ(animation.value.angle * _toRadians)
..scale(animation.value.scale, animation.value.scale),
child: button,
);
}
}
Wybór, kiedy używać AnimatedBuilder i AnimatedWidget, może początkowo wydawać się trudny,
ale pamiętaj, że oba rozwiązania mogą przynieść te same korzyści. Może to pomóc w podjęciu
decyzji. Zacznij od podziału swoich widżetów tak, aby zajmowały się pojedynczym zadaniem,
a podejmowanie takich decyzji stanie się naturalne.
Podsumowanie
W ostatnim rozdziale zagłębiliśmy się w animacje Fluttera. Poznaliśmy podstawowe pojęcia zwią-
zane z animacją, które są definiowane głównie przez klasę Animation.
413
d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących
wykorzystując koncepcje poznane w tym rozdziale. Wreszcie zobaczyliśmy, jak tworzyć własne
niestandardowe obiekty Tween.
Na koniec zobaczyliśmy, jak używać AnimatedBuilder i AnimatedWidget, aby nasz kod animacji
był czystszy i łatwiejszy do zrozumienia.
414
d0765ad53fb82babda2278a311da7afb
d
d0765ad53fb82babda2278a311da7afb
d
d0765ad53fb82babda2278a311da7afb
d