Twoja pierwsza aplikacja Flutter

1. Wprowadzenie

Flutter to opracowany przez Google zestaw narzędzi interfejsu do tworzenia aplikacji na urządzenia mobilne, komputery i komputery przy użyciu jednej bazy kodu. W ramach tego ćwiczenia w Codelabs utworzysz następującą aplikację Flutter:

Aplikacja generuje chłodne nazwy, takie jak „newstay”, „lightstream”, „mainbrake” i „graypine”. Użytkownik może poprosić o następne imię i nazwisko, dodać bieżącą nazwę do ulubionych i sprawdzić ich listę na osobnej stronie. Aplikacja dostosowuje się do różnych rozmiarów ekranu.

Czego się nauczysz

  • Podstawy działania technologii Flutter
  • Tworzenie układów w technologii Flutter
  • Łączenie interakcji użytkowników (np. naciśnięć przycisku) z działaniem aplikacji
  • Utrzymywanie porządku w kodzie Flutter
  • Tworzenie elastycznej aplikacji (na różne ekrany)
  • Spójny wygląd charakter aplikacji

Zaczniesz od podstawowego rusztowania, które pozwoli Ci przejść od razu do interesujących części.

e9c6b402cd8003fd.png

Filip przeprowadzi Cię przez cały moduł.

Kliknij Dalej, aby rozpocząć moduł.

2. Konfigurowanie środowiska Flutter

Edytujący

Aby jak najbardziej ułatwić pracę w programie, zakładamy, że Twoim środowiskiem programistycznym jest Visual Studio Code (VS Code). Jest on bezpłatny i działa na wszystkich największych platformach.

Oczywiście możesz używać dowolnego edytora: Android Studio, inne IDE IntelliJ, Emacs, Vim lub Notepad++. Wszystkie współpracują z platformą Flutter.

W tym ćwiczeniu z programowania zalecamy używanie VS Code, ponieważ instrukcje domyślnie obejmują skróty specyficzne dla kodu w VS. Łatwiej jest powiedzieć „kliknij tutaj” lub „naciśnij ten klawisz” zamiast np. „wykonaj w edytorze odpowiednie działanie, aby wykonać X”.

228c71510a8e868.png

Wybierz cel rozwojowy

Flutter to wieloplatformowy zestaw narzędzi. Twoja aplikacja może działać w dowolnym z tych systemów operacyjnych:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • internet

Zazwyczaj jednak wybiera się jeden system operacyjny, który będzie głównie opracowywany. To jest Twój „cel programowania” – system operacyjny, na którym Twoja aplikacja działa w trakcie tworzenia aplikacji.

16695777c07f18e5.png

Załóżmy, że tworzysz aplikację Flutter na laptopie z systemem Windows. Jeśli jako cel programowania wybierzesz Androida, zazwyczaj podłączysz urządzenie z Androidem do laptopa z systemem Windows za pomocą kabla USB, a Twoja aplikacja będzie działać na podłączonym urządzeniu z Androidem. Jako cel rozwojowy możesz też wybrać Windows. Oznacza to, że Twoja aplikacja będzie działać razem z edytorem jako aplikacja dla systemu Windows.

Wybór sieci jako celu programistycznego może być kuszący. Wadą tego wyboru jest utrata jednej z najbardziej przydatnych funkcji Flutter dla programistów: Stateful Hot Załaduj ponownie. Flutter nie może ponownie wczytywać aplikacji internetowych.

Dokonaj wyboru już teraz. Pamiętaj, że zawsze możesz uruchomić aplikację w innym systemie operacyjnym później. Jasno określone cele rozwojowe sprawiają jednak, że kolejne kroki będą przebiegać sprawniej.

Zainstaluj Flutter

Najnowsze instrukcje instalowania pakietu Flutter SDK są zawsze dostępne na stronie docs.flutter.dev.

Instrukcje na stronie Flutter obejmują nie tylko instalację samego pakietu SDK, ale też narzędzia związane z programowaniem i wtyczki edytora. Pamiętaj, że do wykonania tych ćwiczeń w Codelabs musisz zainstalować tylko to:

  1. Pakiet SDK Flutter
  2. Kod w Visual Studio z wtyczką Flutter
  3. Oprogramowanie wymagane przez wybrany cel programistyczny (np. Visual Studio w przypadku kierowania na system Windows lub Xcode w przypadku kierowania na macOS).

W następnej sekcji utworzysz pierwszy projekt Flutter.

Jeśli do tej pory wystąpiły problemy, niektóre z tych pytań i odpowiedzi (z StackOverflow) mogą okazać się pomocne.

Najczęstsze pytania

3. Utwórz projekt

Tworzenie pierwszego projektu Flutter

Uruchom Visual Studio Code i otwórz paletę poleceń (przy użyciu klawiszy F1, Ctrl+Shift+P lub Shift+Cmd+P). Zacznij pisać „umieść nowy”. Wybierz polecenie Flutter: Nowy projekt.

Wybierz Application (Aplikacja), a potem kliknij folder, w którym chcesz utworzyć projekt. Może to być Twój katalog główny lub adres C:\src\.

Na koniec nadaj projektowi nazwę. Coś takiego jak namer_app lub my_awesome_namer.

260a7d97f9678005.png

Flutter utworzy teraz folder projektu, a VS Code otworzy go.

Teraz zastąpisz zawartość 3 plików podstawowym scaffrem aplikacji.

Kopiuj & Wklej początkową aplikację

W panelu po lewej stronie VS Code zaznacz opcję Explorer i otwórz plik pubspec.yaml.

e2a5bab0be07f4f7.png

Zamień zawartość tego pliku na taką:

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

Plik pubspec.yaml zawiera podstawowe informacje o aplikacji, takie jak jej bieżąca wersja, zależności oraz zasoby, z którymi zostanie ona wysłana.

Następnie otwórz inny plik konfiguracji w projekcie analysis_options.yaml.

a781f218093be8e0.png

Zamień jego zawartość na taką:

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 rygorystyczny powinien być Flutter podczas analizowania kodu. To Twoja pierwsza próba korzystania z Flutter, więc mówisz analizatorowi, by nie wymagał od Ciebie zbyt wiele. Zawsze możesz go później dostroić. W miarę zbliżania się do opublikowania rzeczywistej wersji produkcyjnej aplikacji prawdopodobnie zechcesz bardziej rygorystycznie uściślić analizator.

Na koniec otwórz plik main.dart w katalogu lib/.

e54c671c9bb4d23d.png

Zamień zawartość tego pliku na taką:

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(
          useMaterial3: true,
          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 – jak dotąd cała aplikacja.

W następnej sekcji uruchom aplikację w trybie debugowania i zacznij tworzyć.

4. Dodaj przycisk

Ten krok powoduje dodanie przycisku Dalej, który pozwala wygenerować nową parę słów.

Uruchom aplikację

Najpierw otwórz aplikację lib/main.dart i upewnij się, że masz wybrane urządzenie docelowe. W prawym dolnym rogu karty VS Code znajdziesz przycisk, który pokazuje bieżące urządzenie docelowe. Kliknij, aby go zmienić.

Po otwarciu aplikacji lib/main.dart znajdź przycisk „Odtwórz” b0a5d0200af5985d.png w prawym górnym rogu okna VS Code.

Po około minucie aplikacja zostanie uruchomiona w trybie debugowania. Jeszcze nie wygląda to na dużo:

f96e7dfb0937d7f4.png

Pierwsze ponowne załadowanie z pamięci

Na dole obiektu lib/main.dart dodaj coś do ciągu w pierwszym obiekcie Text i zapisz plik (przy użyciu funkcji Ctrl+S lub Cmd+S). Na 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 się nie zmienia. To słynne gorące odświeżanie przygotowane przez Flutter w czasie pracy. Ponowne wczytywanie „na gorąco” jest wywoływane podczas zapisywania zmian w pliku źródłowym.

Najczęstsze pytania

Dodawanie przycisku

Następnie dodaj przycisk u dołu instancji Column, tuż 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 zostanie ponownie zaktualizowana: pojawi się przycisk, a gdy go klikniesz, w Konsoli debugowania w usłudze VS Code wyświetli się komunikat naciśnięty!.

Szybki kurs Flutter w 5 minut

Mimo że oglądanie konsoli debugowania to dla Ciebie świetna zabawa, zależy Ci na tym, aby przycisk wpłynął na coś bardziej wartościowego. Zanim jednak to zrobisz, przyjrzyj się kodowi w języku lib/main.dart, aby zrozumieć, jak on działa.

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

Na samej górze pliku jest funkcja main(). W obecnej formie informuje tylko usługę Flutter, aby uruchomić aplikację zdefiniowaną w zasadzie 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(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

Klasa MyApp rozszerza się o StatelessWidget. Widżety to elementy, na podstawie których tworzysz każdą aplikację Flutter. Jak widać, nawet sama aplikacja jest widżetem.

Kod w aplikacji MyApp konfiguruje całą aplikację. Tworzy stan całej aplikacji (więcej informacji na ten temat znajdziesz później), nadaje nazwę aplikacji, definiuje motyw wizualny i ustawia „dom” widżet – punkt początkowy aplikacji.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

Następnie klasa MyAppState określa...no...stan aplikacji. Po raz pierwszy korzystasz z platformy Flutter, więc ten kurs z programowania będzie prosty i konkretny. Istnieje wiele zaawansowanych sposobów zarządzania stanem aplikacji w Flutter. Najłatwiej wyjaśnić, jak działa ChangeNotifier, czyli podejście tej aplikacji.

  • MyAppState określa dane, które są niezbędne do działania aplikacji. Obecnie zawiera tylko jedną zmienną z bieżącą losową parą słów. Dodasz go później.
  • Klasa stanu rozszerza zakres ChangeNotifier, co oznacza, że może powiadamiać innych o własnych zmianach. Jeśli na przykład bieżąca para słów się zmieni, niektóre widżety aplikacji muszą o tym wiedzieć.
  • Stan jest tworzony i przekazywany całej aplikacji za pomocą interfejsu ChangeNotifierProvider (kod znajdziesz powyżej w sekcji MyApp). Dzięki temu każdy widżet w aplikacji może uzyskiwać informacje o stanie. d9b6ecac5494a6ff.png

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ą opcją jest MyHomePage – widżet, który został już przez Ciebie zmodyfikowany. Każdy wiersz numerowany poniżej odpowiada komentarzowi z numerem wiersza w powyższym kodzie:

  1. Każdy widżet definiuje metodę build(), która jest automatycznie wywoływana przy każdej zmianie okoliczności widżetu, dzięki czemu jest on zawsze aktualny.
  2. Funkcja MyHomePage śledzi zmiany bieżącego stanu aplikacji za pomocą metody watch.
  3. Każda metoda build musi zwracać widżet lub (zwykle) zagnieżdżone drzewo widżetów. W tym przypadku widżet najwyższego poziomu to Scaffold. W tym ćwiczeniu w programowaniu nie będziesz używać usługi Scaffold, ale jest to przydatny widżet, który można znaleźć w większości rzeczywistych aplikacji Flutter.
  4. Column to jeden z najbardziej podstawowych widżetów układu w Flutter. Zajmuje dowolną liczbę elementów podrzędnych i umieszcza je w kolumnie od góry do dołu. Domyślnie kolumna podrzędne umieszcza swoje elementy podrzędne na górze. Wkrótce zostanie to zmienione i wyśrodkowanie kolumny.
  5. Ten widżet aplikacji Text został przez Ciebie zmieniony w pierwszym kroku.
  6. Ten drugi widżet Text zajmuje appState i ma dostęp do jedynego elementu w klasie, current (czyli WordPair). WordPair udostępnia kilka pomocnych metod pobierania, np. asPascalCase lub asSnakeCase. Używamy tu nazwy asLowerCase, ale możesz ją zmienić teraz, jeśli wolisz inną.
  7. Zwróć uwagę, że w kodzie Flutter często używane są przecinki na końcu. Nie musisz tu podawać tego przecinka, ponieważ children jest ostatnim (i jedynym) elementem tej listy parametrów Column. Ogólnie jednak warto używać przecinków na końcu: dzięki temu dodawanie większej liczby członków jest proste, a dodatkowo pozwalają użyć funkcji automatycznego formatowania Dart, aby w nim umieścić nowy wiersz. Więcej informacji znajdziesz w artykule Formatowanie kodu.

Następnie połącz przycisk ze stanem.

Twoje pierwsze zachowanie

Przewiń do sekcji MyAppState i dodaj metodę getNext.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

Nowa metoda getNext() przypisuje wartość current do nowej losowej metody WordPair. Wywołuje również notifyListeners()(metodę ChangeNotifier), która daje pewność, że każdy oglądający film MyAppState otrzyma powiadomienie.

Teraz wystarczy wywołać metodę getNext z wywołania zwrotnego przycisku.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

Zapisz i wypróbuj aplikację. Po każdym kliknięciu przycisku Dalej powinna zostać wygenerowana nowa losowa parę słów.

W następnej sekcji poprawisz wygląd interfejsu.

5. Ulepsz aplikację

Oto jak obecnie wygląda aplikacja.

3dd8a9d8653bdc56.png

Kiepska sprawa. Główny element aplikacji – losowo generowana para słów – powinien być bardziej widoczny. Jest to przecież główny powód, dla którego użytkownicy korzystają z tej aplikacji. Poza tym zawartość aplikacji nie przydaje się, a cała aplikacja jest nudnie czarna, biały.

Ta sekcja zawiera informacje na temat tych problemów, pracujemy nad projektem aplikacji. Końcowy cel tej sekcji wygląda mniej więcej tak:

2bbee054d81a3127.png

Wyodrębnianie widżetu

Wiersz, który odpowiada za wyświetlenie bieżącej pary słów, wygląda teraz tak: Text(appState.current.asLowerCase). Aby przekształcić go w bardziej złożony widżet, warto wyodrębnić ten wiersz do osobnego widżetu. Używanie osobnych widżetów dla oddzielnych części logicznych interfejsu to ważny sposób zarządzania złożonością w Flutter.

Flutter oferuje pomocnik refaktoryzacyjny do wyodrębniania widżetów, ale zanim z niego skorzystasz, upewnij się, że wyodrębniony wiersz ma dostęp tylko do tego, co jest potrzebne. W tej chwili ta linia ma dostęp do ciągu appState, ale tak naprawdę wystarczy wiedzieć, jaka jest bieżąca para słów.

Dlatego musisz zmodyfikować 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 obszaru appState.

Teraz otwórz menu Refactor (Refaktoryzacja). W VS Code możesz to zrobić na 2 sposoby:

  1. Kliknij prawym przyciskiem myszy fragment kodu, który chcesz zrefaktoryzować (w tym przypadku Text) i z menu wybierz Refaktoryzacja...

LUB

  1. Przesuń kursor na fragment kodu, który chcesz zrefaktoryzować (w tym przypadku Text) i naciśnij Ctrl+. (Win/Linux) lub Cmd+. (Mac).

W menu Refaktoryzacja wybierz Wyodrębnij widżet. Przypisz nazwę, na przykład BigCard i kliknij Enter.

Spowoduje to automatyczne utworzenie nowej klasy (BigCard) na końcu bieżącego pliku. 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 po tej refaktoryzacji.

Dodawanie karty

Teraz czas umieścić ten nowy widżet w pogrubionym interfejsie, który zaplanowaliśmy na początku tej sekcji.

Znajdź w niej klasę BigCard i metodę build(). Tak jak poprzednio wywołaj menu Refaktor w widżecie Text. Tym razem nie wyodrębnisz widżetu.

Zamiast tego wybierz Zawijaj z dopełnieniem. Spowoduje to utworzenie nowego widżetu nadrzędnego wokół widżetu Text o nazwie Padding. Po zapisaniu zauważysz, że losowe słowo ma już większe pole manewru.

Zwiększ dopełnienie z wartości domyślnej wynoszącej 8.0. Możesz na przykład użyć atrybutu typu 20, aby uzyskać większe dopełnienie.

Następnie przejdź o jeden poziom wyżej. Umieść kursor na widżecie Padding, wyciągnij menu Refaktor i wybierz Zawijaj za pomocą widżetu...

Umożliwia to określenie widżetu nadrzędnego. Wpisz „Karta”. i naciśnij Enter.

Obejmuje to widżet Padding, a tym samym Text, z widżetem Card.

6031adbc0a11e16b.png

Motyw i styl

Aby karta bardziej się wyróżniała, pomaluj ją bardziej intensywnym kolorem. A ponieważ zawsze warto zachować jednolity schemat kolorów, kolor możesz wybrać w aplikacji Theme.

Wprowadź poniższe zmiany w metodzie build() metody BigCard.

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 wymagają sporo wysiłku:

  • Najpierw kod wysyła żądanie bieżącego motywu aplikacji za pomocą dodatku Theme.of(context).
  • Następnie kod określa kolor karty, który jest taki sam jak we właściwości colorScheme motywu. Schemat kolorów obejmuje wiele kolorów, a primary to najbardziej widoczny, określający kolor aplikacji.

Karta zostanie pomalowana na główny kolor aplikacji:

a136f7682c204ea1.png

Możesz zmienić ten kolor oraz schemat kolorów całej aplikacji, przewijając w górę do elementu MyApp i zmieniając w tym miejscu kolor ziarna dla elementu ColorScheme.

Zwróć uwagę, że kolor płynnie się animuje. Jest to tzw. animacja niejawna. Wiele widżetów Flutter umożliwia płynną interpolację wartości, dzięki czemu interfejs użytkownika nie tylko „przeskakuje” między stanami.

Podniesiony przycisk pod kartą również zmienia kolor. Takie rozwiązanie daje możliwość korzystania z interfejsu Theme obejmującego całą aplikację zamiast wartości wpisywanych na stałe w kodzie.

TextTheme

Nadal występuje problem z kartą: tekst jest za mały, a jej kolor jest trudny do odczytania. Aby rozwiązać ten problem, wprowadź następujące zmiany w metodzie build() przeglądarki BigCard.

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

// ...

Dlaczego wprowadzamy tę zmianę:

  • Używając opcji theme.textTheme,, uzyskujesz dostęp do motywu czcionki w aplikacji. Ta klasa zawiera elementy takie jak bodyMedium (w przypadku standardowego tekstu o średnim rozmiarze), caption (do wyświetlania podpisów obrazów) lub headlineLarge (w przypadku dużych nagłówków).
  • Właściwość displayMedium to duży styl przeznaczony do wyświetlania tekstu. Słowo display jest tu używane w znaczeniu typograficznym, na przykład w języku displayowym. Z dokumentacji dotyczącej usługi displayMedium wynika, że „style wyświetlania są zarezerwowane dla krótkiego, ważnego tekstu” – tak właśnie jest w naszym przypadku.
  • Właściwość displayMedium motywu mogłaby teoretycznie mieć wartość null. Dart, język programowania, w którym piszesz tę aplikację, nie ma wartości null, więc nie pozwala na wywoływanie metod obiektów, których właściwości są potencjalnie null. W takim przypadku możesz jednak użyć operatora ! („operator bang”), by mieć pewność, że Dart wiesz, co robisz. (w tym przypadku displayMedium z pewnością nie ma wartości null. Wiemy jednak, że to wykracza poza zakres tego ćwiczenia).
  • Wywołanie copyWith() displayMedium zwraca kopię stylu tekstu z zdefiniowanymi przez Ciebie zmianami. W tym przypadku zmieniasz tylko kolor tekstu.
  • Aby go użyć, ponownie otwórz motyw aplikacji. Właściwość onPrimary schematu kolorów określa kolor, którego można użyć w podstawowym kolorze aplikacji.

Aplikacja powinna teraz wyglądać mniej więcej tak:

2405e9342d28c193.png

Jeśli chcesz, możesz jeszcze bardziej zmienić kartę. Oto kilka pomysłów:

  • W copyWith() możesz zmieniać znacznie więcej stylów tekstu niż tylko kolor. Aby wyświetlić pełną listę właściwości, które możesz zmienić, umieść kursor w dowolnym miejscu nawiasów klamrowych copyWith() i naciśnij Ctrl+Shift+Space (Win/Linux) lub Cmd+Shift+Space (Mac).
  • Możesz też zmienić więcej informacji w widżecie Card. Możesz np. zwiększyć cień karty, zwiększając wartość parametru elevation.
  • Poeksperymentuj z kolorami. Oprócz theme.colorScheme.primary jest też .secondary, .surface i wiele innych. Wszystkie te kolory mają swoje odpowiedniki w kategorii onPrimary.

Ułatwienia dostępu

Flutter domyślnie udostępnia aplikacje. Na przykład każda aplikacja Flutter prawidłowo wyświetla cały tekst i interaktywne elementy w aplikacji czytnikom ekranu, takim jak TalkBack i VoiceOver.

d1fad7944fb890ea.png

Czasami trzeba jednak trochę popracować. W przypadku tej aplikacji czytnik ekranu może mieć problemy z wymawianiem niektórych wygenerowanych par słów. Ludzie nie mają problemów z rozpoznaniem 2 wyrazów w słowie cheaphead, ale czytnik ekranu może wymówić w jego środku ph jako f.

Prostym rozwiązaniem jest zastąpienie ciągu pair.asLowerCase elementem "${pair.first} ${pair.second}". W tym drugim przypadku użyto interpolacji ciągów znaków do utworzenia ciągu (np. "cheap head") na podstawie dwóch słów zawartych w zasadzie pair. Użycie 2 osobnych słów zamiast słowa złożonego sprawi, że czytniki ekranu będą je prawidłowo zidentyfikować i będą przydatne dla użytkowników z wadą wzroku.

Możesz jednak zachować wizualną prostotę interfejsu pair.asLowerCase. Użyj właściwości semanticsLabel w narzędziu Text, aby zastąpić zawartość widoczną w widżecie tekstowym 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 wymawiają każdą wygenerowaną parę słów, ale interfejs użytkownika pozostaje bez zmian. Aby to zrobić, użyj czytnika ekranu na swoim urządzeniu.

Wyśrodkuj interfejs

Teraz, gdy losowa para słów ma już wystarczający wygląd, czas umieścić ją pośrodku okna/ekranu aplikacji.

Po pierwsze, pamiętaj, że BigCard jest częścią Column. Domyślnie kolumny kierują dzieci na górę listy, ale można to łatwo zmienić. Otwórz metodę build() na koncie MyHomePage 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'),
          ),
        ],
      ),
    );
  }
}

// ...

Spowoduje to wyśrodkowanie elementów podrzędnych wewnątrz elementu Column wzdłuż jego głównej (pionowej) osi.

b555d4c7f5000edf.png

Elementy podrzędne są już wyśrodkowane wzdłuż osi krzyżowej kolumny (czyli są już wyśrodkowane w poziomie). Jednak sam element Column nie jest wyśrodkowany w Scaffold. Możemy to sprawdzić, korzystając z Inspektora widżetów.

Inspektor widżetów wykracza poza zakres tego ćwiczenia z programowania, ale po podświetleniu elementu Column nie zajmuje on całej szerokości aplikacji. Zajmuje tyle miejsca w poziomie, ile potrzebują jego dzieci.

Możesz po prostu wyśrodkować kolumnę. Najedź kursorem na Column, wywołaj menu Refaktor (za pomocą Ctrl+. lub Cmd+.) i wybierz Zawijaj środkiem.

Aplikacja powinna teraz wyglądać mniej więcej tak:

455688d93c30d154.png

Jeśli chcesz, możesz go trochę zmienić.

  • Widżet Text możesz usunąć powyżej BigCard. Tekst opisowy („losowy AWESOME pomysł:”) nie jest już potrzebny, ponieważ interfejs użytkownika ma sens nawet bez niego. I w ten sposób jest czystszy.
  • Widżet SizedBox(height: 10) możesz też dodać między BigCard a ElevatedButton. Dzięki temu różnica między widżetami będzie większa. Widżet SizedBox zajmuje tylko miejsce i nic nie renderuje. Powszechnie wykorzystuje się go do tworzenia „luk” wizualnych.

Po wprowadzeniu zmian opcjonalny MyHomePage 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'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

A aplikacja wygląda tak:

3d53d2b071e2f372.png

W następnej sekcji możesz dodać możliwość dodawania wygenerowanych słów do ulubionych (lub „Podoba mi się”) generowanych w ten sposób.

6. Dodaj funkcje

Aplikacja działa, a od czasu do czasu pojawiają się nawet ciekawe pary słów. Gdy użytkownik kliknie Dalej, każda para słów znika na zawsze. Lepiej mieć sposób „zapamiętywania” najlepsze sugestie, np. kliknięcie przycisku „Podoba mi się” Przycisk

e6b01a8c90df8ffa.png

Dodaj logikę biznesową

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 lokalizacji MyAppState dodano nową usługę o nazwie favorites. Zainicjowano tę właściwość pustą listą: [].
  • Określono również, że lista może zawierać tylko pary słów: <WordPair>[] (w przypadku kategorii genericowe). Zwiększa to wydajność aplikacji – Dart odmawia nawet uruchomienia aplikacji, jeśli spróbujesz dodać do niej coś innego niż WordPair. Dzięki temu możesz używać listy favorites, wiedząc, że nigdy nie ukrywają się w niej żadne niechciane obiekty (np. null).
  • Dodałeś też nową metodę toggleFavorite(), która usuwa bieżącą parę słów z listy ulubionych (jeśli jest już na liście) lub dodaje ją (jeśli jeszcze jej nie ma). W obu przypadkach kod wywołuje później funkcję notifyListeners();.

Dodaj przycisk

Dzięki logice biznesowej czas zająć się interfejsem użytkownika. Umieszczanie linku „Podoba mi się” po lewej stronie przycisku „Dalej” przycisk wymaga: Row. Widżet Row to poziomy odpowiednik Column widocznego wcześniej.

Najpierw umieść dotychczasowy przycisk w elemencie Row. Przejdź do metody build() funkcji MyHomePage, umieść kursor na obiekcie ElevatedButton, wywołaj menu Refaktor za pomocą funkcji Ctrl+. lub Cmd+. i wybierz Zawijaj wierszem.

Po zapisaniu zauważysz, że Row działa podobnie do Column – domyślnie umieszcza elementy podrzędne z lewej strony. (Column umieścił swoje dzieci na górze). Aby rozwiązać ten problem, możesz zastosować tę samą metodę co wcześniej, ale z użyciem funkcji mainAxisAlignment. Jednak do celów dydaktycznych (naukowych) używaj mainAxisSize. Dzięki temu aplikacja Row nie będzie zajmować całego dostępnego miejsca w poziomie.

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

3d53d2b071e2f372.png

Następnie dodaj przycisk Podoba mi się i połącz go z toggleFavorite(). Jeśli chodzi o wyzwanie, spróbuj zrobić to samodzielnie, bez patrzenia na poniższy blok kodu.

e6b01a8c90df8ffa.png

Nie ma nic złego, jeśli nie zrobisz tego dokładnie w taki sposób, jak pokazano poniżej. Ikona serca nie przejmuj się, chyba że masz ochotę na poważne wyzwanie.

Możesz też popełniać porażki – w końcu to Twoja pierwsza godzina korzystania z Flutter.

252f7c4a212c94d2.png

Oto jeden ze sposobów dodawania drugiego przycisku do usługi MyHomePage. Tym razem utwórz przycisk z ikoną za pomocą konstruktora ElevatedButton.icon(). Na górze metody build wybierz odpowiednią ikonę w zależności od tego, czy bieżąca para słów znajduje się już w ulubionych. Pamiętaj też o korzystaniu z elementu SizedBox, by nieco rozdzielić 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:

Użytkownik nie może zobaczyć ulubionych. Czas dodać do naszej aplikacji oddzielny ekran. Do zobaczenia w następnej sekcji.

7. Dodaj kolumnę nawigacyjną

Większość aplikacji nie zmieści się na jednym ekranie. Ta konkretna aplikacja prawdopodobnie jest dostępna, ale w celach dydaktycznych musisz utworzyć osobny ekran z ulubionymi użytkownikami. Aby przełączać się między 2 ekranami, zaimplementujesz pierwsze StatefulWidget.

f62c54f5401a187.png

Aby jak najszybciej przejść do sedna tego kroku, podziel MyHomePage na 2 osobne widżety.

Zaznacz całą zawartość pola MyHomePage, usuń ją 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 ♥♡ (serc) w kolumnie nawigacji nie przynosi efektu.

388bc25fe198c54a.png

Przeanalizuj wprowadzone zmiany.

  • Po pierwsze zwróć uwagę, że cała zawartość pliku MyHomePage jest wyodrębniana do nowego widżetu o nazwie GeneratorPage. Jedyna część starego widżetu MyHomePage, która nie została wyodrębniona, to Scaffold.
  • Nowy element MyHomePage zawiera element Row z dwójką dzieci. Pierwszy to SafeArea, a drugi to widżet Expanded.
  • SafeArea pilnuje, aby element podrzędny nie był zasłonięty wycięciem sprzętowym ani paskiem stanu. W tej aplikacji widżet otacza NavigationRail, aby na przykład nie zasłaniać przycisków nawigacyjnych przez pasek stanu urządzenia mobilnego.
  • Możesz zmienić wiersz extended: false w NavigationRail na true. Spowoduje to wyświetlenie etykiet obok ikon. W przyszłości dowiesz się, jak zrobić to automatycznie, gdy aplikacja będzie miała wystarczającą ilość wolnego miejsca w poziomie.
  • Na pasku nawigacji znajdują się 2 miejsca docelowe (Główna i Ulubione) z odpowiadającymi im ikonami i etykietami. Definiuje również bieżącą wartość selectedIndex. Jeśli wybrany indeks wynosi 0, pierwsze miejsce docelowe zostanie wybrane, a wybrany indeks równy 1 – drugie miejsce docelowe itd. Na razie ma on stałą wartość zero.
  • Kolumna nawigacji określa też, co się stanie, gdy użytkownik wybierze jedno z miejsc docelowych za pomocą funkcji onDestinationSelected. Obecnie aplikacja jedynie wyświetla żądaną wartość indeksu z parametrem print().
  • Drugim elementem podrzędnym elementu Row jest widżet Expanded. Rozwinięte widżety są bardzo przydatne w wierszach i kolumnach. Pozwalają określić układ, w którym niektóre dzieci zajmują tylko tyle miejsca, ile potrzebują (w tym przypadku SafeArea), a inne widżety powinny zajmować jak najwięcej wolnego miejsca (w tym przypadku Expanded). Widżety Expanded można traktować jako „chciwe”. Jeśli chcesz lepiej zrozumieć rolę tego widżetu, spróbuj opakować widżet SafeArea innym elementem Expanded. Wynikowy układ będzie wyglądał mniej więcej tak:

6bbda6c1835a1ae.png

  • Dwa widżety Expanded dzielą między sobą całą dostępną przestrzeń w poziomie, mimo że pasek nawigacji potrzebował tylko małego wycinka po lewej stronie.
  • W widżecie Expanded znajduje się kolorowa ikona Container, a w kontenerze – GeneratorPage.

Widżety bezstanowe a stanowe

Do tej pory organizacja MyAppState zaspokajała wszystkie potrzeby instytucji państwowych. Dlatego wszystkie utworzone przez Ciebie widżety sąbezstanowe. Same własnych stanów nie można zmieniać. Żaden z widżetów nie może się zmienić – muszą przejść przez MyAppState.

To wkrótce się zmieni.

Musisz w jakiś sposób przechowywać wartość selectedIndex kolumny nawigacji. Tę wartość możesz też zmieniać z poziomu wywołania zwrotnego onDestinationSelected.

Możesz dodać usługę selectedIndex jako kolejną usługę MyAppState. I to zadziała. Można sobie jednak wyobrazić, że stan aplikacji szybko rozrosłby się poza uzasadnienie, gdyby każdy widżet zapisywał w nim swoje wartości.

e52d9c0937cc0823.jpeg

Niektóre stany odnoszą się tylko do pojedynczego widżetu, więc powinien pozostać z nim.

Wpisz StatefulWidget – widżet, który zawiera State. Najpierw przekonwertuj MyHomePage na widżet stanowy.

Umieść kursor na pierwszym wierszu MyHomePage (ten, który zaczyna się od class MyHomePage...) i otwórz menu Refaktora za pomocą opcji Ctrl+. lub Cmd+.. Następnie wybierz Konwertuj na StatefulWidget.

IDE tworzy dla Ciebie nową klasę: _MyHomePageState. Ta klasa rozszerza zakres State, więc może zarządzać własnymi wartościami. Może się zmienić. Zauważ też, że metoda build ze starego, bezstanowego widżetu została przeniesiona do sekcji _MyHomePageState (zamiast pozostawać w widżecie). Przeniesiono dosłownie – nic się nie zmieniło w metodzie build. Teraz po prostu żyje gdzie indziej.

setState

Nowy widżet stanowy musi śledzić tylko jedną zmienną: selectedIndex. Wprowadź w elemencie _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:

  1. Wprowadzasz nową zmienną selectedIndex i inicjujesz ją w polu 0.
  2. Użyjesz tej nowej zmiennej w definicji NavigationRail zamiast zakodowanej na stałe zmiennej 0, która wcześniej była dostępna.
  3. Gdy wywołanie zwrotne onDestinationSelected jest wywoływane, zamiast wyświetlać nową wartość w konsoli, przypisujesz ją do selectedIndex w wywołaniu setState(). To wywołanie jest podobne do używanej wcześniej metody notifyListeners() – gwarantuje, że interfejs się zaktualizuje.

Pasek nawigacyjny reaguje teraz na interakcję użytkownika. Obszar rozwinięty po prawej stronie pozostaje bez zmian. Dzieje się tak, ponieważ kod nie używa parametru selectedIndex do określenia, który ekran ma się wyświetlić.

Użyj wybranego indeksu

Umieść ten kod na początku metody build w _MyHomePageState, 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');
}

// ...

Przyjrzyj się temu fragmentowi kodu:

  1. Kod deklaruje nową zmienną (page) typu Widget.
  2. Następnie instrukcja Switch przypisuje ekran do funkcji page zgodnie z bieżącą wartością w polu selectedIndex.
  3. Nie ma jeszcze żadnej wartości typu FavoritesPage, użyj więc Placeholder; widżet, który w dowolnym miejscu rysuje przekreślony prostokąt, oznaczając daną część interfejsu jako nieukończoną.

5685cf886047f6ec.png

  1. Zgodnie z zasadą failu szybkiego, instrukcja zamiany powoduje też zgłoszenie błędu, jeśli selectedIndex nie ma wartości 0 lub 1. Pomoże to uniknąć błędów w przyszłości. Jeśli kiedykolwiek dodasz nowe miejsce docelowe na szynie nawigacyjnej i zapomnisz zaktualizować ten kod, program ulegnie awarii w trakcie opracowywania (nie będzie można zgadnąć, dlaczego coś nie działa, ani pozwolić na opublikowanie nieprawidłowego kodu w środowisku produkcyjnym).

Skoro page zawiera już widżet, który chcesz wyświetlić po prawej stronie, prawdopodobnie odgadniesz, jaka inna zmiana jest wymagana.

Oto wyniki (_MyHomePageState) wprowadzone po tej pojedynczej zmianie:

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 obiektem GeneratorPage i obiektem zastępczym, który wkrótce stanie się stroną Ulubione.

Reagowanie

Następnie skonfiguruj responsywną kolumnę nawigacyjną. To oznacza, że etykiety będą automatycznie wyświetlane (za pomocą funkcji extended: true), gdy będzie wystarczająco dużo miejsca.

a8873894c32e0d0b.png

Flutter udostępnia kilka widżetów, które pomagają automatycznie dostosowywać aplikacje. Na przykład Wrap to widżet podobny do Row lub Column, który automatycznie zawija elementy podrzędne do następnego „wiersza” („bieg”), gdy nie ma wystarczającej ilości wolnego miejsca w pionie lub poziomie. Dostępne jest FittedBox – widżet, który automatycznie dopasowuje dziecko do dostępnego miejsca zgodnie z Twoimi wymaganiami.

Jednak NavigationRail nie automatycznie pokazuje etykiet, gdy jest wystarczająca ilość miejsca, ponieważ nie może sprawdzić, ile miejsca jest wystarczająco dużo miejsca w każdym kontekście. Decyzja o numerze telefonu należy do Ciebie.

Załóżmy, że chcesz wyświetlać etykiety tylko wtedy, gdy element MyHomePage ma co najmniej 600 pikseli szerokości.

W tym przypadku należy użyć widżetu LayoutBuilder. Umożliwia zmianę drzewa widżetów w zależności od ilości wolnego miejsca.

Aby wprowadzić wymagane zmiany, ponownie użyj menu Flutter Refactor w VS Code. Tym razem jest to jednak nieco bardziej skomplikowane:

  1. W metodzie build w usłudze _MyHomePageState umieść kursor na obiekcie Scaffold.
  2. Wywołaj menu Refaktoryzacja za pomocą klawisza Ctrl+. (Windows/Linux) lub Cmd+. (Mac).
  3. Wybierz Zapakuj za pomocą konstruktora i naciśnij Enter.
  4. Zmień nazwę nowo dodanego elementu Builder na LayoutBuilder.
  5. Zmień listę parametrów wywołania zwrotnego z (context) na (context, constraints).

Wywołanie zwrotne funkcji builder w elemencie LayoutBuilder 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 pionowego na poziomy lub z powrotem.
  • Widżet obok MyHomePage powiększa się, przez co ograniczenia użytkownika MyHomePage są mniejsze
  • I tak dalej

Teraz Twój kod może zdecydować, czy wyświetlić etykietę, wysyłając zapytanie do bieżącego elementu constraints. Wprowadź zmianę w 1 wierszu w metodzie build metody _MyHomePageState:

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 Twoja aplikacja reaguje na środowisko, takie jak rozmiar ekranu, orientacja i platforma. Krótko mówiąc, szybko reaguje.

Jedynym elementem, jaki pozostaje, jest zastąpienie tego elementu Placeholder rzeczywistym ekranem Ulubione. Zostało to omówione w następnej sekcji.

8. Dodaj nową stronę

Pamiętasz widżet Placeholder, który użyliśmy zamiast strony Ulubione?

Czas rozwiązać ten problem.

Jeśli masz ochotę na przygodę, zrób to samodzielnie. Twoim celem jest wyświetlenie listy favorites w nowym bezstanowym widżecie FavoritesPage, a następnie wyświetlenie tego widżetu zamiast Placeholder.

Oto kilka wskazówek:

  • Jeśli chcesz, aby element Column można było przewijać, użyj widżetu ListView.
  • Pamiętaj, aby uzyskać dostęp do instancji MyAppState z dowolnego widżetu przy użyciu context.watch<MyAppState>().
  • Jeśli chcesz też wypróbować nowy widżet, ListTile ma właściwości takie jak title (zwykle dla tekstu), leading (dla ikon i awatarów) oraz onTap (do interakcji). Podobne efekty możesz jednak uzyskać, korzystając z widżetów, które już znasz.
  • Dart pozwala używać pętli for wewnątrz literałów kolekcji. Jeśli na przykład messages zawiera listę ciągów, możesz utworzyć kod podobny do tego:

f0444bba08f205aa.png

Z drugiej strony, jeśli znasz się na programowaniu funkcyjnym, Dart umożliwia pisanie kodu takiego jak messages.map((m) => Text(m)).toList(). Zawsze możesz też utworzyć listę widżetów i odpowiednio dodać do niej listę w metodzie build.

Zaletą samodzielnego dodawania strony Ulubione jest to, że możesz podejmować lepsze decyzje i uzyskiwać więcej informacji. Wadą jest jednak to, że mogą pojawić się problemy, których nie potraficie jeszcze samodzielnie rozwiązać. Pamiętaj: porażki są czymś normalnym i stanowią jeden z najważniejszych elementów uczenia się. Nikt nie oczekuje, że w ciągu pierwszej godziny będziesz w stanie rozwinąć się w technologii Flutter – podobnie jak Ty.

252f7c4a212c94d2.png

Opisane poniżej sposoby to tylko jeden ze sposobów implementacji strony Ulubione. Sposób implementacji zainspiruje Cię do zabawy z kodem oraz do ulepszenia interfejsu i dostosowania go do swoich potrzeb.

Oto nowe zajęcia w 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),
          ),
      ],
    );
  }
}

Widżet działa w następujący sposób:

  • Pobiera bieżący stan aplikacji.
  • Jeśli lista ulubionych jest pusta, wyświetli się wyśrodkowany komunikat: Nie ma jeszcze ulubionych*.*
  • W przeciwnym razie pojawi się lista, którą można przewijać.
  • Na początku listy znajduje się podsumowanie (np. Masz 5 ulubionych*.*).
  • Kod wykonuje iterację między wszystkimi ulubionymi i dla każdej z nich tworzy widżet ListTile.

Teraz wystarczy zastąpić widżet Placeholder widżetem FavoritesPage. I voila!

Ostateczny kod tej aplikacji znajdziesz w repozytorium ćwiczeń z programowania w GitHubie.

9. Dalsze kroki

Gratulacje!

No proszę! Udało Ci się stworzyć niedziałające rusztowanie z Column i 2 widżetami Text, które przekształciłeś w responsywną, uroczą aplikację.

d6e3d5f736411f13.png

Omówione zagadnienia

  • Podstawy działania technologii Flutter
  • Tworzenie układów w technologii Flutter
  • Łączenie interakcji użytkowników (np. naciśnięć przycisku) z działaniem aplikacji
  • Utrzymywanie porządku w kodzie Flutter
  • Tworzenie elastycznych aplikacji
  • Spójny wygląd charakter aplikacji

Co dalej?

  • Eksperymentuj z aplikacją napisaną w tym module.
  • Zapoznaj się z kodem tej zaawansowanej wersji tej samej aplikacji, by dowiedzieć się, jak można dodawać do niej animowane listy, gradienty, przenikanie i inne elementy.