1. Wprowadzenie
Flutter to zestaw narzędzi UI od Google do tworzenia aplikacji mobilnych, internetowych i na komputery z pojedynczej bazy kodu. W tym module utworzysz tę aplikację na platformę Flutter:
Aplikacja generuje ciekawe nazwy, takie jak „newstay”, „lightstream”, „mainbrake” czy „graypine”. Użytkownik może poprosić o następną nazwę, dodać bieżącą do ulubionych i przejrzeć listę ulubionych nazw na osobnej stronie. Aplikacja dostosowuje się do różnych rozmiarów ekranu.
Czego się nauczysz
- Podstawy działania Fluttera
- Tworzenie układów w Flutterze
- Łączenie interakcji użytkowników (np. naciśnięć przycisków) z zachowaniem aplikacji
- Utrzymywanie porządku w kodzie Fluttera
- Dostosowywanie aplikacji do różnych ekranów
- Zachowanie spójnego wyglądu i sposobu działania aplikacji
Zaczniesz od podstawowego szkieletu, dzięki czemu od razu przejdziesz do interesujących Cię części.
Filip przeprowadzi Cię przez cały codelab.
Aby rozpocząć moduł, kliknij Dalej.
2. Konfigurowanie środowiska Flutter
Edytujący
Aby ułatwić Ci wykonanie tych ćwiczeń z programowania, zakładamy, że jako środowiska programistycznego będziesz używać Visual Studio Code (VS Code). Jest bezpłatna i działa na wszystkich głównych platformach.
Możesz oczywiście używać dowolnego edytora: Android Studio, innych środowisk IntelliJ IDE, Emacsa, Vima lub Notepad++. Wszystkie działają z Flutterem.
W tym ćwiczeniu z programowania zalecamy używanie VS Code, ponieważ instrukcje zawierają domyślnie skróty specyficzne dla tego edytora. Łatwiej jest powiedzieć „kliknij tutaj” lub „naciśnij ten klawisz” niż „wykonaj w edytorze odpowiednie działanie, aby zrobić X”.
Wybierz cel programowania
Flutter to zestaw narzędzi wieloplatformowych. Aplikacja może działać w tych systemach operacyjnych:
- iOS
- Android
- Windows
- macOS
- Linux
- internet
Zwykle jednak wybiera się jeden system operacyjny, pod kątem którego będzie się głównie tworzyć aplikacje. Jest to „platforma docelowa” – system operacyjny, w którym działa Twoja aplikacja podczas programowania.
Załóżmy na przykład, że używasz laptopa z Windows do tworzenia aplikacji Flutter. Jeśli jako platformę docelową wybierzesz Androida, zwykle podłączasz urządzenie z Androidem do laptopa z Windows za pomocą kabla USB, a aplikacja w trakcie tworzenia jest uruchamiana na tym podłączonym urządzeniu z Androidem. Możesz też wybrać Windows jako platformę docelową, co oznacza, że rozwijana aplikacja będzie działać jako aplikacja na Windows obok edytora.
Może Cię kusić, aby wybrać sieć jako cel rozwoju. Wadą tego rozwiązania jest utrata jednej z najbardziej przydatnych funkcji programistycznych Fluttera: Stateful Hot Reload. Flutter nie może szybko przeładować aplikacji internetowych.
Wybierz teraz. Pamiętaj, że w każdej chwili możesz uruchomić aplikację w innych systemach operacyjnych. Chodzi tylko o to, że jasny cel rozwoju ułatwia podjęcie kolejnego kroku.
Instalowanie Flutera
Najnowsze instrukcje instalacji pakietu SDK Flutter znajdziesz zawsze na stronie docs.flutter.dev.
Instrukcje na stronie Fluttera obejmują nie tylko instalację samego pakietu SDK, ale także narzędzia związane z platformą docelową i wtyczki do edytora. Pamiętaj, że w tym ćwiczeniu musisz zainstalować tylko te elementy:
- Flutter SDK
- Visual Studio Code z wtyczką Fluttera
- Oprogramowanie wymagane przez wybrane środowisko docelowe (np. Visual Studio w przypadku systemu Windows lub Xcode w przypadku systemu macOS).
W następnej sekcji utworzysz pierwszy projekt Flutter.
Jeśli do tej pory napotkałeś(-aś) problemy, te pytania i odpowiedzi (z StackOverflow) mogą Ci pomóc w ich rozwiązaniu.
Najczęstsze pytania
- Jak znaleźć ścieżkę pakietu Flutter SDK?
- Co zrobić, gdy polecenie Flutter nie zostanie znalezione?
- Jak rozwiązać problem „Waiting for another flutter command to release the startup lock”?
- Jak poinformować Fluttera, gdzie jest zainstalowany pakiet SDK Androida?
- Jak poradzić sobie z błędem Javy podczas uruchamiania
flutter doctor --android-licenses
? - Jak rozwiązać problem z błędem „Nie znaleziono narzędzia
sdkmanager
w Androidzie”? - Jak rozwiązać problem z błędem „Brak komponentu
cmdline-tools
”? - Jak uruchomić CocoaPods na urządzeniu Apple Silicon (M1)?
- Jak wyłączyć automatyczne formatowanie podczas zapisywania w VS Code?
3. Utwórz projekt
Tworzenie pierwszego projektu Fluttera
Uruchom Visual Studio Code i otwórz paletę poleceń (za pomocą klawisza F1
, Ctrl+Shift+P
lub Shift+Cmd+P
). Zacznij wpisywać „flutter new”. Wybierz polecenie Flutter: New Project (Flutter: nowy projekt).
Następnie wybierz Application (Aplikacja) i folder, w którym chcesz utworzyć projekt. Może to być katalog domowy lub np. C:\src\
.
Na koniec nadaj projektowi nazwę. Na przykład namer_app
lub my_awesome_namer
.
Flutter utworzy teraz folder projektu, a VS Code go otworzy.
Teraz zastąp zawartość 3 plików podstawową strukturą aplikacji.
Kopiowanie i wklejanie aplikacji początkowej
W panelu po lewej stronie VS Code upewnij się, że wybrano Eksplorator, i otwórz plik pubspec.yaml
.
Zastąp zawartość tego pliku tymi wierszami:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
Plik pubspec.yaml
zawiera podstawowe informacje o aplikacji, takie jak jej bieżąca wersja, zależności i zasoby, które będą z nią dostarczane.
Następnie otwórz w projekcie inny plik konfiguracji, analysis_options.yaml
.
Zastąp jego zawartość tym kodem:
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
Ten plik określa, jak rygorystycznie Flutter ma analizować Twój kod. Ponieważ to Twoje pierwsze kroki we Flutterze, prosisz analizator, aby nie był zbyt surowy. Zawsze możesz później dostosować to ustawienie. W miarę zbliżania się do opublikowania rzeczywistej aplikacji produkcyjnej prawie na pewno zechcesz ustawić analizator na bardziej rygorystyczny tryb.
Na koniec otwórz plik main.dart
w katalogu lib/
.
Zastąp zawartość tego pliku tymi wierszami:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
Te 50 wierszy kodu to cała aplikacja.
W następnej sekcji uruchom aplikację w trybie debugowania i zacznij tworzyć kod.
4. Dodawanie przycisku
Ten krok dodaje przycisk Dalej, który umożliwia wygenerowanie nowej pary słów.
Uruchamianie aplikacji
Najpierw otwórz lib/main.dart
i upewnij się, że wybrane jest urządzenie docelowe. W prawym dolnym rogu VS Code znajdziesz przycisk, który pokazuje bieżące urządzenie docelowe. Kliknij, aby to zmienić.
Gdy lib/main.dart
jest otwarty, znajdź przycisk „odtwarzaj” w prawym górnym rogu okna VS Code i kliknij go.
Po około minucie aplikacja uruchomi się w trybie debugowania. Na razie nie wygląda to zbyt imponująco:
Pierwsze gorące przeładowanie
U dołu pliku lib/main.dart
dodaj coś do ciągu znaków w pierwszym obiekcie Text
i zapisz plik (za pomocą Ctrl+S
lub Cmd+S
). Przykład:
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
Zwróć uwagę, że aplikacja od razu się zmienia, ale losowe słowo pozostaje takie samo. To słynne stanowe gorące przeładowanie Fluttera w akcji. Szybkie ponowne wczytywanie jest wywoływane po zapisaniu zmian w pliku źródłowym.
Najczęstsze pytania
- Co zrobić, jeśli w VSCode nie działa szybkie przeładowanie?
- Czy muszę nacisnąć „r”, aby w VSCode włączyć szybkie przeładowanie?
- Czy szybkie przeładowanie działa w internecie?
- Jak usunąć baner „Debug”?
Dodawanie przycisku
Następnie dodaj przycisk u dołu Column
, bezpośrednio pod drugim wystąpieniem Text
.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
Gdy zapiszesz zmianę, aplikacja ponownie się zaktualizuje: pojawi się przycisk, a po jego kliknięciu w konsoli debugowania w VS Code pojawi się komunikat button pressed!.
Szybkie szkolenie z Fluttera w 5 minut
Oglądanie konsoli debugowania może być ciekawe, ale przycisk powinien robić coś bardziej przydatnego. Zanim jednak to zrobisz, przyjrzyj się bliżej kodowi w lib/main.dart
, aby zrozumieć, jak działa.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
U góry pliku znajdziesz funkcję main()
. W obecnej postaci informuje tylko Fluttera, aby uruchomił aplikację zdefiniowaną w MyApp
.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
Klasa MyApp
rozszerza klasę StatelessWidget
. Widżety to elementy, z których budujesz każdą aplikację Flutter. Jak widzisz, nawet sama aplikacja jest widżetem.
Kod w MyApp
konfiguruje całą aplikację. Tworzy stan aplikacji (więcej informacji znajdziesz poniżej), nadaje jej nazwę, określa motyw wizualny i ustawia widżet „home” – punkt początkowy aplikacji.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
Następnie klasa MyAppState
definiuje stan aplikacji. To Twoje pierwsze kroki we Flutterze, więc te ćwiczenia z programowania będą proste i skupione na konkretnym zagadnieniu. W Flutterze jest wiele skutecznych sposobów zarządzania stanem aplikacji. Jednym z najłatwiejszych do wyjaśnienia jest ChangeNotifier
, czyli podejście zastosowane w tej aplikacji.
MyAppState
określa dane, których aplikacja potrzebuje do działania. Obecnie zawiera ona tylko jedną zmienną z bieżącą losową parą słów. Dodasz do tego później.- Klasa stanu rozszerza klasę
ChangeNotifier
, co oznacza, że może powiadamiać inne klasy o swoich zmianach. Jeśli na przykład zmieni się bieżąca para słów, niektóre widżety w aplikacji muszą o tym wiedzieć. - Stan jest tworzony i udostępniany całej aplikacji za pomocą
ChangeNotifierProvider
(patrz kod powyżej wMyApp
). Dzięki temu każdy widżet w aplikacji może uzyskać dostęp do stanu.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
Ostatni z nich to MyHomePage
, czyli widżet, który został już zmodyfikowany. Każda ponumerowana linia poniżej odpowiada komentarzowi z numerem linii w powyższym kodzie:
- Każdy widżet definiuje metodę
build()
, która jest automatycznie wywoływana za każdym razem, gdy zmieniają się okoliczności widżetu, dzięki czemu jest on zawsze aktualny. MyHomePage
śledzi zmiany w bieżącym stanie aplikacji za pomocą metodywatch
.- Każda metoda
build
musi zwracać widżet lub (zwykle) zagnieżdżone drzewo widżetów. W tym przypadku widżetem najwyższego poziomu jestScaffold
. W tym module nie będziesz pracować zScaffold
, ale jest to przydatny widżet, który występuje w większości rzeczywistych aplikacji Fluttera. Column
to jeden z najbardziej podstawowych widżetów układu w Flutterze. Przyjmuje dowolną liczbę elementów podrzędnych i umieszcza je w kolumnie od góry do dołu. Domyślnie kolumna umieszcza elementy podrzędne u góry. Wkrótce zmienisz to ustawienie, aby kolumna była wyśrodkowana.- W pierwszym kroku zmieniono widżet
Text
. - Ten drugi widżet
Text
przyjmujeappState
i uzyskuje dostęp do jedynego elementu tej klasy, czylicurrent
(który jestWordPair
).WordPair
udostępnia kilka przydatnych funkcji pobierających, takich jakasPascalCase
czyasSnakeCase
. Używamy tutajasLowerCase
, ale możesz to teraz zmienić, jeśli wolisz jedną z alternatyw. - Zwróć uwagę, że w kodzie Fluttera często używane są przecinki na końcu wiersza. Ten przecinek nie jest potrzebny, ponieważ
children
jest ostatnim (a także jedynym) elementem tej listy parametrówColumn
. Zazwyczaj jednak warto używać przecinków na końcu, ponieważ ułatwiają dodawanie kolejnych elementów i są wskazówką dla automatycznego formatowania kodu w Dart, aby wstawić tam znak nowego wiersza. Więcej informacji znajdziesz w sekcji Formatowanie kodu.
Następnie połącz przycisk ze stanem.
Twoje pierwsze zachowanie
Przewiń do sekcji MyAppState
i dodaj formę płatności getNext
.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
Nowa metoda getNext()
przypisuje do current
nową losową wartość WordPair
. Wywołuje też funkcję notifyListeners()
(metodę ChangeNotifier)
, która zapewnia, że każda osoba oglądająca MyAppState
otrzyma powiadomienie).
Pozostało tylko wywołać metodę getNext
z funkcji zwrotnej przycisku.
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
Zapisz i wypróbuj aplikację. Przy każdym naciśnięciu przycisku Dalej powinna generować nową losową parę słów.
W następnej sekcji upiększysz interfejs.
5. Upiększanie aplikacji
Tak obecnie wygląda aplikacja.
Nie za dobrze. Główny element aplikacji, czyli losowo wygenerowana para słów, powinien być bardziej widoczny. W końcu to główny powód, dla którego użytkownicy korzystają z tej aplikacji. Poza tym zawartość aplikacji jest dziwnie wyśrodkowana, a cała aplikacja jest nudna i czarno-biała.
W tej sekcji zajmiemy się tymi problemami, pracując nad projektem aplikacji. Ostateczny cel tej sekcji to mniej więcej:
Wyodrębnianie widżetu
Wiersz odpowiedzialny za wyświetlanie bieżącej pary słów wygląda teraz tak: Text(appState.current.asLowerCase)
. Aby zmienić go w coś bardziej złożonego, warto wyodrębnić ten wiersz do osobnego widżetu. Oddzielne widżety dla poszczególnych logicznych części interfejsu to ważny sposób na zarządzanie złożonością w Flutterze.
Flutter udostępnia narzędzie do refaktoryzacji, które umożliwia wyodrębnianie widżetów. Zanim go użyjesz, upewnij się, że wyodrębniana linia kodu ma dostęp tylko do potrzebnych elementów. Obecnie wiersz ma dostęp do appState
, ale w rzeczywistości potrzebuje tylko informacji o bieżącej parze słów.
Z tego powodu zmień widżet MyHomePage
w ten sposób:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Nieźle. Widżet Text
nie odnosi się już do całego appState
.
Teraz otwórz menu Refactor (Refaktoryzacja). W VS Code możesz to zrobić na 2 sposoby:
- Kliknij prawym przyciskiem myszy fragment kodu, który chcesz przekształcić (w tym przypadku
Text
), i z menu wybierz Przekształć….
LUB
- Przesuń kursor na kod elementu, który chcesz refaktoryzować (w tym przypadku
Text
), i naciśnijCtrl+.
(Windows/Linux) lubCmd+.
(Mac).
W menu Refactor (Refaktoryzacja) wybierz Extract Widget (Wyodrębnij widżet). Przypisz nazwę, np. BigCard, i kliknij Enter
.
Spowoduje to automatyczne utworzenie na końcu bieżącego pliku nowej klasy BigCard
. Klasa wygląda mniej więcej tak:
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
Zwróć uwagę, że aplikacja działa nawet podczas refaktoryzacji.
Dodawanie karty
Teraz przekształcimy ten nowy widżet w wyrazisty element interfejsu, o którym wspominaliśmy na początku tej sekcji.
Znajdź klasę BigCard
i metodę build()
. Jak poprzednio, wywołaj menu Refactor na widżecie Text
. Tym razem jednak nie wyodrębnisz widżetu.
Zamiast tego wybierz Wrap with Padding (Zawijaj z wypełnieniem). Spowoduje to utworzenie nowego widżetu nadrzędnego wokół widżetu Text
o nazwie Padding
. Po zapisaniu zobaczysz, że losowe słowo ma już więcej miejsca.
Zwiększ odstęp od domyślnej wartości 8.0
. Możesz na przykład użyć wartości 20
, aby uzyskać większe dopełnienie.
Następnie przejdź o 1 poziom wyżej. Umieść kursor na widżecie Padding
, otwórz menu Refactor i kliknij Wrap with widget... (Owiń widżetem...).
Dzięki temu możesz określić widżet nadrzędny. Wpisz „Karta” i naciśnij Enter.
Spowoduje to opakowanie widżetu Padding
, a tym samym również widżetu Text
, widżetem Card
.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
Aplikacja będzie teraz wyglądać mniej więcej tak:
Motyw i styl
Aby karta była bardziej widoczna, pomaluj ją intensywniejszym kolorem. Warto zachować spójny schemat kolorów, dlatego użyj ikony Theme
w aplikacji, aby wybrać kolor.
Wprowadź te zmiany w metodzie BigCard
build()
.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
Te 2 nowe wiersze wykonują wiele zadań:
- Najpierw kod wysyła żądanie dotyczące bieżącego motywu aplikacji za pomocą funkcji
Theme.of(context)
. - Następnie kod określa kolor karty jako taki sam jak wartość właściwości
colorScheme
motywu. Schemat kolorów zawiera wiele kolorów, aprimary
jest najbardziej widocznym kolorem aplikacji.
Karta jest teraz pomalowana kolorem podstawowym aplikacji:
Możesz zmienić ten kolor i schemat kolorów całej aplikacji, przewijając w górę do sekcji MyApp
i zmieniając tam kolor podstawowy ColorScheme
.
Zwróć uwagę, jak kolor płynnie się animuje. Jest to tzw. animacja domyślna. Wiele widżetów Fluttera płynnie interpoluje wartości, dzięki czemu interfejs nie „przeskakuje” między stanami.
Kolor zmieni się też na przycisku pod kartą. Na tym polega zaleta używania w całej aplikacji wartości Theme
zamiast wartości zakodowanych na stałe.
TextTheme
Karta nadal ma problem: tekst jest za mały, a jego kolor utrudnia odczytanie. Aby to naprawić, wprowadź w metodzie BigCard
build()
te zmiany:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
Co się za tym kryje:
- Korzystając z
theme.textTheme,
, uzyskujesz dostęp do motywu czcionki aplikacji. Ta klasa obejmuje elementy takie jakbodyMedium
(standardowy tekst o średniej wielkości),caption
(podpisy obrazów) lubheadlineLarge
(duże nagłówki). - Właściwość
displayMedium
to duży styl przeznaczony do wyświetlania tekstu. Słowo wyświetlanie jest tu używane w sensie typograficznym, np. w określeniu czcionka wyświetlana. DokumentacjadisplayMedium
mówi, że „style wyświetlania są zarezerwowane dla krótkich, ważnych tekstów” – dokładnie tak, jak w naszym przypadku. - Właściwość
displayMedium
motywu teoretycznie może mieć wartośćnull
. Dart, czyli język programowania, w którym piszesz tę aplikację, jest bezpieczny pod względem wartości null, więc nie pozwoli Ci wywoływać metod obiektów, które mogą mieć wartośćnull
. W tym przypadku możesz jednak użyć operatora!
(„operator wykrzyknika”), aby zapewnić Dartowi, że wiesz, co robisz. (displayMedium
w tym przypadku nie jest wartością null. (Wyjaśnienie, dlaczego tak jest, wykracza poza zakres tego ćwiczenia). - Wywołanie funkcji
copyWith()
na elemenciedisplayMedium
zwraca kopię stylu tekstu ze zdefiniowanymi przez Ciebie zmianami. W tym przypadku zmieniasz tylko kolor tekstu. - Aby uzyskać nowy kolor, ponownie otwórz motyw aplikacji. Właściwość
onPrimary
schematu kolorów określa kolor, który dobrze pasuje do użycia na podstawowym kolorze aplikacji.
Aplikacja powinna teraz wyglądać mniej więcej tak:
Jeśli chcesz, możesz dalej modyfikować kartę. Oto kilka pomysłów:
copyWith()
pozwala zmienić wiele elementów stylu tekstu, nie tylko kolor. Aby wyświetlić pełną listę właściwości, które możesz zmienić, umieść kursor w dowolnym miejscu w nawiasachcopyWith()
i naciśnijCtrl+Shift+Space
(Windows/Linux) lubCmd+Shift+Space
(Mac).- Podobnie możesz zmienić więcej ustawień widżetu
Card
. Możesz na przykład powiększyć cień karty, zwiększając wartość parametruelevation
. - Eksperymentuj z kolorami. Oprócz
theme.colorScheme.primary
są też.secondary
,.surface
i wiele innych. Wszystkie te kolory mają swoje odpowiedniki wonPrimary
.
Ulepszanie ułatwień dostępu
Flutter domyślnie udostępnia aplikacje. Na przykład każda aplikacja Flutter prawidłowo udostępnia czytnikom ekranu, takim jak TalkBack i VoiceOver, wszystkie teksty i elementy interaktywne.
Czasami jednak trzeba wykonać pewne czynności. W przypadku tej aplikacji czytnik ekranu może mieć problemy z wymową niektórych wygenerowanych par słów. Ludzie nie mają problemu z rozpoznaniem dwóch słów w wyrazie cheaphead, ale czytnik ekranu może wymówić ph w środku tego słowa jako f.
Rozwiązaniem jest zastąpienie pair.asLowerCase
ciągiem "${pair.first} ${pair.second}"
. Ta druga metoda wykorzystuje interpolację ciągów znaków do utworzenia ciągu znaków (np. "cheap head"
) z 2 słów zawartych w pair
. Używanie dwóch oddzielnych słów zamiast słowa złożonego sprawia, że czytniki ekranu prawidłowo je rozpoznają, co ułatwia korzystanie z witryny osobom z wadami wzroku.
Możesz jednak zachować prostotę wizualną pair.asLowerCase
. Użyj właściwości Text
elementu semanticsLabel
, aby zastąpić treść wizualną widżetu tekstowego treścią semantyczną, która jest bardziej odpowiednia dla czytników ekranu:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
Teraz czytniki ekranu prawidłowo odczytują każdą wygenerowaną parę słów, ale interfejs użytkownika pozostaje bez zmian. Wypróbuj tę funkcję, używając czytnika ekranu na urządzeniu.
Wyśrodkuj interfejs
Teraz, gdy losowa para słów jest prezentowana z odpowiednią oprawą wizualną, czas umieścić ją na środku okna lub ekranu aplikacji.
Pamiętaj, że BigCard
jest częścią Column
. Domyślnie kolumny grupują elementy podrzędne u góry, ale możemy to zmienić. Przejdź do metody MyHomePage
build()
i wprowadź tę zmianę:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Wyśrodkowuje elementy podrzędne w Column
wzdłuż głównej (pionowej) osi.
Elementy podrzędne są już wyśrodkowane wzdłuż osi poprzecznej kolumny (czyli są już wyśrodkowane w poziomie). Ale Column
nie jest wyśrodkowany w Scaffold
. Możemy to sprawdzić za pomocą inspektora widżetów.
Sam inspektor widżetów wykracza poza zakres tego laboratorium, ale możesz zauważyć, że gdy Column
jest podświetlony, nie zajmuje całej szerokości aplikacji. Zajmuje tylko tyle miejsca w poziomie, ile potrzebują jego elementy podrzędne.
Możesz po prostu wyśrodkować samą kolumnę. Umieść kursor na ikonie Column
, wywołaj menu Refactor (za pomocą Ctrl+.
lub Cmd+.
) i wybierz Wrap with Center (Owiń elementem Center).
Aplikacja powinna teraz wyglądać mniej więcej tak:
Jeśli chcesz, możesz jeszcze trochę dopracować ten efekt.
- Możesz usunąć widżet
Text
nadBigCard
. Można argumentować, że tekst opisowy („Losowy GENIALNY pomysł:”) nie jest już potrzebny, ponieważ interfejs jest zrozumiały nawet bez niego. W ten sposób jest czyściej. - Możesz też dodać widżet
SizedBox(height: 10)
międzyBigCard
aElevatedButton
. W ten sposób odległość między tymi dwoma widżetami będzie nieco większa. WidżetSizedBox
zajmuje tylko miejsce i sam w sobie niczego nie renderuje. Jest często używany do tworzenia wizualnych „luk”.
Po wprowadzeniu opcjonalnych zmian plik MyHomePage
będzie zawierać ten kod:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
Aplikacja wygląda tak:
W następnej sekcji dodasz możliwość oznaczania wygenerowanych słów jako ulubionych (lub „polubienia”).
6. Dodawanie funkcji
Aplikacja działa i czasami podaje nawet ciekawe pary słów. Gdy jednak użytkownik kliknie Dalej, każda para słów znika na zawsze. Lepszym rozwiązaniem byłoby „zapamiętywanie” najlepszych sugestii, np. za pomocą przycisku „Lubię to”.
Dodawanie logiki biznesowej
Przewiń do sekcji MyAppState
i dodaj ten kod:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
Sprawdź zmiany:
- Do usługi
MyAppState
dodano nową usługę o nazwiefavorites
. Ta właściwość jest inicjowana pustą listą:[]
. - Określono też, że lista może zawierać tylko pary słów:
<WordPair>[]
, używając typów ogólnych. Dzięki temu aplikacja jest bardziej niezawodna – Dart odmawia nawet uruchomienia aplikacji, jeśli spróbujesz dodać do niej coś innego niżWordPair
. Dzięki temu możesz używać listyfavorites
, mając pewność, że nie ma na niej niechcianych obiektów (np.null
).
- Dodano też nową metodę
toggleFavorite()
, która usuwa bieżącą parę słów z listy ulubionych (jeśli już się na niej znajduje) lub ją dodaje (jeśli jeszcze jej tam nie ma). W obu przypadkach kod wywołuje później funkcjęnotifyListeners();
.
Dodawanie przycisku
Po uporaniu się z „logiką biznesową” czas znowu popracować nad interfejsem użytkownika. Umieszczenie przycisku „Lubię to” po lewej stronie przycisku „Dalej” wymaga Row
. Widżet Row
to poziomy odpowiednik widżetu Column
, który był widoczny wcześniej.
Najpierw umieść istniejący przycisk w tagu Row
. Przejdź do metody MyHomePage
build()
, umieść kursor na ElevatedButton
, wywołaj menu Refaktoryzacja za pomocą Ctrl+.
lub Cmd+.
i wybierz Owiń wierszem.
Po zapisaniu zauważysz, że element Row
działa podobnie do elementu Column
– domyślnie umieszcza elementy podrzędne po lewej stronie. (Column
przeniesiono elementy podrzędne na górę). Aby to naprawić, możesz zastosować to samo podejście co wcześniej, ale z użyciem mainAxisAlignment
. W celach dydaktycznych (edukacyjnych) używaj jednak symbolu mainAxisSize
. To polecenie informuje Row
, aby nie zajmował całej dostępnej przestrzeni poziomej.
Wprowadź te zmiany:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Interfejs wróci do poprzedniego stanu.
Następnie dodaj przycisk Lubię to i połącz go z toggleFavorite()
. Spróbuj najpierw zrobić to samodzielnie, bez patrzenia na blok kodu poniżej.
Nie musisz robić tego dokładnie tak samo jak w przykładzie poniżej. Nie przejmuj się ikoną serca, chyba że chcesz podjąć poważne wyzwanie.
Nie przejmuj się też, jeśli coś Ci nie wyjdzie – to dopiero pierwsza godzina z Flutterem.
Oto jeden ze sposobów dodania drugiego przycisku do MyHomePage
. Tym razem użyj konstruktora ElevatedButton.icon()
, aby utworzyć przycisk z ikoną. U góry metody build
wybierz odpowiednią ikonę w zależności od tego, czy bieżąca para słów jest już w ulubionych. Zwróć też uwagę na ponowne użycie znaku SizedBox
, aby nieco odsunąć od siebie oba przyciski.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Aplikacja powinna wyglądać tak:
Niestety użytkownik nie może wyświetlić ulubionych. Czas dodać do aplikacji osobny ekran. Do zobaczenia w następnej sekcji!
7. Dodawanie kolumny nawigacji
Większość aplikacji nie mieści wszystkich informacji na jednym ekranie. Ta konkretna aplikacja prawdopodobnie mogłaby to zrobić, ale dla celów dydaktycznych utworzysz osobny ekran z ulubionymi użytkownika. Aby przełączać się między dwoma ekranami, musisz zaimplementować pierwszy StatefulWidget
.
Aby jak najszybciej przejść do sedna tego kroku, podziel MyHomePage
na 2 osobne widżety.
Zaznacz cały kod MyHomePage
, usuń go i zastąp tym kodem:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
Po zapisaniu zobaczysz, że wizualna strona interfejsu jest gotowa, ale nie działa. Kliknięcie ikony ♥︎ (serca) na pasku nawigacyjnym nie powoduje żadnej reakcji.
Sprawdź zmiany.
- Zwróć uwagę, że cała zawartość elementu
MyHomePage
została wyodrębniona do nowego widżetuGeneratorPage
. Jedyną częścią starego widżetuMyHomePage
, która nie została wyodrębniona, jestScaffold
. - Nowy
MyHomePage
zawieraRow
z dwójką dzieci. Pierwszy widżet toSafeArea
, a drugi to widżetExpanded
. - Element
SafeArea
zapewnia, że element podrzędny nie jest zasłonięty przez wycięcie w sprzęcie ani pasek stanu. W tej aplikacji widżet otacza elementNavigationRail
, aby zapobiec zasłanianiu przycisków nawigacyjnych przez np. pasek stanu na urządzeniu mobilnym. - Możesz zmienić wiersz
extended: false
wNavigationRail
natrue
. Etykiety pojawią się obok ikon. W dalszej części dowiesz się, jak to zrobić automatycznie, gdy aplikacja będzie miała wystarczająco dużo miejsca w poziomie. - Pasek nawigacyjny zawiera 2 miejsca docelowe (Strona główna i Ulubione) z odpowiednimi ikonami i etykietami. Określa też bieżący
selectedIndex
. Wybrany indeks 0 oznacza pierwsze miejsce docelowe, wybrany indeks 1 oznacza drugie miejsce docelowe itd. Na razie jest na stałe ustawiona na zero. - Określa też, co się stanie, gdy użytkownik wybierze jedno z miejsc docelowych za pomocą gestu
onDestinationSelected
. Obecnie aplikacja po prostu wyświetla żądaną wartość indeksu z symbolemprint()
. - Drugim elementem podrzędnym tagu
Row
jest widżetExpanded
. Rozszerzone widżety są niezwykle przydatne w wierszach i kolumnach – umożliwiają tworzenie układów, w których niektóre elementy zajmują tylko tyle miejsca, ile potrzebują (w tym przypadkuSafeArea
), a inne widżety powinny zajmować jak najwięcej pozostałego miejsca (w tym przypadkuExpanded
).Expanded
Widżety można traktować jako „zachłanne”. Jeśli chcesz lepiej poznać rolę tego widżetu, spróbuj umieścić widżetSafeArea
w innym widżecieExpanded
. Wynikowy układ będzie wyglądać mniej więcej tak:
- Dwa widżety
Expanded
dzielą między sobą całą dostępną przestrzeń poziomą, mimo że pasek nawigacyjny potrzebuje tylko niewielkiego fragmentu po lewej stronie. - W widżecie
Expanded
znajduje się kolorowy elementContainer
, a w kontenerze – elementGeneratorPage
.
Widżety bezstanowe i stanowe
Do tej pory MyAppState
zaspokajał wszystkie potrzeby Twojego stanu. Dlatego wszystkie napisane do tej pory widżety są bezstanowe. Nie zawierają żadnego własnego stanu, który można zmienić. Żaden z widgetów nie może samodzielnie wprowadzać zmian – musi to robić za pomocą MyAppState
.
Wkrótce się to zmieni.
Musisz mieć jakiś sposób na przechowywanie wartości selectedIndex
paska nawigacyjnego. Chcesz też mieć możliwość zmiany tej wartości w wywołaniu zwrotnym onDestinationSelected
.
Możesz dodać selectedIndex
jako kolejną właściwość MyAppState
. I to by się udało. Możesz sobie jednak wyobrazić, że stan aplikacji szybko przekroczyłby rozsądną wielkość, gdyby każdy widżet przechowywał w nim swoje wartości.
Niektóre stany są istotne tylko dla jednego widżetu, więc powinny być z nim powiązane.
Wpisz StatefulWidget
, czyli typ widżetu, który ma State
. Najpierw przekonwertuj MyHomePage
na widżet z zachowywaniem stanu.
Umieść kursor w pierwszym wierszu MyHomePage
(tym, który zaczyna się od class MyHomePage...
) i wywołaj menu Refactor, używając klawisza Ctrl+.
lub Cmd+.
. Następnie kliknij Convert to StatefulWidget (Przekształć w widget stanu).
IDE utworzy dla Ciebie nową klasę _MyHomePageState
. Ta klasa rozszerza klasę State
, więc może zarządzać własnymi wartościami. (Może się samo zmienić). Zwróć też uwagę, że metoda build
ze starego widżetu bezstanowego została przeniesiona do funkcji _MyHomePageState
(zamiast pozostać w widżecie). Została ona przeniesiona w całości – nic w metodzie build
się nie zmieniło. Teraz po prostu znajduje się w innym miejscu.
setState
Nowy widżet stanowy musi śledzić tylko jedną zmienną: selectedIndex
. Wprowadź w _MyHomePageState
te 3 zmiany:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
Sprawdź zmiany:
- Wprowadzasz nową zmienną
selectedIndex
i inicjujesz ją wartością0
. - Tej nowej zmiennej używasz w definicji
NavigationRail
zamiast zakodowanej na stałe wartości0
, która była tam do tej pory. - Gdy wywoływana jest funkcja zwrotna
onDestinationSelected
, zamiast po prostu wyświetlać nową wartość w konsoli, przypisujesz ją do zmiennejselectedIndex
w wywołaniu funkcjisetState()
. To wywołanie jest podobne do metodynotifyListeners()
używanej wcześniej – zapewnia aktualizację interfejsu.
Panel nawigacyjny reaguje teraz na interakcje użytkownika. Rozwinięty obszar po prawej stronie pozostaje bez zmian. Dzieje się tak, ponieważ kod nie używa selectedIndex
do określania, który ekran ma się wyświetlać.
Użyj selectedIndex
Umieść ten kod na początku metody _MyHomePageState
's build
, tuż przed return Scaffold
:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
Sprawdź ten fragment kodu:
- Kod deklaruje nową zmienną
page
typuWidget
. - Następnie instrukcja switch przypisuje ekran do zmiennej
page
zgodnie z bieżącą wartością w zmiennejselectedIndex
. - Ponieważ nie ma jeszcze
FavoritesPage
, użyjPlaceholder
– przydatnego widżetu, który rysuje przekreślony prostokąt w dowolnym miejscu, oznaczając tę część interfejsu jako niedokończoną.
- Zgodnie z zasadą szybkiego wykrywania błędów instrukcja switch zgłasza też błąd, jeśli wartość
selectedIndex
nie wynosi 0 ani 1. Pomaga to uniknąć błędów w przyszłości. Jeśli kiedykolwiek dodasz nowe miejsce docelowe do paska nawigacyjnego i zapomnisz zaktualizować ten kod, program ulegnie awarii w trakcie tworzenia (zamiast pozwolić Ci zgadywać, dlaczego coś nie działa, lub opublikować wadliwy kod w wersji produkcyjnej).
Skoro page
zawiera widżet, który chcesz wyświetlać po prawej stronie, możesz się domyślać, jaka inna zmiana jest potrzebna.
Oto _MyHomePageState
po wprowadzeniu tej ostatniej zmiany:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
Aplikacja przełącza się teraz między naszym GeneratorPage
a elementem zastępczym, który wkrótce stanie się stroną Ulubione.
Reagowanie
Następnie dostosuj pasek nawigacyjny do różnych rozmiarów ekranu. Oznacza to, że etykiety mają być wyświetlane automatycznie (za pomocą extended: true
), gdy jest na nie wystarczająco dużo miejsca.
Flutter udostępnia kilka widżetów, które pomagają tworzyć automatycznie responsywne aplikacje. Na przykład Wrap
to widżet podobny do Row
lub Column
, który automatycznie przenosi elementy podrzędne do następnego „wiersza” (zwanego „ciągiem”), gdy nie ma wystarczająco dużo miejsca w pionie lub poziomie. Jest też widżet FittedBox
, który automatycznie dopasowuje element podrzędny do dostępnego miejsca zgodnie z Twoimi specyfikacjami.
Jednak NavigationRail
nie wyświetla automatycznie etykiet, gdy jest wystarczająco dużo miejsca, ponieważ nie może wiedzieć, co w każdym kontekście oznacza wystarczająco dużo miejsca. To Ty, deweloper, musisz podjąć tę decyzję.
Załóżmy, że chcesz wyświetlać etykiety tylko wtedy, gdy MyHomePage
ma szerokość co najmniej 600 pikseli.
W tym przypadku należy użyć widżetu LayoutBuilder
. Umożliwia zmianę drzewa widżetów w zależności od ilości dostępnego miejsca.
Ponownie użyj menu Refactor (Refaktoryzacja) w VS Code, aby wprowadzić wymagane zmiany. Tym razem jest to jednak nieco bardziej skomplikowane:
- W metodzie
build
klasy_MyHomePageState
umieść kursor naScaffold
. - Wywołaj menu Refactor, naciskając
Ctrl+.
(Windows/Linux) lubCmd+.
(Mac). - Wybierz Wrap with Builder (Owiń za pomocą narzędzia do tworzenia) i naciśnij Enter.
- Zmień nazwę nowo dodanego elementu
Builder
naLayoutBuilder
. - Zmień listę parametrów wywołania zwrotnego z
(context)
na(context, constraints)
.
Wywołanie zwrotne LayoutBuilder
builder
jest wywoływane za każdym razem, gdy zmieniają się ograniczenia. Dzieje się tak na przykład, gdy:
- użytkownik zmienia rozmiar okna aplikacji;
- użytkownik obraca telefon z trybu pionowego do poziomego lub z powrotem;
- Rozmiar widżetu obok
MyHomePage
zwiększa się, co powoduje zmniejszenie ograniczeńMyHomePage
.
Teraz kod może zdecydować, czy wyświetlić etykietę, sprawdzając bieżącą wartość constraints
. Wprowadź tę jednolinijkową zmianę w metodzie _MyHomePageState
build
:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
Teraz aplikacja reaguje na środowisko, w którym działa, np. na rozmiar ekranu, orientację i platformę. Innymi słowy, jest elastyczny.
Pozostało tylko zastąpić ten element Placeholder
rzeczywistym ekranem Ulubione. Omówimy to w następnej sekcji.
8. Dodaj nową stronę
Pamiętasz widżet Placeholder
, którego używaliśmy zamiast strony Ulubione?
Czas to naprawić.
Jeśli masz ochotę, możesz spróbować wykonać ten krok samodzielnie. Twoim celem jest wyświetlenie listy favorites
w nowym widżecie bezstanowym FavoritesPage
, a następnie wyświetlenie tego widżetu zamiast Placeholder
.
Oto kilka wskazówek:
- Jeśli chcesz, aby element
Column
był przewijany, użyj widżetuListView
. - Pamiętaj, że do instancji
MyAppState
możesz uzyskać dostęp z dowolnego widżetu za pomocącontext.watch<MyAppState>()
. - Jeśli chcesz wypróbować nowy widżet,
ListTile
ma właściwości takie jaktitle
(zwykle do tekstu),leading
(do ikon lub awatarów) ionTap
(do interakcji). Podobne efekty możesz jednak uzyskać za pomocą znanych Ci już widżetów. - Dart umożliwia używanie pętli
for
w literałach kolekcji. Jeśli np. zmiennamessages
zawiera listę ciągów tekstowych, możesz użyć kodu takiego jak ten:
Jeśli jednak lepiej znasz programowanie funkcyjne, w Dart możesz też pisać kod w ten sposób: messages.map((m) => Text(m)).toList()
. Oczywiście możesz też utworzyć listę widgetów i imperatywnie dodawać do niej elementy w metodzie build
.
Zaletą samodzielnego dodawania strony Ulubione jest to, że podejmując własne decyzje, możesz się więcej nauczyć. Wadą jest to, że możesz napotkać problemy, których nie będziesz w stanie samodzielnie rozwiązać. Pamiętaj: porażka jest w porządku i jest jednym z najważniejszych elementów nauki. Nikt nie oczekuje, że w pierwszej godzinie opanujesz tworzenie aplikacji we Flutterze, i Ty też nie powinieneś.
Poniżej przedstawiamy jeden ze sposobów wdrożenia strony ulubionych. Sposób implementacji (mamy nadzieję) zainspiruje Cię do eksperymentowania z kodem – ulepsz interfejs i dostosuj go do swoich potrzeb.
Oto nowa klasa FavoritesPage
:
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
Działanie widżetu:
- Pobiera bieżący stan aplikacji.
- Jeśli lista ulubionych jest pusta, wyświetla się wyśrodkowany komunikat: Brak ulubionych.
- W przeciwnym razie wyświetli się lista (z możliwością przewijania).
- Lista zaczyna się od podsumowania (np. Masz 5 ulubionych.).
- Następnie kod przechodzi przez wszystkie ulubione i tworzy widżet
ListTile
dla każdego z nich.
Teraz wystarczy zastąpić widżet Placeholder
widżetem FavoritesPage
. I gotowe.
Końcowy kod tej aplikacji znajdziesz w repozytorium z ćwiczeniami na GitHubie.
9. Dalsze kroki
Gratulacje!
Patrz na siebie! Z niefunkcjonalnej platformy z widżetem Column
i 2 widżetami Text
udało Ci się stworzyć elastyczną i przyjemną w użyciu aplikację.
Omówione zagadnienia
- Podstawy działania Fluttera
- Tworzenie układów w Flutterze
- Łączenie interakcji użytkowników (np. naciśnięć przycisków) z zachowaniem aplikacji
- Utrzymywanie porządku w kodzie Fluttera
- Dostosowywanie aplikacji do różnych urządzeń
- Zachowanie spójnego wyglądu i sposobu działania aplikacji
Co dalej?
- Eksperymentuj z aplikacją napisaną w tym module.
- Zapoznaj się z kodem tej zaawansowanej wersji tej samej aplikacji, aby dowiedzieć się, jak dodawać animowane listy, gradienty, przenikania i inne elementy.
- Śledź postępy w nauce na stronie flutter.dev/learn.