Download as pdf or txt
Download as pdf or txt
You are on page 1of 416

Flutter

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

Tłumaczenie: Łukasz Wójcicki

ISBN: 978-83-283-7826-1

Copyright © Packt Publishing 2019. First published in the English language under
the title ‘Flutter for Beginners – (9781788996082)’.

Polish edition copyright © 2021 by Helion S.A.


All rights reserved.

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.

Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości


lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

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)

Pliki z przykładami omawianymi w książce można znaleźć pod adresem:


https://ftp.helion.pl/przyklady/flutte.zip

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ę.

 Poleć książkę na Facebook.com  Księgarnia internetowa


 Kup w wersji papierowej  Lubię to! » Nasza społeczność
 Oceń książkę

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

Część I. Wprowadzenie do języka Dart 21

Rozdział 1. Wprowadzenie do języka Dart 23


Pierwsze kroki z językiem Dart 23
Ewolucja Darta 24
Jak działa Dart 25
Dart w praktyce 26
Dlaczego Flutter korzysta z języka Dart 29
Zwiększanie produktywności 29
Łatwa nauka 30
Dojrzałość 32
Podstawy języka Dart 33
Operatory 33
Przepływ sterowania i pętle 41
Funkcje 42
Struktury danych, kolekcje i typy ogólne 45
Wprowadzenie do OOP w języku Dart 47
Właściwości OOP 47
Podsumowanie 50
Dalsza lektura 50

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Rozdział 2. Średnio zaawansowane programowanie w języku Dart 51


Klasy i konstruktory w języku Dart 52
Typ wyliczeniowy enum 53
Notacja kaskadowa 53
Konstruktory 54
Metody dostępu — pobierające i ustawiające 56
Pola i metody statyczne 57
Dziedziczenie klas 59
Interfejsy, klasy abstrakcyjne i domieszki 60
Klasy abstrakcyjne 60
Interfejsy 61
Domieszki — dodawanie zachowania do klasy 62
Klasy wywoływane, funkcje najwyższego poziomu i zmienne 64
Biblioteki i pakiety języka Dart 66
Importowanie i korzystanie z biblioteki 66
Tworzenie bibliotek Darta 70
Pakiety Darta 76
Struktury pakietów 77
Stagehand — generator projektów Darta 80
Plik pubspec 81
Zależności pakietów — pub 82
Wprowadzenie do programowania asynchronicznego
z wykorzystaniem obiektów Future i Isolate 86
Obiekty Future 86
Obiekty Isolate 89
Wprowadzenie do testów jednostkowych w języku Dart 91
Pakiet test Darta 92
Pisanie testów jednostkowych 92
Podsumowanie 94

Rozdział 3. Wprowadzenie do Fluttera 95


Porównanie z innymi platformami do tworzenia aplikacji mobilnych 96
Problemy, które Flutter chce rozwiązać 96
Różnice między istniejącymi frameworkami 97
Kompilacja Fluttera (Dart) 103
Kompilacja w fazie rozwoju oprogramowania 103
Kompilacja dla wersji release 103
Obsługiwane platformy 103
Renderowanie Fluttera 104
Technologie webowe 104
Frameworki i widżety OEM 105
Flutter — renderowanie samodzielnie 106
Wprowadzenie do widżetów 106
Kompatybilność 107
Niezmienność 107
Wszystko jest widżetem 107
Hello Flutter 109
Plik pubspec 111
Uruchomienie wygenerowanego projektu 113
Podsumowanie 115

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Część II. Interfejs użytkownika Fluttera


— wszystko jest widżetem 117

Rozdział 4. Widżety: tworzenie layoutów Fluttera 119


Widżety stanowe i bezstanowe 119
Widżety bezstanowe 120
Widżety stanowe 121
Reprezentowanie widżetów stanowych i bezstanowych za pomocą kodu 121
Widżety dziedziczone 126
Właściwość key widżetu 127
Widżety wbudowane 128
Widżety podstawowe 128
Wprowadzenie do wbudowanych widżetów layoutu 133
Kontenery 133
Stylizacja i pozycjonowanie 134
Inne widżety (gesty, animacje i transformacje) 134
Tworzenie interfejsu użytkownika za pomocą widżetów
(aplikacja do zarządzania przysługami) 135
Ekrany aplikacji 135
Ekran główny aplikacji 136
Ekran prośby o przysługę 144
Tworzenie niestandardowych widżetów 147
Podsumowanie 149

Rozdział 5. Obsługa danych wejściowych i gestów użytkownika 151


Obsługa gestów użytkownika 151
Wskaźniki 152
Gesty 152
Gesty w widżetach Material Design 157
Widżety danych wejściowych 157
FormField i TextField 158
Form 160
Walidacja danych wejściowych (Form) 162
Walidacja danych użytkownika 162
Niestandardowa obsługa danych wejściowych i FormField 163
Tworzenie niestandardowej obsługi danych wejściowych 163
Przykład niestandardowego widżetu danych wejściowych 163
Łączymy wszystko razem 167
Ekran przysług 167
Ekran prośby o przysługę 173
Podsumowanie 175

Rozdział 6. Motyw i styl 177


Widżety motywu 177
Widżet Theme 178
Tworzenie motywu w praktyce 180
Klasa Platform 182

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Material Design 183


Widżet MaterialApp 184
Widżet Scaffold 186
Motyw niestandardowy 187
iOS Cupertino 189
CupertinoApp 189
Cupertino w praktyce 190
Korzystanie z niestandardowych czcionek 191
Importowanie czcionek do projektu Fluttera 191
Zastępowanie domyślnej czcionki w aplikacji 193
Dynamiczne style z MediaQuery i LayoutBuilder 193
LayoutBuilder 194
MediaQuery 196
Dodatkowe klasy responsywne 199
Podsumowanie 199

Rozdział 7. Routing: nawigacja między ekranami 201


Omówienie widżetu Navigator 201
Navigator 202
Overlay 202
Route 203
MaterialPageRoute i CupertinoPageRoute 203
Przykład 203
WidgetsApp 207
Trasy nazwane (named routes) 208
Obsługa tras nazwanych 208
Pobieranie wyników z Route 210
Przejścia między ekranami 212
PageRouteBuilder 212
Animacje Hero 214
Widżet hero 214
Implementacja przejść Hero 215
Podsumowanie 221

Część III. Tworzenie profesjonalnych aplikacji 223

Rozdział 8. Wtyczki Firebase 225


Omówienie Firebase 225
Konfigurowanie Firebase 226
Łączenie aplikacji Fluttera z Firebase 229
Uwierzytelnianie Firebase 233
Włączanie usług uwierzytelniania w Firebase 233
Ekran uwierzytelniania 235
Logowanie za pomocą Firebase 236
Baza danych NoSQL z Cloud Firestore 241
Włączanie Cloud Firestore w Firebase 241
Cloud Firestore i Flutter 243
Ładowanie przysług z Firestore 243

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Aktualizowanie przysług w Firebase 246


Zapis przysługi w Firebase 246
Cloud Storage z Firebase Storage 248
Wprowadzenie do Firebase Storage 248
Dodawanie zależności Flutter Storage 249
Przesyłanie plików do Firebase 249
Reklamy z Firebase AdMob 252
Konto AdMob 252
Tworzenie konta AdMob 253
AdMob we Flutterze 255
Wyświetlanie reklam we Flutterze 258
Uczenie maszynowe z wykorzystaniem Firebase ML 260
Dodanie zestawu uczenia maszynowego do Fluttera 260
Korzystanie z detektora etykiet we Flutterze 261
Podsumowanie 263

Rozdział 9. Tworzenie własnej wtyczki Fluttera 265


Tworzenie projektu pakietu/wtyczki 265
Pakiety Fluttera a pakiety Dart 266
Rozpoczynanie projektu pakietu Dart 266
Uruchamianie pakietu wtyczek Fluttera 268
Struktura projektu wtyczki 268
MethodChannel 269
Wdrożenie wtyczki Androida 270
Implementacja wtyczki iOS 271
API Darta 272
Przykład pakietu wtyczek 272
Korzystanie z wtyczki 273
Dodanie dokumentacji do pakietu 274
Pliki dokumentacji 274
Dokumentacja biblioteki 274
Generowanie dokumentacji 275
Publikowanie pakietu 275
Zalecenia dotyczące tworzenia projektu wtyczki 276
Podsumowanie 276

Rozdział 10. Dostęp do funkcji urządzenia z aplikacji Fluttera 279


Uruchomienie adresu URL z aplikacji 279
Wyświetlanie linku 280
Uruchomienie adresu URL 282
Zarządzanie uprawnieniami aplikacji 284
Zarządzanie uprawnieniami we Flutterze 284
Importowanie kontaktu z telefonu 285
Importowanie kontaktu za pomocą contact_picker 286
Uprawnienia do kontaktu za pomocą permission_handler 288
Integracja aparatu w telefonie 289
Robienie zdjęć za pomocą image_picker 290
Uprawnienia do aparatu za pomocą permission_handler 291
Podsumowanie 292

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Rozdział 11. Widoki platformy oraz integracja mapy 295


Wyświetlanie mapy 295
Widoki platformy 296
Tworzenie widżetu widoku platformy 297
Pierwsze kroki z wtyczką google_maps_flutter 301
Dodawanie znaczników do mapy 308
Klasa Marker 308
Dodawanie znaczników w widżecie GoogleMap 309
Dodawanie interakcji na mapie 311
Dynamiczne dodawanie znaczników 311
GoogleMapController 312
Pobieranie GoogleMapController 312
Animowanie kamery mapy do lokalizacji 312
Korzystanie z interfejsu API Google Places 313
Włączanie API Google Places 313
Pierwsze kroki z wtyczką google_maps_webservice 314
Uzyskiwanie adresu miejsca za pomocą wtyczki google_maps_webservice 314
Podsumowanie 316

Część IV. Zaawansowany Flutter


— zasoby dla złożonych aplikacji 319

Rozdział 12. Testowanie, debugowanie i wdrażanie 321


Testowanie we Flutterze — testy jednostkowe oraz widżetów 321
Testy widżetów 322
Debugowanie aplikacji Fluttera 324
Observatory 325
Dodatkowe funkcje debugowania 326
DevTools 327
Profilowanie aplikacji Fluttera 329
Profiler Observatory 329
Tryb profilowania 329
Sprawdzanie drzewa widżetów Fluttera 331
Inspektor widżetów 332
Przygotowywanie aplikacji do wdrożenia 333
Tryb wydania (release mode) 334
Wydawanie aplikacji na Androida 334
Wydawanie aplikacji na iOS 339
App Store Connect 339
Xcode 340
Podsumowanie 341

Rozdział 13. Poprawa komfortu użytkowania 343


Dostępność we Flutterze i dodawanie tłumaczeń do aplikacji 343
Wsparcie Fluttera dla dostępności 344
Internacjonalizacja Fluttera 344
Dodawanie lokalizacji do aplikacji Fluttera 345

10

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Komunikacja między kodem natywnym a Flutterem


z wykorzystaniem kanałów platformy 351
Kanał platformy 351
Kodeki wiadomości 353
Tworzenie procesów pracujących w tle 354
Funkcja Fluttera compute() 354
Przykład compute() 355
Proces pracujący w tle 356
Inicjalizacja obliczeń 357
Dodanie kodu specyficznego dla systemu Android
w celu uruchomienia kodu Darta w tle 360
Klasa HandsOnBackgroundProcessPlugin 360
Klasa BackgroundProcessService 362
Dodanie kodu specyficznego dla systemu iOS
w celu uruchomienia kodu Darta w tle 365
Klasa SwiftHandsOnBackgroundProcessPlugin 366
Podsumowanie 370

Rozdział 14. Operacje graficzne na widżetach 371


Transformacje widżetów za pomocą klasy Transform 371
Widżet Transform 372
Rodzaje transformacji 373
Obrót 373
Skalowanie 374
Translacja 375
Transformacje złożone 376
Stosowanie transformacji do widżetów 377
Obracanie widżetów 377
Skalowanie widżetów 378
Translowanie widżetów 378
Stosowanie wielu transformacji 379
Korzystanie z niestandardowych malarzy i elementów canvas 380
Klasa Canvas 380
Widżet CustomPaint 382
Obiekt CustomPainter 383
Praktyczny przykład 384
Wariant wykresu radialnego 389
Podsumowanie 393

Rozdział 15. Animacje 395


Wprowadzenie do animacji 395
Klasa Animation<T> 395
Korzystanie z animacji 398
Animacja obrotu 398
Animacja skalowania 401
Animacja translacji 403
Wiele transformacji i niestandardowy Tween 404

11

d0765ad53fb82babda2278a311da7afb
d
Spis treści

Korzystanie z AnimatedBuilder 408


Klasa AnimatedBuilder 409
Powrót do naszej animacji 409
Korzystanie z AnimatedWidget 412
Klasa AnimatedWidget 412
Przepisanie animacji za pomocą AnimatedWidget 412
Podsumowanie 413

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.

Po pierwsze, dziękuję zespołowi Fluttera za niesamowite narzędzie, które pomaga społeczności


programistów pomagać innym ludziom.

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.

Chciałbym podziękować moim przyjaciołom, współpracownikom i rodzinie, zwłaszcza mojej


matce Antoninie, ojcu Euclidesowi, mojej siostrze Hellen i mojemu bratu Alanowi za wsparcie
podczas ciężkiej pracy nad książką. Dziękuję również moim nauczycielom, którzy zachęcali mnie
do stawienia czoła wyzwaniom takim jak ta książka w bardziej naturalny i odważny sposób.

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

Ugurcan Yildirim jest entuzjastą frameworków do tworzenia aplikacji mobilnych na Androida


i Fluttera. Ukończył studia prawnicze z tytułem licencjata w dziedzinie informatyki na Bilkent
University w Ankarze. Od 2015 roku pracuje jako inżynier Android w Accenture Industry X.0
w Stambule. Wraz z obiecującym trendem wzrostowym Fluttera, który rozpoczął się w 2018 roku,
zaczął się zajmować jego osobliwościami i eksperymentować z nimi. Od tego czasu przyczynia
się do rozwoju społeczności Fluttera, pisząc artykuły na Medium (@ugurcany) oraz prowa-
dząc prezentacje. Jego najnowszym wkładem jest recenzja tej książki, z której, jego zdaniem,
powinni skorzystać programiści Fluttera.

Chciałbym podziękować Packtowi za umożliwienie mi wniesienia wkładu w stale rozwijający


się wszechświat Fluttera poprzez recenzję jednej z pierwszych i najbardziej wszechstronnych
opublikowanych książek na temat Fluttera. Chciałbym również podziękować moim rodzicom i mojej
żonie Karsu za wsparcie i cierpliwość podczas recenzowania tej książki.

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.

Dla kogo jest ta książka


Ta książka jest przeznaczona dla programistów, którzy chcą nauczyć się od podstaw rewolu-
cyjnej platformy Google, Fluttera. Nie jest wymagana znajomość Fluttera ani Darta. Pomocna
będzie jednak podstawowa znajomość języka programowania.

Co obejmuje ta książka?
Rozdział 1., „Wprowadzenie do języka Dart”, przedstawia podstawy języka Dart.

Rozdział 2., „Średniozaawansowane programowanie w języku Dart”, omawia funkcje pro-


gramowania obiektowego i zaawansowane koncepcje Darta, bibliotek, pakietów i progra-
mowania asynchronicznego.

Rozdział 3., „Wprowadzenie do Fluttera”, wprowadza Cię w świat Fluttera.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Rozdział 4., „Widżety: tworzenie layoutów we Flutterze”, omawia tworzenie layoutów we


Flutterze.

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ł 14., „Manipulacje grafiką widżetów”, omawia tworzenie unikalnych wizualizacji za


pomocą manipulacji graficznych.

Rozdział 15., „Animacje”, daje wgląd w to, jak dodawać animacje do widżetów Flutter.

Jak najlepiej wykorzystać tę książkę?


W miarę przechodzenia przez kolejne rozdziały poznasz niebędne wymagania. Na początek
potrzebna Ci będzie przeglądarka, dzięki której uzyskasz dostęp do witryny DartPad i rozpocz-
niesz zabawę z Dartem.

Aby profesjonalnie tworzyć i publikować aplikacje na iOS, potrzebujesz licencji programisty


(płatnej corocznie), komputera Mac i co najmniej jednego urządzenia do testowania aplikacji.
Wszystko to nie jest absolutnie konieczne do nauki Fluttera, ale może Ci się przydać.

Cały proces instalacji i wymagania środowiska Fluttera są dostępne na oficjalnej stronie


(https://flutter.dev/docs/get-started/install), ale nie martw się: możesz zacząć od absolutnego
minimum i zainstalować dowolne dodatki tylko w razie potrzeby.

18

d0765ad53fb82babda2278a311da7afb
d
Przedmowa

Pobierz przykładowe pliki z kodem źródłowym


Pakiet kodu do książki jest hostowany na GitHubie pod adresem https://github.com/PacktPublishing/
Flutter-for-Beginners. W przypadku aktualizacji kodu zostanie on zmieniony w istniejącym repo-
zytorium GitHub.

Pobierz kolorowe rysunki


Udostępniamy również plik PDF zawierający kolorowe zrzuty ekranu / diagramy wykorzystane
w tej książce. Możesz go pobrać tutaj: https://static.packt-cdn.com/downloads/9781788996082_
ColorImages.pdf

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”.

Blok kodu wygląda następująco:


main() {
var yeahDartIsGreat = "Oczywiście!";
var dartIsGreat = yeahDartIsGreat ?? "Nie wiem";
print(dartIsGreat); // wyświetla Oczywiście!
}

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ć.

W tej sekcji znajdują się następujące rozdziały:


 Rozdział 1., „Wprowadzenie do języka Dart”.
 Rozdział 2., „Średnio zaawansowane programowanie w języku Dart”.
 Rozdział 3., „Wprowadzenie do Fluttera”.

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ę.

W tym rozdziale omówimy następujące tematy:


 Poznanie zasad i narzędzi języka Dart.
 Dlaczego Flutter korzysta z języka Dart?
 Podstawy języka Dart.
 Wprowadzenie do OOP w Dart.

Pierwsze kroki z językiem Dart


Język Dart, opracowany przez Google, jest językiem programowania, którego można używać
do tworzenia aplikacji internetowych, desktopowych, serwerowych i mobilnych. Dart to
język programowania wykorzystywany do kodowania aplikacji Flutter, dzięki czemu oferuje on

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.

Dart to wspaniały, nowoczesny, wieloplatformowy język ogólnego przeznaczenia, który nieu-


stannie ulepsza swoje funkcje, co czyni go bardziej dojrzałym i elastycznym. Właśnie dlatego
zespół platformy Flutter wybrał ten język.

24

d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart

Jak działa Dart


Aby zrozumieć, skąd się wzięła elastyczność języka, musimy wiedzieć, jak możemy uruchomić
kod Dart. Odbywa się to na dwa sposoby:
 maszyna wirtualna Dart [Dart Virtual Machines (VM)],
 kompilacje JavaScriptu.

Spójrz na poniższy diagram:

Dart VM i kompilacja JavaScriptu


Kod w języku Dart można uruchomić w środowisku go obsługującym. Zapewnia ono aplikacji
podstawowe funkcje, takie jak:
 systemy uruchomieniowe,
 biblioteki podstawowe Darta,
 odśmiecanie pamięci.

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.

Tak wygląda narzędzie:

Narzędzia programistyczne Darta


DartPad to doskonały sposób na rozpoczęcie eksperymentowania z językiem bez dodatko-
wego wysiłku. Ponieważ wkrótce będziesz chciał nauczyć się zaawansowanych rzeczy, takich
jak zapis do plików lub korzystanie z bibliotek niestandardowych, musisz mieć skonfiguro-
wane do tego środowisko programistyczne.

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

Najbardziej powszechnymi IDE używanymi do programowania Dart i Flutter są Visual Stu-


dio Code lub VS Code (dla stron i Fluttera) oraz Android Studio lub dowolne IDE od JetBra-
ins, takie jak WebStorm (które jest skoncentrowane na tworzeniu stron). Wszystkie funkcje
Dart w tych IDE są oparte na oficjalnych narzędziach, więc nie ma znaczenia, co wybierzesz
— dostarczone narzędzia będą w większości takie same. Dart SDK zapewnia specjalistyczne
narzędzia dla każdego ekosystemu programistycznego, takie jak programowanie webowe oraz
po stronie serwera.

Niezależny zestaw Dart SDK jest dostarczany z następującymi narzędziami:


 dart (https://dart.dev/tools/dart-vm) — jest to maszyna wirtualna Darta; wykonuje
jego kod. Aby wykonać skrypt Dart, uruchom następujące polecenie:
dart code.dart
 dart2js (https://dart.dev/tools/dart2js) — jest to oryginalny kompilator
Darta-do-JavaScript.
 dartanalyzer (https://github.com/dart-lang/sdk/tree/master/pkg/
analyzer_cli#dartanalyzer) — analizuje kod statycznie (jako typowy linter),
pomagając wcześnie wychwycić błędy.

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.

 dartdoc (https://github.com/dart-lang/dartdoc#dartdoc) — generuje


dokumentację API.
 pub (https://dart.dev/tools/pub/cmd) — menedżer pakietów. Jest to narzędzie,
które można wykorzystać do zarządzania bibliotekami i pakietami.
 dartfmt (https://github.com/dart-lang/dart_ style#readme) — stosuje wytyczne
dotyczące stylu kodowania w Dart.

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.

dart2js to również narzędzie skupiające się na rozwiązaniach webowych, chociaż jest


dostarczane ze standardowym zestawem SDK. Do programowania po stronie serwera
potrzebne są tylko standardowe narzędzia SDK.

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.

 Możemy skorzystać ze składni // komentarz do pisania komentarzy


jednowierszowych. Dart ma również komentarze wielowierszowe
ze składnią /* komentarz */, jak poniżej:
// to jest komentarz w jednym wierszu
/*
To jest długi wielowierszowy komentarz
*/

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

Dlaczego Flutter korzysta z języka Dart


Platforma Flutter ma być przełomowym rozwiązaniem w tworzeniu aplikacji mobilnych, za-
pewniając wszystkie narzędzia potrzebne programiście do tworzenia niesamowitych aplikacji
bez żadnych wad w zakresie wydajności i skalowalności. Flutter ma w swojej podstawowej
strukturze wiele koncepcji dotyczących wydajności aplikacji i interfejsu użytkownika. Aby zapewnić
najlepsze wyniki dla rozwijającego się świata z wysoką wydajnością porównywalną z oficjal-
nymi natywnymi pakietami SDK, Flutter korzysta ze wsparcia Darta, oferując narzędzia,
które przyczyniają się do produktywności programistów w fazie rozwoju i do tworzenia apli-
kacji zoptymalizowanych pod kątem publikacji.

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).

Kompilowanie aplikacji Flutter oraz funkcja hot reload


Podczas pisania lub debugowania kodu będziesz używać maszyny wirtualnej Dart z JIT. Pomaga
to w wykorzystaniu funkcji, takich jak narzędzia do profilowania, hot reload (o której więcej
w rozdziale 3.) oraz wielu innych.

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.

Kolejnym bardzo fajnym narzędziem Darta jest analizator:

To narzędzie pomaga określić potencjalne problemy z typami oraz składnią przed uruchomie-
niem kodu.

DevTools dodaje również ważną wartość do produktywności oferowanej przez plat-


formę Flutter; więcej informacji można znaleźć w rozdziale 12., „Testowanie, debugo-
wanie i wdrażanie”.

Ł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:

Dokumentacja i przewodniki są bardzo jasne i pouczające; ponadto niesamowita społeczność


pomaga programistom uczyć się bez problemów.

Sprawdź oficjalne przewodniki Darta dotyczące nauki: https://dart.dev/guides.

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.

Jedną z doskonałych funkcji, która to ilustruje, jest async-await:

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.

Ponieważ Dart koncentruje się na programistach mobilnych i internetowych, kolejną ważną


dla nich funkcją jest tworzenie interfejsów użytkownika. Mając to na uwadze, składnia Dart jest
łatwa do zrozumienia, gdy myślisz w kategoriach interfejsu użytkownika. Zobaczmy przykład
na następnej stronie.

Te zrzuty ekranu pochodzą z oficjalnej strony Dart: dart.dev.

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.

Podstawy języka Dart


Jeśli znasz już języki programowania inspirowane starym językiem C lub masz doświadczenie
z JavaScriptem, większość składni Dart będzie dla Ciebie łatwa do zrozumienia. Dart zapewnia
najbardziej typowe operatory do manipulowania zmiennymi. Jego wbudowane typy są najczęściej
spotykane w językach programowania wysokiego poziomu, z kilkoma szczegółami. Bardzo
podobne są również funkcje oraz kontrola danych. Przyjrzyjmy się nieco strukturze języka pro-
gramowania Dart, zanim przejdziemy do Fluttera.

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

Jak mogłeś zauważyć, wywołujemy metodę na x, co oznacza, że x jest instancją klasy,


która ma metody. W Dart wszystko jest instancją Object; każdy zdefiniowany typ jest
również instancją Object. Więcej na ten temat można znaleźć w sekcji „Wprowadzenie
do OOP w Dart”.

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.

Dart posiada następujące operatory:


 arytmetyczne,
 inkrementacji i dekrementacji,
 równości i relacji,
 sprawdzania typów i rzutowania,
 logiczne,
 manipulacji bitami,
 null-safe i null-aware (nowoczesne języki programowania zapewniają ten operator,
aby ułatwić obsługę wartości null).

Przyjrzyjmy się każdemu bardziej szczegółowo.

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.

Dart zapewnia również operatory skrótów do łączenia przypisania do zmiennej po innej


operacji. Operatory arytmetyczne lub skróty przypisania to + =, - =, * =, / = i ~ / =.

Operatory inkrementacji i dekrementacji


Operatory inkrementacji i dekrementacji są również dostępne i są implementowane w typie
liczbowym w następujący sposób:
 ++var lub var++, aby zwiększyć o 1 wartość var,
 --var lub var-- aby zmniejszyć o 1 wartość var.

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.

Operatory równości i relacyjne


Operatory równości Dart są następujące:
 == — do sprawdzania, czy operandy są równe;
 != — do sprawdzania, czy operandy są różne.

W przypadku testów relacyjnych operatory są następujące:


 > — do sprawdzania, czy lewy operand jest większy niż prawy;
 < — do sprawdzania, czy lewy operand jest mniejszy od prawego;
 >= — do sprawdzania, czy lewy operand jest większy lub równy prawemu;
 <= — do sprawdzania, czy lewy operand jest mniejszy lub równy prawemu;

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.

Sprawdzanie typów i rzutowanie


Jak już wiesz, Dart ma opcjonalną możliwość podawania typu, więc operatory sprawdzania
typów mogą być przydatne w czasie wykonywania:
 is — do sprawdzania, czy operand ma testowany typ;
 is! — do sprawdzania, czy operand nie ma testowanego typu.

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 |=.

Operatory null-safe oraz null-aware


Podążając za trendem współczesnych języków OOP, Dart zapewnia składnię bezpieczną dla
wartości null, która zwraca wyrażenie null / nie-null.

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

Oprócz typowego operatora przypisania = i operatorów wymienionych Dart zapewnia rów-


nież kombinację między przypisaniem a wyrażeniem null-aware; to znaczy operator ??=, który
przypisuje wartość do zmiennej tylko wtedy, gdy jej bieżąca wartość jest równa null.
Dart zapewnia również operator dostępu null-aware ?., który uniemożliwia dostęp do ele-
mentów składowych obiektu o wartości null.

Zmienne i typy Darta


Prawdopodobnie wiesz już, jak zadeklarować prostą zmienną, używając słowa kluczowego
var, po którym następuje nazwa. Należy zwrócić uwagę na to, że gdy nie określimy wartości
początkowej zmiennej, przyjmuje ona wartość null bez względu na jej typ.

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;

Wartości zmiennej value nie można zmienić po jej zainicjowaniu:


const 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)

Spowoduje to ustawienie wartości obu zmiennych w czasie kompilacji, zamieniając je w cał-


kowicie niezmienne zmienne.

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.

W JavaScripcie liczby są kompilowane do obiektów Number i dopuszczają wartości od


-253 do 253-1.
Ponadto należy pamiętać, że typów num, double i int nie można rozszerzać ani im-
plementować.

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.

Mapy Darta to dynamiczne kolekcje do przechowywania wartości na podstawie kluczy, w których


pobieranie i modyfikacja wartości są zawsze wykonywane przy użyciu skojarzonego klucza.
Zarówno klucz, jak i wartość mogą mieć dowolny typ; jeśli nie określimy typów klucz-wartość,
zostaną one wywnioskowane przez Darta jako Map <dynamic, dynamic>, z kluczami i warto-
ściami typu dynamicznego. Później powiemy coś więcej na temat typów dynamicznych.

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

print("Długość ciągu znaków: ${someString.length} ");


// wyświetla: Długość ciągu znaków: 16
}

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:

Typ Przykład literału


int 10, 1, -1, 5 i 0
double 10.1, 1.2, 3.123 i -1.2
bool true i false
String ”Dart”, ‘Dash’ i ”””ciąg wielowierszowy”””
List [1, 2, 3] i ["jeden", "dwa", "trzy"]
Map {"klucz1": "wartosc1", "b": 2}

Literał to notacja reprezentująca stałą wartość w językach programowania. Prawdopo-


dobnie używałeś już niektórych z nich wcześniej.

Inferencja typów — wprowadzenie dynamiki do pokazu


W poprzednich przykładach zademonstrowaliśmy dwa sposoby deklarowania zmiennych:
używając typu zmiennej, na przykład int i String, lub używając słowa kluczowego var.

Teraz możesz się więc zastanawiać, skąd Dart wie, jaki to typ zmiennej, jeśli nie określisz go
w deklaracji.

Z dokumentacji Dart (https://dart.dev/guides/language/effective-dart/documentation) możesz się


dowiedzieć, że:

„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”.

Oznacza to, że po zadeklarowaniu zmiennej analizator Dart wywnioskuje typ na podstawie


literału lub konstruktora obiektu.

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.

Rozważmy więc następujący kod:


main() {
var a; // tutaj nie zainicjalizowaliśmy var, więc
// typ jest dynamiczny
a = 1; // teraz a jest typu int
a = "a"; // a teraz typu String

print(a is int); // wyświetla false


print(a is String); // wyświetla true
print(a is dynamic); // wyświetla true
print(a.runtimeType); // wyświetla String
}

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.

Przepływ sterowania i pętle


Sprawdziliśmy, jak używać zmiennych i operatorów Dart do tworzenia wyrażeń warunkowych.
Aby pracować ze zmiennymi i operatorami, zazwyczaj musimy zaimplementować pewien prze-
pływ sterowania, dzięki czemu nasz kod Dart obierze odpowiedni kierunek w naszej logice.

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

 pętla for, while i do-while,


 break i continue,
 asserts,
 wyjątki z try / catch i throw.

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.

Próba wywołania zmiennej niebędącej funkcją może spowodować błąd kompilatora.

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?
}

Aby zdefiniować domyślną wartość parametru, należy dodać ją po znaku = bezpośrednio po


definicji parametru:
sayHello(String name, [String additionalMessage = "Witamy w funkcjach Dart!"]) =>
"Witaj $name. $additionalMessage";

Brak określenia parametru skutkuje wyświetleniem domyślnego komunikatu, jak poniżej:


void main() {
var hello = sayHello('mój przyjacielu');
print(hello);
}

43

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 Parametry opcjonalne nazwane — opcjonalna definicja nazwanego parametru


jest tworzona przy użyciu składni {}. Musi on też mieć wszystkie wymagane
parametry:
sayHello(String name, {String additionalMessage}) => "Witaj $name. $
additionalMessage";

Obiekt wywołujący musi określić nazwę opcjonalnego nazwanego parametru w następujący


sposób:
void main() {
print(sayHello('mój przyjacielu'));
// argument jest opcjonalny, wyświetla: Witaj mój przyjacielu. Null

print(sayHello('my friend', additionalMessage: "Jak się masz?"));


// wyświetla: Witaj mój przyjacielu. Jak się masz?
}

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";

Ponownie, do funkcji wywołującej musimy podać nazwę wymaganego parametru nazwanego:


void main() {
var hello = sayHello('mój przyjacielu', additionalMessage:"Jak się masz?");
// brak określenia nazwy parametru spowoduje wyświetlenie wskazówki
//w edytorze lub ręczne uruchomienie dartanalyzer w konsoli
print(hello); // wyświetla "Witaj mój przyjacielu. Jak się masz?"
}
 Funkcje anonimowe — funkcje Darta są obiektami i mogą być przekazywane
jako parametry do innych funkcji. Widzieliśmy to już podczas korzystania z funkcji
forEach() dla elementu iterowalnego.

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();
}

Jeśli przeanalizujesz powyższy kod, funkcja globalFunction z simpleFunction zostanie użyta


zamiast wersji globalnej, ponieważ jest zdefiniowana lokalnie w swoim zakresie.

W funkcji main natomiast używana jest wersja globalna funkcji globalFunction, ponieważ
w tym zakresie wewnętrzna funkcja globalFunction z simpleFunction nie jest zdefiniowana.

Struktury danych, kolekcje i typy ogólne


Dart zapewnia wiele rodzajów struktur do manipulowania zestawem wartości. Listy są sze-
roko stosowane nawet w najprostszych aplikacjach. Typy ogólne są stosowane podczas pracy
z kolekcjami danych powiązanymi z określonym typem, takim jak List lub Map. Zapewniają,
że zbiór będzie miał jednorodne wartości — poprzez określanie typu danych, które może
przechowywać.

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.

Kiedy i po co używać typów ogólnych


Zastosowanie typów ogólnych może pomóc deweloperowi w utrzymaniu i kontrolowaniu za-
chowania kolekcji. Kiedy używamy kolekcji bez określenia dozwolonych typów elementów,
naszym obowiązkiem jest prawidłowe wstawienie elementów. W szerszym kontekście może
to się stać kosztowne, ponieważ musimy wdrożyć walidacje, aby zapobiec błędnym wstawie-
niom i udokumentować to dla zespołu.

45

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Rozważmy następujący przykład kodu; jeśli nazwaliśmy zmienną avengerNames, spodziewamy


się, że będzie to lista nazwisk i nic więcej. Niestety, w zakodowanej formie możemy również
wstawić liczbę do listy, powodując dezorganizację lub zamieszanie:
main() {
List avengerNames = ["Hulk", "Captain America"];
avengerNames.add(1);
print("nazwy Avenger: $avengerNames");
// wyświetla nazwy Avenger: [Hulk, Captain America, 1]
}

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");
}

Typy ogólne i literały Dart


Jeśli przejrzysz przykłady map i list z tego rozdziału, zobaczysz, że użyliśmy literałów [] i {}
do ich zainicjowania. W przypadku typów ogólnych możemy określić typ podczas inicjalizacji,
dodając przedrostek <elementType>[] dla list i <keyType, elementType>{} dla map.

Spójrz na następujący przykład:


main() {
var avengerNames = <String>["Hulk", "Captain America"];
var avengerQuotes = <String, String>{
"Captain America": "I can do this all day!",
"Spider Man": "Am I an Avenger?",
"Hulk": "Smaaaaaash!"
};
}

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

Wprowadzenie do OOP w języku Dart


W Dart wszystko jest obiektem, łącznie z typami wbudowanymi. Po zdefiniowaniu nowej
klasy, nawet jeśli niczego nie rozszerzasz, będzie ona potomkiem obiektu. Dart pośrednio
robi to za Ciebie.

Dart to prawdziwy język zorientowany obiektowo. Nawet funkcje są obiektami, co oznacza,


że możesz wykonać następujące czynności:
 Przypisać funkcję jako wartość zmiennej.
 Przekazać ją jako argument do innej funkcji.
 Zwrócić ją jako wynik funkcji, tak jak w przypadku każdego innego typu, takiego
jak String i int.

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).

Obowiązują również następujące zasady:


 Dart tworzy niejawne metody pobierające i ustawiające wszystkie pola w klasie,
dzięki czemu można zdefiniować, w jaki sposób dane są dostępne dla
konsumentów i jak się zmieniają.
 W Dart, jeśli identyfikator (klasa, członek klasy, funkcja najwyższego poziomu
lub zmienna) zaczyna się od podkreślenia (_), jest prywatna dla jego biblioteki.

48

d0765ad53fb82babda2278a311da7afb
d
Rozdział 1. • Wprowadzenie do języka Dart

Z definicją bibliotek zapoznamy się w rozdziale 2., „Średnio zaawansowane programo-


wanie w Dart”. W tym miejscu omówimy również bardziej szczegółowo, jak działa pry-
watność w 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.

Przejrzeliśmy kilka ważnych koncepcji języka Dart z przydatnymi linkami do oficjalnych


przewodników. Dodatkowo przejrzeliśmy funkcje i specyfikacje parametrów, takie jak nazwane
/ pozycyjne i opcjonalne / wymagane, oraz zrobiliśmy wprowadzenie do OOP.

W następnym rozdziale przejdziemy do koncepcji programowania OOP w języku Dart i jego


cech szczególnych. Przeanalizujemy również kilka ważnych zaawansowanych funkcji Dart do
programowania, zwłaszcza gdy mówimy o rozwoju Fluttera, takich jak programowanie asynchro-
niczne z wykorzystaniem Futures, testy jednostkowe oraz koncepcja pakietów i bibliotek —
prawdopodobnie najważniejsze zagadnienie przy tworzeniu aplikacji Flutter. Sprawdź więc
następny rozdział, aby zapoznać się z bardziej zaawansowanymi tematami dotyczącymi Darta.

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.

W tym rozdziale omówiono następujące tematy:


 Składnia definicji klasy Darta.
 Klasy abstrakcyjne, interfejsy i domieszki.
 Biblioteki i pakiety Darta.
 Dodawanie zależności za pomocą pliku pubspec.yaml.
 Programowanie asynchroniczne z obiektami typu futures i isolates.
 Wprowadzenie do testów jednostkowych.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Klasy i konstruktory w języku Dart


Klasy Darta są deklarowane przy użyciu słowa kluczowego class, po którym następuje nazwa
klasy, klasy nadrzędne i zaimplementowane interfejsy. Następnie treść klasy jest ujęta w parę
nawiasów klamrowych, do których można dodać elementy składowe klasy, takie jak:
 Pola — są to zmienne używane do definiowania danych, które obiekt może
przechowywać.
 Metody dostępu — metody pobierające i ustawiające, jak sugeruje nazwa,
są używane do uzyskiwania dostępu do pól klasy, gdzie get służy do pobierania
wartości, a metoda set służy do modyfikowania odpowiedniej wartości.
 Konstruktor — jest to metoda używana przy tworzeniu klasy, w której inicjalizowane
są pola instancji obiektu.
 Metody — zachowanie obiektu jest definiowane przez działania, które może wykonać.
To są funkcje obiektu.

Zapoznaj się z następującym przykładem definicji małej klasy:


class Person {
String firstName;
String lastName;

String getFullName() => "$firstName $lastName";


}

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).

Typ wyliczeniowy enum


Typ wyliczeniowy jest typem powszechnym używanym w większości języków do reprezento-
wania zestawu skończonych wartości stałych. W Dart nie jest inaczej. Używając słowa klu-
czowego enum, po którym następują stałe wartości, można zdefiniować typ wyliczeniowy:
enum PersonType {
student, employee
}

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;
...
}

Następnie możemy go używać tak jak każdego innego pola:


main() {
print(PersonType.values); // wyświetla [PersonType.student,
//PersonType.employee]
Person somePerson = new Person();
somePerson.type = PersonType.employee;
print(somePerson.type); // wyświetla PersonType.employee
print(somePerson.type.index); // wyświetla 1
}

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.

Składnia kaskadowa pobiera pierwszą zwróconą wartość (w tym przypadku zwróconą


przez new Person()) i zawsze operuje na tej wartości, ignorując następne zwracane
wartości.

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;

Person(String firstName, String lastName) {


this.firstName = firstName;
this.lastName = lastName;
}

String getFullName() => "$firstName $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() {}

Konstruktor nazwany to sposób definiowania alternatywnych konstruktorów dla klasy. W poprzed-


nim przykładzie zdefiniowaliśmy alternatywny konstruktor dla klasy Person bez nazwy.

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);
}

class Employee extends Person {


Employee(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]);

factory Person.fromType([PersonType type]) {


switch (type) {
case PersonType.employee:
return new Employee();
case PersonType.student:
return new Student();
}
return Person();
}
String getFullName() => "$firstName $lastName";
}

enum PersonType { student, employee }

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.

Metody dostępu — pobierające i ustawiające


Jak wspomniano wcześniej, metody pobierające i ustawiające pozwalają nam na dostęp do
pola w klasie, a każde pole ma metody dostępu, nawet jeśli ich nie definiujemy. W poprzednim
przykładzie dla klasy Person, gdy wykonujemy instrukcję somePerson.firstName = "Peter",
wywołujemy metodę dostępu set do pola firstName i podajemy jej wartość "Peter" jako pa-
rametr. Również w tym przykładzie jest używana metoda dostępu get — gdy wywołujemy
metodę getFullName() i łączymy obie nazwy.

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() {}

String get fullName => "$firstName $lastName";


String get initials => "${firstName[0]}. ${lastName[0]}.";
}

main() {
Person somePerson = new Person("clark", "kent");

print(somePerson.fullName); // wyświetla clark kent


print(somePerson.initials); // wyśwetla c. k.

somePerson.fullName = "peter parker";


// nie zdefiniowaliśmy metody ustawiającej dla fullName, więc się nie kompiluje
}

W odniesieniu do poprzedniego przykładu można poczynić następujące ważne obserwacje:


 Nie mogliśmy zdefiniować metody pobierającej lub ustawiającej pola: firstName
i lastName. To dałoby nam błąd kompilacji, ponieważ nazwy członków klasy nie
mogą się powtarzać.
 Funkcja pobierająca initials spowodowałaby błąd dla profilu osoby utworzonego
za pomocą konstruktora nazwanego anonymous, ponieważ nie miałby on wartości
firstName i lastName (mają wtedy wartości null).
 Nie musimy zawsze definiować razem metod get i set. Jak widać, zdefiniowaliśmy
tylko metodę pobierającą dla fullName, więc nie możemy zmodyfikować pola
fullName. (Powoduje to błąd kompilacji, jak wskazano wcześniej).

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.).

Pola i metody statyczne


Jak już wiesz, pola to nic innego jak zmienne przechowujące wartości obiektów, a metody to
proste funkcje, które reprezentują działania obiektu. W niektórych przypadkach możesz
chcieć udostępnić wartość lub metodę między wszystkimi instancjami obiektów klasy. W tym
przypadku możesz dodać do nich modyfikator static w następujący sposób:

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”
}

Stąd możemy zmienić wartość pola statycznego bezpośrednio w klasie:


main() {
Person somePerson = Person("clark", "kent");
Person anotherPerson = Person("peter", "parker");

print(somePerson.fullName); // wyświetla Imię osoby: clark kent


print(anotherPerson.fullName); // wyświetla Imię osoby: peter park

Person.personLabel = "imię:";

print(somePerson.fullName); // wyświetla imię: clark kent


print(anotherPerson.fullName); // wyświetla imię: peter parker
}

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:";

static void printPerson (osoba osoba) {


print ("$personLabel ${person.firstName} ${person.lastName}");
}
}

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ę:";

Person.printsPerson(somePerson); // wyświetla imię: clark kent


Person.printsPerson(anotherPerson); // wyświetla imię: peter park
}

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

String get fullName => "$ firstName $ lastName";

58

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

}
main() {
Person somePerson = Person("clark", "kent");
Person anotherPerson = Person("peter", "parker");

print(somePerson.fullName); // wyświetla clark kent


print(anotherPerson.fullName); // wyświetla peter parker

Person.printsPerson(somePerson); // wyświetla Imię osoby: clark kent


Person.printsPerson(anotherPerson); // wyświetla Imię osoby: peter park
}

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;

Student(String firstName, String lastName, this.nickName)


: super(firstName, lastName);

@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
}

W odniesieniu do poprzedniego przykładu można poczynić następujące obserwacje:


 Student — klasa Student definiuje własnego konstruktora. Jednakże wywołuje
konstruktora klasy Person, przekazując wymagane parametry. Odbywa się to za
pomocą słowa kluczowego super.
 @override — w klasie Student istnieje zastąpiona metoda toString(). W tym miejscu
dziedziczenie ma sens — zmieniamy zachowanie klasy nadrzędnej (w tym przypadku
Object) w klasie potomnej.
 print(student) — jak widać w instrukcji print(student), nie wywołujemy żadnej
metody; metoda toString() jest wywoływana niejawnie.

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.

Interfejsy, klasy abstrakcyjne i domieszki


W Dart abstrakcyjne klasy i interfejsy są ze sobą ściśle powiązane. Dzieje się tak, ponieważ
Dart implementuje interfejsy w nieco inny sposób niż większość typowych języków.

Przyjrzyjmy się najpierw klasom abstrakcyjnym, zanim połączymy je z tematem interfejsów


niejawnych.

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);

String get fullName;


}

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;

Student(this.firstName, this.lastName, this.nickName);

@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.

Domieszki — dodawanie zachowania do klasy


W OOP domieszki są sposobem na włączenie funkcji do klasy bez potrzeby tworzenia skoja-
rzeń, takich jak dziedziczenie.

Najczęstsze konteksty, pozwalające na używanie domieszek, występują w miejscach, w których


może być potrzebne wielokrotne dziedziczenie, ponieważ jest to łatwy sposób na użycie przez
klasy wspólnych funkcji.

W Dart istnieje kilka sposobów na zadeklarowanie domieszki:


 Deklarując klasę i używając jej jako domieszki, pozwalając na użycie jej również
jako obiektu.
 Deklarując klasę abstrakcyjną, zezwalając na użycie jej jako domieszki lub na jej
dziedziczenie, ale bez tworzenia instancji.
 Deklarując ją jako domieszkę, pozwalając na użycie jej tylko jako domieszki.

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...");
}
}

W poprzednim przykładzie utworzyliśmy dwie klasy umiejętności zawodowych: Programming


Skills i ManagementSkills. Teraz możemy ich użyć, dodając słowo kluczowe with do definicji
klasy, na przykład:
class SeniorDeveloper extends Person with ProgrammingSkills,
ManagementSkills {
SeniorDeveloper(String firstName, String lastName) : super(firstName,
lastName);
}

class JuniorDeveloper extends Person with ProgrammingSkills {


JuniorDeveloper(String firstName, String lastName) : super(firstName,
lastName);
}

Obie klasy będą miały metodę coding() bez konieczności implementowania jej w każdej klasie,
ponieważ jest już zaimplementowana w domieszce ProgrammingSkills.

Jak wspomniano wcześniej, istnieje wiele sposobów zadeklarowania domieszki. W poprzed-


nim przykładzie użyliśmy prostej definicji klasy. W ten sposób klasa ProgrammingSkills może
zostać rozszerzona jak zwykła klasa lub nawet zaimplementowana jako interfejs (tracąc właściwość
domieszki):
class AdvancedProgrammingSkills extends ProgrammingSkills {
makingCoffee() {
print("robienie kawy...");
}
}

Tworzenie AdvancedProgrammingSkills w ten sposób nie sprawia, że jest to domieszka.


Klasy domieszek muszą rozszerzać klasę Object i nie deklarować konstruktora.

Innym sposobem zapisania domieszki jest użycie słowa kluczowego mixin:


mixin ProgrammingSkills {
coding() {
print("pisanie kodu ...");
}
}

mixin ManagementSkills {

63

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

manage() {
print("zarządzanie projektem ...");
}
}

Pisanie domieszek w ten sposób zapobiega niepożądanemu zachowaniu, ponieważ domieszki


nie mogą być rozszerzone i są przeznaczone do prawidłowego użycia. Klasy zawodów, które uży-
wają domieszek, pozostają takie same.

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, funkcje najwyższego poziomu


i zmienne
Dart jest bardzo elastyczny, jeśli chodzi o umożliwienie programistom przejęcia kontroli nad
wszystkimi fragmentami kodu, i w przeciwieństwie do wielu języków, można to zrobić na
różne sposoby.

Ponieważ Dart proponuje połączenie zalet nowoczesnych koncepcji OOP z tradycyjnymi,


zawsze możesz wybrać, kiedy i gdzie zastosować różne podejścia.

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");

print(shouldWrite("todo")); // prints false.


// ta funkcja wywołuje wywoływalną klasę ShouldWriteAProgram
// co powoduje niejawne wywołanie metody „call”
}

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.

Funkcje najwyższego poziomu i zmienne


W tym rozdziale zobaczyliśmy, że funkcje i zmienne w Dart można powiązać z klasami jako
składowe — pola i metody klas.

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.

Biblioteki i pakiety języka Dart


Biblioteki to sposób na strukturyzację projektu w oparciu o modułowość, która pozwala programi-
ście podzielić kod na wiele plików i udostępnić część kodu lub modułu innym programistom.

Wiele języków programowania korzysta z bibliotek, aby zapewnić programistom modułowość


— Dart nie jest inny. W Dart biblioteki odgrywają również inną ważną rolę poza strukturo-
waniem kodu. Oznacza to, że określają, co jest widoczne, a co nie dla innych bibliotek.

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.

Importowanie i korzystanie z biblioteki


W rozdziale 1., w sekcji „Funkcje”, zaimportowaliśmy metabibliotekę, aby skorzystać z adno-
tacji @required w niektórych parametrach. Teraz przyjrzyjmy się bardziej szczegółowo in-
strukcji import.

Aby zdefiniować bibliotekę, po prostu tworzymy plik Dart z jakimś kodem.

Spójrz na przykład example_1_importing, aby uzyskać bardziej przejrzystą wizualizację


bibliotek i instrukcji importu. Kod źródłowy tego rozdziału można znaleźć na GitHubie.

W tym przykładzie zdefiniowaliśmy prostą bibliotekę z klasami Person, Student i Employee


oraz typ wyliczeniowy PersonType:
// biblioteka person_lib - zawartość klas została obcięta dla zachowania zwięzłości

class Person {
String firstName;
String lastName;
PersonType type;

Person([this.firstName, this.lastName]);

String get fullName => "$firstName $lastName";


}

enum PersonType { student, employee }

66

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

class Student extends Person {


Student([firstName, lastName]): super(firstName, lastName) {
type = PersonType.student;
}
}
class Employee extends Person {
Employee([firstName, lastName]): super(firstName, lastName) {
type = PersonType.employee;
}
}

Aby ją zaimportować, możemy po prostu dodać instrukcję import library_path; na początku


pliku i przed jakimkolwiek kodem:
import ‘person_lib.dart’;

void main() {

Person person = Person("Clark", "Kent");


// pominięto opcjonalne słowo kluczowe ‘new’

Person student = Student("Clark", "Kent");

print("Osoba: ${person.fullName}, typ: ${person.type}");


print("Student: ${student.fullName}, typ: ${student.type}");
}

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.

Importowanie typu show i hide


Jeśli spojrzysz na poprzedni przykład, zauważysz, że nie wykorzystaliśmy wszystkich dostęp-
nych klas z biblioteki person_lib. Aby kod był bardziej przejrzysty i mniej podatny na błędy
i konflikty nazw, możemy użyć słowa kluczowego show, pozwalającego nam importować tylko
identyfikatory, których chcemy efektywnie używać w naszym kodzie:
// import ‘person_lib.dart’ show Person, 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;

Import prefiksów do bibliotek


W Dart nie ma definicji przestrzeni nazw ani czegoś, co jednoznacznie identyfikuje bibliotekę
w kontekście, w którym jest używana, więc podczas tworzenia nazw identyfikatorów mogą
wystąpić konflikty; to znaczy, że biblioteki mogą definiować funkcję najwyższego poziomu lub
nawet klasę o tej samej nazwie. Chociaż możemy użyć modyfikatorów show i hide, aby jawnie

67

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

ustawić, co chcemy importować z biblioteki, nie jest to wystarczające do rozwiązania problemu,


ponieważ czasami możemy być zainteresowani jakąś klasą lub funkcją najwyższego poziomu
o tej samej nazwie w różnych bibliotekach:

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}");

libraryB.Person personB = libraryB.Person (); // Osoba ‘b’ nie ma


// żadnych argumentów
print ("Osoba B: ${personB}");
}

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.

Kod źródłowy tego rozdziału można znaleźć w serwisie GitHub.


Jak pamiętasz z rozdziału 1., słowo kluczowe as jest również używane do rzutowania
z nadtypu do podtypu.

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

Tworzenie bibliotek Darta


Biblioteka Darta może składać się z jednego pliku lub wielu plików. Tak naprawdę, gdy two-
rzysz plik, tworzysz małą bibliotekę. Ale jeśli wolisz, możesz podzielić definicję biblioteki na
wiele plików. Chociaż jest to mniej powszechne, może być przydatne w zależności od kontek-
stu, szczególnie podczas pracy na przykład z bardzo zależnymi klasami.

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.

Zanim przejdziemy do alternatywnych sposobów definiowania biblioteki, musimy przyjrzeć


się prywatności biblioteki; pomaga to w hermetyzacji, ułatwiając zrozumienie, czy musimy
poprawnie podzielić bibliotekę na wiele plików, czy też nie.

Prywatność elementów biblioteki


Najpopularniejszy sposób kontrolowania prywatności (enkapsulacji kodu — code encapsula-
tion) w większości języków odbywa się na poziomie klasy. Dzieje się tak przez dodanie okre-
ślonego słowa kluczowego, które identyfikuje poziom dostępu członka, np. protected oraz
private w języku Java. Weźmy na przykład pod uwagę następujący diagram:

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 _.

Meta pakiet Darta zapewnia adnotację @protected. Po dodaniu do elementu członkow-


skiego klasy wskazuje, że element członkowski powinien być używany tylko wewnątrz
klasy lub jej podtypów.

70

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

Ponadto należy zauważyć, że ta część Darta prawdopodobnie ulegnie zmianie w przyszłych


wersjach, ponieważ część społeczności Dart uległa wpływowi Java i innych języków zorientowa-
nych obiektowo, w których kontrola prywatności odbywa się na poziomie klasy.

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});

String toString() => "($_type): $firstName $lastName";


}

enum PersonType { student, employee }

class Student extends Person {


Student({firstName, lastName})
: super(firstName: firstName, lastName: lastName) {
_type = PersonType.student;
}
}

class Programmer extends Person {


Programmer({firstName, lastName})
: super(firstName: firstName, lastName: lastName) {
_type = PersonType.employee;
}
}

71

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

W definicji pliku nie ma nic nowego do odnotowania — poza dwoma spostrzeżeniami:


 Plik sam w sobie jest biblioteką, więc nie musimy niczego jawnie deklarować.
 Pole _type jest prywatne dla biblioteki, to znaczy jest dostępne tylko za pomocą
kodu z tej samej biblioteki.

Powiedzmy, że próbujemy użyć tych klas z innej biblioteki w następujący sposób:


main() {
Programmer programmer = Programmer(firstName: "Dean", lastName: "Pugh");

// nie możemy uzyskać dostępu do właściwości _type, ponieważ jest ona prywatna
// dla biblioteki programmer._type = PersonType.employee;

print(programmer);
}

Jak widać, mamy dostęp do wszystkich publicznych identyfikatorów z wcześniej zdefiniowa-


nej biblioteki. Nie możemy uzyskać dostępu do właściwości _type, aby ustawić wartość, mimo
że w metodzie toString() klasy Person jej wartość jest ujawniona.

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.

Dzielenie bibliotek na wiele plików


Widzieliśmy podejście jednoplikowe do definiowania biblioteki, więc teraz przyjrzyjmy się,
jak podzielić definicję biblioteki na wiele plików, aby umożliwić nam zorganizowanie projektu
w małe elementy wielokrotnego użytku (co jest prawdziwym celem korzystania z bibliotek).

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";
}

Kilka spostrzeżeń związanych z powyższym kodem:


 Po słowie kluczowym library następuje identyfikator biblioteki, w tym przypadku
person. Dobrą praktyką jest nazywanie identyfikatora za pomocą tylko małych
liter i znaku podkreślenia jako separatora. Nasz przykład mógłby mieć dowolną
nazwę, taką jak person_lib lub person_library.
 Części biblioteczne (parts) są wymienione tuż pod definicją biblioteki.
 Sam kod niczego nie zmienia.

Składnia part jest zdefiniowana w następujący sposób:


 składnik PersonType jest zdefiniowany w pliku person_types.dart:
part of person;
enum PersonType { student, employee }
 składnik Student jest zdefiniowany w pliku student.dart:
part of person;

class Student extends Person {


Student({firstName, lastName})
: super(firstName: firstName, lastName: lastName) {
_type = PersonType.student;
}
}
 składnik Programmer jest zdefiniowany w pliku programmer.dart:
part of person;

class Programmer extends Person {


Programmer({firstName, lastName})
: super(firstName: firstName, lastName: lastName) {
_type = PersonType.employee;
}
}

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.

Przyjrzyjmy się poniższemu kodowi źródłowemu, który korzysta z biblioteki person:


import 'person_lib/person_library.dart';

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.

Składnia part się zmienia i prawdopodobnie zostanie wycofana w następnej wersji


Darta. W takim przypadku najbardziej prawdopodobną zmianą będzie utworzenie nowej
składni, która ją zastąpi.

Biblioteka wieloplikowa — instrukcja export


Jak już wspomniano, powyższe podejście nie jest idealnym sposobem podziału biblioteki Dart.
Dzieje się tak, ponieważ składnia instrukcji part prawdopodobnie zmieni się w przyszłych
wersjach. Ponadto może się wydawać, że jest to trochę przesadzone i trudne w użyciu, jeśli
chcesz tylko kontrolować widoczność członków 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;

Person({this.firstName, this.lastName, this.type});

74

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

String toString() => "($type): $firstName $lastName";


}

enum PersonType { student, employee }

W tym przypadku biblioteka person nie potrzebuje identyfikatora.

Biblioteka programmer importuje bibliotekę person, aby uzyskać dostęp do jej klasy Person:
// biblioteka programmer zdefiniowana w programmer.dart

import 'person_library.dart';

class Programmer extends Person {


Programmer({firstName, lastName})
: super(firstName: firstName, lastName: lastName, type:
PersonType.employee);
}

W ten sam sposób biblioteka student importuje bibliotekę person:


// biblioteka student zdefiniowana w student.dart

import 'person_library.dart';

class Student extends Person {


Student({firstName, lastName})
: super(
firstName: firstName,
lastName: lastName,
type: PersonType.student,
);
}

Spostrzeżenia dotyczące powyższego kodu:


 Biblioteki programmer oraz student muszą zaimportować bibliotekę person, aby ją
rozszerzyć.
 Ponadto właściwość type z klasy Person została upubliczniona przez usunięcie
prefiksu _. Oznacza to, że mogą uzyskać do niej dostęp inne biblioteki. Ponieważ
właściwość type w tym przypadku nie jest przeznaczona do zmiany i jest inicjowana
w konstruktorze, jest typu final.

Przyjrzyjmy się aplikacji wykorzystującej biblioteki w następujący sposób:


import 'person_lib/programmer.dart';
import 'person_lib/student.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.

W naszym przykładzie najlepszym wyborem będzie biblioteka person:


export 'programmer.dart';
export 'student.dart';

class Person { ... }

enum PersonType { ... }

W ten sposób korzystająca z biblioteki aplikacja wyglądałaby następująco:


import 'person_lib/person_library.dart';

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.

Użycie pakietu biblioteki w projekcie sprawia, że jest to zależność bezpośrednia (immediate


dependency), a używana biblioteka może mieć własne zależności, zwane zależnościami prze-
chodnimi (transitive dependencies).

Jeśli korzystasz z DartPada, czas to zmienić; teraz będziesz potrzebować odpowiedniego


skonfigurowanego środowiska programistycznego Dart, ponieważ zaczniemy pracować
z pakietami.

Ogólnie rzecz biorąc, istnieją dwa rodzaje pakietów Dart: pakiety aplikacji i pakiety bibliotek.

Pakiety aplikacji a pakiety bibliotek


Nie wszystkie pakiety są przeznaczone do udostępniania; sama aplikacja jest również pakie-
tem. Te pakiety mogą być zależne od pakietów bibliotek, ale nie są przeznaczone do użycia
jako zależności w innych projektach.

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.

Chociaż istnieje możliwość zaimportowania biblioteki umieszczonej w podkatalogu src,


nie jest to zalecane, ponieważ ma to być jej wewnętrzna implementacja, a nie część jej
publicznego API. Może to wywołać zmianę działania aplikacji klienckiej.

 lib/simple_package_structure.dart — powszechną praktyką jest dodawanie jednego


lub kilku plików najwyższego poziomu, które eksportują (pamiętaj o instrukcji
export) lokalne src/ bibliotek. Nazwa tego pliku jest zwykle taka sama jak nazwa
pakietu. Jeśli istnieje więcej niż jedna biblioteka, nazwa musi być na tyle prosta,
aby identyfikować ogólne przeznaczenie eksportowanych bibliotek.

78

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

 test/ — testy jednostkowe i analizy porównawcze są zwykle umieszczane odpowiednio


w katalogach test oraz benchmark. Ponadto kod źródłowy w folderze testowym
jest zwykle poprzedzany identyfikatorem _test.

Aby zrozumieć, jak pisać testy jednostkowe, możesz zapoznać się z sekcją Wprowadzenie
do testów jednostkowych Darta.

 README.md, CHANGELOG.md i LICENSE — są to pliki zwykle obecne


w pakietach, które są przeznaczone do publikacji w jakimś publicznym repozytorium,
takim jak pub. Pliki te są również bardzo powszechne w projektach open source.
Czasami obecny jest również plik LICENSE, który określa informacje o prawach
autorskich do kodu źródłowego.
 example/ — jest to ważny element w publikowanych pakietach i może pokazać,
jak dany pakiet może być używany.
 analysis_options.yaml — jest to przydatny plik dla kontroli linta, analizy stylów
i innych sprawdzeń w czasie prekompilacji.

Możesz sprawdzić samouczek dostosowywania analizy w witrynie Dart pod adresem


https://dart.dev/guides/language/analysis-options.

Niektóre dodatkowe pliki związane są z przeznaczeniem projektu:


 tools/ — jest to katalog zawierający skrypty, których można użyć podczas
programowania, w tym narzędzia do manipulowania obrazami, plikami RAW
i wszelkiego rodzaju skryptami, które są prywatne dla pakietu i przydatne dla
programisty.
 doc/ i doc/api — tutaj możesz dodać przydatne informacje o projekcie;
api/podkatalog to miejsce, w którym narzędzie dartdoc (przedstawione
w rozdziale 1.) generuje dokumentację API na podstawie komentarzy kodu.

Pakiety webowe zawierają kilka nowych plików i katalogów; są one następujące:


 Folder lib/ jest typowym miejscem docelowym statycznych zasobów sieciowych,
takich jak obrazy lub pliki .css.
 web/ to katalog używany w projektach aplikacji internetowych. W przeciwieństwie
do folderu lib/, który ma zawierać kod biblioteki, ten ma zawierać kod źródłowy
aplikacji internetowej i punkty wejścia (czyli funkcję main()).

W pakietach uruchamianych z linii poleceń zawarty jest katalog bin:


 Katalog bin/ ma zawierać skrypt, który można uruchomić bezpośrednio z wiersza
poleceń; opisane poniżej narzędzie Stagehand jest takim przykładem.

Struktura projektu Flutter ma pewne podobieństwa do pakietów Dart, o czym więcej


dowiemy się w następnym rozdziale.

79

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Stagehand — generator projektów Darta


Rozpoczęcie nowego projektu Dart wymaga kilku prostych kroków: utworzenia pustego folderu,
dodania do niego pliku pubspec.yaml i opisania pakietu nazwą, wersją itp.

Następnie stopniowo dodajesz potrzebne pliki.

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.

Aby uruchomić narzędzie Stagehand, musimy najpierw zainstalować je w systemie. W prawi-


dłowo skonfigurowanym środowisku Dart uruchom w terminalu następujące polecenie pub:
pub global activate stagehand

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.

To polecenie pobiera pakiet (w tym przypadku Stagehand) z repozytorium pub i instaluje go


w katalogu pamięci podręcznej pakietów Dart w systemie. Położenie tego katalogu zależy
od systemu operacyjnego: $HOME/.pub-cache/bin w systemach opartych na systemie Linux
i AppData\Roaming\Pub\Cache\bin w systemie Windows.

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.

Po prawidłowym zainstalowaniu i skonfigurowaniu narzędzia Stagehand możesz rozpocząć


generowanie projektów Dart:
1. Najpierw utwórz pusty folder z żądaną nazwą pakietu.

Zapoznaj się z opisem pola name w sekcji Plik pubspec, aby zrozumieć, jak poprawnie
nazwać swój pakiet.

2. Następnie w utworzonym folderze wygeneruj strukturę pakietu za pomocą


następującego polecenia:
pub run global stagehand <template>

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.

Możesz użyć znaku #, aby rozpocząć tworzenie komentarza w yaml.

Zależności pakietów — pub


Teraz, gdy rozumiesz najważniejszą rolę pliku pubspec w pakiecie podczas tworzenia aplikacji
Dart, możesz dodać do swojego projektu zależności pakietów innych firm. Istnieją ważne po-
lecenia pub, z którymi można pracować podczas dodawania lub aktualizowania zależności pakietów
do projektu. Musimy również zademonstrować, jak poprawnie określić wersję zależności, której
mamy używać.
Po uruchomieniu nowego projektu Dart, ręcznie lub za pomocą narzędzia takiego jak Stage-
hand, pierwszą rzeczą, którą musisz zrobić, jest uruchomienie następującego polecenia:
pub get

Na przykład poniższy pakiet zawiera tylko następujący plik pubspec:

82

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

Dodatkowo pubspec zawiera następujące treści:


name: adding_dependencies

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

Otrzymujemy następujący pomyślny wynik:


Resolving dependencies...
Got dependencies!

Otrzymamy strukturę plików jak na poniższym zrzucie ekranu:

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

Składnia służąca do określenia zależności jest następująca:


<package>:
<constraints>

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

 Maksymalne ograniczenie — podobnie jak w poprzednim minimalnym


przykładzie, ale dla górnej granicy, możemy na dwa sposoby dodać maksymalną
akceptowalną wersję pakietu, jaką chcemy: json_serializable: '<2.0.1', gdzie
akceptujemy dowolną wersję poniżej określonej, lub json_serializable: '<= 2.0.1',
gdzie akceptujemy dowolną wersję niższą lub równą podanej.
 Zakres — łącząc minimalne i maksymalne ograniczenie, możemy określić
dopuszczalny przedział wersji: json_serializable: '> 1.0.0 <= 2.0.1',
json_serializable: '> 1.0.0 <2.0.1', json_serializable: '> = 1.0.0 <2.0.1'
lub json_serializable: '> = 1.0.0 <= 2.0.1'.
 Zakres semantyczny — jest podobny do zakresu, ale za pomocą znaku daszka
możemy określić zakres od minimalnej akceptowalnej wersji do następnej istotnej
zmiany. Na przykład json_serializable: ^ 1.0.0 jest tym samym co
json_serializable: '> = 1.0.0 <2.0.0', a json_serializable: ^ 0.1.0 jest
równe json_serializable: '> = 0.1.0 <0.2.0' .

Wersjonowanie semantyczne pomaga w korzystaniu z bibliotek przez społeczność i jest szeroko


stosowane. Aby zbadać to bardziej szczegółowo, możesz odwiedzić adres https://www.dartlang.
org/tools/pub/versioning.

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.

Zależności pakietów są podstawowym tematem w rozwoju Dart. Pamiętając o tych koncepcjach,


możesz dodać do swoich projektów przydatne zależności innych firm i zwiększyć produktywność.

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();

print("dalszy ciąg funkcji main");

for (int i = 10; i < 15; i++) {


sleep(Duration(seconds: 1));
print("index z funkcji main: $i");
}
print("koniec funkcji main");
}

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.

Teraz powiedzmy, że funkcja longRunningOperation() jest funkcją asynchroniczną i funkcja


main() może kontynuować wykonywanie bez czekania na zakończenie:
import 'dart:async';

Future longRunningOperation() async {


for (int i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
print("index: $i");
}
}
main() { ... } // funkcja main jest taka sama

Wprowadziliśmy kilka zmian, aby pokazać, jak działa obiekt Future:


 longRunningOperation() ma teraz modyfikator async wskazujący, że zwrócona
będzie funkcja typu Future, która zostanie zakończona po zakończeniu wykonywania
funkcji. Zwróć uwagę, że typem zwracanym jest również Future.
 Zastąpiliśmy wywołanie sleep() wywołaniem Future.delayed. Pokazuje to, jak
działa słowo kluczowe await — działa ono z funkcjami async. Podczas wywoływania
funkcji Future możemy potrzebować jej wyniku, aby kontynuować wykonywanie.
W takim przypadku chcemy przejść do wyświetlenia wyniku dopiero po określonym
opóźnieniu.

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();

print("dalszy ciąg funkcji main");

for (int i = 10; i < 15; i++) {


sleep(Duration(seconds: 1));
print("index z funkcji main: $i");
}

print("koniec funkcji main");


}

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

print("dalszy ciąg funkcji main ");

for (int i = 10; i < 15; i++) {


sleep(Duration(seconds: 1));
print("index z funkcji main: $i");
}
print("koniec funkcji main");
}

Spowoduje to, że metoda main() po prostu będzie kontynuować wykonywanie, co doprowadzi


do otrzymania następującego wyniku:
start długiej operacji
dalszy ciąg funkcji main
index: 0
index z funkcji main: 10
index: 1
index z funkcji main: 11
index: 2
index z funkcji main: 12
index: 3
index z funkcji main: 13
index: 4
index z funkcji main: 14
koniec funkcji main

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.

Zmieńmy poprzednią implementację, aby zamiast tego używała instancji Isolate:


import 'dart:io';
import 'dart:isolate';

Future<void> longRunningOperation(String message) async {


for (int i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
print("index: $i");
}
}

main() {
print("start długiej operacji");

Isolate.spawn(longRunningOperation, "Hello");

print("dalszy ciąg funkcji main");

for (int i = 10; i < 15; i++) {


sleep(Duration(seconds: 1));
print("index z funkcji main: $i");
}

print("koniec funkcji main");


}

Jak widać, kod ma drobne zmiany:


 Funkcja longRunningOperation() staje się instancją Isolate, czyli pozostaje prostą
funkcją.

90

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

 Aby przekazać proces Isolate do wykonania, używamy metody spawn () z klasy


Isolate. Potrzeba dwóch argumentów — funkcji, która ma zostać utworzona,
i parametru, który ma zostać przekazany do funkcji.

Uruchamiając powyższy kod, zauważysz inny wynik wyjściowy:


start długiej operacji
dalszy ciąg funkcji main
Hello
index z funkcji main: 10
index: 0
index z funkcji main: 11
index: 1
index z funkcji main: 12
index: 2
index z funkcji main: 13
index: 3
index z funkcji main: 14
koniec funkcji main

Teraz kod obu tych funkcji działa niezależnie po wykonaniu spawn na obiekcie Isolate.

Podczas kompilacji do JavaScriptu obiekty Isolate są przekształcane na workery we-


bowe. Więcej na temat workerów webowych możesz przeczytać w artykule W3Schools
pod adresem https://www.w3schools.com/html/html5_webworkers.asp.

Wprowadzenie do testów jednostkowych


w języku Dart
W każdym języku możemy napisać kod, który spełnia jakiś cel; jednakże aby napisać wydajny
i wolny od błędów kod, musimy użyć każdego dostępnego zasobu.

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

Pakiet test Darta


Pakiet test nie jest częścią samego zestawu SDK, więc musi zostać zainstalowany jako normalna
zależność innej firmy. Powinieneś już wiedzieć, jak to zrobić.

Aby uzyskać więcej informacji, sprawdź przykład 4_unit_tests w kodzie źródłowym


tego rozdziału na GitHubie. Kod testu znajduje się w folderze test/.

W tym przykładzie (wygenerowanym za pomocą narzędzia Stagehand) istnieje zależność progra-


mistyczna; jest ona wymagana tylko podczas fazy developmentu:
dev_dependencies:
test: ^1.0.0

Dzięki temu możemy używać bibliotek pakietu test do pisania testów jednostkowych.

Pisanie testów jednostkowych


Teraz załóżmy, że chcemy utworzyć funkcję sumującą dwie liczby:
class Calculator {
num sumTwoNumbers (num a, num b) {
// TODO
}
}

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();
});

test('calculator sumTwoNumbers() sum the both numbers', () {


expect(calculator.sumTwoNumbers(1, 2), 3);
});
}

W poprzednim przykładzie rozpoczęliśmy od zaimportowania biblioteki pakietu test, która udo-


stępnia funkcje, na przykład: setUp(), test() i expect(). Każda z funkcji ma określone zadania:
 setUp() wykona wywołanie zwrotne (callback), które przekazujemy przed każdym
z testów z zestawu testów.

92

d0765ad53fb82babda2278a311da7afb
d
Rozdział 2. • Średnio zaawansowane programowanie w języku Dart

 test() jest testem samym w sobie; otrzymuje opis i wywołanie zwrotne


z implementacją testową.
 expect() jest używana do tworzenia asercji dotyczących testu. W poprzednim
przykładzie po prostu tworzymy asercję, że suma 1 + 2 powinna dać liczbę 3.

Aby wykonać test, używamy następującego polecenia:


pub run test <test_file>

Dla poprzedniego przykładu polecenie wyglądałoby (z katalogu głównego projektu) w nastę-


pujący sposób:
pub run test test/calculator_tests.dart

Zanim skutecznie zaimplementujemy metodę sumTwoNumbers(), wynik testu będzie wyglądał


następująco:
00:01 +0 -1: calculator sumTwoNumbers() sum the both numbers [E]
Expected: <3>
Actual: <null>

package:test_api expect
test\calculator_tests.dart 12:7 main.<fn>

00:01 +0 -1: Some tests failed.

Po poprawnym zaimplementowaniu metody sumTwoNumbers() zobaczymy:


00:01 +1: All tests passed!

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);
});
});
}

Zwróć uwagę na wynik poprzedniego testu:


00:01 +1 -1: sum tests calculator sumTwoNumbers() sum null as it was 0 [E]
NoSuchMethodError: The method '_addFromInteger' was called on null.

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.

To było wprowadzenie do testów jednostkowych w Dart. O wszystkich możliwościach do-


wiesz się, zapoznając się z pakietem test pod adresem https://pub.dev/packages/test.

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ć.

Na koniec zbadaliśmy programowanie asynchroniczne przy użyciu obiektów future i isolate.


Poznaliśmy również testy jednostkowe Darta.

W następnym rozdziale zaczniemy korzystać z frameworku Flutter.

Dodatkowo będziesz kontynuował pracę z dotychczas zdobytą wiedzą na temat Darta.

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.

Postępuj zgodnie z instrukcjami konfiguracji środowiska frameworku Flutter:


https://flutter.dev/docs/get-started/install.

W tym rozdziale zostaną omówione następujące tematy:


 Porównanie z innymi platformami do tworzenia aplikacji mobilnych.
 Kompilacja Fluttera.
 Renderowanie Fluttera.
 Wprowadzenie do widżetów.
 Podstawowa struktura projektu wykonanego przy użyciu Fluttera.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Porównanie z innymi platformami


do tworzenia aplikacji mobilnych
Chociaż Flutter jest stosunkowo nowy, przez lata doczekał się wielu zmian. Na początku nosił
nazwę Sky — po pierwszym pojawieniu się na Dart Developer Summit 2015 i przedstawieniu
przez Erica Seidela. Został zaprezentowany jako ewolucja niektórych wcześniejszych ekspery-
mentów Google’a, mających na celu stworzenie czegoś lepszego dla telefonów komórkowych
pod względem rozwoju i wygody dla użytkownika, z głównym celem w postaci renderowania
z wysoką wydajnością. W 2016 roku został zaprezentowany jako Flutter, a wraz z pierwszą wersją
alfa w maju 2017 był już dostępny dla systemów iOS i Android. Potem zaczął dojrzewać, a społecz-
ność zaczęła rosnąć. Ewoluował aż do pierwszego stabilnego wydania pod koniec 2018 roku.

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.

Problemy, które Flutter chce rozwiązać


Od początku swojego istnienia framework Flutter miał zapewnić użytkownikowi lepsze wra-
żenia dzięki wydajnemu wykonywaniu aplikacji. Jednak to nie jedyna obietnica Fluttera. Sku-
piono się również na rozwiązaniu niektórych problemów z tworzeniem aplikacji mobilnych
na wiele platform:
 Długie / droższe cykle rozwojowe — aby sprostać wymaganiom rynku, musisz
zdecydować się na tworzenie dla pojedynczej platformy lub tworzenie wielu
zespołów. Ma to pewne konsekwencje pod względem kosztów, czasu wykonania
projektu i różnych możliwości natywnych frameworków.
 Wiele języków do nauczenia — jeśli programista chce programować na wiele
platform, musi nauczyć się robić coś w jednym systemie operacyjnym i języku
programowania, a później tego samego w innym systemie operacyjnym i języku
programowania. Ma to z pewnością wpływ na produktywność dewelopera.
 Długi czas kompilacji — niektórzy programiści mogli już doświadczyć, jak czas
kompilacji może wpływać na produktywność. Na przykład w Androidzie programiści
doświadczają niekiedy wielu długich czasów kompilacji po kilku minutach kodowania
(ta sytuacja ewoluuje i teraz jest o wiele lepiej, ale było to bardzo uciążliwe).

96

d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera

 Istniejące efekty uboczne rozwiązań wieloplatformowych — przyjmujesz istniejący


framework wieloplatformowy (czyli React Native, Xamarin, Ionic, Cordova) w celu
obejścia powyższych problemów, ale może to mieć wpływ na wydajność, projekt
lub wrażenia użytkownika.

Zobaczmy teraz, jak Flutter przeciwdziała tym problemom.

Różnice między istniejącymi frameworkami


Istnieje wiele wysokiej jakości i akceptowalnych frameworków i technologii.

Oto niektóre z nich:


 Xamarin,
 React Native,
 Ionic,
 Cordova.

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.

Przyjrzyjmy się bardziej szczegółowo każdemu z tych punktó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.

Kilka punktów, które sprawiają, że wydajność Fluttera jest świetna:


 Flutter jest właścicielem pikseli — framework ten renderuje aplikację piksel
po pikselu (patrz następna sekcja), współdziałając bezpośrednio z silnikiem
graficznym Skia.
 Brak dodatkowych warstw lub dodatkowych wywołań API systemu operacyjnego
— ponieważ Flutter sam renderuje aplikacje, nie potrzebuje dodatkowych
wywołań, aby korzystać z widżetów OEM.
 Flutter jest kompilowany do kodu natywnego — framework używa kompilatora
Dart AOT do tworzenia kodu natywnego. Oznacza to, że nie ma narzutów
w konfigurowaniu środowiska do interpretowania kodu Dart w locie i działa tak
jak natywna aplikacja, uruchamiając się szybciej niż frameworki, które wymagają
jakiegoś interpretera.

Pełna kontrola nad interfejsem użytkownika


Platforma Flutter tworzy samodzielnie UI, renderując komponenty wizualne bezpośrednio
do elementów canvas, jak widzieliśmy wcześniej, nie wymagając niczego więcej niż elementy
canvas platformy, więc nie jest to ograniczone regułami i konwencjami. W większości przy-
padków frameworki po prostu odtwarzają to, co oferuje platforma w inny sposób. Na przykład
inne platformy oparte na WebView odtwarzają komponenty wizualne przy użyciu elementów
HTML ze stylami CSS. Inne frameworki emulują tworzenie komponentów wizualnych i prze-
kazują je na platformę urządzenia, która będzie renderować widżety OEM jak natywnie opra-
cowaną aplikację. Nie mówimy tutaj o wydajności, więc co jeszcze oferuje Flutter, nie korzy-
stając z widżetów OEM i wykonując zadanie samodzielnie?

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

Więcej o zestawach interfejsu użytkownika platformy przeczytamy w rozdziale 4.

 Osiągalne wymagania dotyczące projektowania interfejsu użytkownika: Flutter


zapewnia czysty i solidny interfejs API z możliwością odtwarzania layoutów, które
są wierne wymaganiom projektowym. W przeciwieństwie do frameworków
webowych, które opierają się na regułach CSS i mogą być duże i skomplikowane,
a nawet sprzeczne, Flutter upraszcza to, dodając reguły semantyczne, które
można wykorzystać do tworzenia złożonych, ale wydajnych i pięknych layoutów.
 Bardziej płynny wygląd i działanie: oprócz natywnych zestawów widżetów
Flutter stara się zapewnić natywne środowisko platformy, na której działa
aplikacja, więc czcionki, gesty i interakcje są implementowane w sposób specyficzny
dla platformy, zapewniając naturalne wrażenia użytkownika, podobnie jak
aplikacja natywna.

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.

Teraz przyjrzymy się bliżej Dartowi.

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.

Mając to na uwadze, Flutter potrzebował języka programowania, który pozwoliłby mu osią-


gnąć te cele, a Dart wydaje się idealnie pasować do frameworka z następujących powodów:
 Kompilacja AOT i JIT — Dart jest wystarczająco elastyczny, aby zapewnić różne
sposoby uruchamiania kodu, więc Flutter używa AOT z myślą o wydajności podczas
kompilowania aplikacji w wersji release oraz szybkiej kompilacji JIT dla aplikacji
w fazie rozwoju, mając na celu szybkie przepływy pracy i zmiany kodu.

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

 Łatwa nauka — Dart to elastyczny, solidny, nowoczesny i zaawansowany język.


Chociaż wciąż ewoluuje, ma dobrze zdefiniowaną strukturę zorientowaną
obiektowo, znaną z funkcji języków dynamicznych i statycznych, aktywną społeczność
i dobrze zorganizowaną dokumentację.
 Deklaratywny interfejs użytkownika — we Flutterze używamy deklaratywnego
stylu do układania widżetów, co oznacza, że są one niezmienne i stanowią tylko
lekkie „projekty”. Aby zmienić interfejs użytkownika, widżet wyzwala przebudowę
samego siebie i jego poddrzewa. W przeciwnym, imperatywnym stylu (najbardziej
powszechnym) możemy zmienić określone właściwości komponentów po ich
utworzeniu.

Uwaga: zapoznaj się z oficjalnym wprowadzeniem do deklaratywnego interfejsu użytkownika


Fluttera: https://flutter.dev/docs/get-started/flutter-for/declarative

 Składnia Darta dla layoutu — w odróżnieniu od wielu frameworków, które mają


oddzielną składnię dla layoutu, we Flutterze layout jest tworzony (razem z narzędziami
do debugowania wydajności renderowania layoutu) za pomocą kodu Dart, co ma
na celu większą elastyczność i łatwość tworzenia środowiska programistycznego.

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.

Wspierana przez Google’a platforma ma wszelkie narzędzia potrzebne do odniesienia sukcesu


w społeczności, do tego dochodzą wsparcie zespołu Google, obecność na dużych wydarze-
niach, takich jak Google IO, oraz inwestycje w ciągłe doskonalenie bazy kodu. Od premiery
trzeciej wersji beta na Google IO 2018 do pierwszej stabilnej wersji wydanej podczas Flutter
Live Event pod koniec 2018 roku jej rozwój jest ewidentny:
 ponad 200 milionów użytkowników aplikacji Flutter,
 ponad 3000 aplikacji Fluttera w Sklepie Play,
 ponad 250 000 nowych programistów,
 34. najpopularniejsze repozytorium oprogramowania na GitHubie — na początku
2019 roku znalazło się w pierwszej piętnastce.

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.

Framework open source


Wsparcie dużej firmy, takiej jak Google, jest fundamentalne dla frameworka takiego jak Flutter
(patrz na przykład frameworka React, który jest utrzymywany przez Facebooka). W miarę
wzrostu popularności rozwiązania jeszcze ważniejsze staje się wsparcie społeczności.

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.

Zasoby i narzędzia dla programistów


Wsparcie programistów frameworku Flutter dotyczy licznych kwestii, od dokumentacji i za-
sobów edukacyjnych po dostarczanie narzędzi pomagających zwiększyć produktywność:
 Dokumentacja i zasoby edukacyjne — strony internetowe Fluttera są bogate
w zasoby dla programistów pochodzących z innych platform. Znajduje się tu wiele
przykładów i przypadków użycia, na przykład słynne Google Codelabs
(https://codelabs.developers.google.com/?cat= Flutter).
 Narzędzia wiersza poleceń i integracja z IDE — narzędzia Dart, które pomagają
w analizowaniu, uruchamianiu i zarządzaniu zależnościami, są również częścią
Fluttera. Poza tym Flutter ma także polecenia pomagające w debugowaniu,
wdrażaniu, sprawdzaniu renderowania layoutu i integracji z IDE za pomocą
wtyczek Dart. Poniżej widać listę różnych poleceń.

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

Kompilacja Fluttera (Dart)


Sposób budowania aplikacji ma fundamentalne znaczenie dla tego, jak będzie ona działać na
platformie docelowej. To ważny krok dotyczący wydajności. Chociaż niekoniecznie musisz to
wiedzieć w przypadku każdego rodzaju aplikacji, wiedza o tym, jak zbudowana jest aplikacja,
pomaga zrozumieć i zmierzyć możliwe ulepszenia.

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.

Kompilacja w fazie rozwoju oprogramowania


W fazie rozwoju oprogramowania Flutter używa kompilacji JIT. Dzięki temu dostępne są
ważne funkcje programistyczne, takie jak hot reload, wspomniana w poprzedniej sekcji.
Ze względu na moc kompilatora Dart interakcje między kodem a symulatorem / urządzeniem
są naprawdę szybkie, a debugowanie umożliwia programistom obejrzenie kodu źródłowego.

Kompilacja dla wersji release


Dla wersji release debugowanie nie jest konieczne, a nacisk kładziony jest na wydajność.
Flutter wykorzystuje technikę powszechną dla silników gier. Korzystając z trybu AOT, kod
Dart jest kompilowany do kodu natywnego, a aplikacja ładuje bibliotekę Flutter i deleguje do
niej renderowanie, dane wejściowe i obsługę zdarzeń przy użyciu silnika Skia.

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”.

Nie będziemy wchodzić bardziej szczegółowo w aspekty kompilacji Fluttera, ponieważ


wykraczają one poza zakres tej książki. Aby uzyskać więcej informacji, możesz przeczytać
artykuły znajdujące się pod adresami: https://flutter.dev/docs/resources/faq#how-does-
-flutterrun-my-code-on-android i https://flutter.dev/docs/resources/faq#how-does-flutter-
-run-my-code-on-ios.

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

Frameworki i widżety OEM


Innym sposobem renderowania widżetów jest dodanie warstwy nad widżetami platformy, ale
bez zmiany sposobu, w jaki system efektywnie renderuje komponenty wizualne:

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.

Flutter — renderowanie samodzielnie


Flutter decyduje się wykonać całą ciężką pracę samodzielnie. Jedyne, czego potrzebuje do
stworzenia UI za pomocą platformy SDK, to dostęp do API usług i elementów Canva:

Flutter przenosi widżety i renderowanie do aplikacji, gdzie jest dostosowywany i rozszerzany.


Za pomocą elementów Canva może rysować wszystko, a także uzyskiwać dostęp do zdarzeń, aby
samodzielnie obsługiwać dane wejściowe i gesty użytkownika. Pośrednik we Flutterze jest
realizowany przez kanały platformy, o czym bardziej szczegółowo dowiemy się w rozdziale 13.,
„Poprawa komfortu użytkowania”.

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.

Terminy stan i reaktywność są dobrze znane w programowaniu w stylu React, rozpowszech-


nianym przez słynną bibliotekę React Facebooka.

Wszystko jest widżetem


Widżety Fluttera występują wszędzie w aplikacji. Może nie wszystko jest widżetem, ale prawie
wszystko. We Flutterze nawet aplikacja jest widżetem i dlatego ta koncepcja jest tak ważna.
Widżet reprezentuje część interfejsu użytkownika, ale nie oznacza to, że jest tylko czymś, co
jest widoczne. Może to być dowolny element z poniższych:
 Element wizualny / strukturalny, który jest podstawowym elementem
strukturalnym, takim jak widżety Button lub Text.
 Element specyficzny dla layoutu, który może definiować położenie, marginesy
lub wypełnienie, np. widżet Padding.
 Element stylu, który może pomóc w kolorowaniu i tworzeniu motywu elementu
wizualnego / strukturalnego, na przykład widżet Theme.
 Element interakcji, który pomaga reagować na interakcje na różne sposoby,
na przykład widżet GestureDetector.

W następnym rozdziale sprawdzimy przykłady użycia tych widżetów.

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.

Drzewo elementów ma we Flutterze ważne zadanie. Pomaga mapować elementy ekranowe do


drzewa widżetów. Określa również, w jaki sposób przebiega odbudowa widżetu w scenariuszach
aktualizacji. Kiedy widżet się zmieni i będzie wymagał przebudowy, spowoduje to aktualizację

108

d0765ad53fb82babda2278a311da7afb
d
Rozdział 3. • Wprowadzenie do Fluttera

odpowiedniego elementu. Element przechowuje typ odpowiedniego widżetu i odniesienie do


jego elementów podrzędnych. Na przykład w przypadku zmiany pozycji widżetu element spraw-
dzi typ odpowiedniego nowego widżetu, a jeśli będzie mu pasował, zaktualizuje się o nowy opis
widżetu.

Drzewo elementów można zobaczyć jako prerenderowane drzewo pomocnicze do drzewa


widżetów. Jeśli potrzebujesz więcej informacji na ten temat, możesz sprawdzić oficjalną
dokumentację: https://docs.flutter.io/flutter/widgets/Element-class.html.

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.

Uruchomienie powyższego polecenia spowoduje wygenerowanie folderu o podanej nazwie


z przykładowym projektem Fluttera. Za chwilę przeanalizujemy projekt. Po pierwsze, dobrze
jest wiedzieć, że istnieje kilka przydatnych opcji do manipulowania wynikowym projektem
za pomocą polecenia flutter create. Najważniejsze z nich to:
 --org — służy do zmiany organizacji właściciela projektu. Jeśli umiesz programować
na Androida lub iOS, to wiesz, że jest to odwrotna nazwa domeny i służy do
identyfikowania nazw pakietów w systemie Android oraz jako prefiks w identyfikatorze
pakietu iOS. Wartość domyślna to com.example.
 -s, --sample — większość oficjalnych przykładów użycia widżetów ma unikalny
identyfikator, który możesz wykorzystać do szybkiego sklonowania przykładu na
swój komputer — za pomocą tego argumentu.

Ilekroć przeglądasz witrynę Flutter (https://docs.flutter.dev), możesz pobrać z niej przy-


kładowy ID i użyć go z tym argumentem.

 -i, --ios-language i -a, --android-language — są używane do określenia języka


dla natywnego kodu części projektu i tylko wtedy, gdy planujesz pisać natywny
kod danej platformy. W rozdziale 13. zobaczymy, jak dodać kod natywny do projektu.
 --project-name — służy do zmiany nazwy projektu. Musi to być prawidłowy
identyfikator pakietu Dart, jak już widzieliśmy na stronie opisu formatu pubspec
(https://dart.dev/tools/pub/pubspec):
„Nazwy pakietów powinny składać się wyłącznie z małych liter, z podkreśleniami
do oddzielania słów ’tak_jak_tutaj’. Używaj tylko podstawowych liter łacińskich

109

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

i cyfr arabskich: [az0-9_]. Upewnij się też, że nazwa jest prawidłowym


identyfikatorem Dart — czy nie zaczyna się od cyfry i nie jest słowem zastrzeżonym ”.
 Jeśli nie określisz tego parametru, spróbuje użyć tej samej nazwy co nazwa katalogu
wyjściowego. Zauważ, że ten argument musi być ostatnim na podanej liście.

Zobaczmy typową strukturę projektu Flutter, utworzoną za pomocą poprzedniego polecenia,


flutter create hello_world:

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”.

Następnie, w pliku pubspec, mamy sekcję zależności (dependencies):


environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"

dependencies:
flutter:
sdk: flutter

# Poniższy tekst dodaje czcionkę Cupertino Icons do Twojej aplikacji.


# Skorzystaj z klasy CupertinoIcons dla ikon stylu iOS.
cupertino_icons: ^0.1.2

dev_dependencies:
flutter_test:
sdk: flutter

Spójrz teraz na wyjaśnienie poprzedniego kodu:


 Rozpoczynamy od właściwości environment ze zdefiniowanymi ograniczeniami
dla wersji Dart SDK. Możesz używać wersji dostarczonej przez narzędzie,
ponieważ na jej podstawie następują również aktualizacje Flutter SDK.

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.

 Następnie mamy właściwość dependecies, która zawiera główną zależność aplikacji


Fluttera, czyli Flutter SDK (składa się z wielu podstawowych pakietów Fluttera).
 Jako dodatkową zależność generator dodaje pakiet cupertino_icons, który zawiera
zasoby ikon używanych przez wbudowane widżety Flutter Cupertino (więcej na ten
temat w następnym rozdziale).
 Właściwość dev_dependencies zawiera tylko zależność pakietu flutter_test
dostarczoną przez sam Flutter SDK, a także zawiera rozszerzenia specyficzne
dla Fluttera do już znanego pakietu test Darta.

W ostatnim bloku pliku znajduje się dedykowana sekcja flutter:


flutter:
uses-material-design: true

# Aby dodać zasoby do aplikacji, dodaj sekcję zasobów, na przykład:


# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# ...
# Aby dodać niestandardowe czcionki do aplikacji, dodaj sekcję
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
#

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/

Dotyczy to wszystkich plików w katalogu. Zwróć uwagę na znak / na końcu.


 fonts — ta właściwość umożliwia dodawanie niestandardowych czcionek
do aplikacji. Więcej informacji na ten temat znajdziesz w rozdziale 6. w sekcji
„Korzystanie z niestandardowych czcionek”.

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.

Uruchomienie wygenerowanego projektu


Wygenerowany projekt używa domyślnego szablonu Fluttera. Aplikacją jest licznik napisany
w stylu programowania React we Flutterze. Szczegóły sprawdzimy w następnym rozdziale,
kiedy będzie mowa o różnych widżetach, których możemy użyć do stworzenia naszej aplikacji.
W przykładzie hello_flutter, który utworzyliśmy wcześniej za pomocą polecenia flutter
create, MyApp jest głównym widżetem aplikacji.

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

Otrzymasz coś podobnego do poniższego zrzutu ekranu:

113

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Możesz sprawdzić, jak zarządzać emulatorami Androida na https://developer.android.com/


studio/run/managing-avds. W przypadku symulatorów urządzeń iOS należy użyć narzędzia
deweloperskiego XCode Simulator. Więcej informacji można znaleźć w witrynie dokumen-
tacji firmy Apple (https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/
iOS_Simulator_Guide/GettingStartedwithiOSSimulator/GettingStartedwithiOSSimulator.html).

Po stwierdzeniu, że mamy podłączone urządzenie, na którym można uruchomić aplikację,


możemy użyć następującego polecenia:
flutter run

Zobacz poniższy zrzut ekranu:

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

Widzieliśmy również, jak rozpocząć projekt aplikacji Flutter z narzędziami frameworkowymi,


podstawową strukturą plików projektu i właściwościami pliku pubspec. Na koniec zobaczyliśmy,
jak uruchomić projekt na emulatorze.

Kod źródłowy tego rozdziału można znaleźć w serwisie GitHub.

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

W tej sekcji przeczytasz o sposobie pracy Fluttera z interfejsem użytkownika, wprowadzaniu


danych użytkownika i zasobach dostępnych na potrzeby tworzenia rozbudowanych interfej-
sów użytkownika.

W tej sekcji zawarte są następujące rozdziały:


 Rozdział 4., „Widżety: tworzenie layoutów Fluttera”.
 Rozdział 5., „Obsługa danych wejściowych i gestów użytkownika”.
 Rozdział 6., „Motyw i styl”.
 Rozdział 7., „Routing: nawigacja między ekranami”.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Widżety stanowe / bezstanowe.
 Wbudowane widżety.
 Wbudowane widżety layoutów.
 Tworzenie niestandardowych widżetów.

Widżety stanowe i bezstanowe


Z rozdziału 3. dowidzieliśmy się, że widżety odgrywają ważną rolę w funkcjonowaniu aplikacji
Flutter. Są to elementy, które tworzą interfejs użytkownika; są reprezentacją kodu tego, co
jest widoczne dla użytkownika.

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.

Flutter posiada także koncepcję widżetów dziedziczonych (typ InheritedWidget),


który jest również rodzajem widżetu, ale różni się nieco od pozostałych dwóch typów,
o których wspomnieliśmy. Sprawdzimy to po szczegółowym zapoznaniu się z przykła-
dem hello_flutter z rozdziału 3.

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.

Kod źródłowy szczegółowo zbadamy za chwilę, kiedy prześledzimy projekt domyślnie


wygenerowany za pomocą narzędzia Fluttera, używany w poprzednim rozdziale.

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.

Reprezentowanie widżetów stanowych i bezstanowych


za pomocą kodu
W poprzednim rozdziale wygenerowaliśmy projekt Fluttera za pomocą następującego polecenia:
flutter create

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

Aplikacja demonstracyjna Flutter z poprzedniego zrzutu ekranu jest przydatna do pokazania


obu typów widżetów w praktyce.

Reprezentacja widżetu bezstanowego za pomocą kodu


Zacznijmy od zapoznania się z reprezentacją widżetu bezstanowego w postaci kodu. Pierw-
szym widżetem bezstanowym w aplikacji jest sama klasa aplikacji:
class MyApp extends statelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

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:

Widżet, który opakowuje wiele widżetów, powszechnie wymaganych w aplikacjach Material


Design.

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.

Korzystając z klasy Navigator, MaterialApp umożliwia definiowanie widżetów, które


mają być wyświetlane dla określonych ścieżek (routes) z logiczną historią nawigacji
stron, poprzez zarządzanie stosem wstecznym (ścieżki i nawigację stron będziemy sprawdzać
w rozdziale 7.).

Reprezentacja widżetu stanowego za pomocą kodu


MyHomePage jest widżetem stanowym, dlatego jest zdefiniowany za pomocą obiektu State, _MyHome
PageState, który zawiera właściwości wpływające na jego wygląd:
class MyHomePage extends statefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;

@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

Poniższa klasa _MyHomePageState reprezentuje obiekt State widżetu MyHomePage:


class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

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

Text to wbudowany widżet używany do wyświetlania tekstu na ekranie. Więcej infor-


macji o wbudowanych widżetach pojawi się w następnej sekcji.

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.

Zwróć uwagę na użycie właściwości onPressed widżetu FloatingActionButton.

FloatingActionButton to pływający przycisk akcji ze stylu Material Design, a ta właściwość


odbiera wywołanie zwrotne funkcji, które zostanie wykonane po naciśnięciu:

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

Istnieje kilka bardzo częstych przypadków użycia InheritedWidget we Flutterze. Jednym


z najczęstszych zastosowań jest klasa Theme, która pomaga opisać kolory dla całej aplikacji.
Przyjrzymy się temu w rozdziale 5.

Właściwość key widżetu


Jeśli spojrzysz na oba konstruktory klas statelessWidget i statefulWidget, zauważysz parametr
o nazwie key. Jest to ważna właściwość dla widżetów we Flutterze. Pomaga w renderowaniu
z drzewa widżetów do drzewa elementów. Oprócz typu i odniesienia do odpowiedniego widżetu
element ten zawiera również klucz identyfikujący widżet w drzewie. Właściwość key pomaga
zachować stan widżetu między przebudowami. Najczęstszym zastosowaniem key jest sytuacja,
w której mamy do czynienia z kolekcjami widżetów tego samego typu; bez kluczy drzewo elemen-
tów nie wiedziałoby, który stan odpowiada danemu widżetowi, ponieważ wszystkie miałyby
ten sam typ. Na przykład za każdym razem, gdy widżet zmienia swoją pozycję lub poziom
w drzewie widżetów, w drzewie elementów następuje dopasowanie, pozwalające zobaczyć,
co należy zaktualizować na ekranie, aby odzwierciedlić nową strukturę widżetu. Gdy widżet

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",
)

Najczęściej używane właściwości widżetu Text są następujące:


 style — klasa określająca style tekstu. Udostępnia właściwości, które pozwalają
na zmianę koloru tekstu, tła, rodziny czcionek (umożliwiając użycie niestandardowej
czcionki z zasobów; zobacz rozdział 3.), wysokość linii, rozmiar czcionki i tak dalej.
 textAlign — kontroluje wyrównanie tekstu w poziomie. Dostępne są opcje takie
jak wyśrodkowanie lub wyjustowanie.
 maxLines — umożliwia określenie maksymalnej liczby wierszy tekstu, które
zostaną obcięte w przypadku przekroczenia limitu.

128

d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera

 overflow — określa, w jaki sposób tekst zostanie obcięty w przypadku przepełnienia.


Dostępne są opcje takie jak limit maksymalnej liczby wierszy. Tę właściwość
można wykorzystać, aby np. na końcu zdania dodać wielokropek.

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)
)

Oprócz Image istnieją inne powszechnie używane właściwości:


 height/width — aby określić ograniczenia rozmiaru obrazu;
 repeat — aby kopiować obraz, w celu wypełnienia dostępnego miejsca;
 aligment — aby wyrównać obraz w określonej pozycji w jego granicach;
 fit — aby określić, w jaki sposób obraz powinien zostać umieszczony
w dostępnej przestrzeni.

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.

Material Design i widżety iOS Cupertino


Wiele widżetów we Flutterze wywodzi się w pewien sposób z wytycznych specyficznych dla plat-
formy: Material Design lub iOS Cupertino. Pomaga to deweloperowi w możliwie najłatwiej-
szym przestrzeganiu wytycznych specyficznych dla platformy.

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

 FlatButton — płaski przycisk to sekcja wydrukowana na widżecie Material, która


reaguje na dotknięcia przez rozpryskiwanie / falowanie kolorem.
 IconButton — przycisk z ikoną to obrazek wydrukowany na widżecie Material,
który reaguje na dotknięcie przez rozpryskiwanie / falowanie.

Na podstawie wytycznych dotyczących Material Design działanie komponentu Ink można


wyjaśnić w następujący sposób:
Element zapewniający promieniujący efekt w postaci wizualnego tętnienia rozszerzają-
cego się na zewnątrz pod wpływem dotyku użytkownika.
 DropDownButton — pokazuje aktualnie wybrany element i strzałkę, która otwiera
menu do wyboru innego elementu.
 PopUpMenuButton — wyświetla menu po naciśnięciu.

W przypadku stylu Cupertino systemu iOS Flutter udostępnia klasę CupertinoButton.

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.

W iOS Cupertino struktura jest inna z określonymi przejściami i zachowaniami.

Dostępne klasy iOS Cupertino to CupertinoPageScaffold i CupertinoTabScaffold, które skła-


dają się zazwyczaj z następujących elementów:

131

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 CupertinoNavigationBar — górny pasek nawigacyjny. Zwykle używany


z CupertinoPageScaffold.
 CupertinoTabBar — dolny pasek zakładek, który zwykle używany
z CupertinoTabScaffold.

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.

Aby zobaczyć wszystkie dostępne właściwości widżetów TextField i CupertinoTextField,


przejdź do oficjalnej strony dokumentacji widżetów: https://api.flutter.dev/flutter/material/
TextField-class.html i https://api.flutter.dev/flutter/cupertino/CupertinoTextField-class.html.

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

 CupertinoSlider — podobny do Slider w Material Design.


 CupertinoSwitch — działanie podobne do Switch z Material Design.

Selektory daty i godziny


W przypadku Material Design Flutter udostępnia selektory daty i godziny za pośrednictwem
funkcji showDatePicker i showTimePicker, które budują i wyświetlają okno dialogowe Material
Design dla odpowiednich akcji. Po stronie Cupertino iOS dostępne są widżety CupertinoDatePicker
i CupertinoTimerPicker, zgodnie z poprzednim stylem CupertinoPicker.

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.

Więcej informacji można znaleźć w katalogu widżetów Fluttera na stronie flutter.io:


https://flutter.io/docs/development/ui/widgets.

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

Najpopularniejszymi kontenerami we Flutterze są widżety Row i Column. Mają właściwość children,


która wymaga, aby lista widżetów była wyświetlana w określonym kierunku (to jest pozioma lista
w przypadku Row lub pionowa lista w przypadku Column).

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.

Inne widżety (gesty, animacje i transformacje)


Flutter zapewnia widżety do wszystkiego, co jest związane z interfejsem użytkownika. Na przy-
kład gesty, takie jak przewijanie lub dotykanie, będą powiązane z widżetem, który zarządza
gestami. Animacjami i transformacjami, takimi jak skalowanie i obrót, również zarządzają okre-
ślone widżety. Niektóre z nich szczegółowo omówimy w kolejnych rozdziałach, kiedy będziemy
opracowywać części małej aplikacji.

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

Tworzenie interfejsu użytkownika


za pomocą widżetów
(aplikacja do zarządzania przysługami)
Teraz, gdy znamy już niektóre z dostępnych widżetów Fluttera, czas zacząć tworzyć małą
aplikację, którą będziemy rozbudowywać w trakcie dalszej lektury.

Aplikacja, którą zamierzamy opracować, będzie służyła do zarządzania przysługami. Będzie


to mała sieć, w której przyjaciel może prosić innego przyjaciela o przysługę, a ten może ją przyjąć
lub odmówić jej wykonania. Zaakceptowanie przysługi powoduje jej wpisanie na listę rzeczy
do zrobienia. To jak aplikacja notująca rzeczy do zrobienia, w której zadania są proponowane
przez znajomych użytkownika i tylko użytkownik może je zaakceptować lub odrzucić. W tej
aplikacji poznamy wiele koncepcji, które mogą pomóc w tworzeniu aplikacji.

W kolejnych rozdziałach będziemy dodawać funkcje do aplikacji, stopniowo poznając różne


elementy składające się na aplikację 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.

Ekran główny aplikacji


Pierwszy ekran aplikacji to ekran główny, który będzie składał się z czterech zakładek z listą
przysług i ich statusami:
 Oczekujące przysługi — przysługi, o które prosili niektórzy przyjaciele, a na które
jeszcze nie odpowiedzieliśmy.
 W toku / spełnianie przysług — przyjęte przysługi czyli takie, którymi
zajmujemy się teraz — zobacz pierwszy rysunek na następnej stronie.
 Ukończone przysługi — już ukończone przysługi.
 Odmowa przysług — lista przysług, których zrobienia odmówiliśmy
(nie przyjęliśmy) — zobacz drugi rysunek na następnej stronie.

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),
),
),
);
}

Pierwszym widżetem obecnym w poddrzewie widżetów FavorsPage jest widżet Default


TabController , który obsługuje za nas zmianę zakładki. Następnie mamy widżet Scaffold,
który implementuje podstawową strukturę Material Design. Tutaj używamy już niektórych z tych
elementów, w tym paska aplikacji i pływającego przycisku akcji. Ten widżet jest bardzo przy-
datny do projektowania aplikacji zgodnych z Material Design, ponieważ zapewnia przydatne
właściwości w oparciu o wytyczne:
 W AppBar dodaliśmy tytuł za pomocą widżetu Text. W niektórych przypadkach
możemy również dodać do niego akcje lub niestandardowy layout. Tutaj dodaliśmy
instancję TabBar u dołu (bottom) paska aplikacji, która pokaże dostępne karty.
 We FloatingActionButton również nic zbytnio nie zmieniliśmy; dodaliśmy ikonę
tylko za pomocą widżetu Icon, który zawiera ikonę Material Design dostarczoną
przez platformę.
 Właściwość body widżetu Scaffold to miejsce, w którym projektujemy sam layout.
Jest zdefiniowany w następujący sposób: widżet TabBarView wyświetla odpowiedni
widżet dla wybranej karty w zdefiniowanej wcześniej instancji DefaultTabController.
Tym, co wymaga uwagi, jest jego własność children; dopasowuje zakładki i zwraca
odpowiedni widżet skojarzony z daną zakładką.

Elementy paska zakładek Tab są tworzone przez metodę _buildCategoryChip() w następujący


sposób:

139

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

class FavorsPage extends statelessWidget {


// … pola metody budowania i inne
Widget _buildCategoryTab(String title) {
return Tab(
child: Text(title),
);
}
}

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

Widget _favorsList(String title, List<Favor> favors) {


return Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
child: Text(title),
padding: EdgeInsets.only(top: 16.0),
),
Expanded(
child: ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: favors.length,
itemBuilder: (BuildContext context, int index) {
final favor = favors[index];
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),
),
);
},
),
),
],
);
}
}

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.

Zmiana fizyki listy za pomocą BouncingScrollPhysic() powoduje, że lista ma efekt odbijania


przewijania widoczny na listach systemu iOS.

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];.

Pozostała część kreatora listy wygląda następująco:


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),
),
);

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();
}

Funkcja _itemFooter() zwraca widżet w zależności od statusu przysługi.

Statusy przysług są definiowane przez getters w klasie Favor:


 W fazie żądania (przysługa nie została jeszcze zaakceptowana lub odrzucona)
zwracamy widżet Row z dwoma instancjami FlatButton z odpowiednimi
dostępnymi akcjami: odmów lub zrób. FlatButton to przycisk Material Design,
który nie ma ani elewacji, ani koloru tła.
 W fazie wykonywania zwracamy widżet Row z odrzuconymi lub zakończonymi
akcjami jako FlatButtons.
 Aby uzyskać stan ukończenia, wyświetlamy datę i godzinę zakończenia
sformatowaną za pomocą klasy DateFormat z Darta wewnątrz widżetu Chip,
aby odróżnić ją od reszty tekstu.
 W statusie odmowy zwracamy widżet Container bez ograniczeń rozmiaru; to jest
pusty kontener (nie zajmuje miejsca na layoucie).

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ę!

Ekran prośby o przysługę


Ekran prośby o przysługę będzie miejscem, w którym nastąpi interakcja użytkownika z apli-
kacją. Na razie przyjrzymy się tylko układowi tego ekranu. W miarę upływu czasu będziemy
łączyć elementy, aby wybrać znajomego, który poprosi o przysługę, a także zapisać przysługę
w zewnętrznej bazie danych Firebase:

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;

RequestFavorPage({Key key, this.friends}) : super(key: key);

@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.

Metoda build() widżetu zaczyna się w następujący sposób:


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Requesting a favor"),
leading: CloseButton(),
actions: <Widget>[
FlatButton(
child: Text("SAVE"), textColor: Colors.white, onPressed: ()
{}),
],
),
body: ... // kontynuacja poniżej
...

appBar zawiera tutaj dwie nowe właściwości:


 leading, czyli widżet wyświetlany przed tytułem. W tym przypadku używamy
widżetu CloseButton, który jest przyciskiem zintegrowanym z widżetem Navigator
(więcej o tym w rozdziale 7.).
 actions, która otrzymuje listę widżetów do wyświetlenia po tytule; w tym przypadku
wyświetlamy instancję FlatButton, za pomocą której zapiszemy prośbę o przysługę.

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)],
),

Widżet TextFormField umożliwia wprowadzanie tekstu. Dodając do niego inputFormatters,


możemy skonfigurować jego wygląd na ekranie. Tutaj po prostu ograniczamy całkowitą długość
wpisywanego tekstu do 200 znaków za pomocą klasy LengthLimitingTextInputFormatter,
która jest udostępniana przez bibliotekę flutter/services.

Sprawdź wszystkie dostarczone narzędzia z pakietu flutter/services na stronie pakietu:


https://api.flutter.dev/flutter/services/services-library.html.

 Widżet DateTimePickerFormField, który umożliwia użytkownikowi wybranie


instancji DateTime i mapowanie go na typ DateTime Darta:
DateTimePickerFormField(
inputType: InputType.both,
format: DateFormat("EEEE, MMMM d, yyyy 'at' h:mma"),
editable: false,
decoration: InputDecoration(
labelText: 'Date/Time', hasFloatingPlaceholder: false),
onChanged: (dt) {},
),

146

d0765ad53fb82babda2278a311da7afb
d
Rozdział 4. • Widżety: tworzenie layoutów Fluttera

Widżet DateTimePickerFormField nie jest wbudowanym widżetem Fluttera. To jest wtyczka


innej firmy z biblioteki datetime_picker_formfield. Definiujemy niektóre właściwości, aby
zmienić jej wygląd:
 inputType — czy wybrać datę, godzinę lub obie wartości.
 format — typ DateFormat Darta definiujący format reprezentacji ciągu znaków.
 editable — czy widżet ma być ręcznie edytowany przez użytkownika.
 decoration — służy do definiowania dekoracji dla pola wejściowego (za pomocą
Material Design). Zauważ, że nie zdefiniowaliśmy go dla innych pól wejściowych.
 onChanged — wywołanie zwrotne z nową wartością wybraną przez użytkownika.

Aby dowiedzieć się o wszystkich dostępnych opcjach i sposobie korzystania z widżetu


DateTimePickerFormField, odwiedź stronę https://pub.dartlang.org/packages/datetime_
picker_formfield.

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.

Tworzenie niestandardowych widżetów


Podczas tworzenia interfejsów użytkownika za pomocą Fluttera zawsze będziemy musieli
tworzyć niestandardowe widżety; nie możemy i nie chcemy od tego uciekać. W końcu Flutter
tak dobrze umożliwia kompozycję widżetów do tworzenia unikalnych interfejsów.

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

Wróćmy więc do naszego widżetu FavorsPage i przekonwertujmy metody małych widżetów


na nowe, niestandardowe, małe widżety.

Metodę _favorsList() (zobacz załączony kod źródłowy) można refaktoryzować do nowego


widżetu FavorsList. Następnie właściwość itemBuilder widżetu FavorsList może zostać zmie-
niona w celu zwrócenia widżetu FavorCardItem, który zwraca element karty:
class FavorCardItem extends statelessWidget {
final Favor favor;

const FavorCardItem({Key key, this.favor}) : super(key: key);

@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.

Dzięki technice korzystania z podzielonych widżetów przekażemy platformie wystarczającą


ilość informacji o naszych widżetach, które będą zachowywać się jak widżety wbudowane i będą
mogły być optymalizowane jak widżety wbudowane.

Polecam przeczytanie interesującego posta na blogu na temat wydajności widżetów:


https://iiro.dev/splitting-widgets-to-methods-performance-antipattern/.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Obsługa gestów użytkownika.
 Wprowadzenie do widżetów przyjmujących dane wejściowe.
 Walidacja danych wejściowych.
 Tworzenie niestandardowej obsługi danych wejściowych.

Obsługa gestów użytkownika


Aplikacja mobilna byłaby niczym bez jakiejkolwiek interaktywności. Struktura Fluttera umoż-
liwia obsługę gestów użytkownika w każdy możliwy sposób, od prostych dotknięć po gesty prze-
ciągania i przesuwania. Zdarzenia ekranowe w systemie gestów Fluttera są podzielone na dwie
warstwy w następujący sposób:
 Warstwy wskaźników — są to warstwy, które mają zdarzenia wskaźnika
(pointer) i reprezentują interakcje użytkownika ze szczegółami takimi jak
lokalizacja dotyku i kierunek ruchu na ekranie urządzenia.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 Gesty — gesty we Flutterze to zdarzenia interakcji na najwyższym poziomie


definicji i być może już widziałeś niektóre z nich w akcji, na przykład dotknięcia,
przeciągnięcia i skalowanie. Są one również najbardziej typowym sposobem
implementacji obsługi zdarzeń.

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.

Sprawdź stronę dokumentacji klasy Listener pod adresem


https://api.flutter.dev/flutter/widgets/Listener-class.html.

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

 Dotknięcie — pojedyncze dotknięcie / dotknięcie ekranu urządzenia.


 Dwukrotne dotknięcie — podwójne szybkie dotknięcie tego samego miejsca na
ekranie urządzenia.
 Naciśnięcie i długie naciśnięcie — naciśnięcie ekranu urządzenia, podobne do
dotknięcia, ale dotykanie ekranu przez długi czas przed zwolnieniem.
 Przeciąganie — naciśnięcie rozpoczynające się od dotknięcia wskaźnika ekranu
w jakimś miejscu, który następnie jest przesuwany i zatrzymywany w innym
miejscu na ekranie urządzenia.
 Przewijanie — podobny do gestu przeciągania. We Flutterze różnią się kierunkiem;
gesty przewijania obejmują zarówno przeciągnięcia poziome, jak i pionowe.
 Skalowanie — dwa wskaźniki używane do przeciągania w celu wykonania gestu
skali. Jest to również podobne do gestu powiększania.

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)

class _TapWidgetExampleState extends State<TapWidgetExample> {


int _counter = 0;

@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 .

Kod źródłowy rozdziału 5. można znaleźć na GitHubie.

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
);

Jedyną różnicą w stosunku do poprzedniego elementu jest przypisana właściwość onDoubleTap,


która będzie wywoływana za każdym razem, gdy podwójne naciśnięcie zostanie szybko wykonane
w tym samym miejscu na ekranie.

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
);

Jedyną różnicą w stosunku do poprzedniego elementu jest przypisana właściwość onLongPress,


która będzie wywoływana za każdym razem, gdy zostanie wykonane dotknięcie i przytrzymane
przez jakiś czas — długie naciśnięcie — przed zwolnieniem z ekranu.

Przeciąganie, przewijanie i skalowanie


Gesty przeciągania, przesuwania i skalowania są podobne, a we Flutterze musimy zdecydo-
wać, którego użyć w każdej sytuacji, ponieważ nie można ich używać razem w tym samym
widżecie GestureDetector.

154

d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika

Gesty przeciągania są podzielone na gesty pionowe i poziome. Nawet wywołania zwrotne są


we Flutterze rozdzielone.

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.

Widżety Transform będziemy dokładniej sprawdzać w rozdziale 14.

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.

Kod źródłowy pliku gestures/lib/example_widgets/pan_example_event.dart można znaleźć


w serwisie GitHub.

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

Kod tutaj jest bardzo podobny do poprzednich. Mamy trzy właściwości:


 _resizing — służy do aktualizacji tekstu wyświetlanego przez użytkownika
podczas zmiany rozmiaru za pomocą gestu skalowania.
 _scaleCount — gromadzi całkowitą liczbę zdarzeń skalowania wykonanych
od początku do końca.
 _scale — przechowuje wartość skalowania z parametru ScaleUpdateDetails,
a następnie jest stosowana do widżetu Tekst za pomocą konstruktora skali
widżetu Transform.

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.

Gesty w widżetach Material Design


Widżety Material Design i iOS Cupertino mają wiele gestów wyabstrahowanych do niektó-
rych właściwości za pomocą wewnętrznego widżetu GestureDetector. Na przykład widżety,
takie jak RaisedButton, używają widżetu InkWell obok zdarzenia dotknięcia. Wykonuje on
efekt rozpryśnięcia na widżecie docelowym. Ponadto właściwość onPressed RaisedButton
ujawnia funkcjonalność dotknięcia, której można użyć do zaimplementowania akcji przycisku.
Rozważmy następujący przykład:
// część pliku main.dart (dołączony przykład katalogu „input”)
RaisedButton(
onPressed: () {
print("Running validation");
// ... walidacja
},
child: Text("validate"),
)

Element potomny Text jest wyświetlany w RaisedButton, a jego naciśnięcie jest obsługiwane
w metodzie onPressed, jak wspomniano wcześniej.

Widżety danych wejściowych


Zarządzanie gestami to dobry punkt wyjścia do interakcji z użytkownikiem, ale oczywiście nie wy-
starczy. Pobieranie danych użytkownika jest tym, co wprowadza nowe treści do wielu aplikacji.

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

Chociaż możemy samodzielnie zarządzać wszystkimi danymi wprowadzanymi przez użytkownika


(powiedzmy, w widżecie głównym, który przechowuje wszystkie pola wejściowe), może to być
kłopotliwe, ponieważ może prowadzić do tego, że będziemy mieć wiele pól, i prawdopodobnie
zwiększy się złożoność kodu. Podzielenie wszystkich widżetów danych wejściowych na małe
części pomaga, ale nie rozwiązuje wszystkich problemów.

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.

Wiele wbudowanych widżetów wejściowych Fluttera jest dostarczanych z odpowiednią imple-


mentacją widżetu FormField. Na przykład widżet TextField ma TextFormField. Widżet
TextFormField ułatwia dostęp do wartości TextField, a także dodaje do niej zachowania zwią-
zane z formularzem (takie jak walidacja).

Widżet TextField umożliwia użytkownikowi wprowadzanie tekstu za pomocą klawiatury. Widżet


TextField udostępnia metodę onChanged, której można użyć do nasłuchiwania zmian jego bieżącej
wartości. Innym sposobem nasłuchiwania zmian jest użycie kontrolera (zobacz sekcję „Korzysta-
nie z kontrolera”).

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"),
);

Po utworzeniu instancji TextEditingController wewnątrz controller ustawiliśmy właściwość


widżetu TextField, aby „sterował” widżetem tekstowym:
TextField(
controller: _controller,
);

158

d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika

Jak widać, możemy również ustawić wartość początkową dla TextField.

TextEditingController jest powiadamiany za każdym razem, gdy widżet TextField ma nową


wartość.

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.

Sprawdź pełny przykład w załączonych plikach rozdziałów.

Dostęp do stanu FormField


Jeśli używamy widżetu TextFormField, sprawy stają się prostsze:
final _key = GlobalKey<FormFieldState<String>>();
...
TextFormField (
key: _key,
);

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.

Klasa FormFieldState<String> udostępnia również inne przydatne metody i właściwości do ob-


sługi FormField:
 validate() — wywoła funkcję zwrotną walidatora (validator) widżetu, która
powinna sprawdzić jego aktualną wartość i zwrócić komunikat o błędzie lub
wartość null, jeśli jest poprawna.
 hasError i errorText — wynikają z poprzednich walidacji przy użyciu poprzedniej
funkcji. Na przykład w widżetach Material Design powoduje to dodanie niewielkiego
tekstu w pobliżu pola, zapewniając użytkownikowi odpowiednią informację
zwrotną o błędzie.
 save() — wywoła onSaved na widżecie. Jest to akcja, która ma miejsce, gdy dane
wejściowe zostaną wprowadzone przez użytkownika (podczas zapisywania).
 reset() — ustawi pole w jego stanie początkowym, z wartością początkową
(jeśli istnieje), usuwając również błędy walidacji.

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.

Dostęp do stanu Form


Zapewnienie dostępu do aktualnego stanu obiektu Form jest przydatne, abyśmy mogli zaini-
cjować (init) jego walidację, zapisać jego zawartość lub zresetować go z dowolnego miejsca
w drzewie widżetów (za pomocą przycisku). Istnieją dwa sposoby uzyskania dostępu do State
skojarzonego z widżetem Form.

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

Kiedy używamy Form.of(context), przekazujemy bieżący kontekst widżetu. W poprzednim przy-


kładzie kontekstem używanym w wywołaniu zwrotnym onPressed będzie kontekst InputForm
InheritedStateExamplesWidget, a zatem przeszukanie drzewa nie spowoduje pomyślnego
znalezienia widżetu Form. Korzystając z widżetu Builder, delegujemy jego kompilację do wywoła-
nia zwrotnego, tym razem przy użyciu odpowiedniego kontekstu (podrzędnego), a kiedy wyszuka
on drzewo, z powodzeniem znajdzie instancję FormState.

Walidacja danych wejściowych (Form)


Obsługa wielu widżetów FormField jest w porządku, gdy mówimy o kilku wartościach, ale gdy
ilość danych rośnie, organizowanie ich na ekranie, prawidłowe sprawdzanie ich poprawności
i szybkie przekazywanie opinii użytkowników może stać się trudniejsze. Dlatego Flutter udostęp-
nia widżet Form.

Walidacja danych użytkownika


Sprawdzanie poprawności danych wprowadzanych przez użytkownika to jedna z głównych
funkcji widżetu Form. Aby dane wprowadzone przez użytkownika były spójne, należy je koniecznie
sprawdzić, ponieważ użytkownik prawdopodobnie nie zna wszystkich dozwolonych wartości.

Widżet Form, w połączeniu z instancjami FormField, pomaga programistom wyświetlać odpowiedni


komunikat o błędzie, jeśli niektóre wartości wejściowe wymagają korekty przed zapisaniem
za pomocą funkcji save().

Widzieliśmy już, w poprzednich przykładach Form, jak zweryfikować wartości pól:


1. Utwórz widżet Form z elementem FormField.
2. Zdefiniuj logikę walidacji dla każdej właściwości FormField validator.
TextFormField(
validator: (String value) {
return value.isEmpty ? "The value cannot be empty" : null;
},
)
3. Wywołaj validate() na FormState przy użyciu jego klucza (key) lub metody Form.of
omówionej wcześniej. Spowoduje to wywołanie każdej podrzędnej metody
FormField validate(), a jeśli walidacja zakończy się pomyślnie, zwróci wartość
true, w przeciwnym razie zaś zwróci wartość false.
4. validate() zwraca wartość bool, abyśmy mogli manipulować jego wynikiem i na jego
podstawie wykonywać naszą logikę.

162

d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika

Niestandardowa obsługa danych


wejściowych i FormField
Widzieliśmy, jak widżety Form i FormField pomagają obsługiwać dane wejściowe oraz spraw-
dzają ich poprawność. Wiemy również, że Flutter jest wyposażony w serię widżetów danych
wejściowych, które są wariantami FormField, a więc zawiera funkcje pomocnicze, na przykład
do uzyskiwania dostępu do danych i sprawdzania ich poprawności.

Rozszerzalność i elastyczność Fluttera dostępna jest wszędzie we frameworku. Zatem tworzenie


niestandardowych pól jest logicznie możliwe. Możemy dodać własną metodę wprowadzania,
stworzyć walidację za pomocą wywołania zwrotnego validator, a także użyć metod save() i reset().

Tworzenie niestandardowej obsługi danych wejściowych


Tworzenie niestandardowej obsługi danych wejściowych we Flutterze jest tak proste jak
utworzenie normalnego widżetu, z dodatkowymi metodami opisanymi wcześniej. Zwykle ro-
bimy to poprzez rozszerzenie widżetu FormField<inputType>, gdzie inputType to typ wartości
widżetu wejściowego.

Tak więc typowy proces wygląda następująco:


1. Utwórz niestandardowy widżet, który rozszerza widżet Stateful (w celu śledzenia
wartości) i akceptuje dane wejściowe od użytkownika, opakowując inny widżet
danych wejściowych lub dostosowując cały proces, na przykład za pomocą gestów.
2. Utwórz widżet, który rozszerza FormField i w zasadzie wyświetla widżet wejściowy
utworzony w poprzednim kroku, a także udostępnia jego pola.

Przykład niestandardowego widżetu danych wejściowych


Później, w rozdziale 8., zobaczymy, jak dodać uwierzytelnianie do naszej aplikacji. Na razie
będziemy tworzyć niestandardowy widżet, który będzie podobny do używanego w tym kroku.

Uwierzytelnianie będzie oparte na usługach uwierzytelniania bazy Firebase, które używają


numeru telefonu; aby pomyślnie się zalogować, podany numer telefonu otrzymuje sześciocy-
frowy kod weryfikacyjny, który musi być zgodny z wartością serwera. Na razie to wszystkie
informacje, które musimy znać, aby utworzyć niestandardowy widżet danych wejściowych.
Oto jak to będzie wyglądać — zobacz rysunek na następnej stronie.

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

Później, na ekranie logowania, będziemy używać wtyczki stworzonej przez społeczność


Fluttera — code_input, aby zastąpić ten widżet. Więcej informacji można znaleźć pod
adresem https://pub.dev/packages/code_input.

Tworzenie widżetu do obsługi danych wejściowych


Zaczynamy od stworzenia normalnego niestandardowego widżetu. Tutaj przedstawiamy nie-
które właściwości. Pamiętaj, że w prawdziwej aplikacji prawdopodobnie ujawnilibyśmy więcej niż
pokazane tutaj właściwości, ale w tym przykładzie to wystarcza:
class VerificationCodeInput extends StatefulWidget {
final BorderSide borderSide;
final onChanged;
final controller;
... // inne części zostały usunięte dla zwięzłości
}

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,
);
}
}

Jak widać, widżet to po prostu TextField z pewnymi predefiniowanymi właściwościami:


 WhitelistingTextInputFormatter pozwala nam określić wyrażenie regex
z dozwolonymi znakami wejściowymi. Ustawiając typ klawiatury za pomocą
keyboardType: TextInputType.number, możemy również ograniczyć dozwolone
znaki do liczb.
 LengthLimitingTextInputFormatter określa maksymalny limit znaków dla danych
wejściowych.
 Ponadto obramowanie jest dodawane za pośrednictwem klasy
OutlineInputBorder.

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ą.

Zmiana widżetu na widżet FormField


Aby zmienić widżet w widżet FormField, zaczynamy od utworzenia widżetu, rozszerzającego
klasę FormField, która jest typu StatefulWidget z pewnymi funkcjami Form.

Tym razem zacznijmy od sprawdzenia obiektu State skojarzonego z nowym widżetem.


Zróbmy to, dzieląc go na części:
// początkowa część _VerificationCodeFormFieldState
final TextEditingController _controller = TextEditingController(text: "");

@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.

Pozostała część widżetu wygląda następująco:


void _controllerChanged() {
didChange(_controller.text);
}

@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

FormFieldState<String> createState() =>


_VerificationCodeFormFieldState();
}

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.

Łączymy wszystko razem


Teraz, gdy wiemy, jak używać zdarzeń gestów i widżetów obsługujących wprowadzanie danych,
aby dodać interakcję użytkownika do naszych ekranów aplikacji, nadszedł czas, aby rozsze-
rzyć naszą aplikację o te funkcje. Wróćmy do naszych ekranów, aby dodać do nich kilka gestów
i weryfikacji danych wejściowych.

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

Ekran przysług — inne (rozmyte i zachodzące) informacje nie są tutaj ważne.

3. Podobnie jak w poprzednim przypadku, ale tym razem zaakceptowana prośba


o przysługę oczekuje na zakończenie, a te przyciski pozwalają użytkownikowi
zrezygnować lub zrealizować przysługę; oznacza to, że dotknięcie ich powoduje
zmianę statusu przysługi odpowiednio na odrzucona (Refused) i ukończona
(Completed).
4. Na końcu mamy przycisk Request a favor — poproś o przysługę, który po dotknięciu
po prostu otwiera drugi ekran aplikacji, co pozwala nam poprosić niektórych
naszych znajomych o przysługę.
Jak widać, będziemy mieć do czynienia z gestami dotykania, przewijania i przesuwania.
Wszystkie z nich można wykonać bezpośrednio za pomocą GestureDetector, ale
ponieważ używamy widżetów Button i ListView, to się trochę zmienia. Pamiętaj,
że wbudowane widżety Fluttera składają się również z wielu innych wbudowanych
widżetów, więc pośrednio będziemy mieć do czynienia z GestureDetector.

W praktyce dotknięcia będziemy obsługiwać samodzielnie, ponieważ pozostałe gesty są obsługi-


wane przez widżety, których używaliśmy: przewijanie za pomocą ListView oraz przesuwanie
i dotknięcie za pomocą TabBar i TabView.

168

d0765ad53fb82babda2278a311da7afb
d
Rozdział 5. • Obsługa danych wejściowych i gestów użytkownika

Gesty dotknięcia na zakładkach przysług


Jak mówiliśmy wcześniej, DefaultTabController zmienia aktualnie widoczny widżet zakładki,
gdy użytkownik dotknie pasek zakładek lub przesunie palcem w lewo lub w prawo w widoku.
Korzystając z tego widżetu, nie musimy określać kontrolera w potomkach TabBar i TabView.

Więcej informacji na temat widżetu TabController można znaleźć na stronie dokumen-


tacji pod adresem https://docs.flutter.io/flutter/material/TabController-class.html.

Gesty dotknięcia na FavorCard


Korzystając z właściwości favor widżetu FavorCardItem, możemy sterować jego stanem, zmie-
niając jego wartości na accepted i completed. Nie spowoduje to jednak usunięcia elementu z bie-
żącej listy i dodania go do nowej listy docelowej. Aby to zrobić, musielibyśmy uzyskać dostęp
do bieżącej listy, usunąć stamtąd przysługę i dodać ją do nowej listy, w zależności od naciśnię-
tego przycisku.

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.

Tworzenie FavorsPage jako StatefulWidget


Pierwszym krokiem do uczynienia naszej aplikacji interaktywną jest zmiana FavorsPage na
StatefulWidget:
class FavorsPage extends StatefulWidget {
FavorsPage({
Key key,
}) : super(key: key);

@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

Obsługa akcji odmowy


Oto jak wygląda przycisk Refuse (odrzuć) po tym, jak już skorzystaliśmy z wyżej wymienionej
funkcjonalności:
// część pliku hands_on_input/lib/main.dart dla klasy FavorCardItem
// metoda _itemFooter
FlatButton (
child: Text("Refuse"),
onPressed: () {
FavorsPageState.of(context).refuseToDo(favor);
// zmieniliśmy _itemFooter, aby uzyskać kontekst, więc
// możesz jej użyć do pobrania stanu strony przysług
},
)

Wywołując FavorsPageState.of(context) uzyskujemy dostęp do aktualnego stanu typu


FavorsPageState skojarzonego z kontekstem.

Aby zastosować zmianę, wywołujemy metodę refuseToDo(favor) z klasy FavorsPageState, która


jest zaimplementowana w następujący sposób:
void refuseToDo(Favor favor) {
setState(() {
pendingAnswerFavors.remove(favor);

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.

Obsługa akcji Wykonaj

Tak wygląda obsługa przycisku Do (Wykonaj) po zastosowaniu powyższej techniki:


FlatButton(
child: Text("Do"),
onPressed: () {
FavorsPageState.of(context).acceptToDo(favor);
},
)

Metoda acceptToDo(favor) jest wykonywana w następujący sposób:


void acceptToDo(Favor favor) {
setState(() {
pendingAnswerFavors.remove(favor);

acceptedFavors.add(favor.copyWith(accepted: true));
});
}

Jak widać, jest prawie taka sama jak metoda refuseToDo(); jedyne różnice dotyczą docelowej listy
i statusu akceptacji.

Akcje Give up (Zrezygnuj) i Complete (Ukończ) również są bardzo podobne do poprzed-


nich. Sprawdź załączone pliki źródłowe, aby zobaczyć, jak wyglądają.

Dotknięcie przycisku Request a favor (Poproś o przysługę)


Gdy użytkownik naciśnie przycisk akcji ze znakiem plus u dołu strony, powinien zobaczyć na
ekranie widżet RequestFavorPage:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RequestFavorPage(
friends: mockFriends,
),
),
);

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.

Ekran prośby o przysługę


Ekran prośby o przysługę (requesting a favor) ma kilka własnych gestów do wykonania:

Oto jak działa ten proces:


1. Przycisk close jest już obsługiwany przez widżet CloseButton wraz z widżetem
Navigator (jest to obsługiwane wewnętrznie za nas).
2. Przycisk SAVE potwierdzi wprowadzone informacje od użytkownika i wyśle prośbę
o przysługę do znajomego.

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.

Widżet RequestFavorPage również musi zostać przekonwertowany na StatefulWidget, ponieważ


będziemy musieli przechowywać informacje i manipulować nowymi prośbami o przysługę za
pomocą działań. Będzie to miejsce, w którym później zapiszemy przysługę w Firebase.

Ponownie używamy tego sposobu do scentralizowania wszystkich działań związanych


z przysługami w naszej aplikacji. Rozwiązaniem dla rzeczywistej aplikacji może być architek-
tura, taka jak MVP, MVVM lub BloC.

Walidacja danych wejściowych za pomocą widżetu Form


Aby móc sprawdzać wszystkie pola jednocześnie podczas zapisywania, musimy dodać widżet
Form do naszego layoutu. Odbywa się to poprzez zwykłe opakowanie naszych widżetów widżetem
Form . Ustawiliśmy również właściwość key naszego Form za pomocą instancji GlobalKey
(_formKey w obiekcie State poniższego kodu), abyśmy mogli użyć go później w metodzie save():
class RequestFavorPageState extends State<RequestFavorPage> {
final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
// zwraca poddrzewo widżetu opakowane w Form. ukryte dla zwięzłości.
}
}

Metoda save() wygląda podobnie do poprzednich:


FlatButton(
child: Text("SAVE"),
textColor: Colors.white,
onPressed: () {
RequestFavorPageState.of(context).save(); // moglibyśmy wywołać save()
// bezpośrednio
// dopóki jestesmy tej samej klasie.
// Celowo pozostawiony dla
// zilustrowania przykładu
},
)

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

Tworzenie interfejsów użytkownika z wbudowanymi motywami i stylami sprawi, że aplikacja


będzie wyglądać profesjonalnie i stanie się łatwa w użyciu. Ponadto framework umożliwia
tworzenie niestandardowych i unikalnych motywów i stylów. Dowiesz się, jak dostosować
wygląd aplikacji, dodając niestandardowe czcionki, używając motywów i odkrywając standardy plat-
form, a mianowicie iOS Cupertino i Google Material Design. Ponadto wyjaśnię, jak używać
zapytań, aby skorzystać z dynamicznego stylizowania.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Widżety motywu.
 Material Design.
 iOS Cupertino.
 Korzystanie z niestandardowych czcionek.
 Dynamiczne style osiągane za pomocą MediaQuery i LayoutBuilder.

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.

Podczas stylizowania widżetów w iOS Cupertino dostępne są również odpowiedniki


CupertinoTheme i CupertinoThemeData odpowiednio dla Theme i ThemeData, które są obecne
w pakiecie widżetów Material Design.

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.

Oto co mówi dokumentacja klasy ThemeData (https://api.flutter.dev/flutter/material/ThemeData-


class.html):
Jasność motywu aplikacji. Używana przez widżety, takie jak przyciski, do określenia,
jaki kolor wybrać, gdy nie jest używany kolor podstawowy lub akcentujący.

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.

Wybierając kolory podstawowe i akcentujące, należy poeksperymentować z odpowiednimi


parametrami primaryColorBrightness i accentColorBrightness. Flutter szacuje jasność na podsta-
wie niektórych obliczeń jaskrawości kolorów, ale zawsze dobrze jest samemu ją sprawdzić.

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.

Przejdźmy teraz do tworzenia motywów.

Tworzenie motywu w praktyce


Stylizowanie widżetów we Flutterze można wykonać na kilka sposobów, a wszystko, co dotyczy
stylów, opiera się na widżecie Theme. Czas sprawdzić, jak to działa. Załóżmy, że mamy prostą apli-
kację, jak poniżej:
class MyAppDefaultTheme extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Center(
child: Text(
"Simple Text",
textDirection: TextDirection.ltr,
),
),
);
}
}

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.

Możemy również dostosować stylizację; oto przykład:


class MyAppCustomTheme extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Center(
child: Theme(
data: Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.copyWith(
display1: TextStyle(
color: Colors.yellow,
),
),
),
child: Text(
"Simple Text",
textDirection: TextDirection.ltr,
style: Theme.of(context).textTheme.display1,
),
),
),
);
}
}

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

 Tworzymy kopię domyślnego widżetu Theme i zmieniamy tylko jego właściwość


textTheme. Funkcja copyWith nie jest obowiązkowa, jednak pojawia się bardzo
często podczas tworzenia aplikacji Fluttera, więc przyzwyczaj się do niej!
 Tak jak poprzednio, tym razem tworzymy kopię textTheme z motywu bazowego
i zmieniamy tylko jego właściwość display1 na nowy obiekt stylu Text.

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.

Poprzednie przykłady zostały zdefiniowane w różnych klasach aplikacji. Możesz znaleźć


kod źródłowy themes/lib/main.dart na GitHubie i wypróbować go samodzielnie, komen-
tując poprzednią funkcję runApp i odkomentowując tę, którą chcesz przetestować.

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

class PlatformSpecificWidgets extends StatelessWidget {


@override
Widget build(BuildContext context) {
return Platform.isAndroid
? MaterialApp(
theme: ThemeData(primaryColor: Colors.grey),
)
: CupertinoApp(
theme: CupertinoThemeData(primaryColor:
CupertinoColors.lightBackgroundGray),
);
}
}

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.

Zajrzyj na stronę z dokumentacją, https://docs.flutter.io/flutter/dart-io/Platform-class.html,


aby dowiedzieć się więcej o tej ważnej klasie.

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/.

Pierwszym podstawowym widżetem do stosowania wytycznych dotyczących materiałów w aplika-


cjach Flutter jest widżet MaterialApp. Zobaczmy szczegółowo, jak to działa.

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
),
),
),
);
}
}

W tym kodzie wyraziliśmy zamiar przestrzegania wytycznych dotyczących Material Design,


dodając widżet MaterialApp jako nasz widżet główny. Spowoduje to zwrócenie nieatrakcyjnego
stylu DefaultTextStyle, co poinformuje dewelopera, że nie korzysta skutecznie z Material Design
w widżetach Text. Wynik poprzedniego kodu jest następujący:

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.

Najprostszym przykładem jest widżet Material; ma właściwość DefaultTextStyle i inne typowe


właściwości Material Design, takie jak elevation dla efektu cienia.

Zwróć uwagę, że tym razem nie udostępniliśmy również właściwości textDirection widżetu Text.

Jedną z funkcji MaterialApp jest umożliwienie nam zastosowania internacjonalizacji do naszej


aplikacji, a textDirection jest oparte na ogólnym Locale.

W rozdziale 13. przyjrzymy się, jak pracować z lokalizacją.

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:

W tym przypadku skorzystaliśmy z niektórych komponentów Material Design, a także wi-


dżetu Scaffold.

Oto niektóre z użytych elementów:


 Pasek aplikacji wyświetlany u góry aplikacji zwykle zawiera tytuł i działania
kontekstowe użytkownika, takie jak filtry lub ustawienia. W tym przykładzie,
za sprawą właściwości appbar widżetu Scaffold, pokazujemy widżet AppBar,
który ma tytuł, i TabBar do wyświetlania zakładek.

186

d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl

 Pływający przycisk akcji jest jednym z najbardziej znanych elementów Material


Design; jest to pływający okrągły przycisk, zwykle wyświetlany w prawym
dolnym rogu ekranu. W tym przykładzie zawiera główne działanie aplikacji —
Poproś o przysługę, zgodnie z wytycznymi Material Design.

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:

Strona przysług [inne (zamazane) informacje nie są tutaj ważne]

Zaczniemy od stworzenia niestandardowej definicji lightTheme. Istnieje kilka sposobów kolorowa-


nia naszej aplikacji; jednym z nich jest ustawienie niestandardowych kolorów dla każdej właści-
wości koloru w klasie ThemeData (ma właściwości dla każdego z dostępnych widżetów Material
Design, takich jak zakładki lub przyciski). Kluczem jest eksperymentowanie z właściwościami
koloru i wytycznymi.

Pamiętaj, że zawsze możesz nadpisać definicję Theme aplikacji w drzewie widżetów


(na przykład z innym kolorem zakładki), umieszczając ją w innym widżecie Theme.

Teraz zdefiniujmy ThemeData:


final lightTheme = ThemeData(
primarySwatch: Colors.lightGreen,

187

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

primaryColor: Colors.lightGreen.shade600,// nie jest konieczne gdy


// zdefiniowany jest primarySwatch
// jak wyżej
accentColor: Colors.orangeAccent.shade400,
primaryColorBrightness: Brightness.dark,
cardColor: Colors.lightGreen.shade100,
);

Zdefiniowaliśmy nowy widżet ThemeData, który jest domyślnie jasnozielony, i zmodyfikowaliśmy


jego podstawową primarySwatch. Użyliśmy koloru opartego na palecie Material Design, gdzie mo-
żemy zdefiniować kilka kolorów dla aplikacji.

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 (),
);

Jak można się spodziewać, aplikacja wykorzystuje zaimportowany lightTheme za pośrednic-


twem swojej właściwości theme.

Do zdefiniowania schematu kolorów aplikacji użyliśmy narzędzia kolorów ze strony Ma-


terial Design. Aby uzyskać więcej informacji, zajrzyj na https://material.io/tools/color/.
Kolejna wskazówka: jeśli używasz macOS do programowania, Edytor motywów Material
Design może pomóc w stworzeniu własnego motywu. Sprawdź to na https://material.io/
tools/theme-editor/.

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.

Za pomocą stylu uzyskaliśmy na przykład większe nagłówki list:


final titleStyle = Theme.of(context).textTheme.title;

Otrzymujemy styl titleStyle z motywu aplikacji i stosujemy go bezpośrednio do widżetu Text:


Text(
title,
style: titleStyle,
)

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.

Podobnie jak widżety Material Design, CupertinoApp, CupertinoPageScaffold i Cupertino


TabScaffold to główne widżety Cupertino dostępne w aplikacji Fluttera.

Nie wchodzimy tutaj w szczegóły widżetów CupertinoPageScaffold i CupertinoTabScaffold.


Możesz sprawdzić te i wszystkie dostępne widżety Cupertino na stronie
https://flutter.io/docs/development/ui/widgets/cupertino.

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.

Wraz z theme, CupertinoApp dodaje właściwości pomocnicze do lokalizacji, a także nawigację


między ekranami, które omówimy w rozdziale 7.

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).

Poeksperymentujmy z niektórymi widżetami iOS Cupertino w naszej aplikacji.

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.

Możemy zaprojektować pierwszy ekran przysług dla Cupertino:

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ć.

Kod źródłowy dla przykładu hands_on_cupertino_theme możesz znaleźć na GitHubie —


są tu wszystkie instrukcje warunkowe i zmiany, pozwalające korzystać z widżetów Cuper-
tino. Część omawiająca theme jest pomijana, ponieważ działa w bardzo podobny sposób
do Material Design.

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.

Korzystanie z niestandardowych czcionek


Material Design i Cupertino zapewniają dobre czcionki do projektowania aplikacji, ale czasami
warto zmienić domyślną czcionkę na taką, która jest bardziej skoncentrowana na marce / produkcie.

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.

Importowanie czcionek do projektu Fluttera


W poniższym przykładzie będziemy importować i używać niestandardowej czcionki jako domyśl-
nej dla całej aplikacji.

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.

Sprawdź różne czcionki dostępne na https://fonts.google.com/.

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

Jak widać, zdefiniowaliśmy czcionkę w kilku sekcjach:


 Pole family nazywa czcionkę w kontekście frameworku. Nie musi pasować
do nazw plików czcionek. Będziemy się do niego odwoływać w kodzie.
 Następnie mamy pole fonts, a po nim listę pól asset dla importowanej czcionki.
Wszystkie określone zasoby zostaną uwzględnione w pakiecie zasobów aplikacji.
Musimy określić każdy zasób (asset) ze szczegółami odpowiadającymi jego stylowi:
 weight — określa wagę czcionki w zasobie. Odpowiada wartościom wyliczenia
FontWeight zastosowanym podczas tworzenia layoutu, więc określ go poprawnie.
 style — określa, czy plik zasobu odpowiada zwykłemu konturowi czcionki,
czy wariantowi kursywy. Te wartości odpowiadają wyliczeniu FontStyle.

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.

Po zaimportowaniu czcionki do projektu zastosujmy czcionkę do naszych widżetów Text.

192

d0765ad53fb82babda2278a311da7afb
d
Rozdział 6. • Motyw i styl

Zastępowanie domyślnej czcionki w aplikacji


Następnym krokiem jest uaktywnienie czcionki w aplikacji. Możemy to zrobić w motywie
głównym w widżetach MaterialApp i CupertinoApp lub, jeśli wolimy, możemy dodać czcionkę bez-
pośrednio do widżetu Text poprzez jego właściwość style:
final lightTheme = ThemeData (
fontFamily: "Ubuntu",
primarySwatch: Colors.lightGreen,
primaryColor: Colors.lightGreen.shade600,
accentColor: Colors.orangeAccent.shade400,
primaryColorBrightness: Brightness.dark,
cardColor: Colors.lightGreen.shade100,
);

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.

Jeśli spróbujesz użyć pogrubionego wariantu niestandardowej rodziny czcionek, która


nie została zadeklarowana w pliku pubspec.yaml, framework użyje bardziej ogólnych
plików dla czcionki i spróbuje ekstrapolować kontury dla żądanej wagi i stylu.

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.

Dynamiczne style z MediaQuery


i LayoutBuilder
Dostosowanie layoutu do platformy może pomóc nam dotrzeć do większej liczby odbiorców.
Ale inną rzeczą, którą należy sobie uświadomić, jest ogromna liczba różnych urządzeń, co stwarza
inne wyzwania dla programistów.

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ć.

Główne klasy Fluttera pomocne w tym zadaniu to LayoutBuilder i MediaQuery.

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).

Tak to wygląda w różnych orientacjach:

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.

Zobaczmy, jak użyć klasy MediaQuery, aby utworzyć responsywny layout.

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

class FavorsList rozszerza StatelessWidget {


// ... ukryte dla zwięzłości
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
child: Text(
title,
style: titleStyle,
),
padding: EdgeInsets.only(top: 16.0),
),
Expanded(
child: _builldCardsList(context),
),
],
);
}

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

class FavorsList extends StatelessWidget {


// ... ukryte dla zwięzłości

Widget _builldCardsList(BuildContext context) {


final screenWidth = MediaQuery.of(context).size.width;
final cardsPerRow = max(screenWidth ~/ kFavorCardMaxWidth, 1);
// funkcja max() z pakietu dart:math
if (screenWidth > 400) {
return GridView.builder(
physics: BouncingScrollPhysics(),
itemCount: favors.length,
scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) {
final favor = favors[index];
return FavorCardItem(favor: favor);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 2.8,
crossAxisCount: cardsPerRow,
),
);
}

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);
},
);
}
}

Aby zmiana rozmiaru działała prawidłowo, wprowadziliśmy pewne zmiany w FavorCardItem.


Kod źródłowy przykładu hands_on_mediaquery możesz znaleźć na GitHubie.

W powyższym kodzie widać, że możemy podzielić dostępną szerokość ekranu (pobraną


z MediaQuery.of(context).size.width) przez żądaną maksymalną szerokość zakładki (kFavorCard
MaxWidth) i zapisać ją w zmiennej cardsPerRow. Później używamy tego sposobu do spraw-
dzenia, czy jest miejsce na jeszcze jedną zakładkę w rzędzie. Jeśli jest, to następnie wyświetlamy
zakładki za pomocą widżetu GridView wyświetlającego kolumny cardsPerRow. Jeśli nie ma miejsca
na więcej niż jedną zakładkę, wyświetlamy widżet ListView, jak poprzednio. Oto wynik:

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

Dodatkowe klasy responsywne


Istnieje kilka innych widżetów, które pomagają w tworzeniu responsywnych layoutów:
 CustomMultiChildLayout zapewnia swobodę wyboru sposobu rozmieszczenia
zestawu widżetów podrzędnych na ekranie przy użyciu klasy delegatów:
MultiChildLayoutDelegate.
 FittedBox zmienia rozmiar i położenie swoich potomków zgodnie z określonym
dopasowaniem. Zajrzyj na https://docs.flutter.io/flutter/painting/BoxFit-class.html,
aby zobaczyć dostępne wartości.
 AspectRatio próbuje wymusić rozmiar swojego potomka zgodnie z określonym
współczynnikiem proporcji.

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

Aplikacje mobilne są zwykle zorganizowane na wielu ekranach. We Flutterze routingiem ekranów


zarządza widżet Navigator. Zawiaduje on stosem nawigacji, wkładając do niego nową trasę (route)
lub zdejmując poprzednią.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Omówienie widżetu Navigator.
 Omówienie tras (routes).
 Poznanie przejść.
 Odkrywanie animacji Hero.

Omówienie widżetu Navigator


Aplikacje mobilne często zawierają więcej niż jeden ekran. Jeśli jesteś programistą na Androida
lub iOS, prawdopodobnie znasz klasy Activity lub ViewController, które reprezentują odpowied-
nio ekrany na tych platformach.

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

Oto główne elementy warstwy nawigacyjnej:


 Navigator — menedżer Route.
 Overlay — Navigator używa go do określenia wyglądu tras.
 Route — punkt końcowy nawigacji.

Navigator
Widżet Navigator jest głównym elementem, który przenosi zadania z jednego ekranu na drugi.

W większości przypadków będziemy przełączać ekrany i przekazywać między nimi dane, co


jest kolejnym ważnym zadaniem widżetu Navigator.

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.

Stos / historia nawigacji


Jak już być może zauważyłeś, metoda push() dodaje nowy ekran na górę stosu nawigacji. Pop()
z kolei usuwa go ze stosu nawigacji.

Podsumowując, stos nawigacji to stos ekranów, które weszły na scenę dzięki metodzie push()
widżetu Navigator.

Stos nawigacji jest również nazywany historią nawigacji.

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.

Więcej szczegółów na temat tej klasy można znaleźć w dokumentacji: https://api.flutter.dev/flutter/


widgets/RouteSettings-class.html.

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

builder: (BuildContext context) => _screen1(context));


},
),
textDirection: TextDirection.ltr,
);
}
_screen1(BuildContext context) {...} // ukryte dla zwięzłości
_screen2(BuildContext context) {...} // ukryte dla zwięzłości
}

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.

Widżet Navigator zawiera właściwość onGenerateRoute, wywołanie zwrotne odpowiedzialne


za tworzenie widżetu Route na podstawie obiektu RouteSettings przekazanego jako argument.

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.

Wracając do przykładu, spójrzmy na wywołanie zwrotne onPressed widżetu RaisedButton, w któ-


rym wprowadzamy nowy widżet Route do nawigacji. Wartość, którą przekazujemy stąd do metody
push, jest podobna do wartości zwracanej przez wywołanie zwrotne onGenerateRoute we wcze-
śniej dodanym widżecie Navigator.

Podsumowując, nasz górny widżet Navigator używa wywołania zwrotnego onGenerateRoute


tylko do zainicjowania nawigacji poprzez podanie początkowego widżetu Route. Później, za po-
mocą przycisków na ekranie, dodawane są nowe widżety Route do nawigacji za pomocą metody
push() z widżetu Navigator:

205

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

// przycisk na ekranie 2, aby przejść wstecz


onPressed: () {
Navigator.of(context).pop();
},
// _NavigatorDirectlyAppState

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ć.

Aby zarządzać przyciskiem wstecz, musimy skorzystać z widżetu WidgetsBindingObserver,


którego można użyć do reagowania na komunikaty związane z cyklem życia aplikacji. Jak wi-
dać w kodach źródłowych na GitHubie (w katalogu Navigation), najpierw przekonwertowaliśmy
naszą aplikację na Stateful i dodaliśmy WidgetsBindingObserver jako domieszkę do naszej klasy
State. Uruchomiliśmy również obserwatora w initState() za pomocą WidgetsBinding.instance.
addObserver(this); i zatrzymaliśmy obserwatora za pomocą WidgetsBinding.instance.remove
Observer(this); on dispose(). Przy takiej konfiguracji możemy nadpisać metodę didPopRoute()
z WidgetsBindingObserver i zarządzać zdarzeniami w przypadku, gdy system powie aplikacji,
aby wyznaczyła trasę. Metoda didPopRoute() jest opisana w dokumentacji w następujący sposób:
[Jest] wywoływana, gdy system nakazuje aplikacji wskazanie bieżącej trasy. Na przy-
kład w systemie Android jest to wywoływane, gdy użytkownik naciśnie przycisk Wstecz.

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,
...
)
}

Możemy też dodać metodę didPopRoute():


@override
Future<bool> didPopRoute() {
return Future.value(_navigatorKey.currentState.pop());
}

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();
}

class _NavigatorWidgetsAppState extends State<NavigatorWidgetsApp> {


@override
Widget build(BuildContext context) {
return WidgetsApp(
color: Colors.blue,
home: Builder(
builder: (context) => _screen1(context),
),

207

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

pageRouteBuilder: <Void>(RouteSettings settings, WidgetBuilder


builder) {
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
_screen1(BuildContext context) {...} // ukryte dla zwięzłości
_screen2(BuildContext context) {...} // ukryte dla zwięzłości
}

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.

Trasy nazwane (named routes)


Nazwa trasy jest ważnym elementem nawigacji. Jest to identyfikacja trasy z jej menedżerem,
widżetem Navigator.

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 „/”.

Obsługa tras nazwanych


Nasz poprzedni przykład użycia widżetu WidgetsApp jest bardzo prosty, ale możemy go jeszcze
lepiej zorganizować. Używając tras nazwanych, możemy wykonać następujące czynności:

208

d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami

 W przejrzysty sposób zorganizować ekrany.


 Scentralizować tworzenie ekranów.
 Przekazać parametry do ekranów.

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ść.

Podczas korzystania z tworzenia Routes na żądanie, przekazywanie argumentów wydaje


się łatwiejsze, ponieważ widżet zostanie utworzony w odpowiednim momencie i można
dostosować tworzenie, przekazując argumenty zgodnie z potrzebami.

Pobieranie wyników z Route


Kiedy zostanie utworzona trasa, tzn. że chcemy coś za jej pomocą uzyskać — na przykład, gdy po-
prosimy użytkownika o coś na nowej trasie, możemy uzyskać wynik za pomocą metody pop().

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

Na ekranie 2 po prostu zwracamy coś podczas wykonywania metody pop z Navigatora:


// część navigation_widgetsapp_navigation_result.dart
class _NavigatorResultAppState
extends State<NavigatorResultApp> {
Widget _screen2(BuildContext context) {
// ... hidden for brevety
RaisedButton(
child: Text("Back to Screen 1"),
onPressed: () {
Navigator.of(context).pop("Good bye from screen 2");
},
),
...
}

Drugi argument w metodzie pop to wynik z trasy.

Na ekranie wywołującym musimy pobrać wynik:


// część navigation_widgetsapp_navigation_result.dart
class _NavigatorResultAppState
extends State<NavigatorResultApp> {

Widget _screen1(BuildContext context) {


// ... ukryte dla zwięzłości

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

Przejścia między ekranami


Z punktu widzenia użytkownika zmiana ekranów musi być płynna. Widzieliśmy, że widżety
Navigator działają na widżecie Overlay, aby zarządzać Routes. Na tym poziomie zarządza się rów-
nież przejściem między trasami.

Jak widzieliśmy, MaterialPageRoute i CupertinoPageRoute to klasy, które dodają trasę modalną


do widżetu Overlay z adaptacyjnym przejściem platformy między starą i nową trasą.

W systemie Android pierwszego przejścia dokonujemy za pomocą przesunięcia ekranu. Przejście


wyjściowe działa tak samo w odwrotnej kolejności. W systemie iOS strona wsuwa się od pra-
wej, a przy wyjściu z niej przesuwa się z drugiej strony. Flutter pozwala również dostosować
to zachowanie, dodając własne przejścia między ekranami.

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.

PageRouteBuilder zawiera wiele wywołań zwrotnych i właściwości pomocnych w definicji


PageRoute. Oto kilka przykładów:
 transitionsBuilder — wywołanie zwrotne obiektu Builder dla przejścia, w którym
budujemy przejście z poprzedniej trasy do nowej trasy.
 transitionDuration — czas trwania przejścia.
 barrierColor i barrierDismitable — definiuje częściowo ukryte trasy modelu
i niepełnoekranowe.

Więcej informacji na temat klasy PageRouteBuilder można znaleźć w pełnej dokumentacji:


https://api.flutter.dev/flutter/widgets/PageRouteBuilder-class.html.

Niestandardowe przejścia w praktyce


Możemy stworzyć niestandardowe przejście i zastosować je globalnie w naszej aplikacji za pomocą
pageRouteBuilder:
// część navigation_transition.dart
class _NavigatorTransitionAppState extends State<NavigatorTransitionApp> {
@override

212

d0765ad53fb82babda2278a311da7afb
d
Rozdział 7. • Routing: nawigacja między ekranami

Widget build(BuildContext context) {


return WidgetsApp(
color: Colors.blue,
routes: {
'/': (context) => _screen1(context),
'/2': (context) => _screen2(context),
},
pageRouteBuilder: <Void>(RouteSettings settings, WidgetBuilder
builder) {
return PageRouteBuilder(
transitionsBuilder:
(BuildContext context, animation, secondaryAnimation, widget) {
return new SlideTransition(
position: new Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: widget,
);
},
pageBuilder: (BuildContext context, _, __) => builder(context),
);
},
);
}
...
}

W ten sposób zmieniamy domyślne przejście z klasy MaterialPageRoute na nasze niestandar-


dowe przejście podczas przesuwania. Robimy to w następujący sposób:
 Nasz pageRouteBuilder zwraca teraz instancję PageRouteBuilder.
 Implementujemy wywołanie zwrotne pageBuilder, aby normalnie zwrócić nasze
widżety, wywołując funkcję zwrotną.
 Implementujemy wywołanie zwrotne transitBuilder, aby zwrócić nowy widżet,
zwykle instancję AnimatedWidget lub podobną. Tutaj zwracamy widżet
SlideTransition, który zawiera dla nas logikę animacji: przejście od lewej do prawej,
aż stanie się w pełni widoczny.

Nie omówiliśmy jeszcze szczegółowo animacji. Przejdź do rozdziału 15.

Innym sposobem wdrażania niestandardowych przejść jest tworzenie na żądanie (on-demand)


obiektu Route. W takim przypadku dobrym podejściem byłoby rozszerzenie klasy PageRouteBuilder
i utworzenie przejścia wielokrotnego użytku.

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

Dokumentacja Fluttera zawiera świetne wyjaśnienia i przykłady dotyczące animacji


Hero. Nie wahaj się jej sprawdzić pod adresem: https://flutter.dev/docs/development/ui/
animations/hero-animations.

Implementacja przejść Hero


Zamierzamy zmienić naszą aplikację Favors, aby zawierała animację Hero między ekranem
listy twoich przysług (Your favors) a ekranem prośby o przysługę (Requesting a favor), tak aby
po dotknięciu pływającego przycisku nastąpiło jego płynne przejście na następną stroną. Ten
sam efekt działa podczas powrotu z ekranu prośby o przysługę do ekranu Twoich przysług:

Twoje przysługi — inne (nakładające się) informacje nie są tutaj ważne

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

builder: (context) => RequestFavorPage(


friends: mockFriends,
),
),
);
},
tooltip: 'Ask a favor',
child: Icon(Icons.add),
),
),
...
}

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
),
);
}
...

Sprawdź plik hands_on_hero na GitHubie.

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.

W naszym poprzednim przykładzie animowaliśmy nasz FloatingActionButton do całego widżetu


Request a favor. To daje fajny efekt przejścia przycisku do nowego ekranu. Nie pokazuje jed-
nak najlepszych możliwości animacji Hero — dzielenia się elementami między ekranami. Po-
nadto widżet FloatingActionButton i widżet docelowy Scaffold nie mają ze sobą nic wspól-
nego w poddrzewie widżetów, co powoduje, że zgodnie z dokumentacją nasz efekt nie jest
najlepszy z możliwych.

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.

Tym razem zamiast MaterialPageRoute użyliśmy PageRouteBuilder, ponieważ nie chcemy


efektów Material Design w tym przejściu. Aby uzyskać szczegółowe informacje, sprawdź
dokumentację PageRouteBuilder pod adresem: https://api.flutter.dev/flutter/widgets/
PageRouteBuilder-class.html.

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,
),
),
),
),
],
),
),
),
);
}
}

W ten sam sposób zdefiniowany jest _itemHeader():


Widget _itemHeader(BuildContext context, Favor favor) {
final headerStyle = Theme.of(context).textTheme.display2;

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.

Sprawdź dokumentację widżetu Hero: https://docs.flutter.io/flutter/widgets/Hero-class.html


i wypróbuj inne rozwiązania.

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.

W następnym rozdziale przeniesiemy naszą ulubioną aplikację na wyższy poziom, integrując ją


z usługami Firebase.

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.

W tej sekcji znajdują się następujące rozdziały:


 Rozdział 8., „Wtyczki Firebase”.
 Rozdział 9., „Tworzenie własnej wtyczki Fluttera”.
 Rozdział 10., „Dostęp do funkcji urządzenia z aplikacji Fluttera”.
 Rozdział 11., „Widoki platformy oraz integracja mapy”.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Konfigurowanie projektu Firebase.
 Uwierzytelnianie Firebase.
 Cloud Firestore.
 Firebase Storage.
 Firebase AdMob.
 Firebase ML Kit.

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ę.

Najważniejsze spośród oferowanych technologii to:


 Hosting — umożliwia wdrażanie aplikacji jednostronicowych, progresywnych
aplikacji webowych lub witryn statycznych.
 Baza danych czasu rzeczywistego — baza danych NoSQL (nierelacyjna baza
danych) w chmurze. Dzięki niej możemy przechowywać i synchronizować dane
w czasie rzeczywistym.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 Cloud Firestore — baza danych typu NoSQL, skupiająca się na dużych


i skalowalnych aplikacjach, które zapewniają zaawansowaną obsługę zapytań
w porównaniu z bazą danych czasu rzeczywistego.
 Funkcje chmurowe — funkcje uruchamiane przez wiele produktów Firebase,
na przykład wspomniane powyżej, a także przez użytkownika (przy użyciu
pakietu SDK). Możemy opracować skrypty reagujące na zmiany w bazie danych,
uwierzytelnianie użytkowników i nie tylko.
 Monitorowanie wydajności — zbieraj i analizuj informacje o aplikacjach
z perspektywy użytkownika.
 Uwierzytelnianie — ułatwia rozwój warstwy uwierzytelniania aplikacji, poprawiając
wrażenia użytkownika i bezpieczeństwo. Umożliwia korzystanie z wielu dostawców
uwierzytelniania, takich jak e-mail / hasło, uwierzytelnianie telefoniczne, a także
Google, Facebook i inne systemy logowania.
 Komunikacja w chmurze Firebase — przesyłanie wiadomości w chmurze ——
aby wymieniać wiadomości między aplikacjami a serwerem (aplikacje na Androida,
iOS i webowe).
 AdMob — wyświetla reklamy, aby zarabiać na aplikacjach.
 Zestaw do uczenia maszynowego — narzędzia do implementacji zaawansowanych
zasobów uczenia maszynowego (ML — machine learning) w dowolnej aplikacji.

Flutter zawiera różne wtyczki do współpracy z Firebase. W następnych sekcjach będziemy


używać niektórych z nich, aby zintegrować naszą aplikację z tymi niesamowitymi usługami.

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.

Pierwszym krokiem jest utworzenie projektu aplikacji Firebase.

Robimy to za pomocą narzędzia konsoli Firebase (https://console.firebase.google.com/). Pozwala


nam ono zarządzać wszystkimi naszymi projektami Firebase, włączać / wyłączać określone tech-
nologie i monitorować użycie:
1. To jest początkowy ekran konsoli Firebase, na którym można zobaczyć ostatnie
projekty, a także dodać nowy projekt (zobacz pierwszy rysunek na następnej
stronie).
2. Proces inicjowania projektu Firebase jest prosty i łatwy do przeprowadzenia,
jak pokazano na drugim zrzucie ekranu na następnej stronie.

226

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

227

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

3. Projekt zostanie wygenerowany w ciągu kilku sekund w poniżej pokazany sposób.

4. Po utworzeniu projektu nastąpi przekierowanie do jego ekranu, jak pokazano


poniżej.

5. Poniższy ekran przedstawia wszystkie opcje dotyczące projektu, a także skrót


do jego ustawień.

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.

Łączenie aplikacji Fluttera z Firebase


Jak widzieliśmy wcześniej, możliwe jest skonfigurowanie wielu aplikacji z wielu platform, aby po-
łączyć się z projektem Firebase. Na stronie projektu Firebase mamy możliwość dodania apli-
kacji na iOS, Androida i przeglądarkę internetową.

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.

Konfigurowanie aplikacji na Androida


Możemy skonfigurować aplikację na Androida za pomocą skrótu asystenta konfiguracji Androida
na stronie ogólnego projektu, którą widzieliśmy wcześniej:

229

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Spowoduje to przeniesienie ze strony konfiguracji do aplikacji na Androida pokazanej na poniż-


szym zrzucie ekranu:

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ę
}
}

2. Następnie w android/app/build.gradle musimy aktywować wtyczkę i dodać


biblioteczną zależność ‘androidx.annotation’, jak pokazano w poniższym kodzie.
// część android /app/build.gradle
...
dependencies {
implementation 'androidx.annotation:annotation:1.0.2'
...
}

// firebase
// Dodaj poniższą linię na końcu pliku:
apply plugin: 'com.google.gms.google-services'

Biblioteka androix.annotation nie jest bezpośrednio związana z Firebase. Powinniśmy


ją jednak dodać, ponieważ niektóre biblioteki potrzebują jej wewnętrznie, na przykład
te z Firebase.

3. Na koniec, podczas uruchomienia poniższej komendy, wszystko zostanie


skonfigurowane w środowisku Androida.
flutter packages get

231

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Konfiguracja aplikacji na iOS


W przypadku wersji na iOS proces wygląda bardzo podobnie. Rozpoczynając od konfiguracji
w konsoli Firebase, ustawiamy nazwę pakietu, tak jak to zrobiliśmy dla Androida.

Następnie możemy pobrać wygenerowany plik GoogleService-Info.plist (odpowiednik google-


-services.json na iOS) i dodać go do katalogu projektu iOS ios/Runner. Ważne jest, aby zrobić
to w Xcode, otwierając na nim projekt iOS i przeciągając plik do Xcode, dzięki czemu zostanie
on zarejestrowany do włączenia podczas kompilacji.

Krok dodawania pliku GoogleService-Info.plist zmienia się w zależności od wersji wtyczek


Fluttera. Sprawdź najbardziej odpowiedni sposób pod tym adresem: https://firebase.google.
com/docs/flutter/setup.

W przeciwieństwie do tego, jak to wygląda w Androidzie, nie ma potrzeby dodawania określonych


zależności iOS dla Firebase. Następnym krokiem jest praca w kontekście Fluttera.

FlutterFire
Aplikacje Fluttera korzystają z zestawu wtyczek Fluttera, aby uzyskać dostęp do usług Firebase.

FlutterFire zawiera konkretne implementacje dla platform iOS i Android.

Sprawdź stronę wtyczek FlutterFire, aby uzyskać więcej informacji na temat najnowszych
wersji wtyczek Firebase: https://firebaseopensource.com/projects/flutter/plugins/.

Dodanie zależności FlutterFire do projektu Fluttera


Powinniśmy dodać wtyczkę bazową do naszego projektu jako początkową podstawową zależność,
jak pokazano w poniższym kodzie:
# część pubspec.yaml
dependencies:
...
firebase_core: 0.2.5 # Wtyczka bazowa Firebase

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

Uwaga dla Androida:


Ponieważ używamy najnowszych wersji wtyczek Firebase opartych na wersjach zależ-
ności AndroidX, nasz projekt aplikacji został przeniesiony do AndroidX. Ze względu
na problemy z jego kompatybilnością polecam przeczytać więcej pod tym adresem:
https://flutter.dev/docs/development/packages-and-plugins/androidx-compatibility.

Wykonanie polecenia flutter packages get kończy proces instalacji, co oznacza, że możemy
teraz rozpocząć pracę z wtyczkami.

Jeśli uznasz to za łatwiejsze, możesz postępować zgodnie z oficjalną dokumentacją Firebase


dotyczącą inicjalizacji Firebase we Flutterze: https://firebase.google.com/docs/flutter/setup.

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.

Włączanie usług uwierzytelniania w Firebase


Aby włączyć usługi uwierzytelniania w Firebase, musimy odwiedzić sekcję Authentication
(Uwierzytelnianie) w konsoli Firebase, jak pokazano na poniższym zrzucie ekranu:

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.

Po tej konfiguracji możemy rozpocząć pracę nad kodem Fluttera.

Aby uwierzytelnić się za pomocą prawdziwych numerów i otrzymać kod weryfikacyjny,


musisz podpisać swoją aplikację dla wersji release. Więcej o wersji release dowiemy się
w rozdziale 12.

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.

Więcej na temat tego widżetu możesz zobaczyć na stronie material.io: https://material.io/


archive/guidelines/components/steppers.html.

Logowanie za pomocą Firebase


Cały kod możesz sprawdzić w załączonym projekcie hands_on_firebase. Główne funkcje to
_sendVerificationCode() i _executeLogin() z LoginPageState.

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.

Dostosowaliśmy wygląd działań wykonywanych w każdym kroku; sprawdź szczegółowo


metodę _stepControlsBuilder w klasie LoginPageState. Sprawdź również dokumentację tej
właściwości widżetu Stepper: https://api.flutter.dev/flutter/material/Stepper/controls
Builder.html.

236

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

Wysyłanie kodu weryfikacyjnego


Pierwszym etapem uwierzytelnienia telefonicznego jest wysłanie przez serwer (w naszym przy-
padku Firebase) kodu weryfikacyjnego SMS-em pod numer telefonu wpisany przez użytkownika.

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

void _sendVerificationCode() async {


final PhoneCodeSent codeSent = (String verId, [int forceCodeResend]) {
_verificationId = verId;
_goToVerificationStep();
};

final PhoneVerificationCompleted verificationSuccess = (FirebaseUser


user) {
_loggedIn();
};

final PhoneVerificationFailed verificationFail = (AuthException


exception) {
goBackToFirstStep();
};

final PhoneCodeAutoRetrievalTimeout autoRetrievalTimeout = (String verId) {


this._verificationId = verId;
};

await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: _phoneNumber,
codeSent: codeSent,
verificationCompleted: verificationSuccess,
verificationFailed: verificationFail,
codeAutoRetrievalTimeout: autoRetrievalTimeout,
timeout: Duration(seconds: 0),
);
}

Metoda verifyPhoneNumber jest wykonywana asynchronicznie (z inną async i zwraca Future),


więc przed wywołaniem jest potrzebne słowo kluczowe await.

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

 phoneNumber — numer telefonu, na który ma zostać wysłany kod weryfikacyjny.


 codeSent — wywoływana, gdy kod jest wysyłany na numer phoneNumber.
 verificationCompleted — wywoływana, gdy kod jest automatycznie
potwierdzany przez pakiet SDK auth Firebase.
 verificationFailed — wywoływana, gdy wystąpi błąd podczas weryfikacji
numeru telefonu.
 timeout — maksymalny czas oczekiwania biblioteki na automatyczną
weryfikację, 0 oznacza brak timeoutu.
 codeAutoRetrievalTimeout — wywoływane po osiągnięciu określonego limitu
czasu, co oznacza, że automatyczna weryfikacja nie działała poprawnie (chyba
że jest ustawione na 0).
 Wywołanie zwrotne codeSent spowoduje przejście widżetu Stepper do drugiego
kroku, w którym użytkownik powinien wpisać swój kod weryfikacyjny.

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.

Weryfikacja kodu SMS


Drugim krokiem jest sprawdzenie, czy użytkownik podał poprawny kod, tym samym powi-
nien on zalogować się do aplikacji. Odbywa się to w metodzie signInWithCredential, jak po-
kazano tutaj:
// metoda _executeLogin (LoginPageState) login_page.dart

void _executeLogin() async {


setState(() {
_showProgress = true;
});

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ć.

Aktualizacja profilu i statusu logowania


Obiekt użytkownika Firebase zawiera nie tylko numery telefonów, ale również zestaw infor-
macji o innej metodzie logowania, na przykład za pomocą adresu e-mail, oraz właściwości,
które pomagają zdefiniować profil użytkownika, takie jak jego nazwa i adres URL zdjęcia. Tutaj,
w ostatnim kroku procesu logowania, możemy zapisać profil użytkownika oraz jego displayName,
aby inni użytkownicy mogli go łatwo zidentyfikować. Odbywa się to w metodzie _saveProfile (),
jak pokazano poniżej:
// część klasy LoginPageState
void _saveProfile() async {
setState(() {
_showProgress = true;
});

final user = await FirebaseAuth.instance.currentUser();

final updateInfo = UserUpdateInfo();


updateInfo.displayName = _displayName;

await user.updateProfile(updateInfo);

// ... ostatnia część jest wyjaśniona poniżej


}

Metoda currentUser() jest przydatna w przypadku wszelkich działań związanych z zalogowanym


użytkownikiem. W takiej sytuacji możemy pobrać i zaktualizować żądane informacje (na razie
nazwę użytkownika).

UserUpdateInfo to klasa pomocnicza do przechowywania danych aktualizacji; w następnej sekcji


będziemy używać jeszcze jednej właściwości do przechowywania adresu URL zdjęcia profilu
użytkownika.

239

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Ponieważ wiemy, że użytkownik jest zalogowany, możemy go przekierować do strony Favors


za pomocą dobrze znanej klasy Navigator w następujący sposób:
// końcowa część _saveProfile () LoginPage
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => FavorsPage(),
),
);

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ź.

To wszystkie informacje, jeśli chodzi o uwierzytelnianie telefoniczne; w następnej sekcji będziemy


przechowywać nasze przysługi w Cloud Firestore.

240

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

Baza danych NoSQL z Cloud Firestore


Cloud Firestore firmy Firebase to elastyczna i skalowalna baza danych NoSQL w chmurze.
Pomaga nam w tworzeniu aplikacji czasu rzeczywistego z technologiami synchronizacji między
klientami, dzięki czemu nasza aplikacja jest szybka i funkcjonalna.

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łączanie Cloud Firestore w Firebase


Jeśli pamiętasz, pierwszym krokiem jest włączenie niezbędnych usług w Firebase. W tym przy-
padku chcemy włączyć technologię Cloud Firestore w Firebase:

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

Cloud Firestore i Flutter


Jak widzieliśmy wcześniej, FlutterFire zapewnia zestaw wtyczek dla różnych technologii.

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.

Ładowanie przysług z Firestore


Korzystamy z Firestore za pomocą klasy Firestore z biblioteki Darta cloud_firestore.

W funkcji initState() FavorsPageState dodajemy wywołanie watchFavorsCollection().

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.

W watchFavorsCollection() zaczynamy ładować przysługi z Firebase, jak pokazano tutaj:


// część favors_page.dart watchFavorsCollection
class FavorsPageState extends State<FavorsPage> {

@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);
});

// aktualizacja naszych list


setState(() {
this.completedFavors = newCompletedFavors;

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'],
);

Konstruktor fromMap otrzymuje ID (identyfikator dokumentu) i instancję Map z odpowiednimi


polami. Jak widać, jest to proste użycie domyślnego konstruktora z parametrami, przyjmują-
cymi dane pochodzące z Firebase:

To samo dotyczy obiektu Friend. Sprawdź klasę Favor dla tego przykładu.

 W zależności od statusu przysługi jest umieszczany na odpowiedniej liście.


 Oprócz tego tworzony jest zestaw przyjaciół, a każdy znajomy od danej przysługi
jest dodawany do zestawu. Ponieważ zestawy (Set) pozwalają na pojedyncze
wystąpienie każdego obiektu, nie będą obecni powtarzający się znajomi.

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.

 Na końcu, listy instancji State są aktualizowane, aby spowodować przebudowę


layoutu.

245

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Aktualizowanie przysług w Firebase


Wcześniej, gdy używaliśmy fałszywych danych, zmienialiśmy w pamięci tylko nasze listy. Teraz
musimy zaktualizować nasze odpowiednie dokumenty dotyczące przysług w Firebase, aby uru-
chomiło to nasze wcześniej zdefiniowane wywołanie zwrotne, które spowoduje przebudowę i ak-
tualizację naszych layoutów.

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.

Metoda _updateFavorOnFirebase jest używana w przypadku wcześniej zdefiniowanych metod:


complete, giveUp, acceptToDo i refuseToDo. To wszystko, czego potrzebujemy, aby zaktualizować
dane w Firebase i odzwierciedlić zmiany w layoucie aplikacji.

Zapis przysługi w Firebase


W klasie RequestFavorPageState musimy dodać kod, aby wstawić nową przysługę do naszej
kolekcji przysług w Firestore. Modyfikacji dokonamy w metodzie _save(), która do tej pory
nic nie zapisywała:
// część pliku request_favors_page.dart
void save(BuildContext context) async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save(); // 1
final currentUser = await FirebaseAuth.instance.currentUser();
//2

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
}
}

Proces zapisywania jest zdefiniowany w następujący sposób:


1. Sprawdzamy i zapisujemy pola Form. Oznacza to, że przechowujemy wartość pól
tekstowych opisu, terminu i znajomego jako zmienne do późniejszego wykorzystania.
Istnieją inne sposoby uzyskiwania wartości pól form; ten jest prosty i czysty.
2. Otrzymujemy aktualnie zalogowanego użytkownika, ponieważ potrzebujemy
o nim informacji, aby wypełnić prośbę o przysługę — dzięki czemu znajomy,
którego dotyczy prośba, będzie wiedział, kto prosi go o przysługę.
3. Wywołujemy nową metodę narzędziową _saveFavorOnFirebase(), która tworzy
wywołanie Firebase, z nową instancją Favor utworzoną z wartościami
pochodzącymi z Form, jak pokazano poniżej.
_saveFavorOnFirebase(Favor favor) async {
await Firestore.instance
.collection('favors')
.document() // nie przekazujemy żadnego id dokumentu
.setData(favor.toJson());
}

Jak widać, wywołanie jest bardzo podobne do poprzedniego kodu aktualizującego


dane. Jedyną różnicą jest to, że nie przechodzimy do konkretnego dokumentu
w wywołaniu metody document(). Spowoduje to, że Firestore wygeneruje nowy
unikalny identyfikator, a następnie zamapuje go na nowy dokument, w którym
później ustawimy dane.
4. Po zapisaniu pobieramy trasę, dzięki czemu wracamy do poprzedniego ekranu.

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.

Cloud Storage z Firebase Storage


Firebase Storage to świetna platforma do przechowywania plików w chmurze. Najbardziej typowe
przypadki jej użycia to przechowywanie zdjęć lub filmów od użytkowników, bez żadnych ograni-
czeń; możesz przechowywać dowolny rodzaj danych potrzebnych w Twojej aplikacji. Za pomocą
tego potężnego mechanizmu przechowywania danych są zaspokajane potrzeby aplikacji.

Wprowadzenie do Firebase Storage


Podobnie jak poprzednie usługi, Firebase Storage w pierwszym kroku wyjaśnia potrzebę za-
bezpieczenia danych, co pokazano tutaj rysunku na następnej stronie.

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.

W przypadku aplikacji w świecie rzeczywistym ponownie zaleca się utworzenie najlepszych


reguł, które mogą pomóc w ochronie danych użytkownika: https://firebase.google.com/
docs/storage/security.

248

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

Po tym wstępnym kroku możemy dodać biblioteki specyficzne dla Fluttera i rozpocząć etap
programowania.

Dodawanie zależności Flutter Storage


Oprócz poprzednich wtyczek FlutterFire zapewnia wtyczkę do Firebase Storage. Musimy dodać
zależność do naszego pubspec.yaml, jak pokazano w poniższym kodzie:
dependencies:
firebase_storage: ^2.1.0 # Cloud Firestore

Po uzyskaniu zależności za pomocą flutter packages get jesteśmy gotowi do użycia Firebase
Storage w naszym projekcie.

Przesyłanie plików do Firebase


Zamierzamy dodać funkcję przesyłania plików do Firebase Storage dla naszej aplikacji Favors.
W sekcji Profile procesu logowania, po pomyślnym zalogowaniu się użytkownika, możemy
dodać funkcję, dzięki której użytkownik ma możliwość dodania zdjęcia do swojego profilu.

Możesz to sprawdzić w aplikacji na dole ekranu logowania, co pokazano na poniższym zrzu-


cie ekranu:

249

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Dodaliśmy również do zależności kolejną przydatną bibliotekę, image_picker, dzięki czemu


możemy pobrać obraz z kamery i przesłać go do magazynu Firebase w celu wykorzystania go
jako zdjęcia profilowego użytkownika.

Aby szczegółowo sprawdzić możliwość wykorzystania aparatu i wtyczkę image_picker,


przeczytaj rozdział 10., w szczególności sekcję Integracja aparatu w telefonie.

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

void _saveProfile() async {


setState(() {
_showProgress = true;
});

final user = await FirebaseAuth.instance.currentUser();

final updateInfo = UserUpdateInfo();


updateInfo.displayName = _displayName;
updateInfo.photoUrl = await uploadPicture(user.uid);

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

StorageUploadTask uploadTask = ref.putFile(_imageFile,


StorageMetadata(contentType: 'image/png')); // 2
StorageTaskSnapshot lastSnapshot = await uploadTask.onComplete; // 3
return await lastSnapshot.ref.getDownloadURL(); // 4
}

Zadanie uploadu do Firebase Storage jest podzielone na następujące małe kroki:


1. Najpierw tworzymy odniesienie do nowego obiektu w Storage. Jak widać,
łączymy wywołania child() , tworząc folder o nazwie Profiles i plik
z identyfikatorem użytkownika w nazwie.
2. Następnie tworzymy zadanie uploadu, które zainicjuje przesyłanie do Firebase.
Zwróć uwagę na parametr StorageMetadata; tworzymy typ zawartości obrazu,
ponieważ będzie przechowywany właśnie obraz.
3. Tutaj czekamy na odniesienie Future do zadania uploadu, uzyskując ostatnią
migawkę zadania (wynik).
4. Na koniec otrzymujemy adres URL pliku; jest to adres do pobrania pliku
Firebase, dający nam dostęp do magazynu plików (Storage).

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.

W następnej sekcji zajmiemy się wtyczką Firebase AdMob.

Reklamy z Firebase AdMob


Google AdMob to technologia do obsługi reklamy mobilnej, służąca do generowania przychodów.
Dodawanie reklam do aplikacji to powszechna metoda zarabiania i dobre rozwiązanie w przy-
padku bezpłatnych aplikacji.

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.

Na stronie apps.admob.com/ możemy tworzyć i zarządzać wszystkimi naszymi aplikacjami.

252

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

Pamiętaj, że projekty Firebase i aplikacje AdMob nie są bezpośrednio połączone, dopóki


ręcznie nie połączysz aplikacji i projektu/aplikacji Firebase. Sytuacja może wyglądać inaczej,
gdy będziesz czytać tę książkę. W tej chwili jednak wszystko jest oddzielone: aplikacje
z AdMob są rejestrowane niezależnie od Firebase i musimy je połączyć ręcznie.

Tworzenie konta AdMob


Za pomocą poprzedniego linku mamy możliwość założenia naszego konta AdSense i AdMob.

Możesz postępować zgodnie z instrukcjami na stronie, aby utworzyć nowe konto na wzór
— zobacz pierwszy rysunek na następnej stronie.

Teraz jesteśmy gotowi do zarządzania naszymi aplikacjami. W przypadku Fluttera tworzymy


dwie aplikacje — jedną na Androida i jedną na iOS (zobacz drugi rysunek na następnej
stronie).

253

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Zarządzamy naszymi aplikacjami i otrzymujemy unikalny identyfikator dla każdej z nich.

Tworzenie aplikacji w portalu AdMob polega po prostu na wykonaniu czynności konfi-


guracyjnych. Upewnij się, że tworzysz jedną aplikację dla każdej platformy.

254

d0765ad53fb82babda2278a311da7afb
d
Rozdział 8. • Wtyczki Firebase

Po pomyślnym dodaniu aplikacji do AdMob pojawi się następujące okno:

Będziemy używać tych identyfikatorów do wyświetlania banerów w naszej aplikacji.

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.

Klasa FirebaseAdMob jest punktem wyjścia do dodawania banerów do aplikacji. W przeciwień-


stwie do wcześniejszych wtyczek Firebase, które pobierają wszystkie informacje potrzebne
do uruchomienia z plików google-services.json (Android) i GoogleService-info.plist (iOS), w tym
przypadku potrzebujemy dodatkowego ustawienia, zanim będziemy mogli skutecznie korzystać
z wtyczki.

Musimy ręcznie zainicjować wtyczkę za pomocą naszych identyfikatorów aplikacji. Można to


zrobić w dowolnym momencie. Na przykład w naszej aplikacji Favors możemy to zrobić za
pomocą głównej metody, jak pokazano tutaj:
void main() {
FirebaseAdMob.instance.initialize(
appId: Platform.isAndroid
? 'ca-app-pub-3940256099942544~3347511713' // replace with your
Android app id
: 'ca-app-pub-3940256099942544~1458002511', // replace with your
iOS app id
);
runApp(MyApp());
}

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

 Korzystając z reklam testowych dostarczonych przez Google. W tym celu używamy


zestawu reklam pozorowanych, bez rzeczywistego ruchu w naszych reklamach
aplikacji.

To ustawienie jest naprawdę ważne, ponieważ generowanie nieprawidłowego ruchu


do naszych aplikacji może skutkować unieważnieniem konta. Dlatego upewnij się, że
podczas programowania używasz reklam testowych; dowiedz się więcej tutaj:
https://developers.google.com/admob/android/test-ads, a potem zmień id na rzeczywisty
identyfikator aplikacji z urządzeniami testowymi.

 Dodając urządzenia testujące z naszymi prawdziwymi identyfikatorami. Jest to


preferowana opcja, ponieważ oznacza, że nasze reklamy mają prawdziwy wygląd.

W przypadku korzystania z emulatorów Androida lub symulatorów iOS są one automa-


tycznie konfigurowane jako urządzenia testowe. W przypadku prawdziwych urządzeń
przy pierwszym uruchomieniu poprawnie skonfigurowanej aplikacji AdMob identyfika-
tor urządzenia testowego pojawi się w LogCat (Android) lub logach Konsoli (iOS).
Użyj tego identyfikatora, aby oznaczyć swoje urządzenie jako urządzenie testowe.
Więcej informacji znajdziesz tutaj: https://developers.google.com/admob/ios/test-ads
i https://developers.google.com/admob/android/test-ads.

Dodatkowy krok dla Androida


Dla systemu Android musimy wykonać dodatkowy krok, czyli dodać ten sam identyfikator apli-
kacji AdMob, który został użyty do zainicjowania wtyczki FirebaseAdMob. Dodajemy go do
pliku AndroidManifest.xml z następującym kodem:
<!-- AndroidManifest.xml -->
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
</application>

Odbywa się to poprzez dodanie wartości <meta-data> zawierającej ten sam identyfikator aplikacji,
który został wcześniej skonfigurowany.

Dodatkowy krok dla iOS


Dla iOS także musimy dodać ten sam identyfikator aplikacji AdMob, który został użyty do zaini-
cjowania wtyczki FirebaseAdMob. Dodajemy go do pliku Info.plist z następującym kodem:
<!-- Info.plist -->
<plist version="1.0">
<dict>
...
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string> // zastąp

257

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

// id Twojej aplikacji iOS


...
</dict>

Odbywa się to poprzez dodanie wpisu do sekcji <dict> zawierającej ten sam identyfikator
aplikacji, który został wcześniej skonfigurowany dla systemu iOS.

Wyświetlanie reklam we Flutterze


Po prawidłowym skonfigurowaniu inicjalizacji wtyczki AdMob możemy zacząć wyświetlać
różnego rodzaju reklamy, np. banery. Reklamy, w przeciwieństwie do wielu widoków Fluttera, są
wyświetlane w inny sposób niż widżety. Nie mają węzła w drzewie.

Aby wyświetlać reklamy, zmienimy RequestFavorPageState. Po zapisaniu żądania będziemy


wyświetlać baner reklamowy u dołu ekranu i pełnoekranową reklamę InterstitialAd.

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;

W initState() przygotowujemy reklamy w następujący sposób:


_bannerAd = BannerAd(
adUnitId: BannerAd.testAdUnitId,
size: AdSize.banner,
)
..load()
..show();

_interstitialAd = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
)..load();

Więcej typów reklam można zobaczyć tutaj: https://pub.dev/packages/firebase_admob.

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.

testAdUnitId z klas Ad używamy do tworzenia fałszywych reklam, czyli prostych reklam


testowych. Możesz tworzyć / konfigurować jednostki reklamowe w portalu AdMob.

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.

Firebase ML Kit zapewnia wiele narzędzi, takich jak:


 Rozpoznawanie tekstu (OCR) — rozpoznaje tekst na zdjęciach. Dostępne
jako funkcje na urządzeniu i w chmurze.
 Wykrywanie twarzy — wykrywa twarze na obrazie, identyfikuje kluczowe cechy
twarzy i pobiera kontury wykrytych twarzy. Dostępne jako funkcjonalność
na urządzeniu.
 Skanowanie kodów kreskowych — skanuje wiele typów kodów kreskowych.
Dostępne na urządzeniu.
 Etykietowanie obrazów — rozpoznaje elementy obrazu. Dostępne jako funkcje
na urządzeniu i w chmurze.
 Rozpoznawanie punktów orientacyjnych — rozpoznaje dobrze znane punkty
orientacyjne na obrazie. Dostępne jako funkcjonalność w chmurze.
 Identyfikacja języka — określa język ciągu tekstu. Dostępne jako funkcjonalność
na urządzeniu.
 Niestandardowy model wnioskowania — używa niestandardowego modelu
TensorFlow Lite (https://www.tensorflow.org/lite) z zestawem ML. Dostępne jako
funkcjonalność na urządzeniu.

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ą.

Dodanie zestawu uczenia maszynowego do Fluttera


Podobnie jak w przypadku poprzednich wtyczek FlutterFire musimy dodać zależność ML Kit
do naszego pubspec.yaml w następujący sposób:
dependencies:
firebase_ml_vision: ^0.6.0 # ML Vision

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

Korzystanie z detektora etykiet we Flutterze


Jak widzieliśmy, mamy wiele narzędzi dostarczonych przez Firebase ML Kit; w tym przykładzie
uruchomimy detektor etykiet na obrazie, aby obraz został zinterpretowany. Biblioteka poda nam
informacje o tym, co to może być za obraz. Może to być przydatne przy wstępnym przetwarzaniu
i filtrowaniu obrazów.

W zależności od usługi, z której chcemy skorzystać, musimy dodać określone biblioteki na pozio-
mie systemu.

W przypadku etykietowania obrazów musimy dodać bibliotekę etykiet (OCR) na natywnym


poziomie naszego projektu.

W systemie Android odbywa się to w pliku android/app/build.gradle, po prostu za sprawą


pobrania kodu natywnego, który umożliwia rozpoznawanie encji w obrazie:
dependencies {
...
api 'com.google.firebase:firebase-ml-vision-image-label-model:16.2.0'
}

To kolejny krok, ale jest opcjonalny. Możemy dodać to do AndroidManifest.xml w następujący


sposób:
<application ...>
...
<meta-data
android:name="com.google.firebase.ml.vision.DEPENDENCIES"
android:value="ocr" />
<!—Aby używać wielu modeli: android:value="ocr,label,barcode,face" -->
</application>

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.

Uwaga: Podfile prawdopodobnie istniałby, gdybyś spróbował uruchomić aplikację Flutter


na iOS, ponieważ podczas kompilacji otrzyma odpowiednie pody dla wtyczek Fluttera.
Więc Podfile może już mieć jakąś zawartość.

Następnie dodaj zależność do etykietowania obrazu w Podfile za pomocą następującego kodu:


pod ‘Firebase/MLVisionLabelModel’

Teraz wykonaj następujące polecenie:


pod install

261

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Całą niezbędną konfigurację dla każdej technologii można szczegółowo zobaczyć na


stronie wtyczki: https://pub.dartlang.org/packages/firebase_ml_vision.

Po dodaniu zależności możemy wykryć encje w obrazie.

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().

Metoda _labelImage() wygląda następująco:


// część login_page.dart

_labelImage() async {
if (_imageFile == null) return;

setState(() {
_labeling = true;
});

final FirebaseVisionImage visionImage =


FirebaseVisionImage.fromFile(_imageFile); //1

final LabelDetector labelDetector =


FirebaseVision.instance.labelDetector(); //2

List<Label> labels = await labelDetector.detectInImage(visionImage);


//3

setState(() {
_labels = labels;
_labeling = false;
});
}

Aby wykryć encje, wykonujemy kilka kroków:


1. Tworzymy instancję FirebaseVisionImage z przechwyconego obrazu.
2. Następnie tworzymy instancję Firebase LabelDetector.
3. Przetwarzamy obraz za pomocą LabelDetector; zwróci to kolekcję obiektów Label,
które zostaną wyświetlone później.

Pamiętaj, że wszystkie przetwarzane informacje mają przypisaną wartość ufności.

Przechwytując prosty obraz z aplikacji aparatu emulatora Androida, przedstawiający pokój


i kilka mebli, otrzymujemy kilka etykiet, jak pokazano tutaj:

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.

Na tym kończymy integrację etykietowania obrazów w naszej aplikacji.

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.

Tworzenie projektu pakietu/wtyczki


Jak widzieliśmy, tworzenie w pełni funkcjonalnych aplikacji Fluttera polega na wykorzystaniu
jednego lub więcej pakietów udostępnionych przez społeczność w ekosystemach Flutter/Dart.
Tworzenie wszystkiego od podstaw byłoby niepraktyczne dla większości aplikacji, ponieważ
musielibyśmy wielokrotnie opracowywać kod specyficzny dla platformy, co wydłuża i spowalnia
cykl rozwoju.

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

W tym rozdziale zamierzamy wygenerować prosty projekt wtyczki Fluttera i przeanalizować


jego strukturę. Wygenerowana wtyczka zawiera przykład, który zawiera metodę uzyskującą
wersję platformy, czyli aktualnie działającej wersji systemu operacyjnego. Jest to prosta wtyczka,
za którą nie kryje się nic specjalnego, ale która jest dobrym wprowadzeniem do projektów
wtyczek.

Pakiety Fluttera a pakiety Dart


W rozdziale 2. widzieliśmy, jak wyglądają pakiety Dart i jak są zarządzane przez narzędzie
publikacji. We Flutterze nie jest inaczej; pakiety Fluttera to nic innego jak pakiety Dart, które
mogą zawierać funkcje specyficzne dla Fluttera, a zatem są od niego zależne.

Istnieją dwa rodzaje pakietów Flutter:


 Pakiety Dart — istnieją proste pakiety Darta, mogące zapewnić przydatne
biblioteki, które nie są zależne od frameworka Flutter i dlatego mogą być używane
w dowolnym środowisku Darta: webowym, desktopowym, serwerowym itp.
Pakiety specyficzne dla Fluttera, które są od niego zależne, mogą być używane
tylko w kontekście Fluttera.
 Pakiety wtyczek — istnieją pakiety, które zawierają implementacje funkcji zależne
od platformy (Java/Kotlin w Androidzie i ObjC/Swift w iOS), a część Dart to nic
innego jak API, które tłumaczy wywołania na poziom Fluttera. Jeśli sprawdzisz
pakiety używane w naszej aplikacji Favors, takie jak Firebase lub image_picker,
zobaczysz, że to pakiety wtyczek, które zawierają implementacje natywne dla platformy
z interfejsem API napisanym w Dart.

Rozpoczynanie projektu pakietu Dart


Aby utworzyć pakiet Dart we Flutterze, użyjemy dobrze znanego narzędzia Fluttera create.
Jeden z argumentów tego narzędzia (--template) określa rodzaj pakietu, który tworzymy: pa-
kiet aplikacji, pakiet Dart lub pakiet wtyczek. Używamy argumentu --template, aby utworzyć
nowy pakiet Dart:
flutter create --template=package simple_package

Spowoduje to wygenerowanie projektu o nazwie simple_package, który zawiera prosty pro-


jekt pakietu Dart. Wygenerowana struktura projektu jest tak prosta jak pakiet Dart i nie ma
nic specyficznego dla Fluttera — zobacz rysunek na następnej stronie.

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.

Omówmy teraz pakiety wtyczek.

267

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Uruchamianie pakietu wtyczek Fluttera


Aby utworzyć pakiet wtyczek we Flutterze, tym razem ponownie używamy narzędzia create
z szablonem plugin:
flutter create --template=plugin hands_on_platform_version -a kotlin -i swift

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.

Spowoduje to wygenerowanie projektu o nazwie hands_on_platform_version, który zawiera


projekt pakietu Flutter. Wygenerowana struktura projektu jest podobna do pakietu aplikacji
Fluttera.

Struktura projektu wtyczki


W poprzedniej sekcji wygenerowaliśmy projekt wtyczki, aby rozpocząć analizę. Przyjrzyjmy
się teraz jego konkretnym częściom. Projekt jest domyślnym przykładem wtyczki Flutter; jedyne,
co robi, to zwraca wersję systemu operacyjnego uruchomionego urządzenia.

Istnieją jednak pewne różnice:


 Foldery ios/ i android/ nie zawierają natywnych aplikacji, które uruchamiają
środowisko uruchomieniowe Fluttera. Zamiast tego zawierają natywne klasy,
które są punktami wejścia do określonych natywnych implementacji. Sprawdzimy
to dokładnie później.
 Katalog example/ to prosty pakiet aplikacji Flutter — podpakiet wewnątrz pakietu
wtyczki.
 lib/hands_on_show_toast.dart to API Darta dla wtyczki.
// pubspec.yaml

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.

Wdrożenie wtyczki Androida


Jak widzieliśmy, domyślny szablon projektu generuje mały kod, który pobiera wersję plat-
formy. Rzućmy okiem na wygenerowany kod w HandsOnPlatformVersionPlugin.kt, który znaj-
duje się w pakiecie com.example.hands_on_platform_version systemu Android. Ten pojedynczy
plik jest punktem wejścia wtyczki:
// HandsOnPlatformVersionPlugin.kt

class HandsOnPlatformVersionPlugin: MethodCallHandler {


companion object {
fun registerWith(registrar: Registrar) { // 1
val channel = MethodChannel(registrar.messenger(),
"hands_on_platform_version")
channel.setMethodCallHandler(HandsOnPlatformVersionPlugin())
}
}

override fun onMethodCall(call: MethodCall, result: Result) { // 2


if (call.method == "getPlatformVersion") { // 3
result.success("Android ${android.os.Build.VERSION.RELEASE}")
//4
} else {
result.notImplemented() // 5
}
}
}

Wywołanie metody wtyczki przebiega w następujący sposób:


1. Ta pierwsza metoda statyczna jest używana przez frameworka Fluttera do
przygotowania wtyczki tak, aby była dostępna z kontekstu Darta. Zasadniczo
tworzy instancję MethodChannel i ustawia procedurę obsługi metody jako bieżącą
klasę. Podsumowując, ustanawia połączenie między Dartem a kodem natywnym.

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

2. Metoda onMethodCall jest wywoływana zawsze, gdy odpowiednie API Darta


wymaga do działania kodu natywnego; oznacza to, że po stronie Darta będzie
żądanie do frameworku, aby uruchomić natywny kod z określoną zarejestrowaną
nazwą i parametrami. W metodzie są dwa argumenty:
 MethodCall — opisuje żądanie,

 Result — przekazuje wyniki do kontekstu Darta.


3. Pierwszym krokiem do uruchomienia określonego kodu jest sprawdzenie, co
wywołujący chce wykonać. W tym przypadku sprawdzana jest nazwa metody.
Wtyczka może mieć wiele metod; dlatego jest to potrzebne.
4. Korzystając z obiektu Result, dostarczamy wynik metody, używając wywołania
zwrotnego success, aby przekazać z powrotem żądaną wartość.
5. Wywołanie zwrotne notImplemented(), również z klasy Result, może służyć do
powiadamiania wywołującego, że żądana metoda nie ma odpowiedniej implementacji.
Na tej samej zasadzie istnieje wywołanie zwrotne error do obsługi błędów.

Implementacja wtyczki iOS


Po stronie iOS kod Swift wygląda podobnie do kodu Kotlin:
// SwiftHandsOnPlatformVersionPlugin.swift

public class SwiftHandsOnPlatformVersionPlugin: NSObject, FlutterPlugin {


public static func register(with registrar: FlutterPluginRegistrar) {
// 1
let channel = FlutterMethodChannel(name: "hands_on_platform_version",
binaryMessenger: registrar.messenger())
let instance = SwiftHandsOnPlatformVersionPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}

public func handle(_ call: FlutterMethodCall, result: @escaping


FlutterResult) {
result("iOS " + UIDevice.current.systemVersion)
}
}

Proces wygląda podobnie do tego na Androidzie, ale są pewne drobne różnice:


 Metoda handle jest w Kotlinie odpowiednikiem metody onMethodCall z iOS.
Zauważ, że nie sprawdza wywołania metody z argumentu FlutterMethodCall.
Chociaż działa to dla wtyczki z pojedynczą metodą, zawsze dobrze jest sprawdzić
metodę wywołującą, aby wyjaśnić, co ona obsługuje.
 FlutterResult służy do wysyłania danych z powrotem do kontekstu Darta.
Istnieją również stałe typy dla równoważnych błędów i niezaimplementowanych
przypadków: FlutterError i FlutterMethodNotImplemented.

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

static Future<String> get platformVersion async { // 2


final String version = await
_channel.invokeMethod('getPlatformVersion'); // 3
return version;
}
}

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.

Przykład pakietu wtyczek


Katalog example/ zawiera prostą aplikację Fluttera, która zależy od utworzonej wtyczki.
Sprawdź plik pubspec.yaml:
// example/pubspec.yaml

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.

Pamiętaj, że — jak widzieliśmy w rozdziale 2. — możesz określić zależność wtyczki od


repozytorium pub, ścieżek lub repozytoriów kodów źródłowych.

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';

Użycie następuje z wywołaniem metody wtyczki:


await HandsOnPlatformVersion.platformVersion;

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
});
}

Możemy tutaj wyróżnić kilka punktów:


 Wywołanie metody jest typu asyc, ponieważ komunikaty platformy są asynchroniczne.
 Przy obsłudze komunikatów platformy może wystąpić błąd, więc obsługujemy
wyjątek typu try / catch PlatformException, który pomaga w inspekcji błędów.

273

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 To sprawdzenie pomaga odrzucić wynik z platformy, jeśli widżet zostanie do tego


czasu usunięty z drzewa.
 Stan jest aktualizowany, aby widżet został odbudowany i wyświetlał wersję
platformy pobraną z wtyczki.

Dodanie dokumentacji do pakietu


Wtyczki Fluttera są ważnymi elementami w tworzeniu aplikacji. Ekosystem Fluttera rośnie i każ-
dego dnia udostępniane są społeczności nowe przydatne wtyczki. Jednak takie wtyczki muszą
jasno opisywać, jak powinny być prawidłowo używane. Odbywa się to na podstawie konkretnej
dokumentacji.

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ę.

Stworzymy dokumentację biblioteczną w API Darta, dodając komentarze do dokumentacji za


pomocą odpowiednich dyrektyw (patrz rozdział 2.) — ze składnią ///:
/// To jest komentarz do dokumentu i można go dodać do dowolnego członka biblioteki.

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.

Zapoznaj się z oficjalnymi wskazówkami dotyczącymi pisania dobrej dokumentacji dla


pakietów Fluttera: https://www.dartlang.org/guides/language/effectivedart/documentation.

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.

Najpierw skonfiguruj środowisko roota Fluttera w następujący sposób:


export FLUTTER_ROOT=~/dev/flutter (dla macOS lub Linux)
set FLUTTER_ROOT=~/dev/flutter (dla Windows)

Dokumentację pakietu można wygenerować, uruchamiając następujące polecenie:


cd ~/dev/mypackage
$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dartdoc (dla macOS lub Linux)
%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dartdoc (dla Windows)

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.

Więcej informacji na temat polecenia publish można znaleźć tutaj: https://dart.dev/tools/


pub/publishing.

Zalecenia dotyczące tworzenia projektu


wtyczki
Wtyczki Fluttera świetnie nadają się do przyspieszania tworzenia aplikacji. Wspieranie społecz-
ności poprzez udostępnianie wtyczki jest również świetne, jednak planując opublikowanie
wtyczki, należy wziąć pod uwagę kilka kwestii, aby była ona naprawdę użyteczna i akceptowana:
 Wspieraj wiele platform — wtyczki przeznaczone dla jednej platformy od samego
początku działają nieprawidłowo. Flutter jest frameworkiem wieloplatformowym,
musimy więc myśleć w ten sposób, ponieważ wtyczki będą używane do tworzenia
aplikacji, które będą działać na wielu platformach.
 Napisz dobrą dokumentację — Flutter zapewnia narzędzia ułatwiające tworzenie
i publikowanie pakietu wraz z całą dokumentacją; jedynym wymaganym zadaniem
jest napisanie tego dokumentu.
 Najpierw wyszukaj istniejące wtyczki — może myślisz o stworzeniu kolejnej
wtyczki dla Fluttera. Wcześniej sprawdź, czy nie została już opracowana przez
innych programistów, abyś mógł jej używać, a nawet wnieść do niej swój wkład
— zobacz rysunek na następnej stronie.

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.

Uruchomienie adresu URL z aplikacji


Do tej pory widzieliśmy, jak możemy używać wtyczek Fluttera, aby dodawać określone funkcje do
aplikacji. Na przykład w aplikacji Favors dla zdjęcia profilowego użytkownika użyliśmy wtyczki,
która uruchamia aplikację aparatu i czeka na plik obrazu: image_picker. Ta wtyczka działa dla

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.

Wtyczka image_picker również wykonuje to zadanie.

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.

Sprawdź kod do uruchamiania adresów URL z aplikacji dostępnej w GitHubie w katalogu


Chapter11 | hands_on_url_handler.

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.

Potrzebne są minimalne zmiany, aby wyświetlić linki i uczynić je klikalnymi:


1. Najpierw dodajemy wtyczkę jako zależność.
dependencies:
flutter_linkify: ^2.1.0 # Wtyczka Flutter Linkify
2. Następnie w widżecie FavorCardItem zamieniamy jego opis potomka Text
na nowy widżet Linkify.

Tak to wyglądało wcześniej:


// w metodzie budowania klasy FavorCardItem, opis przysługi
Text(
favor.description,
style: bodyStyle,
),

Tak to wygląda teraz:


import 'package:flutter_linkify/flutter_linkify.dart'; // import biblioteki wtyczki

// w metodzie budowania klasy FavorCardItem, opis przysług


Linkify(
text: favor.description,
humanize: true,
),

Dzięki temu link będzie klikalny i będzie miał inny styl:

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.

Uruchomienie adresu URL


Teraz, gdy mamy już linki, które wyświetlają się i działają w aplikacji, musimy sprawić, by działały
poprawnie. Jeśli jesteś programistą Androida lub iOS, możesz wiedzieć, jak uruchomić adres URL.
We Flutterze, jak można się spodziewać, ta funkcjonalność musi być obsługiwana w sposób
specyficzny dla platformy.

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.

Integrujemy wtyczkę z funkcją FavorCardItem handleLinkClick, w której po prostu wywołujemy


funkcję launch(url), przekazując adres URL pochodzący z wywołania zwrotnego Linkify:
// element opisu
Linkify(
text: favor.description,
humanize: true,
onOpen: handleLinkClick,
),
...
// obsługa kliknięcia
handleLinkClick(LinkableElement link) async {
if (await canLaunch(link.url)) { // 1
await launch(link.url); // 2
}
}

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

Zarządzanie uprawnieniami aplikacji


Systemy Android i iOS mają własne zasady bezpieczeństwa dotyczące danych użytkowników
lub sprzętu. Uprawnienia mają na celu ochronę prywatności użytkownika. Aplikacja, niezależnie
od tego, czy jest natywna, czy nie, musi prosić o pozwolenie na dostęp do danych użytkownika,
takich jak na przykład informacje z kamery.

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.

Możesz sprawdzić dostępne uprawnienia dla iOS tutaj: https://developer.apple.com/


library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/Cocoa-
Keys.html#//apple_ref/doc/uid/TP40009251-SW1.

W systemie Android, w pliku android/app/src/main/AndroidManifest.xml wymienione są


uprawnienia. Android, oprócz uprawnień użytkownika, posiada koncepcję uprawnień syste-
mowych; aby Twoja aplikacja miała dostęp do internetu, na przykład w celu pobierania danych
z Firebase, musi mieć domyślnie dodane pozwolenie android.permission.INTERNET do szablonu
Flutter.

Sprawdź oficjalny przewodnik dotyczący uprawnień w systemie Android (https://developer.


android.com/guide/topics/permissions/overview), aby dowiedzieć się więcej o tym, jak funk-
cjonują w systemie.

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.

Zarządzanie uprawnieniami we Flutterze


Ponieważ oba systemy mają własne zarządzanie uprawnieniami, musimy wziąć to pod uwagę
podczas korzystania z chronionych zasobów. We Flutterze, aby zażądać niezbędnych upraw-
nień, musimy zejść na poziom platformy.

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>

Uprawnienie do korzystania z internetu nie jest dodawane domyślnie w pliku Android


 Manifest. Framework Flutter używa tego uprawnienia do debugowania oraz hot reloadingu.

Dzięki społeczności Flutter mamy kilka wtyczek, które pomogą nam w tym zadaniu. Dobrym
przykładem jest permissions_handler.

Korzystanie z wtyczki Permissions_handler


Wtyczka permissions_handler zapewnia wysokopoziomowy interfejs API dla żądań i sprawdzania
statusu uprawnień. Udostępnia ona zestaw uprawnień za pomocą wyliczenia PermissionGroup
i zapewnia odpowiednie uproszczenie każdego z nich na odpowiedniej platformie. Każda
grupa uprawnień jest odwzorowywana na odpowiadające jej uprawnienia w systemie. Główne
metody dostarczane za pomocą wtyczki są następujące:
 requestPermissions — aby zażądać dostępu do określonego zasobu.
 checkPermissionStatus — aby sprawdzić stan dostępu do określonego zasobu.
 openAppSettings — aby otworzyć ustawienia aplikacji, dzięki czemu użytkownik
może zobaczyć / zmienić określony zasób.

W przypadku systemu Android istnieje również metoda shouldShowRequestPermissionRationale.

Dostępne metody i mapę uprawnień możesz sprawdzić na stronie wtyczki:


https://pub.dartlang.org/packages/permission_handler.

Importowanie kontaktu z telefonu


Z punktu widzenia użytkownika ręczne wpisywanie numeru telefonu w celu złożenia prośby
o przysługę nie jest preferowane, ponieważ metoda ta jest podatna na pomyłki.

Importowanie kontaktu z telefonu użytkownika jest zadaniem specyficznym dla platformy.


Ostatnim punktem jest uruchomienie selektora kontaktów platformy i uzyskanie z niego pojedyn-
czego kontaktu.

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.

Importowanie kontaktu za pomocą contact_picker


Wtyczka contact_picker będzie wykorzystywana do importowania kontaktu w momencie prośby
o przysługę.

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:

W akcji onPressed przycisku importu przekierujemy użytkownika do ekranu kontaktów, aby


mógł wybrać kontakt.

286

d0765ad53fb82babda2278a311da7afb
d
Rozdział 10. • Dostęp do funkcji urządzenia z aplikacji Fluttera

Rzućmy okiem na kod. Najpierw dodajemy dwa pola do klasy RequestFavorPageState:


// request_favors_page.dart

class RequestFavorPageState extends State<RequestFavorPage> {


final ContactPicker _contactPicker = ContactPicker();
Friend _importedFriend;
...
}

Oto w czym pomogą nam te dwa pola:


 _contactPicker zapewnia funkcjonalność wtyczki.
 _importedFriend przechowuje zaimportowanego znajomego z kontaktów, jeśli taki
istnieje.

Dzięki temu będziemy mogli łatwo zaimportować kontakt. Następnie dodajemy wywołanie
zwrotne onPressed dla przycisku Import contact:
onPressed: () {
_importContact();
},

Potem importujemy kontakt metodą _importContact():


void _importContact() async {
Contact contact = await _contactPicker.selectContact(); // 1
if (contact != null) {
setState(() {
_importedFriend = Friend(
name: contact.fullName,
number: contact.phoneNumber.number,
); // 2
});
}
}

Import kontaktu odbywa się w kilku krokach:


1. Najpierw uruchamiamy selektor kontaktów za pomocą metody selectContact
z klasy ContactPicker wtyczki.
2. Po upewnieniu się, że użytkownik wybrał kontakt (contact! = null), tworzymy
nową instancję Friend na podstawie wybranych informacji kontaktowych.

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.

Uprawnienia do kontaktu za pomocą permission_handler


Chociaż informacje kontaktowe są zasobami chronionymi przez użytkownika, nie potrzebujemy
żadnych specjalnych uprawnień, aby zaimportować kontakt za pomocą wtyczki contact_picker,
ponieważ nie czytamy ich bezpośrednio, ale za pośrednictwem interfejsów API specyficznych
dla platformy.

Zobaczymy jednak, jak poprosić o pozwolenie na korzystanie z kontaktów, ponieważ może to


być przydatne w przyszłości.

Jeśli pamiętasz, każda platforma ma swój sposób obsługi uprawnień i na tej podstawie musimy
wdrażać odpowiednie żądania.

Uprawnienia do kontaktu w systemie Android


W Androidzie musimy dodać prośbę o pozwolenie na użycie kontaktu w pliku AndroidManifest,
więc zmieńmy plik android/app/src/(main|debug|profile)/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">
...
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
</manifest>

Dodając uprawnienie READ_CONTACTS, deklarujemy w systemie Android, że potrzebujemy dostępu


do listy kontaktów użytkowników; WRITE_CONTACTS, jak można się domyślić, deklaruje potrzebę za-
pisania nowych kontaktów do systemu.

Zachowanie tego rekordu zależy od wersji systemu, w którym jest zainstalowana aplikacja.
Sprawdź to tutaj: https://developer.android.com/training/permissions/requesting.

Uprawnienia do kontaktu w iOS


W iOS musimy podać odpowiedni opis w pliku Info.plist, aby użytkownik wiedział, dlaczego
aplikacja potrzebuje żądanych uprawnień. Odbywa się to w pliku ios/Runner/Info.plist:

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.

Sprawdzanie i żądanie uprawnienia we Flutterze (permission_handler)


Załóżmy, że nasza aplikacja potrzebuje pozwolenia na dostęp do kontaktów, aby złożyć prośbę
o przysługę (na przykład, gdybyśmy chcieli wyświetlić wszystkie kontakty w naszej aplikacji,
aby użytkownik mógł je wybrać). Tworzymy funkcję _checkPermissions, aby w razie potrzeby
sprawdzić i poprosić o pozwolenie, a następnie wykonujemy następujące kroki:
1. Najpierw uzyskujemy status pozwolenia z API.
void _checkPermissions() async {
PermissionStatus status = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.contacts);
2. Następnie sprawdzamy, czy status jest inny niż przyznany, czyli czy nie został już
nadany przez użytkownika.
if (status != PermissionStatus.granted)
3. Na koniec, jeśli zezwolenie nie zostanie przyznane (status! =
PermissionStatus.granted), prosimy o to.
await
PermissionHandler().requestPermissions([PermissionGroup.contacts]);
}

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.

Integracja aparatu w telefonie


Funkcja aparatu jest obecna w wielu aplikacjach, a integrację z nią można przeprowadzić na
kilka sposobów. Moglibyśmy na przykład zaimplementować kod samodzielnie, ale dzięki spo-
łeczności Flutter udostępnia wiele wtyczek umożliwiających dostęp do kamery. Oto niektóre
z najbardziej znanych wtyczek:

289

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

 camera — dzięki tej wtyczce możemy wyświetlać podgląd kamery bezpośrednio


we Flutterze, robić zdjęcia lub nagrywać wideo.
 image_picker — ta wtyczka bardzo stara się uprościć zadanie; prosimy tylko o zdjęcie
z aparatu lub galerii, a resztą zajmuje się wtyczka.

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.

Robienie zdjęć za pomocą image_picker


Flutter nie komunikuje się bezpośrednio z interfejsem API aparatu, ponieważ ten zasób znajduje
się na poziomie platformy. Wtyczka image_picker, jak sama nazwa wskazuje, pomaga w wyborze
zdjęcia. Umożliwia importowanie plików graficznych z galerii i wykonywanie nowych zdjęć
aparatem.

Najpierw dodajemy zależność do pliku pubspec.yaml i pobieramy ją za pomocą polecenia


flutter packages get:
dependencies:
image_picker: ^0.5.0 # Image picker

Wybieranie obrazów kontrolujemy w tym samym miejscu, w którym użytkownik wpisuje


swoją nazwę wyświetlaną po zalogowaniu, w ostatnim kroku widżetu Stepper. Gdy użytkownik
naciśnie mały obraz awatara, kamera otwiera się, aby zrobić zdjęcie:
// login_page.dart

// część klasy LoginScreenState


void _importImage() async {
final image = await ImagePicker.pickImage(source: ImageSource.camera);
setState(() {
_imageFile = image;
});
}

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

Uprawnienia do aparatu za pomocą permission_handler


Wtyczka image_picker sama obsługuje prośby o pozwolenie, ale w tym przypadku ponownie
użyjemy wtyczki permission_handler, aby poprosić o dostęp do kamery.

Uprawnienia do aparatu w systemie Android


W Androidzie uprawnienia do kamery musimy zadeklarować w pliku AndroidManifest, tak jak zro-
biliśmy to dla kontaktów, więc zmieniamy plik android/app/src/(main|debug|profile)/Android
Manifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">
...
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

Dodając uprawnienie CAMERA, deklarujemy w systemie Android, że potrzebujemy dostępu do ka-


mery. Dodatkowo możemy użyć innego tagu manifest Androida:
<manifest ...>
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
</manifest>

Tag uses-feature deklaruje, że nasza aplikacja do prawidłowego działania potrzebuje aparatu


(ponownie w naszym przypadku nie jest to cała prawda; wymagany argument może być ustawiony
na true, jeśli to konieczne). Jeśli jest wartość true, aplikacja będzie dostępna tylko dla urzą-
dzeń z dostępną kamerą.

Uprawnienia do aparatu w iOS


Podobnie jak w przypadku kontaktów, w iOS musimy podać odpowiedni opis w pliku Info.plist,
aby użytkownik wiedział, dlaczego aplikacja potrzebuje uprawnień aparatu.

Zapoznaj się z kodem z pliku ios/Runner/Info.plist:


<dict>
...
<key>NSCameraUsageDescription</key>
<string>You can add a profile picture right from the camera</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require access to the microphone.</string>
</dict>

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

Prośba o dostęp do aparatu we Flutterze (permission_handler)


W ustawieniach profilu, po zalogowaniu, możemy dodać zdjęcie profilowe. Proces proszenia o po-
zwolenie na dostęp do kamery jest bardzo podobny do żądania dostępu do kontaktów użytkownika.

Tworzymy funkcję sprawdzającą i proszącą o pozwolenie w razie potrzeby:


void _checkPermissions() async {
PermissionStatus status = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.camera); // 1
if (status != PermissionStatus.granted) { // 2
await PermissionHandler().requestPermissions([PermissionGroup.camera]);
// 3
}
}

Metoda jest bardzo podobna do tej z przykładu importu kontaktów:


1. Za pomocą API uzyskujemy status uprawnienia do kamery.
2. Testujemy, czy status jest inny niż przyznany (jeśli uprawnienie zostało już nadane
przez użytkownika).
3. Na końcu, jeśli zezwolenie nie zostanie udzielone, prosimy o nie.

Odpowiednim momentem do wywołania tej funkcji jest etap wyboru obrazu profilu, wewnątrz
metody _importImage():
void _importImage() async {
await _checkPermissions();
...
}

Chociaż potrzebujemy pozwolenia na skorzystanie z aparatu, wtyczka image_picker już o to po-


prosiła za nas, więc wszystko będzie działać.

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.

Skorzystaliśmy z wtyczek url_launcher i flutter_linkify, aby wyświetlić łącze do użytkownika


w opisie aplikacji Favor. Następnie dodaliśmy wtyczkę permission_handler do zarządzania upraw-
nieniami aplikacji. Użyliśmy również wtyczki contact_picker do zaimportowania kontaktu
z listy kontaktów użytkownika, a za pomocą wtyczki permission_handler dodaliśmy sprawdzenie
uprawnień i prośbę o dostęp do kontaktów.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Wyświetlanie mapy.
 Dodanie znacznika do mapy.
 Dodawanie interakcji na mapie.
 Korzystanie z 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.

Wyświetlanie mapy w aplikacjach Fluttera wymaga pewnych zmian w domyślnej aplikacji.


Zacznijmy więc od zrozumienia, czym są te dostosowania, a następnie dodajmy obsługę widoków
platformy.

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ę.

Widoki platformy są ważnymi elementami w takich frameworkach jak Flutter, ponieważ


umożliwiają wypełnienie niektórych luk podczas ich ewolucji. Jest jednak kilka punktów,
które należy rozważyć przed rozpoczęciem pracy z nimi:
 W systemie Android wymagany jest API w wersji 20 lub wyższej.
 W systemie iOS konfiguracja tej funkcji wymaga dodatkowych czynności
(patrz poniższe sekcje).
 Jak już wspominano, osadzanie widoków jest kosztowne dla frameworka i należy
go unikać, gdy tylko jest to możliwe.

296

d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy

 PlatformView wypełnia całą dostępną przestrzeń elementu nadrzędnego,


podobnie jak widżet kontenera.
 PlatformView jest elementem drzewa widżetów jak każdy inny widżet.

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.

Włączanie widoków platformy w iOS


Wczesne wersje widoków platformy były obsługiwane tylko w systemie Android. W chwili
pisania tej książki implementacja osadzania UIKitView na iOS jest nadal we wstępnej wersji.
Musimy więc zmienić plik ios/Runner/Info.plist aplikacji i dodać określone ustawienie:
<plist version="1.0">
<dict>
...
<key>io.flutter.embedded_views_preview</key>
<string>YES</string>
</dict>
</plist>

Umożliwi to włączenie funkcjonalności dla aplikacji iOS, dzięki czemu będziemy mogli korzystać
z niej w naszej aplikacji.

Tworzenie widżetu widoku platformy


Kiedy tworzymy widżet widoku platformy to w zasadzie opakowujemy natywny widok
iOS/Android we Flutterze. Proces tworzenia widoku platformy jest podobny do tworzenia
wtyczek i wymaga dodania kodu natywnego do 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).

Sprawdź plik hands_on_platform_views na GitHubie, aby uzyskać pełny kod wtyczki.

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

 Ich właściwości są podobne, a my definiujemy widżet viewType, który chcemy


utworzyć, oraz jego parametry (creationParams) i parametry creationParamsCodec:
 viewType — typ widoku jest używany przez system widoku platformy Flutter,
aby wskazać, którego natywnego widoku zamierzamy użyć — podobnie jak
w systemie wtyczek.
 creationParams — to są argumenty, które chcemy przekazać do natywnego
tworzenia widoku — w naszym przypadku tekstu (text), który ma być pokazany.
 creationParamsCodec — określa, która metoda transferu danych parametrów
wystąpi podczas wysyłania creationParams do kodu natywnego.

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.

Tworzenie widoku Androida


Tworzenie i rejestrowanie widoków platformy na każdej platformie to bardzo podobny proces;
musimy tylko zarządzać różnicami w językach i interfejsach API widoku natywnego. Najprostszym
sposobem rozpoczęcia tworzenia widoku platformy jest zapisanie go w rejestrze widoków
platformy, podobnie jak w przypadku tworzenia wtyczki Fluttera. Ponadto, ponieważ mamy
do czynienia z projektem wtyczki, odbywa się to razem z jej rejestracją:
class HandsOnPlatformViewsPlugin{
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
registrar
.platformViewRegistry()
.registerViewFactory(
"com.example.handson/textview",
HandsOnTextViewFactory());
}
}
}

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

HandsOnTextViewFactory wygląda następująco:


class HandsOnTextViewFactory :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, id: Int, args: Any): PlatformView
{
val params = args as Map<String, Any> // 1
val text = if (params.containsKey("text")) { // 2
params["text"] as String? ?: ""
} else ""
return HandsOnTextView(context, text) // 3
}
}

Klasa fabryczna musi rozszerzać PlatformViewFactory i implementować metodę tworzenia.

Ta metoda jest odpowiedzialna za utworzenie określonego typu widoku. Wygląda następująco:


1. Otrzymuje argumenty args jako parametr i może użyć ich do skonfigurowania
widoku.
2. Pobiera wartość tekstową text z Map otrzymanego w parametrze.
3. Na koniec zwraca instancję HandsOnTextView.

Zwróć uwagę na wartość StandardMessageCodec.INSTANCE przekazaną do klasy nadrzędnej fabryki.


Musi ona mieć ten sam typ creationParamsCodec zdefiniowany w Dart, aby platforma mogła prze-
nosić argumenty ze strony Darta do strony natywnej.
Klasa HandsOnTextView jest natywną klasą widoku:
class HandsOnTextView internal constructor(context: Context, text: String)
: PlatformView {
private val textView: TextView = TextView(context)

init {
textView.text = text
}

override fun getView(): View {


return textView
}

override fun dispose() {}


}

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

Tworzenie widoku iOS


W systemie iOS proces jest bardzo podobny do procesu w Android, ale jest kilka punktów
dotyczących składni, które się różnią. Rejestrujemy fabrykę tak jak wcześniej, w sekcji „Tworzenie
widoku Androida”:
public class SwiftHandsOnPlatformViewsPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let viewFactory = HandsOnTextViewFactory()
registrar.register(viewFactory, withId: "com.example.handson/textview")
}
}

Następnie sprawiamy, że klasa HandsOnTextViewFactory może zwrócić wersję widoku dla sys-
temu iOS:
public class HandsOnTextViewFactory: NSObject, FlutterPlatformViewFactory {

public func create(


withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return HandsOnTextView(frame, viewId: viewId, args: args)
}

public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol


{
return FlutterStandardMessageCodec.sharedInstance()
}
}

Tutaj fabryka musi zaimplementować protokół FlutterPlatformViewFactory, z metodami


create i createArgsCodec:
 create() musi zwracać widok iOS, który ma być osadzony w kontekście Fluttera,
jak getView() dla wersji na Androida.
 createArgsCodec() musi zwrócić odpowiednią wersję createParamsCodec .
Tak jak wcześniej, w iOS używamy standardowego kodeka
FlutterStandardMessageCodec.sharedInstance().

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.

Przyjrzyjmy się teraz, jak korzystać z widżetu platformy.

300

d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy

Wykorzystanie widżetu widoku platformy


Korzystanie z widżetu platformy jest tak proste, jak używanie zwykłego widżetu. Oprócz spe-
cyficznej konfiguracji przewidzianej wcześniej dla platformy iOS, nie potrzeba nic więcej.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(
alignment: Alignment.center,
color: Colors.red,
child: SizedBox(
height: 100,
child: HandsOnTextView(
text: "Text from Platform view",
),
),
),
);
}

Aby uzyskać pełny kod wtyczki, sprawdź przykład hands_on_platform_views na GitHubie.

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.

Należy zauważyć, że osadzanie widoków platformy jest kosztowną operacją, ponieważ


silnik Fluttera musi zarządzać zasobami wymaganymi przez każdy z nich. Dlatego należy
unikać korzystania z nich, gdy dostępny jest odpowiednik Fluttera.

Pierwsze kroki z wtyczką google_maps_flutter


Jak wspomniano wcześniej, wtyczka google_maps_flutter polega na widokach platformy, aby
wyświetlać mapy w aplikacjach Fluttera, jak widzieliście w poprzedniej sekcji.

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ć.

 initialCameraPosition — służy do konfiguracji początkowej widocznej części


mapy.
 cameraTargetBounds — służy do zmiany granic ramki geograficznej dla celu
kamery, czyli wybranej części mapy.
 rotateGesturesEnabled, scrollGesturesEnabled, tiltGesturesEnabled
i zoomGesturesEnabled — włączają / wyłączają odpowiednie gesty.

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.

Wszystkie dostępne właściwości klasy GoogleMap można sprawdzić na https://pub.dev/docu-


mentation/google_maps_flutter/latest/google_maps_flutter/GoogleMap-class.html.

Wyświetlanie mapy za pomocą wtyczki google_maps_flutter


Wtyczki GoogleMaps można użyć do wyświetlenia mapy we Flutterze, na przykład — zobacz
pierwszy rysunek na następnej stronie.

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

Włączanie Maps API w Google Cloud Console


Przed użyciem widżetu GoogleMap musimy uzyskać ważny klucz API dla Map Google z plat-
formy Google Maps. Proces odbywa się za pomocą Maps Platform w Google Cloud Console,
https://cloud.google.com/maps-platform — zobacz drugi rysunek na następnej stronie.

303

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Zobaczmy, jak działa ten proces:


1. Wybierz opcję GET STARTED. Jesteśmy prowadzeni przez proces włączania
API. Najpierw wybieramy interfejsy API, które chcemy włączyć.

304

d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy

2. Następnie wybieramy projekt, dla którego chcemy włączyć Maps API.

3. Teraz musimy skonfigurować rozliczanie dla projektu. Korzystanie z Google Maps


Platform jest bezpłatne, ale do połączenia z projektem potrzebne jest konto
rozliczeniowe. Po utworzeniu / włączeniu konta rozliczeniowego dla projektu
włączamy API.

305

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

4. Na końcu otrzymujemy klucz API do wykorzystania w naszej aplikacji mobilnej.

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.

Integracja Google Maps API dla Androida


W przypadku platformy Android musimy zmienić plik android/src/main/AndroidManifest.xml
i dodać tag meta-data zawierający klucz API, który otrzymaliśmy z Maps Console:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hands_on_maps">
<application ... >
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR KEY HERE"/>
</application>
</manifest>

Integracja Google Maps API dla iOS


W iOS zmieniamy plik ios/Runner/AppDelegate.swift, dodając kod odpowiedzialny za usta-
wienie klucza API we wtyczce:
import UIKit
import Flutter
import GoogleMaps

@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”).

Wyświetlanie mapy we Flutterze


Po prawidłowym zainicjowaniu wtyczki na obu platformach możemy skorzystać z widżetu
GoogleMap w naszej aplikacji. W minimalnej implementacji wystarczy dodać go do naszego layoutu:
// część widżetu MapPage
@override
Widget build(BuildContext context) {
...
return GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(51.178883, -1.826215),
zoom: 10.0
),
);
...
}
...

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.

Możemy to zmienić za pomocą wcześniej poznanych właściwości widżetu GoogleMap dotyczących


gestów.

307

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Dodawanie znaczników do mapy


Wyświetlenie mapy w aplikacji to dopiero początek tworzenia aplikacji opartej na mapach.
Dodawanie informacji o miejscach jest jednym z najczęstszych zadań podczas pracy z ma-
pami. Zobaczmy, jak możemy dodać znaczniki do utworzonej wcześniej mapy, używając klasy
Marker dostarczonej przez wtyczkę.

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

 icon — jest to ikona znacznika w formacie BitmapDescriptor


 markerId — jest to unikalny identyfikator znacznika na mapie.
 infoWindow — jest to okno informacyjne mapy Google, które jest wyświetlane po
dotknięciu znacznika.

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.

Dodawanie znaczników w widżecie GoogleMap


Jak widzieliśmy wcześniej, widget GoogleMap posiada właściwość markers, która oczekuje, że
zostanie do niego przekazana kolekcja Set instancji Marker. Zobaczmy, jak dodać znaczniki,
ustawiając właściwość markers:
1. Najpierw dodajemy pole _markers do klasy MapPage, aby przechowywać losowy
zestaw znaczników (instancje Marker).
class MapPage extends StatelessWidget {
final _markers = {
Marker(
position: LatLng(51.178883, -1.826215),
markerId: MarkerId('1'),
infoWindow: InfoWindow(title: 'Stonehenge'),
icon: BitmapDescriptor.defaultMarker
),
Marker(
position: LatLng(41.890209, 12.492231),
markerId: MarkerId('2'),
infoWindow: InfoWindow(title: 'Colosseum'),
icon: BitmapDescriptor.defaultMarker
),
Marker(
position: LatLng(36.106964, -112.112999),
markerId: MarkerId('3'),
infoWindow: InfoWindow(title: 'Grand Canyon'),
icon: BitmapDescriptor.defaultMarker
),
};
...
}
2. Następnie wystarczy ustawić właściwość markers w widżecie GoogleMap.
@override
Widget build(BuildContext context) {
return GoogleMap(
initialCameraPosition:
CameraPosition(target: LatLng(51.178883, -1.826215),
zoom: 10.0),

309

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

markers: _markers,
);
}

Jeśli dotkniemy znacznika, zostanie wyświetlony odpowiedni obiekt InfoWindow z ustawionym


tytułem.

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

Dodawanie interakcji na mapie


Dodanie znaczników do mapy pomaga wzbogacić zawarte w niej informacje kontekstowe; jest
to jednak zdecydowanie za mało dla prawdziwej aplikacji opartej na mapach. Ważna jest także
obsługa zdarzeń czy zmiana mapy w zależności od potrzeb użytkownika. Zobaczmy, jak możemy
dynamicznie dodawać znaczniki do mapy i używać klasy GoogleMapController do programowej
interakcji z kamerą mapy.

Dynamiczne dodawanie znaczników


Jak wspomniano wcześniej, musimy przekazać znaczniki podczas tworzenia widżetu GoogleMap,
więc pierwszym krokiem jest uczynienie naszego widżetu MapPage widżetem StatefulWidget
i przebudowanie jego poddrzewa za każdym razem, gdy chcemy dodać nowy znacznik.

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,
),

Wartość jest po prostu przechowywana, jak wspomniano wcześniej:


void _cameraMove(CameraPosition position) {
_cameraCenter = position.target;
}

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.

Klasa GoogleMapController udostępnia metody sterujące widżetu GoogleMap. Obecnie jedy-


nymi dostępnymi metodami są:
 animateCamera — rozpoczyna animowaną zmianę położenia kamery na mapie.
 moveCamera — zmienia położenie kamery na mapie bez animacji.

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;
},
),

_mapController to pole instancji widżetu MapPage, którego będziemy używać do interakcji


z kamerą mapy.

Animowanie kamery mapy do lokalizacji


Dodaliśmy rząd przycisków, które użytkownik może nacisnąć, aby skupić się na określonym
miejscu. Korzystając z jednego z tych przycisków, wywołamy nową metodę _animateMapCameraTo,
na przykład dla Stonehenge:
RaisedButton(
child: Text("Stonehenge"),
onPressed: () {
_animateMapCameraTo(_stonehengePosition);
},
),

312

d0765ad53fb82babda2278a311da7afb
d
Rozdział 11. • Widoki platformy oraz integracja mapy

Nowa metoda odpowiada za żądanie aktualizacji kamery:


void _animateMapCameraTo(LatLng position) {
_mapController.animateCamera(CameraUpdate.newLatLng(position));
}

Jak widać, za pomocą wcześniej pobranej instancji GoogleMapController możemy wysłać animację
kamery w nowe miejsce na mapie.

Kod pozostałych przycisków jest bardzo podobny. Ponownie sprawdź hands_on_maps


w serwisie GitHub, aby uzyskać szczegółowe informacje na temat przykładu integracji
mapy.

Korzystanie z interfejsu API Google Places


Z oficjalnej strony internetowej możemy dowiedzieć się:
Google Places API to usługa, która zwraca informacje o miejscach za pomocą żądań HTTP.
Miejsca są definiowane w interfejsie API jako przedsiębiorstwa, lokalizacje geograficzne
lub inne ważne punkty.

Usługa może być wykorzystywana na kilka sposobów, takich jak:


 Uzyskanie listy miejsc na podstawie lokalizacji użytkownika lub ciągu wyszukiwania.
 Uzyskanie szczegółowych informacji o konkretnym miejscu, w tym opinii
użytkowników.
 Dostęp do milionów zdjęć związanych z miejscami, przechowywanych w bazie
danych Google Places.
 Usługa przewidywania zapytań w oparciu o tekstowe wyszukiwanie geograficzne,
zwracanie sugerowanych zapytań w miarę wpisywania przez użytkownika oraz
automatyczne wypełnianie nazwy i / lub adresu miejsca w trakcie wpisywania
przez użytkownika.

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.

Włączanie API Google Places


Podobnie jak w przypadku pakietu SDK Map Google, interfejs API Google Places należy włączyć
w konsoli programisty Google pod adresem https://console.cloud.google.com/apis/library/
places-backend.googleapis.com:

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.

Pierwsze kroki z wtyczką google_maps_webservice


Wtyczka google_maps_webservice to wtyczka społeczności Dart, która oferuje klientowi inter-
fejs API Google Places. Dzięki tej wtyczce możemy nawiązywać połączenia z usługą interne-
tową Google bez konieczności samodzielnego tworzenia żądań.

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.

Uzyskiwanie adresu miejsca


za pomocą wtyczki google_maps_webservice
Przede wszystkim musimy dodać wtyczkę jako zależność w pliku pubspec.yaml naszego projektu
i pobrać ją za pomocą polecenia flutter packages get:
dependencies:
google_maps_webservice: ^0.0.12

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,
);
}

Metoda wykorzystuje metodę searchNearbyWithRadius klasy GoogleMapsPlaces. To zapytanie


wyszukuje w serwisie internetowym Google miejsca w pobliżu określonej lokalizacji, uszere-
gowane według znaczenia, przy czym najbliższe miejsca są na pierwszym miejscu.

Chcąc skorzystać z utworzonej metody, zmieniamy naszą funkcję _addMarkerOnCameraCenter,


tak by odpytywała adres miejsca przed dodaniem go do mapy:
void _addMarkerOnCameraCenter() async {
final places = await _queryLatLngNearbyPlaces(_cameraCenter);
final firstMatchName =
places.results.length > 0 ? places.results.first.name : "";

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.

A po dodaniu znacznika do mapy zawiera on nazwę lokalizacji:

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.

W następnym rozdziale przyjrzymy się dostępnym narzędziom Fluttera do zaawansowanego


tworzenia aplikacji.

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.

W tej sekcji znajdują się następujące rozdziały:


 Rozdział 12., „Testowanie, debugowanie i wdrażanie”.
 Rozdział 13., „Poprawa komfortu użytkownika”.
 Rozdział 14., „Operacje graficzne na widżetach”.
 Rozdział 15., „Animacje”.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Testowanie widżetów Fluttera.
 Debugowanie aplikacji Fluttera.
 Profilowanie wydajności aplikacji Fluttera.
 Sprawdzanie drzewa widżetów Fluttera.
 Przygotowywanie aplikacji do wdrożenia.

Testowanie we Flutterze — testy


jednostkowe oraz widżetów
Ręczne testowanie aplikacji mobilnych ma fundamentalne znaczenie, o ile musimy stale dodawać
funkcje do aplikacji. Istnieje wiele sposobów testowania aplikacji Fluttera, z których każdy
wiąże się z pewnym poziomem korzyści i nie różni się zbytnio od testowania innych aplikacji.

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”.

Różnica polega na kontekście Fluttera, ta funkcja konfiguruje instancję WidgetTester do interakcji


ze środowiskiem, jak wspomniano wcześniej.

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 })

Przekonajmy się, jak działa. Najpierw sprawdźmy jej opis:


 description — pomaga w dokumentacji testu, a konkretniej opisuje, jakie funkcje
widżetów są testowane.

322

d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie

 callback — to jest WidgetTesterCallback. To wywołanie zwrotne odbiera instancję


WidgetTester, dzięki czemu możemy wchodzić w interakcję z widżetem i dokonywać
naszych walidacji. To jest treść testu, w której piszemy naszą logikę testu.
 skip — ustawiając tę flagę, możemy pominąć test.
 timeout — jest to maksymalny czas, przez jaki testowe wywołanie zwrotne może
działać.

Przykład testu widżetu


Kiedy generujemy projekt Fluttera, automatycznie dodajemy zależność pakietu flutter_test,
a w katalogu test/ generowany jest przykładowy test. Sprawdźmy to.

Najpierw w pubspec.yaml dodajemy zależność pakietu flutter_test:


dev_dependencies:
flutter_test:
sdk: flutter

Zwróć uwagę, że wersja pakietu nie została określona. Ponadto źródło jest skonfiguro-
wane jako Flutter SDK.

Następnie możemy sprawdzić podstawowy test widżetów w pliku test/widget_test.dart:


void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async
{
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

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

 Jeśli w pewnym momencie zajdzie potrzeba przebudowania widżetu, możemy


skorzystać z metody tester.pump().
 W testach widżetów ważne są dwa dodatkowe elementy, find i expect():
 Klasa Finder pozwala nam przeszukiwać określone widżety w drzewie. Stała
find zapewnia narzędzia (Finders) do wyszukiwania i przeglądania drzewa
widżetów pod kątem określonych widżetów.

Sprawdź wszystkie dostępne Finders dostarczone przez find:


https://api.flutter.dev/flutter/flutter_driver/CommonFinders-class.html.

 Metoda expect() jest używana w połączeniu z Matchers do tworzenia asercji


na widżetach znalezionych za pomocą Finders. Matcher pomaga zweryfikować
znalezioną cechę widżetu za pomocą oczekiwanej wartości.

Przeanalizujmy poprzednie asercje testu widżetu:


1. Na początku znajduje się asercja dotycząca obecności pojedynczego widżetu
z tekstem 0 i żadnego z tekstem 1.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
2. Następnie wykonuje się tap(), a po nim żądanie pump(). Zdarzenie dotknięcia
następuje w widżecie, który zawiera ikonę Icons.add.
await tester.tap(find.byIcon(Icons.add));
await tester.pump()
3. Ostatnim krokiem jest ponowne sprawdzenie, czy wyświetlany jest poprawny
tekst. Ale tym razem stała findOneWidget służy do sprawdzenia, czy widoczny jest
tylko tekst 1.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);

Podobnie jak w przypadku stałej find, dostępnych jest wiele Matchers; findNothing i findOneWidget
to tylko niektóre z nich.

Sprawdź wszystkie dostępne Matchers w dokumentacji biblioteki flutter_test:


https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html.

Debugowanie aplikacji Fluttera


Debugowanie jest ważnym elementem tworzenia oprogramowania. Małe błędy, dziwne zacho-
wania i złożone błędy można rozwiązać za pomocą debugowania. Możemy wykonać następujące
czynności:

324

d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie

 Tworzenie asercji logicznych.


 Określanie potrzebnych ulepszeń.
 Znajdowanie wycieków pamięci.
 Wykonanie analizy przebiegu aplikacji.

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

Istnieją przeglądarki internetowe z ograniczeniami w wyświetlaniu narzędzia Observatory.


Sprawdź to pod adresem: https://github.com/dart-lang/sdk/issues/34107.

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.

Dodatkowe funkcje debugowania


Dart zapewnia dodatkowe funkcje, które pomagają w zaawansowanym debugowaniu. Do-
stępne są różne warianty typowych narzędzi, które mogą uczynić proces debugowania jeszcze
bardziej użytecznym. Są to:
 Instrukcja debuger() — zwana także programowym punktem przerwania; tutaj
możemy dodać punkt przerwania tylko wtedy, gdy oczekiwany warunek jest
prawdziwy.

326

d0765ad53fb82babda2278a311da7afb
d
Rozdział 12. • Testowanie, debugowanie i wdrażanie

void login(String username, String password) {


debuger(when: password == null);
...
}

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).

Więcej na temat logowania we Flutterze możesz zobaczyć tutaj:


https://flutter.dev/docs/testing/debugging#print-and-debugprint-with-flutter-logs.

 Asercje — assert() służy do przerywania wykonywania aplikacji, gdy warunek


nie jest spełniony. Jest podobna do metody debuger(), ale zamiast wstrzymywać
wykonywanie, przerywa wykonywanie, rzucając wyjątek AssertionError.

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

Możesz też to zrobić w taki sposób:


flutter packages pub global activate devtools

327

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Następnie możemy uruchomić narzędzie za pomocą tego polecenia:


pub global run devtools

lub za pomocą polecenia:


flutter packages pub global run devtools

Wejdź na wyświetloną stronę w przeglądarce internetowej, a otrzymasz coś podobnego do


poniższego zrzutu ekranu:

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

Profilowanie aplikacji Fluttera


Flutter ma na celu dostarczanie aplikacji o wysokiej wydajności z dużą liczbą klatek na sekundę
i płynnością. Podobnie jak debugowanie, które może pomóc w znalezieniu błędów i nie tylko,
profilowanie jest kolejnym przydatnym narzędziem, które może pomóc programistom w znajdo-
waniu wąskich gardeł w aplikacji, zapobieganiu wyciekom pamięci lub poprawianiu wydajno-
ści aplikacji.
Narzędzie Observatory ponownie jest tym narzędziem, które pozwala nam sprawdzić wydaj-
ność aplikacji Fluttera.
Podobnie jak debuger, to narzędzie jest również opakowane w IDE.

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.

Jedynym narzutem dodanym do aplikacji są wymagania potrzebne do włączenia profilowania


(to znaczy, że Observatory może łączyć się z procesem aplikacji).

Kolejnym ważnym aspektem profilowania jest konieczność posiadania fizycznego urządzenia.


Symulatory i emulatory nie odzwierciedlają rzeczywistej wydajności prawdziwych urządzeń.
Ponieważ sprzęt jest inny, można wpływać na metryki aplikacji, a analiza musi być poprawna.

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).

IDE oferują również tryb profilowania za pośrednictwem swoich konkretnych interfej-


sów, więc kiedy zobaczysz ten tryb w wybranym IDE, wiesz, co to znaczy.

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

Nakładka wydajności. Inne (nakładające się) informacje nie są tutaj ważne

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.

Sprawdzanie drzewa widżetów Fluttera


Dzięki debugowaniu i profilowaniu możemy wykryć i rozwiązać wiele problemów związa-
nych z wydajnością, zanim pojawią się one w środowisku produkcyjnym. Oprócz tego mo-
żemy zmierzyć koszt wykonywania aplikacji w trakcie jej tworzenia.

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.

Inspektor Fluttera w DevTools


W obsługiwanych środowiskach IDE wtyczka umożliwia już dostęp do inspektora widżetów
za pomocą znajdującego się pod spodem narzędzia inspektora widżetów Fluttera. Jest on również
dostępny w pakiecie DevTools:

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.

Podobnie jak w przypadku narzędzi do profilowania i debugera, szczegółowe badanie drzewa


widżetów może być niezwykle pomocne w znalezieniu problemów z layoutem, które byłyby
trudne bez wizualizacji drzewa.

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

Gdy ją włączymy, drzewo będzie wyglądało znacznie prościej:

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.

Przygotowywanie aplikacji do wdrożenia


Flutter ma na celu oferowanie programistom najlepszych możliwych zasobów do pracy, dlatego
sens mają różne kompilacje, profilowanie i rodzaje wydań aplikacji. Podczas przygotowywania
aplikacji do wydania nie mają za to sensu takie kwestie jak kompilacja w locie zapewniana
przez Dart JIT; zamiast tego najlepiej jest posiadać mniejszą, zoptymalizowaną i wydajną aplikację
dostarczaną przez kompilator Dart AOT.

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

Tryb wydania (release mode)


W trybie wydania informacje dotyczące debugowania są usuwane z aplikacji, a kompilacja
jest wykonywana z myślą o wydajności. Pamiętaj, że w trybie wydania, takim jak profilowanie,
aplikację można uruchomić tylko na urządzeniach fizycznych, również z tych samych powodów.

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.

Wydawanie aplikacji na Androida


W systemie Android .apk to format, który ma zostać opublikowany w sklepie Google Play.
Kiedy uruchamiamy polecenia flutter build apk lub flutter build appbundle, generujemy
plik gotowy do wdrożenia.

W chwili pisania tej książki częściowo obsługiwany jest również format pakietu aplikacji
na Androida.

Zanim wygenerujemy plik do wdrożenia i opublikowania w jakimkolwiek sklepie, musimy upew-


nić się, że wszystkie informacje są poprawne (czyli nazwa i pakiet), wszystkie potrzebne za-
soby są dostarczone i wykonaliśmy wszelkie specyficzne operacje dla platformy.

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.

Pamiętaj też o prawidłowym skonfigurowaniu projektu w konsoli Firebase i dodaniu do projektu


pliku google-services.json (możesz użyć tego samego wygenerowanego dla rozdziału 8.).

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

Tak wyglądają uprawnienia manifestu w naszej aplikacji Favors:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">

<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ć.

Uprawnienie android.permission.INTERNET jest używane przez platformę Flutter z narzędziem


Observatory, więc jeśli Twoja aplikacja działa w trybie offline, możesz ją usunąć podczas kom-
pilacji wydania (nie jest to nasz przypadek, ponieważ używamy technologii Firebase).

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>

Pamiętaj, że w AdMob podczas tworzenia programowania możemy używać kluczy testowych,


aby nasze testy nie były oceniane jako niewłaściwe użycie interfejsu API.

AndroidManifest — nazwa i ikona aplikacji


Do tej pory w naszych testach, gdy uruchamiamy aplikację, widać, że ikoną aplikacji jest logo
Fluttera. Przed wydaniem musimy ją zamienić na naszą niesamowitą, unikalną ikonę, aby upew-
nić się, że nasi użytkownicy wyróżniają naszą aplikację wśród milionów innych.

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>

Tak więc wprowadzamy dwie zmiany w tym tagu:


 Zmieniamy wartość label na ostateczną nazwę naszej aplikacji, po której nasi
użytkownicy ją rozpoznają.
 Możemy też zamienić ikonę aplikacji (zastępując domyślne logo Fluttera) za pomocą
wartości icon:
 W systemie Android zasoby obrazów, takie jak ikona, znajdują się w katalogu
android/app/src/main/res/. W tym katalogu znajduje się wiele folderów
z wariantami zasobów dla określonych regionów, rozmiarów ekranu, wersji
systemu itd.

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.

 Aby w pełni zastąpić ikonę aplikacji, musimy zastąpić plik ic_launcher.png


w każdym z folderów mipmap-xxxdpi.

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.

build.gradle — identyfikator i wersje aplikacji


Wartość identyfikatora aplikacji sprawia, że jest ona unikalna w Sklepie Play i systemie Android.
Dobrą praktyką jest użycie domeny organizacji jako pakietu i umieszczenie po niej nazwy aplikacji.
W naszym przypadku jako ID aplikacji używamy com.example.handson.

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

Możesz znaleźć ten kod w pliku android/app/build.gradle, w sekcji defaultConfig:


defaultConfig {
applicationId "com.example.handson"
minSdkVersion 16
targetSdkVersion 28
multiDexEnabled true
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

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.

build.gradle — podpisywanie aplikacji


Podpisywanie jest ostatnim, ale najważniejszym krokiem przed publicznym udostępnieniem
aplikacji, nawet jeśli nie chcesz publikować w sklepie Google Play. To podpis potwierdza własność
aplikacji — krótko mówiąc, każdy, kto ma podpis, jest właścicielem aplikacji. Jest to potrzebne
na przykład do publikowania aktualizacji aplikacji.

Zacznij od przyjrzenia się sekcji buildTypes w pliku build.gradle:


buildTypes {
release {
signingConfig signingConfigs.debug
}
}

Zawiera właściwość signingConfig wskazującą na domyślną konfigurację podpisywania. Musimy


to zmienić na naszą konfigurację podpisywania z powodów wymienionych wcześniej. Robimy
to, wykonując następujące kroki:
1. Generujemy nasz plik keystore (możesz używać tego samego keystore dla wielu
aplikacji). Odbywa się to za pomocą następującego polecenia:
keytool -genkey -v -keystore DESTINATION_FILEPATH -keyalg RSA -keysize 2048 -
validity 10000 -alias key
Postępuj zgodnie z wyświetlanymi instrukcjami, a spowoduje to wygenerowanie
keystore w ścieżce DESTINATION_FILEPATH, na przykład <katalog
użytkownika/myrelease-key.keystore>. Powinieneś teraz odwołać się do tego
pliku w pliku build.gradle.

337

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

2. Utwórz plik android/key.properties z następującą zawartością:


storePassword = <hasło używane do generowania klucza>
keyPassword = <hasło używane do generowania klucza>
keyAlias = klucz
storeFile = ścieżka do pliku magazynu kluczy
(tj. </katalog użytkownika/my-releaseasekey.keystore>)
3. Następnie w build.gradle ładujemy ten nowy plik key.properties i tworzymy dla niego
nową klasę signingConfig:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
FileInputStream(keystorePropertiesFile))
}
android{
...
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
4. Wystarczy dodać fragment kodu przed sekcją android, a następnie zadeklarować
konfigurację podpisywania w podsekcji signingConfigs. Na koniec zamień właściwość
signingConfig w opcji release w poprzedniej sekcji buildTypes — na nową:
android {
...
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}

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.

Wydawanie aplikacji na iOS


Wydawanie aplikacji w systemie iOS może być bardziej złożone w porównaniu z systemem
Android. Chociaż podczas programowania możesz wykonywać testy na swoim urządzeniu,
upublicznienie aplikacji wymaga posiadania ważnego konta programisty Apple z możliwością
publikowania w App Store, ponieważ jest to jedyny obsługiwany kanał publikowania aplikacji.

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.

Sprawdź również obecność pliku GoogleService-Info.plist w katalogu ios/Runner (zobacz roz-


dział 8., aby przypomnieć sobie, jak zaimportować go do Xcode).

App Store Connect


W systemie Android nie musimy niczego konfigurować w konsoli Play Store, zanim apk będzie go-
towy do publikacji. Gdy już to zrobimy, możemy utworzyć rejestr w Konsoli Google Play; wypełnić
opis, szczegóły i ustawienia marketingowe; następnie przesyłamy nasz plik apk i publikujemy.

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

2. Następnie tworzymy aplikację w portalu App Store Connect, wybierając identyfikator


pakietu (Bundle ID), który zarejestrowaliśmy w poprzednim kroku. (W przypadku
naszej aplikacji Favors jest to prawie taka sama wartość jak applicationId
systemu Android).

Po wykonaniu tych kroków w App Store Connect kończymy proces w Xcode.

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.

Xcode — szczegóły aplikacji i identyfikator pakietu


W zakładce General projektu Runner możemy edytować wyświetlaną nazwę aplikacji (Display
Name), czyli nazwę naszej aplikacji. Podobnie ustawiliśmy nazwę aplikacji Androida Hands On:
Favors i ustawiliśmy identyfikator pakietu (Bundle ID) na com.biessek.handson.favorsapp.

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'
);

Xcode — podpisywanie aplikacji


Podobnie jak w przypadku Androida, potrzebujemy sposobu, aby potwierdzić własność aplikacji.
Xcode zarządza tym za nas; nie musimy bezpośrednio dotykać żadnego pliku. Kiedy rejestrujemy
się jako programista Apple i rejestrujemy w programie Apple Developer, mamy to wszystko gotowe.

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

1. W Xcode wybierz Product | Archive, aby utworzyć archiwum kompilacji.


2. Następnie wybierz utworzone archiwum kompilacji za pomocą polecenia flutter
build ios.
3. Kliknij przycisk Validate… Jeśli zostaną zgłoszone jakiekolwiek problemy,
rozwiąż je i utwórz kolejną kompilację.
4. Po pomyślnym sprawdzeniu poprawności archiwum kliknij Upload to App
Store… ..

Mamy aplikację na iOS gotową do publikacji. Możemy ją opublikować w TestFlight (prywatnej


aplikacji testowej z zaufanymi użytkownikami) lub w App Store.

Aby dowiedzieć się, co wybrać, przeczytaj oficjalną dokumentację: https://help.apple.com/


xcode/mac/current/#/dev442d7f2ca.

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ść.

W tym rozdziale zostaną omówione następujące tematy:


 Dostępność we Flutterze.
 Dodawanie tłumaczeń do aplikacji.
 Komunikacja między kodem natywnym a Flutterem za pomocą kanałów platformy.
 Tworzenie procesów w tle.
 Dodanie kodu specyficznego dla systemu Androida w celu uruchomienia kodu
Darta w tle.
 Dodanie kodu specyficznego dla systemu iOS w celu uruchomienia kodu Darta w tle.

Dostępność we Flutterze i dodawanie


tłumaczeń do aplikacji
Dodanie internacjonalizacji do aplikacji mobilnej przyczynia się do rozwoju rynku i umożli-
wia jej dotarcie do większej liczby użytkowników. Również stworzenie w pełni intuicyjnej
aplikacji sprawia, że trafia ona do wielu odbiorców i oferuje im lepsze wrażenia.

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.

Wsparcie Fluttera dla dostępności


Prawidłowe wdrożenie ułatwień dostępu w aplikacjach mobilnych poprawia komfort użytko-
wania i pomaga zwiększyć liczbę instalacji, jednocześnie zmniejszając liczbę odinstalowań.
Flutter ma pewne komponenty zapewniające wsparcie dostępności:
 Kontrast — Flutter udostępnia narzędzia, dzięki czemu programista może
odpowiednio pokolorować widżety z wystarczającym kontrastem.

Sprawdź zalecane przez W3C specyfikacje kontrastu: https://www.w3.org/


TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html.

 Duże czcionki — we Flutterze widżety tekstowe uwzględniają ustawienie


systemu operacyjnego podczas określania rozmiarów czcionek. Są powiększane,
jeśli użytkownik tego sobie życzy.

W systemie Android i iOS możemy włączyć duże czcionki poprzez ustawienia dostępności
w konfiguracjach systemu operacyjnego.

 Czytniki ekranu — TalkBack w Androidzie i VoiceOver w iOS umożliwiają


niedowidzącym użytkownikom otrzymywanie informacji głosowych o zawartości
ekranu.

Flutter udostępnia programistom widżet Semantics, który umożliwia deskrypcję znacze-


nia widżetów, dzięki czemu czytniki ekranu mogą działać poprawnie. Sprawdź dokumen-
tację widżetów: https://api.flutter.dev/flutter/widgets/Semantics-class.html.

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.

Dodawanie lokalizacji do aplikacji Fluttera


Lokalizacja we Flutterze to, jak każda inna rzecz, widżet. Zamierzamy użyć pakietu flutter_
localizations, aby skonfigurować tłumaczenia prostej aplikacji wyświetlającej pojedynczą
wiadomość, Hello Flutter. Będziemy obsługiwać język angielski, hiszpański i włoski.

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.

Na przykład klasa AppLocalizations byłaby bardzo podobna we wszystkich aplikacjach, z wy-


jątkiem wbudowanych zasobów ciągów. Oto jak to wygląda:

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();
});
}

static AppLocalizations of(BuildContext context) {


return Localizations.of<AppLocalizations>(context, AppLocalizations);
}

String get title {


return Intl.message(
'Hello Flutter',
name: 'title',
desc: 'The application title'
);
}

String get hello {


return Intl.message('Hello', name: 'hello');
}
}

Klasa AppLocalizations służy do hermetyzacji zasobów. Można ją podzielić na cztery główne


części:
 Funkcja load — powoduje ona załadowanie zasobów ciągu z żądanego Locale, jak
widać w parametrze.
 Funkcja of — jest to funkcja pomocnicza, jak w przypadku każdego innego
InheritedWidget, ułatwiająca dostęp do dowolnego ciągu z dowolnej części
kodu aplikacji.
 Funkcje get — zawiera ona listę dostępnych zasobów tłumaczonych w naszej
aplikacji. Zwróć uwagę na zwracany wynik opakowany w Intl.message; spowoduje on,
że narzędzie intl wyszuka tę klasę i zapełni dla nas pliki initializeMessages
tłumaczeniami.
 initializeMessages — ta metoda zostanie wygenerowana przez narzędzie intl.
Zauważ, że importowany plik „l10n/messages_all.dart”, który zostanie wygenerowany
w następnych krokach, zawiera metodę, która skutecznie ładuje przetłumaczone
komunikaty.

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;
}
}

Definicję klasy można podzielić na trzy główne części:


 Funkcja load — oto informacje z dokumentacji:
Metoda load musi zwracać obiekt, który zawiera kolekcję powiązanych zasobów
(zazwyczaj definiowanych z jedną metodą na zasób).

Zwracamy naszą klasę AppLocalizations.load.


 isSupported — jak sama nazwa wskazuje, zwraca wartość true, jeśli aplikacja
obsługuje funkcję receivedlocale.
 shouldReload — zasadniczo, jeśli ta metoda zwróci wartość true, wszystkie widżety
aplikacji zostaną odbudowane po załadowaniu zasobów. Zwykle będziesz chciał
zwrócić wartość true, jeśli Twoja aplikacja dynamicznie zmienia ustawienia
regionalne.

Generowanie plików .arb za pomocą intl_translation


Po zdefiniowaniu tych klas musimy utworzyć nasze tłumaczenia. Jak widać w klasie App
Localizations, do przetłumaczenia są tylko dwa zasoby ciągów: title i hello. Jak wspomniano
wcześniej, proces tłumaczenia odbywa się na plikach .arb. Dlatego musimy zdefiniować pliki
.arb dla każdego z obsługiwanych języków (w naszym przypadku angielskiego, hiszpańskiego
i włoskiego), a pliki te muszą zawierać zasoby tekstowe przetłumaczone na język docelowy.

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

flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/


app_localization.dart

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.

Następnie, kiedy wszystko jest przetłumaczone, musimy przygotować go do użycia w naszej


aplikacji.

Proces jest odwrotnością generowania plików .arb:


flutter pub pub run intl_translation:generate_from_arb --outputdir=lib/
l10n lib/app_localization.dart lib/l10n/intl_en.arb lib/l10n/intl_es.arb lib/
l10n/intl_it.arb

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

Pamiętaj, że klasa AppLocalization używa initializeMessages z pliku messages_all.dart. Teraz jest


gotowa do udostępnienia lokalizowanych zasobów dla aplikacji.

Korzystanie z przetłumaczonych zasobów


Mając wszystkie wygenerowane pliki i wszystkie zasoby przetłumaczone i gotowe do użycia,
musimy teraz odpowiednio je wykorzystać w aplikacji. Aby to zrobić, musimy ustawić kilka właści-
wości klasy MaterialApp. Oto jak wygląda nasza klasa aplikacji:
class MyApp rozszerza StatelessWidget {
// Ten widżet jest główny dla Twojej aplikacji.
@override
Widget build(BuildContext context) {
return new MaterialApp(
localizationsDelegates: [
AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [Locale("en"), Locale("es"), Locale("it")],
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context).title,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}

Musimy ustawić właściwości localizationsDelegates i supportedLocales. Powtarzasz supported


Locales swoich delegatów i ustawiasz tablicę localizationDelegates za pomocą App
LocalizationsDelegate, a także GlobalDelegates z pakietu flutter_localizations.

W dokumentacji zwróć uwagę na następujące kwestie:


Elementy listy localizationsDelegates to fabryki, które tworzą kolekcje zlokalizowanych
wartości. GlobalMaterialLocalizations.delegate udostępnia lokalizowane ciągi i inne
wartości dla biblioteki Material Components. GlobalWidgetsLocalizations.delegate definiuje
domyślny kierunek tekstu, od lewej do prawej lub od prawej do lewej, dla biblioteki
widżetów.

Tak więc zarówno GlobalWidgetsLocalizations, jak i GlobalMaterialLocalizations są obowiąz-


kowe, jeśli chcemy, aby nasza aplikacja była całkowicie zlokalizowana.

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

Widget build(BuildContext context) {


return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).title),
),
body: Center(
child: Text(
AppLocalizations.of(context).hello,
style: Theme.of(context).textTheme.display1,
),
),
);
}
}

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ń:

Teraz, gdy zakończyliśmy internacjonalizację Fluttera, przejdźmy do komunikacji między kodem


natywnym a Flutterem.

350

d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania

Komunikacja między kodem natywnym


a Flutterem z wykorzystaniem kanałów
platformy
Flutter od 2018 roku zyskuje coraz więcej użytkowników — wraz z wydaniem swojej pierwszej
stabilnej wersji. Jednym z najważniejszych powodów tej sytuacji są ułatwienia umożliwiające
opracowanie pięknego, dynamicznego i płynnego interfejsu użytkownika. Jednak to nie wszystko,
czego może potrzebować aplikacja mobilna; musi radzić sobie z interfejsami API różnych
platform hosta, ponieważ zależy od niego wiele funkcji, takich jak:
 Bluetooth, kamera, czujniki i lokalizacja.
 Uprawnienia użytkownika.
 Powiadomienia.
 Przechowywanie plików i preferencji.
 Udostępnianie informacji innym aplikacjom.

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

Podobnie jak wtyczki, PlatformViews, przedstawiony wcześniej w rozdziale 11., również


wykorzystuje mechanizm kanału platformy do wymiany danych.

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.

Jest to możliwe dzięki zastosowaniu standardowych kodeków wiadomości Fluttera. Kodeki


wiadomości są odpowiedzialne za tłumaczenie danych z jednego języka na inny. Dostępnych
jest wiele różnych kodeków wiadomości, a jeśli to konieczne, możemy stworzyć własne. Są to:
 BinaryCodec — są to niezakodowane komunikaty binarne reprezentowane za pomocą
ByteData. W systemie Android wiadomości będą reprezentowane za pomocą
java.nio.ByteBuffer. W systemie iOS wiadomości będą reprezentowane przy
użyciu NSData.
 JSONMessageCodec — są to wiadomości JSON zakodowane w UTF-8. W systemie
Android wiadomości są dekodowane przy użyciu biblioteki org.json. W systemie
iOS wiadomości są dekodowane przy użyciu biblioteki NSJSONSerialization.
 StringCodec — są to wiadomości typu String zakodowane w UTF-8. W systemie
Android wiadomości będą reprezentowane za pomocą java.util.String. W systemie
iOS wiadomości będą reprezentowane za pomocą NSString.
 StandardMessageCodec — używa standardowego kodowania binarnego Fluttera.
Dekodowane wartości będą używać List<dynamic> i Map<dynamic, dynamic>,
niezależnie od zawartości. Wartości wiadomości są tłumaczone z typów Darta na
typy Android / iOS.

Zapoznaj się z oficjalną dokumentacją dotyczącą klasy StandardMessageCodec,


aby zobaczyć, jak wartości są mapowane z Darta na natywne i odwrotnie:
https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html.

MethodChannel domyślnie używa dostarczonego przez Fluttera standardu StandardMessageCodec,


aby przeprowadzić serializację / deserializację danych, gdy wysyłamy / odbieramy wiadomości.

353

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Tworzenie procesów pracujących w tle


W rozdziale 2. widzieliśmy podejście Darta do programowania współbieżnego: izolaty. Dzięki
temu możemy tworzyć niezależne workery, które są podobne do wątków, ale nie współużytkują
pamięci i komunikują się ze sobą tylko za pośrednictwem wymiany wiadomości.

W kontekście aplikacji mobilnych musimy również zadbać o współbieżność. Ponieważ długie


operacje mogą powodować opóźnienia w renderowaniu i tak dalej, Flutter zapewnia łatwy sposób
tworzenia izolatu — za pomocą funkcji compute().

Funkcja Fluttera compute()


Metoda compute() ma ułatwiać zadanie tworzenia nowego izolatu, wysyłania do niego wiado-
mości i otrzymywania odpowiedzi. Jej zapis wygląda następująco:
Future<R> compute <Q, R>( ComputeCallback<Q, R> callback, Q message, {String
debugLabel })

Kilka parametrów opisuje żądanie do nowego izolatu:


 callback — jest to funkcja najwyższego poziomu, wykonywana w nowym izolacie.
Uwaga dla ComputeCallback. Istnieją ogólne adnotacje <Q, R>; pierwsza, Q,
oznacza typ wejścia wywołania zwrotnego, a R oznacza typ wyniku obliczenia.
Uwaga z dokumentacji:
Argument wywołania zwrotnego musi być funkcją najwyższego poziomu, a nie
zamknięciem, instancją lub statyczną metodą klasy.
 message — jest to wartość parametru typu Q, która zostanie wysłana do callback.
Uwaga z dokumentacji:
Istnieją ograniczenia dotyczące wartości, które mogą być wysyłane do
i odbierane z izolatów. Te ograniczenia wyznaczają wartości Q i R.
 debugLabel — można go użyć podczas programowania, nadając nazwę izolatowi
w celu lepszego rozróżnienia w narzędziu interfejsu użytkownika Observatory
podczas profilowania.

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.");
}

print('finished calculating fibo');


}

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);

Ta bardzo przydatna funkcja wyodrębnia całą konfigurację potrzebną do uruchomienia i ko-


munikacji z nowym izolatem. Wysyłamy ją, z parametrami lub bez, i pobieramy opcjonalną
odpowiedź zwrotną.

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.

Proces pracujący w tle


Chociaż jest to bardzo przydatne, funkcja compute() może nie być tym, czego potrzebujemy
we wszystkich przypadkach. Jak wskazano wcześniej, izolat podrzędny utworzony przez funk-
cję compute() zostaje zakończony za każdym razem, gdy kończy się izolat nadrzędny.

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.

W przypadkach użycia, w których potrzebujemy, aby kod działał niezależnie od interfejsu


użytkownika aplikacji, możemy utworzyć izolaty headless, to znaczy izolat, który nie jest powią-
zany z izolatem głównym aplikacji, a jeśli izolat główny zostanie zakończony, nie wpłynie to
na jego wykonanie.

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.

Zapoznaj się z informacjami pod adresem: https://github.com/flutter/flutter/issues, gdzie


znajdziesz alternatywne rozwiązania dotyczące przetwarzania w tle.

Komunikację można uprościć w następujący sposób:

Bezpośrednia komunikacja między izolatami głównymi i pracującymi w tle jest opcjonalna


i trudna do utrzymania, ponieważ musimy zadbać o aspekty związane z ich pracą. Do tej pory
najprostszym sposobem jest wysyłanie żądań, a nawet używanie systemu operacyjnego, który
steruje pracą izolatu w tle niezależnie od głównego izolatu aplikacji.

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

użyć w swoich aplikacjach. Wtyczka będzie odpowiedzialna za wyodrębnienie procesu two-


rzenia izolatu, tak aby był on przezroczysty dla aplikacji. Jego jedyną metodą jest calculateIn
BackgroundProcess, wywoływana z aplikacji:
HandsOnBackgroundProcess.calculateInBackgroundProcess();

Zapoznaj się z kompletnym przykładem hands_on_background_process w serwisie GitHub.

Ten poprzedni kod wywołuje metodę wtyczki, odpowiedzialną za zainicjowanie procesu,


który wygląda następująco:
const pluginChannel = MethodChannel('com.example.handson/plugin_channel');

class HandsOnBackgroundProcess {
static void calculateInBackgroundProcess() async {
final callbackHandle = PluginUtilities.getCallbackHandle(
backgroundIsolateMain
);

await pluginChannel.invokeMethod(
"initBackgroundProcess",
[callbackHandle.toRawHandle()]
);
}
}

Jak widać, przede wszystkim definiujemy kanał metody o nazwie com.example.handson/


plugin_channel dla wywołań wtyczki; jest to zazwyczaj pierwszy krok przy tworzeniu wtyczki.
Następnie w metodzie CalcInBackgroundProcess() wykonujemy następujące czynności:
 Otrzymujemy uchwyt do nowego punktu wejścia izolatu pracującego w tle.
Używamy narzędzia PluginUtilities.getCallbackHandle dostarczonego przez
framework, aby uzyskać identyfikator wywołania zwrotnego, który ma zostać
przekazany do natywnej strony aplikacji. W ten sposób później, po stronie natywnej,
możemy pobrać to wywołanie zwrotne i razem z nim uruchomić w tle izolat jako
punkt wejścia.
 Po uzyskaniu uchwytu wywołujemy metodę „initBackgroundProcess”, przekazując
go do niej. Ta metoda wykona wspomniane wcześniej zadanie izolatu.

Przyjrzyjmy się najpierw punktowi wejścia izolatu Dart, a następnie sprawdźmy kod potrzebny
do jego prawidłowego działania.

Izolat pracujący w tle


Wywołanie zwrotne Dart przekazywane do natywnej części wtyczki przez uchwyt jest odpo-
wiedzialne za obliczenie Fibonacciego, tak jak poprzednio. Jednak to nie jest dokładnie to samo:
void backgroundIsolateMain() {
print('background isolate entry point running');

358

d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania

const backgroundchannel = MethodChannel(


'com.example.handson/background_channel'
);
WidgetsFlutterBinding.ensureInitialized();

backgroundchannel.setMethodCallHandler((MethodCall call) async {


if (call.method == 'calculate') {
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: 500));
print("first: $first, second: $second.");
}

print('finished calculating fibo');


backgroundchannel.invokeMethod("calculationFinished");
}
});
backgroundchannel.invokeMethod("backgroundIsolateInitialized");
}

Jak widać, jest kilka zmian:


1. Izolat rozpoczyna się od ustawienia kanału metody, ale nie tego samego co wcześniej.
Teraz tworzymy jeden o nazwie com.example.handson/background_channel. Służy
on do nawiązania komunikacji z natywnym kodem wykonywanym w tle (usługa na
Androidzie i wykonanie w tle na iOS).
2. Ustawia uchwyt dla metody calculate, tak aby kod natywny mógł ją wywołać w celu
rozpoczęcia obliczeń. Chociaż nie jest to tak naprawdę potrzebne w tym przypadku
(moglibyśmy rozpocząć obliczenia bezpośrednio w treści punktu wejścia), jest
dobrym przykładem.
3. Po skonfigurowaniu kanału metody powiadamiamy stronę natywną za pomocą
wywołania backgroundIsolateInitialized. Teraz po stronie Darta wszystko jest
już gotowe.

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

Dodanie kodu specyficznego


dla systemu Android
w celu uruchomienia kodu Darta w tle
W Androidzie istnieje koncepcja usług, która jest idealnym sposobem uruchamiania kodu aplika-
cji w tle, niezależnie od wykonywania głównej aplikacji. Zasadniczo musimy utworzyć metodę
Service, powiązać z nią nowy izolat pracujący w tle i uruchomić.

Przeczytaj oficjalną dokumentację dotyczącą usług Androida: https://developer.android.com/


reference/android/app/Service.

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)
}
}
...
}

Jak widać, skonfigurowany został kanał metody o nazwie com.example.handson/plugin_channel,


który jest używany do inicjalizacji obliczeń za pomocą metody initBackgroundProcess:
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result?)
{
val args = call.arguments() as? ArrayList<*>
if (call.method == "initBackgroundProcess") {
val callbackHandle = args?.get(0) as? Long ?: return
executeBackgroundIsolate(context, callbackHandle)
}
}

360

d0765ad53fb82babda2278a311da7afb
d
Rozdział 13. • Poprawa komfortu użytkowania

W celu obsłużenia metody initBackgroundProcess pobierany jest uchwyt wywołania zwrotnego


pochodzący z Darta. Aby mógł być poprawnie pobrany, jest parsowany do typu Long (int w Dart)
zgodnie z klasą StandardMessageCodec.

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)
}
...

Po pierwsze, metoda przechowuje wartość uchwytu w pliku SharedPreferences. Następnie wyko-


nywane jest żądanie wykonania usługi w tle za pomocą metody startBackgroundService():
 Współdzielone preferencje są używane w systemie Android do przechowywania
danych klucz-wartość w prosty i zapewniający prywatność sposób. Są używane
w naszym przykładzie, ponieważ nie możemy przekazywać parametrów
do konstruktorów Service (nie mogą one pobierać argumentów).

Więcej informacji na temat współdzielonych preferencji można znaleźć w oficjalnej do-


kumentacji: https://developer.android.com/training/data-storage/shared-preferences.

 startBackgroundService() wysyła żądanie do systemu Android w celu


zainicjowania usługi w tle:
...
private fun startBackgroundService(context: Context) {
val intent = Intent(
context,
BackgroundProcessService::class.java
)
context.startService(intent)
}
...

Pozostała część zadania jest wykonywana w klasie BackgroundProcessService.

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.

Ponownie ważne jest sprawdzenie wspomnianej wcześniej dokumentacji usługi (Service)


Android, aby zrozumieć, jak działa cykl życia.

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().

Metoda startBackgroundIsolate() jest główną i najbardziej złożoną metodą w tej klasie.


Jest odpowiedzialna za skonfigurowanie struktury potrzebnej do działania izolatu w tle.

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 = FlutterNativeView(this, true)


val path = FlutterMain.findAppBundlePath(applicationContext)
val args = FlutterRunArguments()
args.bundlePath = path
args.entrypoint = callback.callbackName
args.libraryPath = callback.callbackLibraryPath

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

4. Po uruchomieniu izolatu tła tworzymy instancję dla kanału metody działającej


w tle o nazwie com.example.handson/background_channel, tak jak zrobiliśmy to po
stronie Darta.
5. Ostatnim krokiem jest zarejestrowanie instancji wtyczki w rejestrze Fluttera
za pomocą właściwości sPluginRegistrantCallback. Ta właściwość musi zostać
w jakiś sposób przekazana ręcznie do klasy Service. Dlaczego? Flutter automatycznie
rejestruje wtyczkę w głównym wątku, gdy jej używasz (pamiętaj o statycznej
metodzie registerWith, którą musimy zaimplementować dla naszych wtyczek).
Za pomocą PluginRegistrantCallback robimy to ręcznie. Dzięki temu możemy
zarejestrować wtyczkę w dowolnym miejscu, na przykład tam, gdzie registerWith
nie jest wyszukiwane (w tym przypadku nasz Service).

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)
}

override fun registerWith(registry: PluginRegistry?) {


GeneratedPluginRegistrant.registerWith(registry)
}
}

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.
}

Nasza instancja BackgroundProcessService jest zdefiniowana jako metoda obsługi wywołań


kanału metody w tle:
 Metoda o nazwie backgroundIsolateInitialized jest wywoływana z izolatu w tle,
gdy jest gotowa, i w odpowiedzi na to rozpoczynamy obliczenia wywołujące calculate
w tym samym kanale.
 Ponadto za każdym razem, gdy obliczenia zostaną zakończone, a izolat tła Darta
wywoła metodę calculationFinished, nasza instancja FlutterNativeView, która
przechowuje izolat, zostanie zniszczona, a usługa zatrzymana przez wywołanie
metody shutdownService() — usuwa ona powiadomienie zdefiniowane wcześniej
i zabija usługę.

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.

Dodanie kodu specyficznego dla systemu


iOS w celu uruchomienia kodu Darta w tle
W iOS jest inaczej. Wykonywanie w tle jest znacznie bardziej ograniczone niż w przypadku
Androida. Koncepcja usługi (Service) nie istnieje i mamy kilka momentów, kiedy możemy
uruchomić kod w tle.

Większość przypadków użycia obejmuje UIBackgroundModes, w których aplikacja może


definiować obsługiwane tryby w tle, a następnie uruchamiać określone rodzaje wykonywania
w tle. Możemy na przykład wykonać następujące czynności:
 W trybie pracy w tle Audio i AirPlay ustawić aplikację jako zdolną do odtwarzania
treści dźwiękowych lub nagrywania dźwięku w tle.
 Otrzymywać aktualizacje lokalizacji w tle.
 Kiosk to tryb pobierania, w którym aplikacja może pobierać i przetwarzać
zawartość czasopism lub gazet w tle.

365

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Sprawdź oficjalny przewodnik dotyczący wykonywania w tle w dokumentacji systemu iOS:


https://developer.apple.com/documentation/uikit.

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.

Wykonanie izolatu w tle w metodzie executeBackgroundIsolate() przebiega w następujący sposób:


private func executeBackgroundIsolate(handle: Int64) {
_backgroundRunner = FlutterEngine.init(
name: "BackgroundProcess",
project: nil,

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!
)
}

Ponownie, wykonanie możemy podzielić na kilka kroków:


1. Najpierw zapisujemy instancję klasy FlutterEngine we właściwości
_backgroundRunner. Ta instancja będzie naszą wtyczką Fluttera, która stanie się
mostem, takim jak FlutterNativeView był na Androidzie.
2. Następnie pobieramy punkt wejścia z uchwytu wywołania zwrotnego za
pośrednictwem narzędzia FlutterCallbackCache.lookupCallbackInformation().
Wszystkie informacje są takie same, jak te, które otrzymujemy w Androidzie.
Tutaj używamy entrypoint i uri , aby uruchomić izolat w tle przez wywołanie
_backgroundRunner!.Run(withEntrypoint: entrypoint, libraryURI: uri).
3. Po uruchomieniu izolatu ostatnia część jest bardzo podobna do Androidowej.
Tworzymy kanał o nazwie com.example.handson/background_channel
do komunikacji i ustawiamy jego uchwyt jako samą instancję wtyczki.
4. Na koniec rejestrujemy wtyczkę w tle poprzez wywołanie zwrotne
_registerPlugins, tak jak w przypadku PluginRegistrantCallback w Androidzie.

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

Po uruchomieniu izolatu w tle możemy ponownie obsługiwać połączenia z kanału w tle:


public func handle(_ call: FlutterMethodCall, result: @escaping
FlutterResult) {

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
}
}

Chociaż jest inaczej, podstawowa idea obsługi metod jest podobna:


1. Gdy wywoływana jest metoda o nazwie backgroundIsolateInitialized,
wywołujemy odpowiednią metodę calculate, więc wykonuje ona obliczenia
i loguje do konsoli Fluttera. Wcześniej rejestrujemy zadanie iOS w tle. To powiadomi
system, że potrzebujemy trochę więcej czasu, aby zakończyć naszą pracę i zapobiec
jej nieoczekiwanemu zamknięciu. Pamiętaj, iOS jest bardzo restrykcyjny w przypadku
zadań wykonywanych w tle.
2. W wywołaniu calculationFinished po prostu powiadamiamy system za pomocą
UIApplication.shared.endBackgroundTask (self.taskID!), że nasze zadanie zostało
zakończone i można bezpiecznie przenieść naszą aplikację do stanu zawieszenia
(suspended).

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.

Dowiedz się więcej o wątkach w iOS: https://flutter.dev/docs/get-started/flutter-for/


ios-devs#threading--asynchronicity.

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.

Następnie sprawdziliśmy, jak możemy dodawać tłumaczenia do aplikacji Fluttera, generując


pliki .arb, tworząc wiele tłumaczeń, importując je do Darta i stosując je do naszej klasy MaterialApp.

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.

W tym rozdziale zostaną omówione następujące tematy:


 Transformacje widżetów za pomocą klasy Transform.
 Rodzaje transformacji.
 Dodawanie transformacji do widżetów.
 Korzystanie z niestandardowych malarzy (painter) i elementów canvas.

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.

Zrozumienie klasy Matrix4


U podstaw przekształceń geometrycznych leży matematyka. We Flutterze transformacje są repre-
zentowane w macierzy 4D. Oprócz takich metod jak dodawanie lub mnożenie macierzy, klasa
Matrix4 zawiera metody pomagające w konstruowaniu i manipulowaniu transformacjami geo-
metrycznymi. Oto niektóre z nich:

372

d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach

 rotation — rotateX(), rotateY() i rotateZ() to przykłady metod obracających


macierz względem określonej osi.
 scale — scale(), z niektórymi wariantami, służy do zastosowania skali na
macierzy przy użyciu wartości odpowiednich osi (x, y i z) lub poprzez
reprezentacje wektorowe z klasami Vector3 i Vector4.
 translation — tak jak poprzednio, możemy translować macierz za pomocą metody
translate() z określonymi wartościami x, y lub z oraz instancjami Vector3 i Vector4.
 skew — służy do pochylania macierzy wokół osi X za pomocą skewX() lub osi Y za
pomocą skewY().

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.

Można to osiągnąć za pomocą wariantu konstruktora Transform.rotate. Zobaczmy, jak to wygląda:


Transform.rotate({
Key key,
@required double angle,
Offset origin,
AlignmentGeometry alignment: Alignment.center,
bool transformHitTests: true,
Widget child
})

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.

A tak wygląda konstruktor fabryczny Transform.translate():


Transform.translate({
Key key,
@required Offset offset,
bool transformHitTests: true,
Widget child
})

375

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Tutaj mamy jeszcze mniej właściwości w porównaniu z poprzednimi transformacjami. Różnice są


następujące:
 Brak właściwości transform i alignment — transformacja zostanie zastosowana
przez wartość offset, więc nie potrzebujemy macierzy transformacji.
 Offset — tym razem offset określa po prostu translację, która ma zostać
zastosowana na widżecie podrzędnym; różni się to od poprzednich transformacji,
gdzie wpływa się na punkt początkowy zastosowanej transformacji.

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.

Transformacje złożone można wykonać na dwa sposoby:


 Używając domyślnego konstruktora widżetu Transform i generując żądaną
transformację przy użyciu metod dostarczonych przez Matrix4.
 Używając wielu widżetów Transform w sposób zagnieżdżony z konstruktorami
fabrycznymi rotate(), scale() i translate(), przez co uzyskujemy ten sam efekt,
ale powodujemy, że nasze drzewo widżetów jest większe, niż potrzeba.

376

d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach

Stosowanie transformacji do widżetów


Jak widzieliśmy do tej pory, widżet Transform może pomóc nam zmodyfikować naturalny wygląd
widżetu. Stosowanie transformacji do widżetów jest tak proste, jak dodanie widżetu Transform
jako elementu nadrzędnego widżetu, który chcemy zmodyfikować. Sprawdźmy, jakie alternatywy
możemy wykorzystać, aby zastosować transformacje do widżetów.

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: () {},
),
);

Domyślnego konstruktora można również użyć z Matrix4 określającym translację:


Transform(
transform: Matrix4.translationValues(100, 300, 0),
child: RaisedButton(
child: Text("translated to bottom"),
onPressed: () {},
),
);

Musimy tylko określić właściwość transform z instancją Matrix4 opisującą tłumaczenie.

Stosowanie wielu transformacji


Jak już wspomniano, mamy dwa sposoby dodawania wielu transformacji do widżetów.

Pierwsza polega na dodaniu wielu widżetów Transform powyżej żądanego widżetu:


Transform.translate(
offset: Offset(70, 200),
child: Transform.rotate(
angle: -45 * (math.pi / 180.0),
child: Transform.scale(
scale: 2.0,
child: RaisedButton(
child: Text("multiple transformations"),
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.

Kiedy jednocześnie dodajemy do widżetu wiele transformacji, musimy zwrócić uwagę


na ich kolejność. Poeksperymentuj sam: zamiana pozycji widżetów Transform przyniesie
różne rezultaty.

379

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Alternatywnie możemy użyć domyślnego konstruktora Transform z transformacją za pomocą


obiektu Matrix4:
Transform(
alignment: Alignment.center,
transform: Matrix4.translationValues(70, 200, 0)
..rotateZ(-45 * (math.pi / 180.0))
..scale(2.0, 2.0),
child: RaisedButton(
child: Text("multiple transformations"),
onPressed: () {},
),
);

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.

Korzystanie z niestandardowych malarzy


i elementów canvas
Flutter ma na celu zapewnienie programistom najlepszych możliwych narzędzi do konstruowania
interfejsów użytkownika aplikacji — bez ograniczeń. Prawdopodobnie już jesteś o tym prze-
konany, dzięki licznym widżetom, które oferuje, łatwości ich rozszerzania i wszechświatowi
możliwości, jakie oferuje framework.

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.

Domyślnie region przycinania Canvas jest nieskończony, więc wszystkie regiony są


prawidłowe.

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.

Szczegóły konstrukcyjne CustomPaint


Widżet CustomPaint działa po prostu jako pomost między drzewem widżetów (będąc widże-
tem) a warstwą malowania niższego poziomu z dostępem do Canvas. Aby go utworzyć, musimy
mieć instancję CustomPainter, ponieważ nie ma sensu mieć CustomPaint bez malarza.

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

Właściwości związane z malarzem możemy zobaczyć na poniższym zrzucie ekranu:

Ilustruje to kolejność rysowania: najpierw wykonywane są operacje painter, następnie child,


a na końcu foregroundPainter (jeśli istnieje) rysuje na child.

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
)

Jedyne dwa argumenty, które otrzymuje, są następujące:


 canvas, na którym skutecznie rysujemy za pomocą metod draw*().
 size określa granice rysunku, które powinniśmy wziąć pod uwagę.

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
)

Otrzymuje tylko argument oldDelegate, który odpowiada ostatniemu delegatowi (instancji


klasy this CustomPainter), który był odpowiedzialny za malowanie na CustomPaint. Za każ-
dym razem, gdy zwraca false, malowanie może zostać zoptymalizowane (nie oznacza to, że
malowanie nie zostanie wywołane). Powinniśmy porównać starego i obecnego delegata, aby
zobaczyć, czy jakiekolwiek dane związane z malowaniem są różne, a następnie zwrócić w tym
przypadku wartość true.

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.

Zaczniemy od widżetu wykresu kołowego, w którym pobieramy wartości wycinków i rysujemy je


proporcjonalnie w kole. Tak to będzie wyglądać — zobacz rysunek na następnej stronie.

Teraz zdefiniujmy nowy widżet PieChart za pomocą klas Canvas i CustomPaint.

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;
...
}

Jedyne właściwości opisujące widżet to values i colors:


 values — to lista (List), która reprezentuje każdą z wartości sekcji. Tutaj,
dla uproszczenia, używamy wartości int, ale może to być dowolny typ, pracujący
z dowolną logiką.
 colors — to lista (List) zawierająca kolory, których należy użyć do pomalowania
każdej sekcji na wykresie.

384

d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach

Przyjrzyjmy się teraz metodzie build() tego widżetu:


@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: CustomPaint(
painter: PieChartPainter(
values,
colors
),
),
),
],
);
}

Jest kilka kwestii, na które musimy zwrócić uwagę:


 Aby widżet CustomPaint istniał, musi mieć określony rozmiar, ponieważ cała jego
logika malowania powinna odbywać się na skończonym płótnie. Jak widzieliśmy
wcześniej, widżet CustomPaint definiuje swój rozmiar na podstawie ograniczeń
child. My nie mamy dziecka, więc trzeba go w jakiś sposób ograniczyć.
Moglibyśmy to zrobić na przykład za pomocą SizedBox, ale nie byłby idealny.

385

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Zamiast tego umieściliśmy go wewnątrz widżetu Row, wypełniając jego dostępną


przestrzeń poziomą, otaczając go widżetem Expanded.
 Widżet CustomPaint pobiera naszego niestandardowego malarza, PieChartPainter,
przez właściwość painter.

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.

Zobaczmy, jak je zdefiniujemy.

Nadpisywanie metody shouldRepaint


W naszym przykładzie values i colors opisują rysunek, więc za każdym razem, gdy zmieni
się któryś z nich, musimy przemalować nasz widżet. Należy więc odzwierciedlić to w metodzie
shouldRepaint w następujący sposób:
// część pliku pie_chart.dart klasy PieChartPainter

@override
bool shouldRepaint(PieChartPainter oldDelegate) {
return !ListEquality().equals(oldDelegate.values, values) ||
!ListEquality().equals(oldDelegate.colors, colors);
}

Nadpisywanie metody paint


Za narysowanie naszego wykresu odpowiada metoda paint. Oto jak to definiujemy:
// część pliku pie_chart.dart klasy PieChartPainter

@override
void paint(Canvas canvas, Size size) {
var center = Offset(size.width / 2, size.height / 2);
var radius = (size.width * 0.75) / 2;

Rect chartRect = Rect.fromCircle(


center: center,
radius: radius,
);

int total = values.reduce((a, b) => a + b);

_paintCircle(canvas, total, chartRect);


}

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.

Metoda _paintCircle() jest początkowo zdefiniowana w następujący sposób:


void _paintCircle(Canvas canvas, int total, Rect chartRect) {
Paint sectionPaint = Paint()..style = PaintingStyle.fill;

double startAngle = -90;


for (var i = 0; i < values.length; i++) {
final value = values[i];
final color = colors[i];

double sweepAngle = ((value * 360.0) / total);


sectionPaint.color = color;
canvas.drawArc(
chartRect,
startAngle * _toRadians,
sweepAngle * _toRadians,
true,
sectionPaint,
);

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

1. Najpierw definiujemy startAngle. Aby rozpocząć rysowanie łuków, wybraliśmy


kąt początkowy -90º. Na tarczy zegara odpowiadałoby to godzinie 12. Jeśli jako
początek wybraliśmy 0º, byłoby to równoważne godzinie 3 lub, jak podano
w dokumentacji metody drawArc:
… zero radianów to punkt po prawej stronie owalu, który przecina poziomą linię
przecinającą środek prostokąta oraz z dodatnimi kątami biegnącymi zgodnie
z ruchem wskazówek zegara wokół owalu.
2. Na końcu przechodzimy przez każdą z wartości, aby narysować każdy z łuków:
1. Najpierw obliczamy kąt rozwarcia łuku, który jest niczym innym jak kątem
łuku w danym okręgu. Jak powiedziano wcześniej, uzyskuje się go za pomocą
prostej reguły trzech, w której zadajemy pytanie: Jeśli całkowita wartość jest
równoważna kątowi odchylenia 360°, jaki jest kąt odchylenia (ile stopni)
bieżącej wartości wycinka?
2. Następnie ustawiamy kolor naszego obiektu Paint na bieżący kolor wycinka.
Nasz zdefiniowany wcześniej obiekt Paint ma styl PaintingStyle.fill, co oznacza,
że kształt narysowany za pomocą tego obiektu zostanie wypełniony danym
kolorem. W naszym przypadku właśnie tego chcemy.
3. I w końcu rysujemy łuk na podstawie właściwości startAngle i sweepAngle
(sprawdź poniższe wyjaśnienie).

Zobaczmy, jak możemy użyć metody drawArc z klasy Canvas, aby narysować nasze wycinki.

Tak wygląda metoda drawArc:


void drawArc (
Rect rect,
double startAngle,
double sweepAngle,
bool useCenter,
Paint paint
)

Przyjrzyjmy się wartościom, które przekazaliśmy do tej funkcji:


 rect — służy jako przewodnik rysowania. Łuk zostanie narysowany wewnątrz
danego prostokąta, z kątem rozwarcia względem środka prostokąta.
 startAngle — określa, gdzie rozpocząć rysowanie łuku. Pamiętaj, że 0°
to równoważnik godziny 3.
 sweepAngle —określa, ile łuku zostanie pobrane z owalu. Obliczamy to na
podstawie total i każdej wartości wycinka.
 useCenter — pomaga nam manipulować sposobem rysowania łuku,
jak wspomniano w dokumentacji:
Jeśli ma wartość true, łuk jest zamknięty z powrotem do środka, tworząc sektor
okręgu. W przeciwnym razie łuk nie zostanie zamknięty, tworząc odcinek okręgu.

388

d0765ad53fb82babda2278a311da7afb
d
Rozdział 14. • Operacje graficzne na widżetach

 Paint — definiuje sposób rysowania naszego łuku. Tutaj tworzymy łuk


wypełniony stylem PaintingStyle.fill i ustawiamy jego kolor za pomocą
podanego koloru wycinka.

Jak widać, musimy przekonwertować wartości kąta na radiany przed wysłaniem ich do funkcji
drawArc.

Wariant wykresu radialnego


Aby pomóc Ci zrozumieć potencjał używania widżetów CustomPaint, stwórzmy kolejny wi-
dżet, tym razem do narysowania wykresu radialnego, takiego jak ten:

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

// część widżetu radial_chart.dart RadialChart

@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;

Rect chartRect = Rect.fromCircle(


center: center,
radius: radius,
);

int total = values.reduce((a, b) => a + b);

_paintTotal(canvas, total, chartRect);


_paintCircle(canvas, total, chartRect);
}

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;

double startAngle = -90;


for (var i = 0; i < values.length; i++) {
final value = values[i];
final color = colors[i];

double sweepAngle = ((value * 360.0) / total);

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.

To wszystko, co zmieniliśmy, aby uzyskać taki wykres radialny:

391

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Wreszcie, spójrzmy na metodę _paintTotal(), która rysuje tekst:


void _paintTotal(Canvas canvas, int total, Rect chartRect) {
final totalPainter = TextPainter(
maxLines: 1,
text: TextSpan(
style: textStyle,
text: "$total",
),
textDirection: textDirection,
);

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

Aby narysować tekst na płótnie, wykonujemy następujące kroki:


1. Najpierw tworzymy instancję obiektu TextPainter, który definiuje wygląd tekstu
podczas rysowania, tak jak robi to klasa Paint dla kształtów. W naszym przypadku
definiujemy go jako pojedynczą linię i pobieramy style i textDirection z widżetu
RadialChart.
2. Następnie upewniamy się, że wywołujemy funkcję layout() z instancji
TextPainter. To wywołanie obliczy wizualną pozycję glifów do malowania tekstu.
3. Znając rozmiar tekstu, możemy go poprawnie wypozycjonować. Aby umieścić
tekst dokładnie na środku wykresu, po prostu translujemy środek wykresu
prostokąta za pomocą połowy rozmiaru tekstu.

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

Wbudowane animacje Fluttera można łączyć i rozszerzać, co zaspokaja potrzeby programistów


w stosunku do UX. W tym rozdziale dowiesz się o nich dużo więcej, przeczytasz o używaniu
animacji Tween do zarządzania osią czasu i krzywą animacji oraz używaniu AnimatedBuilder
i AnimatedWidget do dodawania i łączenia pięknych animacji.

W tym rozdziale zostaną omówione następujące tematy:


 Poznanie podstaw animacji.
 Korzystanie z animacji.
 Korzystanie z AnimatedBuilder.
 Korzystanie z AnimatedWidget.

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.

Oprócz przechowywania informacji o animacji klasa ta udostępnia wywołania zwrotne, dzięki


czemu inne klasy mogą wiedzieć, jak działają animacje, a także znać ich aktualny stan i wartość.

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Klasa Animation<T> jest odpowiedzialna tylko za przechowywanie i udostępnianie tych war-


tości. Nie wie nic o wizualnym sprzężeniu zwrotnym, o tym, co jest rysowane na ekranie, ani
o tym, jak to narysować (czyli funkcje build()).

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 generuje sekwencję (niekoniecznie liniową) wartości między określonymi


wartościami minimalnymi i maksymalnymi. Proces ten jest również znany jako interpolacja,
która, jak wspomniano wcześniej, jest nie tylko liniowa — można ją zdefiniować jako funkcję
krokową lub krzywą. Flutter zapewnia wiele funkcji i udogodnień do obsługi animacji. Są one
następujące:
 AnimationController — wbrew temu, co sugeruje nazwa tej funkcji, nie jest ona
używana do sterowania obiektami animacji, ale pomaga w ich samodzielnym
sterowaniu — rozszerza klasę Animation i nadal jest animacją.
 CurvedAnimation — jest to animacja, która stosuje Curve do innej animacji.
 Tween — pomaga stworzyć liniową interpolację między wartością początkową
i końcową.

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.

Sprawdźmy konstruktor AnimationController i przeanalizujmy jego główne właściwości:

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
})

Jak widać, niektóre właściwości są oczywiste, ale przyjrzyjmy się im:


 value — jest to początkowa wartość animacji i jeśli nie zostanie określona,
domyślnie jest to lowerBound.
 duration — czas trwania animacji.
 debugLabel — to jest ciąg znaków pomocny podczas debugowania. Identyfikuje
kontroler w danych wyjściowych debugowania.
 lowerBound — nie może być null; jest to najmniejsza wartość animacji, przy której
uznaje się ją za odrzuconą, zwykle jest to wartość początkowa podczas uruchamiania.
 upperBound — również nie może to być null; jest to największa wartość animacji,
przy której uznaje się ją za zakończoną, zazwyczaj jest to wartość końcowa
podczas działania.
 animationBehavior — konfiguruje sposób, w jaki AnimationController zachowuje
się, gdy animacje są wyłączone. Jeśli jest to AnimationBehavior.normal, czas
trwania animacji zostanie skrócony, a jeśli jest to AnimationBehavior.preserve,
AnimationController utrzyma swoje zachowanie.
 vsync — jest to instancja TickerProvider, której kontroler będzie używać do
uzyskiwania sygnału za każdym razem, gdy zostanie wyzwolona ramka.

Sprawdź wszystkie dostępne metody uruchamiania animacji z klasą AnimationController:


https://api.flutter.dev/flutter/animation/AnimationController-class.html.

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

animacji do przodu, następnie do tyłu, wykorzystując odpowiednio jej właściwości curve


i reverseCurve.

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.

Zanim użyjemy niestandardowej implementacji Tween, wróćmy do naszych transformacji wi-


dżetów z ostatniego rozdziału, stosując transformację w animowany sposób. Uzyskamy ten
sam efekt końcowy, ale w płynny i lepszy sposób.

Animacja obrotu
Za pomocą klasy AnimationController możemy uzyskać bardziej płynny obrót przycisku —
zobacz rysunek na następnej stronie.

Aby uzyskać pełny kod, sprawdź przykład hands_on_animations w serwisie GitHub.

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();
}
},
),
);
}

Jak widać, należy zwrócić uwagę na dwie ważne rzeczy:


 Wartość kąta jest teraz definiowana za pomocą właściwości _angle zamiast
bezpośredniego przypisywania do literału.
 We właściwości onPressed sprawdzamy, czy _animation jest zakończony, a jeśli
tak, to powtarzamy od początku.

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

W tej klasie należy zwrócić uwagę na kilka rzeczy:


 Mamy obiekt StatefulWidget o nazwie RotationAnimations, wykorzystujący klasę
SingleTickerProviderStateMixin, którą widzieliśmy wcześniej — zapewnia
wymagany obiekt Ticker do uruchomienia naszego kontrolera.
 Poza tym mamy właściwość _angle, która służy do definiowania aktualnego kąta
naszego przycisku. Możemy użyć metody setState(), aby spowodować, że przycisk
zostanie zbudowany z nowym kątem.
 I na koniec dostajemy nasz obiekt _animation, który trzyma animację i pozwala nam
nią zarządzać.

Funkcja initState() z naszej klasy State jest idealnym miejscem do ustawienia i uruchomienia
animacji:
@override
void initState() {
super.initState();

_animation = createRotationAnimation();
_animation.forward();
}

Jak widać, definiujemy naszą animację za pomocą metody createRotationAnimation() i urucha-


miamy ją, wywołując jej funkcję forward(). Zobaczmy teraz, jak zdefiniowano animację:
createRotationAnimation() {
var animation = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);

animation.addListener(() {
setState(() {
_angle = (animation.value * 360.0) * _toRadians;
});
});

return animation;
}

Tworzenie animacji możemy podzielić na dwie ważne części:


 Definicja animacji, w której ustawiamy właściwość debugLabel na potrzeby
debugowania; vsync, aby mogła mieć Ticker i wiedziała, kiedy utworzyć nową
wartość animacji; i wreszcie czas trwania animacji — duration.
 Drugim ważnym krokiem jest nasłuchiwanie zmian wartości animacji. Tutaj,
ilekroć animacja ma nową wartość, otrzymujemy i mnożymy ją przez 360 stopni,
tak aby uzyskać proporcjonalną wartość obrotu.

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),
);

var animation = controller.drive(CurveTween(


curve: Curves.bounceIn,
));

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.

Zobaczmy teraz, jak tworzyć animacje skalowania.

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),
);

var animation = controller.drive(Tween<Offset>(


begin: Offset.zero,
end: Offset(70, 200),
));

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.

Wiele transformacji i niestandardowy Tween


Jak zapewne pamiętasz, możemy tworzyć wiele transformacji za pomocą klasy Matrix4.

W przypadku animacji sytuacja wygląda podobnie; możemy łączyć animacje, uruchamiać


jedną po drugiej i odtwarzać je — wszystko w naszych rękach. Aby utworzyć animację, możemy
po prostu utworzyć wiele wartości transformacji w oparciu o pojedynczy obiekt Animation.

404

d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje

W ten sposób możemy osiągnąć coś takiego:

Możemy wykonać następujące kroki:


1. W naszej klasie możemy zdefiniować wiele wartości, na przykład:
class _ComposedAnimationsState extends State<ComposedAnimations>
with SingleTickerProviderStateMixin {
Offset _offset = Offset.zero;
double _scale = 1.0;
double _angle = 0.0;
...
}
2. Za każdym razem, gdy zmienia się wartość animacji, możemy na jej podstawie
obliczyć nasze wartości:
animation.addListener(() {
setState(() {
_offset = Offset(animation.value * 70, animation.value * 200);
_scale = 1.0 + animation.value;
_angle = 360 * animation.value;
});
});
}
3. Następnie stosujemy wartości, które obliczyliśmy na każdym etapie wykonywania
animacji w naszej metodzie build():
_composedAnimationButton() {
return Transform.translate(
offset: _offset,
child: Transform.rotate(
angle: _angle * _toRadians,
child: Transform.scale(
scale: _scale,
child: RaisedButton(
child: Text("multiple animation"),
onPressed: () {

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;

// none zwraca początkowy stan transformacji


// z domyślnym skalowaniem, bez obrotu lub translacji
static ButtonTransformation get none => ButtonTransformation(
scale: 1.0,
angle: 0.0,
offset: Offset.zero,
);
}

Następnie rozszerzamy klasę Tween o nasz zdefiniowany typ:


class CustomTween extends Tween<ButtonTransformation> {

CustomTween({ButtonTransformation begin, ButtonTransformation end} ):


super(begin: begin, end: end,);

@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,
);

ButtonTransformation operator *(double t) => ButtonTransformation(


scale: scale * t,
angle: angle * t,
offset: offset * t,
);
}

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),
);

var animation = controller.drive(CustomTween(


begin: ButtonTransformation.none, // initial state of the animation
end: ButtonTransformation(

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.

Możemy zbudować wiele jednoczesnych animacji przy użyciu oddzielnych obiektów


Animation, zazwyczaj ustawiając ten sam AnimationController, co ich element nad-
rzędny. Gwarantujemy, że będą zsynchronizowane, ponieważ będziemy używać tego
samego obiektu Ticker.

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.

Powrót do naszej animacji


Aby rozbić nasz kod, zmodyfikować naszą animację i uczynić ją łatwiejszą w utrzymaniu, zaczy-
namy oddzielać to, czego potrzebujemy dla każdego zadania. Zwykle potrzebne są trzy rzeczy:
 animation — tutaj nie musimy niczego zmieniać. Nasz kontroler AnimationController
nadal będzie taki sam.
 Dodaj widżet AnimatedBuilder do naszej metody build() — będziemy wyodrębniać
znaczną część kodu związanego z animacją przycisku, aby zrobiło się
przejrzyściej.
 Widżet child — w naszym przypadku jest to po prostu RaisedButton, który zmienia się
w zależności od postępu animacji:
class _AnimationBuilderAnimationsState extends
State<AnimationBuilderAnimations>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<ButtonTransformation> _animation;

@override
void initState() {
super.initState();

_animation = createAnimation();
_controller.forward();
}
...
}

409

d0765ad53fb82babda2278a311da7afb
d
Flutter i Dart 2 dla początkujących

Jak widać, mamy tutaj kilka zmian:


 Nie mamy już pola ButtonTransformation, ponieważ będziemy nim zarządzać
w naszym nowym widżecie.
 Oddzielamy AnimationController od naszego obiektu Animation. Jest to bardzo
powszechne i lepsze, niż wykonywanie wszędzie rzutowania typów.
 I wreszcie w metodzie createAnimation() jest tylko drobna zmiana.
createAnimation() {
_controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);

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.

Następnie modyfikujemy metodę build(), aby używała nowego widżetu:


@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
child: Center(
child: ButtonTransition(
animation: _animation,
child: RaisedButton(
child: Text("AnimatedBuilder animation"),
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else if (_controller.status == AnimationStatus.dismissed) {
_controller.forward();
}
},
),
),
),
);
}

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,
),
);
}
}

Zasadniczo ButtonTransition obsługuje modyfikację swojego elementu podrzędnego (Raised


Button) bez dotykania go. Ważne kroki dla metody build() wyglądają następująco:
1. Najpierw dodajemy widżet AnimatedBuilder do drzewa widżetów.
2. Przekazana do niej klasa child zostanie nam dostarczona w metodzie builder
z uwzględnieniem optymalizacji. Całe poddrzewo child nie musi być
odbudowywane za każdym razem, gdy animacja jest aktualizowana.
Przytrzymanie go i ponowne umieszczenie pomaga frameworkowi odbudować
tylko potrzebne widżety w metodzie builder.

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

3. Metoda builder tworzy drzewo poniżej z wymaganymi zmianami animacji. Zwróć


uwagę, że nie musimy się martwić o wykrywanie zmian animacji; metoda builder
będzie wywoływana za każdym razem, gdy animacja zostanie zaktualizowana.

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.

Przepisanie animacji za pomocą AnimatedWidget


W naszym przypadku korzystanie z AnimatedWidget wymagałoby po prostu zmodyfikowania
widżetu ButtonTransition. Jednak, jak pamiętasz, kryje się za tym koncepcja. Aby to zrobić,
musimy rozszerzyć klasę AnimatedWidget i przekształcić nasz widżet w animowany przycisk
w jego metodzie build().

412

d0765ad53fb82babda2278a311da7afb
d
Rozdział 15. • Animacje

Zaczynamy od zdefiniowania naszego nowego widżetu opartego na AnimatedWidget:


class AnimatedButton extends AnimatedWidget {
final RaisedButton button;

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,
);
}
}

Zdefiniowaliśmy nasz widżet AnimatedButton pochodzący z klasy AnimatedWidget. Możemy


tu podkreślić dwie podstawowe kwestie:
 Jedyną rzeczą, którą musimy przekazać do superklasy AnimatedWidget, jest obiekt
animacji, dzięki czemu może ona słuchać aktualizacji animacji i odbudowywać się
we właściwym czasie.
 W metodzie build() uzyskujemy dostęp do animacji z właściwości Listenable
klasy nadrzędnej i używamy wartości animacji tak jak poprzednio.

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.

Omówiliśmy ważne klasy, które zapewnia platforma, w tym AnimationController, CurvedAnimation


i Tween. Zrewidowaliśmy również nasze przykłady Transformation i dodaliśmy do nich animacje,

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.

To wszystko. W tej książce próbowałem omówić kilka podstawowych, ale fundamentalnych


koncepcji tej niesamowitej platformy. Mam nadzieję, że coś Ci się spodobało i czegoś się
nauczyłeś: to mnie motywuje do dalszej pracy.

414

d0765ad53fb82babda2278a311da7afb
d
d0765ad53fb82babda2278a311da7afb
d
d0765ad53fb82babda2278a311da7afb
d

You might also like