Twoja pierwsza aplikacja Flutter

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.

e9c6b402cd8003fd.png

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

228c71510a8e868.png

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.

16695777c07f18e5.png

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:

  1. Flutter SDK
  2. Visual Studio Code z wtyczką Fluttera
  3. 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

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.

260a7d97f9678005.png

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.

e2a5bab0be07f4f7.png

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.

a781f218093be8e0.png

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

e54c671c9bb4d23d.png

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” b0a5d0200af5985d.png 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:

f96e7dfb0937d7f4.png

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

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 w MyApp). Dzięki temu każdy widżet w aplikacji może uzyskać dostęp do stanu.

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

  1. 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.
  2. MyHomePage śledzi zmiany w bieżącym stanie 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żetem najwyższego poziomu jest Scaffold. W tym module nie będziesz pracować z Scaffold, ale jest to przydatny widżet, który występuje w większości rzeczywistych aplikacji Fluttera.
  4. 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.
  5. W pierwszym kroku zmieniono widżet Text.
  6. Ten drugi widżet Text przyjmuje appState i uzyskuje dostęp do jedynego elementu tej klasy, czyli current (który jest WordPair). WordPair udostępnia kilka przydatnych funkcji pobierających, takich jak asPascalCase czy asSnakeCase. Używamy tutaj asLowerCase, ale możesz to teraz zmienić, jeśli wolisz jedną z alternatyw.
  7. 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ów Column. 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.

3dd8a9d8653bdc56.png

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:

2bbee054d81a3127.png

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:

  1. Kliknij prawym przyciskiem myszy fragment kodu, który chcesz przekształcić (w tym przypadku Text), i z menu wybierz Przekształć….

LUB

  1. Przesuń kursor na kod elementu, który chcesz refaktoryzować (w tym przypadku Text), i naciśnij Ctrl+. (Windows/Linux) lub Cmd+. (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:

6031adbc0a11e16b.png

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, a primary jest najbardziej widocznym kolorem aplikacji.

Karta jest teraz pomalowana kolorem podstawowym aplikacji:

a136f7682c204ea1.png

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 jak bodyMedium (standardowy tekst o średniej wielkości), caption (podpisy obrazów) lub headlineLarge (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. Dokumentacja displayMedium 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 elemencie displayMedium 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:

2405e9342d28c193.png

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 nawiasach copyWith() i naciśnij Ctrl+Shift+Space (Windows/Linux) lub Cmd+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ść parametru elevation.
  • Eksperymentuj z kolorami. Oprócz theme.colorScheme.primary są też .secondary, .surface i wiele innych. Wszystkie te kolory mają swoje odpowiedniki w onPrimary.

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.

d1fad7944fb890ea.png

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

b555d4c7f5000edf.png

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:

455688d93c30d154.png

Jeśli chcesz, możesz jeszcze trochę dopracować ten efekt.

  • Możesz usunąć widżet Text nad BigCard. 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ędzy BigCardElevatedButton. W ten sposób odległość między tymi dwoma widżetami będzie nieco większa. Widżet SizedBox 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:

3d53d2b071e2f372.png

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

e6b01a8c90df8ffa.png

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 nazwie favorites. 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ć listy favorites, 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 MyHomePagebuild(), 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.

3d53d2b071e2f372.png

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.

e6b01a8c90df8ffa.png

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.

252f7c4a212c94d2.png

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.

f62c54f5401a187.png

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.

388bc25fe198c54a.png

Sprawdź zmiany.

  • Zwróć uwagę, że cała zawartość elementu MyHomePage została wyodrębniona do nowego widżetu GeneratorPage. Jedyną częścią starego widżetu MyHomePage, która nie została wyodrębniona, jest Scaffold.
  • Nowy MyHomePage zawiera Row z dwójką dzieci. Pierwszy widżet to SafeArea, a drugi to widżet Expanded.
  • 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 element NavigationRail, aby zapobiec zasłanianiu przycisków nawigacyjnych przez np. pasek stanu na urządzeniu mobilnym.
  • Możesz zmienić wiersz extended: falseNavigationRail na true. 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łównaUlubione) 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 symbolem print().
  • Drugim elementem podrzędnym tagu Row jest widżet Expanded. 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 przypadku SafeArea), a inne widżety powinny zajmować jak najwięcej pozostałego miejsca (w tym przypadku Expanded). ExpandedWidżety można traktować jako „zachłanne”. Jeśli chcesz lepiej poznać rolę tego widżetu, spróbuj umieścić widżet SafeArea w innym widżecie 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ń poziomą, mimo że pasek nawigacyjny potrzebuje tylko niewielkiego fragmentu po lewej stronie.
  • W widżecie Expanded znajduje się kolorowy element Container, a w kontenerze – element GeneratorPage.

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.

e52d9c0937cc0823.jpeg

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:

  1. Wprowadzasz nową zmienną selectedIndex i inicjujesz ją wartością 0.
  2. Tej nowej zmiennej używasz w definicji NavigationRail zamiast zakodowanej na stałe wartości 0, która była tam do tej pory.
  3. Gdy wywoływana jest funkcja zwrotna onDestinationSelected, zamiast po prostu wyświetlać nową wartość w konsoli, przypisujesz ją do zmiennej selectedIndex w wywołaniu funkcji setState(). To wywołanie jest podobne do metody notifyListeners() 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:

  1. Kod deklaruje nową zmienną page typu Widget.
  2. Następnie instrukcja switch przypisuje ekran do zmiennej page zgodnie z bieżącą wartością w zmiennej selectedIndex.
  3. Ponieważ nie ma jeszcze FavoritesPage, użyj Placeholder – przydatnego widżetu, który rysuje przekreślony prostokąt w dowolnym miejscu, oznaczając tę część interfejsu jako niedokończoną.

5685cf886047f6ec.png

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

a8873894c32e0d0b.png

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:

  1. W metodzie build klasy _MyHomePageState umieść kursor na Scaffold.
  2. Wywołaj menu Refactor, naciskając Ctrl+. (Windows/Linux) lub Cmd+. (Mac).
  3. Wybierz Wrap with Builder (Owiń za pomocą narzędzia do tworzenia) 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 LayoutBuilderbuilder 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 _MyHomePageStatebuild:

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żetu ListView.
  • 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 jak title (zwykle do tekstu), leading (do ikon lub awatarów) i onTap (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. zmienna messages zawiera listę ciągów tekstowych, możesz użyć kodu takiego jak ten:

f0444bba08f205aa.png

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

252f7c4a212c94d2.png

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

d6e3d5f736411f13.png

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.