Twoja pierwsza aplikacja Flutter

1. Wprowadzenie

Flutter to udostępniany przez Google pakiet narzędzi do tworzenia interfejsu użytkownika, który umożliwia tworzenie aplikacji mobilnych, internetowych i na komputery z pojedynczej bazy kodu. W tym laboratorium programistycznym skompilujesz aplikację Flutter:

Aplikacja generuje fajne nazwy, takie jak „newstay”, „lightstream”, „mainbrake” czy „graypine”. Użytkownik może poprosić o następną nazwę, ustawić bieżącą nazwę jako ulubioną i sprawdzać listę ulubionych nazw na osobnej stronie. Aplikacja jest elastyczna i dobrze działa na różnych rozmiarach ekranu.

Czego się nauczysz

  • Podstawy działania Fluttera
  • Tworzenie układów w Flutterze
  • Powiązanie interakcji użytkowników (np. naciśnięć przycisków) z zachowaniem w aplikacji
  • Zarządzanie kodem Fluttera
  • Dostosowywanie aplikacji do różnych ekranów
  • Utrzymywanie spójnego wyglądu i działania aplikacji

Zaczniesz od podstawowej ramki, dzięki której możesz od razu przejść do interesujących Cię fragmentów.

e9c6b402cd8003fd.png

Filip poprowadzi Cię przez cały proces.

Kliknij Dalej, aby rozpocząć moduł.

2. Konfigurowanie środowiska Flutter

Edytor

Aby to ćwiczenie było jak najprostsze, zakładamy, że jako środowiska programistycznego używasz Visual Studio Code (VS Code). Jest bezpłatna i działa na wszystkich głównych platformach.

Oczywiście możesz użyć dowolnego edytora: Android Studio, innych środowisk IntelliJ, Emacsa, Vim lub Notepad++. Wszystkie działają z Flutterem.

W tym ćwiczeniu zalecamy użycie VS Code, ponieważ instrukcje domyślnie odnoszą się do skrótów klawiszowych w tym edytorze. Łatwiej jest powiedzieć „kliknij tutaj” lub „naciśnij ten klawisz” zamiast „wykonaj odpowiednie działanie w edytorze, aby wykonać X”.

228c71510a8e868.png

Wybierz cel rozwoju

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

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

Jednak zwykle wybiera się jeden system operacyjny, w którym głównie będziesz tworzyć aplikacje. Jest to „docelowy system operacyjny” – system operacyjny, w którym aplikacja działa podczas tworzenia.

16695777c07f18e5.png

Załóżmy, że do tworzenia aplikacji Flutter używasz laptopa z systemem Windows. Jeśli wybierzesz Androida jako platformę docelową, zwykle podłączysz urządzenie z Androidem do laptopa za pomocą kabla USB, a aplikacja w trakcie tworzenia będzie działać na tym podłączonym urządzeniu. Możesz też wybrać system Windows jako platformę do tworzenia aplikacji, co oznacza, że aplikacja w trakcie tworzenia będzie działać jako aplikacja na Windowsa obok edytora.

Możesz być skłonny wybrać sieć jako cel rozwoju. Minusem tego rozwiązania jest utrata jednej z najprzydatniejszych funkcji programowania w Flutterze: szybkiego odświeżania stanu. Flutter nie obsługuje funkcji ponownego wczytywania aplikacji internetowych na gorąco.

Wybierz teraz. Pamiętaj, że zawsze możesz uruchomić aplikację w innych systemach operacyjnych. Mając jasny cel rozwoju, łatwiej jest przejść do następnego kroku.

Instalowanie Fluttera

Najnowsze instrukcje instalacji pakietu Flutter SDK znajdziesz na stronie docs.flutter.dev.

Instrukcje na stronie Flutter obejmują nie tylko instalację samego pakietu SDK, ale też narzędzia związane z docelowym środowiskiem programowania i wtyczki edytora. Pamiętaj, że w ramach tego ćwiczenia wystarczy zainstalować te elementy:

  1. Flutter SDK
  2. Visual Studio Code z wtyczką Flutter
  3. Oprogramowanie wymagane do wybranego środowiska programistycznego (np. Visual Studio w przypadku systemu Windows lub Xcode w przypadku systemu macOS).

W następnej sekcji utworzysz swój pierwszy projekt Flutter.

Jeśli do tej pory wystąpiły problemy, możesz znaleźć pomoc w tych pytaniach i odpowiedziach (z StackOverflow).

Najczęstsze pytania

3. Utwórz projekt

Tworzenie pierwszego projektu Flutter

Uruchom Visual Studio Code i otwórz paletę poleceń (za pomocą F1 lub Ctrl+Shift+P lub Shift+Cmd+P). Zacznij wpisywać „flutter new”. Wybierz polecenie Flutter: Nowy projekt.

Następnie wybierz Aplikacja, a potem folder, w którym utworzysz projekt. Może to być katalog domowy lub coś w tym stylu: C:\src\.

Na koniec nadaj projektowi nazwę. Może to być namer_app lub my_awesome_namer.

260a7d97f9678005.png

Flutter tworzy folder projektu, a VS Code go otwiera.

Teraz nadpisz zawartość 3 plików podstawowym szkieletem aplikacji.

Kopiowanie i wklejanie początkowej aplikacji

W panelu po lewej stronie w VS Code sprawdź, czy wybrana jest karta Eksplorator, 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.6.0

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

Plik pubspec.yaml zawiera podstawowe informacje o aplikacji, takie jak jej obecna wersja, zależności i zasoby, z którymi zostanie dostarczona.

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

a781f218093be8e0.png

Zastąp zawartość tego pliku tym:

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ć kod. Ponieważ to Twoja pierwsza przygoda z Flutterem, każesz analizatorowi odpuścić. Zawsze możesz to zmienić później. W miarę zbliżania się do publikacji rzeczywistej aplikacji produkcyjnej warto zastosować bardziej rygorystyczne ustawienia analizatora.

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 to cała aplikacja.

W następnej sekcji uruchom aplikację w trybie debugowania i zacznij ją rozwijać.

4. Dodawanie przycisku

Ten krok dodaje przycisk Dalej, który umożliwia wygenerowanie nowego połączenia słów.

Uruchom aplikację.

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 wskazuje aktualne urządzenie docelowe. Kliknij, aby to zmienić.

Gdy lib/main.dart jest otwarte, w prawym górnym rogu okna VS Code kliknij przycisk „odtwórz” b0a5d0200af5985d.png.

Po około minucie aplikacja uruchomi się w trybie debugowania. Na razie nie wygląda to na wiele:

f96e7dfb0937d7f4.png

Pierwsze gorące przeładowanie

U dołu obiektu lib/main.dart dodaj coś do ciągu w pierwszym obiekcie Text i zapisz plik (za pomocą elementu 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 zmienia się natychmiast, ale losowe słowo pozostaje takie samo. To słynne stanowe odświeżanie na gorąco w Flutterze. Gorące przeładowanie jest uruchamiane po zapisaniu zmian w pliku źródłowym.

Najczęstsze pytania

Dodawanie przycisku

Następnie dodaj przycisk u dołu 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 zmiany, aplikacja zostanie ponownie zaktualizowana. Pojawi się przycisk. Gdy go klikniesz, w konsoli debugowania w VS Code pojawi się komunikat button pressed! (przycisk został naciśnięty).

Szybki kurs Fluttera w 5 minut

Chociaż oglądanie konsoli debugowania może być ciekawe, chcesz, aby przycisk robił coś bardziej znaczącego. Zanim to zrobisz, przyjrzyj się bliżej kodom w pliku lib/main.dart, aby zrozumieć, jak działają.

lib/main.dart

// ...

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

// ...

U samej góry pliku znajduje się funkcja main(). W obecnej postaci tylko informuje 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(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

Klasa MyApp rozszerza klasę StatelessWidget. Widżety to elementy, z których tworzysz każdą aplikację Flutter. Jak widzisz, nawet sama aplikacja jest widżetem.

Kod w MyApp konfiguruje całą aplikację. Tworzy stan dotyczący całej aplikacji (więcej na ten temat znajdziesz poniżej), nadaje nazwę aplikacji, definiuje motyw wizualny i ustawia widżet „Początek” jako punkt wyjścia aplikacji.

lib/main.dart

// ...

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

// ...

Następnie klasa MyAppState definiuje stan aplikacji. To Twoje pierwsze zetknięcie się z Flutterem, więc to ćwiczenie z programowania będzie proste i skupione na tym temacie. W Flutterze można zarządzać stanem aplikacji na wiele skutecznych sposobów. Najłatwiejszym do wyjaśnienia jest ChangeNotifier, czyli podejście stosowane w tej aplikacji.

  • MyAppState określa dane, których aplikacja potrzebuje do działania. Obecnie zawiera tylko jedną zmienną z bieżącą losową parą słów. Dodamy je 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 przekazywany całej aplikacji za pomocą ChangeNotifierProvider (patrz kod powyżej w funkcji 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 widżet to MyHomePage, który został już przez Ciebie zmodyfikowany. Każda z wymienionych poniżej ponumerowanych linii odpowiada komentarzowi w danym wierszu w powyższym kodzie:

  1. Każdy widget definiuje metodę build(), która jest automatycznie wywoływana za każdym razem, gdy zmieniają się okoliczności, aby widget był zawsze aktualny.
  2. MyHomePage śledzi zmiany bieżącego stanu aplikacji za pomocą metody watch.
  3. Każda metoda build musi zwracać widżet lub (co jest bardziej typowe) zagnieżdżone drzewo widżetów. W tym przypadku widżet najwyższego poziomu to Scaffold. W tym Codelab nie będziesz pracować z elementem Scaffold, ale jest to przydatny widget, który występuje w przeważającej większości aplikacji Flutter.
  4. Column to jeden z najprostszych widżetów układu w Flutterze. Funkcja ta bierze dowolną liczbę elementów podrzędnych i umieszcza je w kolumnie od góry do dołu. Domyślnie kolumna umieszcza elementy podrzędne na górze. Wkrótce zmienisz to tak, aby kolumna była wyśrodkowana.
  5. W pierwszym kroku zmieniono widget Text.
  6. Ten drugi widżet Text przyjmuje appState i dostępuje do jedynego elementu tej klasy, current (który jest WordPair). WordPair udostępnia kilka przydatnych metod dostępu, takich jak asPascalCase czy asSnakeCase. Tutaj używamy asLowerCase, ale możesz to zmienić, jeśli wolisz inną opcję.
  7. Zwróć uwagę, że kod Fluttera często używa przecinków końcowych. Ten przecinek nie jest tu potrzebny, ponieważ children jest ostatnim (i jedynym) elementem na liście parametrów Column. Zwykle jednak warto używać przecinków końcowych: ułatwiają one dodawanie kolejnych elementów, a także służą jako wskazówka dla automatycznego formatowania Darta, aby wstawić tam znak nowego wiersza. Więcej informacji znajdziesz w artykule Formatowanie kodu.

Następnie połączysz przycisk ze stanem.

Pierwsze zachowanie

Przewiń do 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 nową losową wartość WordPair do zmiennej current. Wywołuje też funkcję notifyListeners()(metoda funkcji ChangeNotifier)), która powiadamia wszystkich oglądających MyAppState.

Teraz wystarczy wywołać metodę getNext z połączenia zwrotnego 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 uatrakcyjnisz interfejs użytkownika.

5. Upięklenie aplikacji

Aplikacja wygląda obecnie tak.

3dd8a9d8653bdc56.png

Niezbyt dobrze. Główny element aplikacji, czyli losowo wygenerowana para słów, powinien być bardziej widoczny. To przecież główny powód, dla którego użytkownicy korzystają z tej aplikacji. Ponadto zawartość aplikacji jest dziwnie przesunięta, a cała aplikacja jest czarno-biała.

W tej sekcji omawiamy te problemy i podpowiadamy, jak rozwiązać je za pomocą funkcji projektowania aplikacji. Ostateczny cel tej sekcji brzmi mniej więcej tak:

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ć tę pozycję w osobnym widżecie. 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 służące do wyodrębniania widżetów, ale zanim z niego skorzystasz, upewnij się, że wyodrębniany wiersz ma dostęp tylko do tego, czego potrzebuje. Obecnie wiersz uzyskuje dostęp do appState, ale tak naprawdę musi wiedzieć tylko, jaka jest bieżąca para 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 elementu 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 przerobić (w tym przypadku Text), i w menu kliknij Przerób….

LUB

  1. Przesuń kursor na fragment kodu, który chcesz przerobić (w tym przypadku Text), i kliknij Ctrl+. (Windows/Linux) lub Cmd+. (Mac).

W menu Refactor (Refaktoryzacja) wybierz Extract Widget (Wyodrębnij widget). Przypisz nazwę, np. 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 nadal, mimo tego, że została przebudowana.

Dodawanie karty

Czas przekształcić nowy widget w śmiałą część interfejsu, jaką sobie wyobrażaliśmy na początku tej sekcji.

Znajdź klasę BigCard i metodę build() w jej obrębie. Tak jak poprzednio, otwórz menu Refactor w widżecie Text. Tym razem nie będziesz wyodrębniać widżetu.

Zamiast tego wybierz Zawijanie z wypełnieniem. Spowoduje to utworzenie nowego widżetu nadrzędnego o nazwie Padding, który będzie zawierał widżet Text. Po zapisaniu zobaczysz, że słowo losowe ma już więcej miejsca na oddech.

Zwiększ odstęp od wartości domyślnej 8.0. Na przykład, aby uzyskać więcej miejsca, użyj wartości 20.

Następnie przejdź o jeden poziom wyżej. Najedź kursorem na widget Padding, otwórz menu Refactor i wybierz Zakończ za pomocą widgeta….

Dzięki temu możesz określić widżet nadrzędny. Wpisz „Karta” i naciśnij Enter.

Oznacza to, że widżet Padding, a zarazem widżet Text, jest otoczony widżetem Card.

6031adbc0a11e16b.png

Motyw i styl

Aby wyróżnić kartę, pomaluj ją na bardziej intensywny kolor. Pamiętaj, że warto zachować spójny schemat kolorów, dlatego wybierz kolor za pomocą Theme w aplikacji.

Wprowadź te zmiany w metodie build() klasy 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 wykonują wiele zadań:

  • Najpierw kod wysyła żądanie bieżącego motywu aplikacji za pomocą funkcji Theme.of(context).
  • Następnie kod określa kolor karty jako taki sam jak właściwość colorScheme motywu. Schemat kolorów zawiera wiele kolorów, a primary jest najbardziej widocznym, wyróżniającym 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 MyApp i zmieniając tam kolor nasion dla ColorScheme.

Zwróć uwagę, jak kolor płynnie się zmienia. Jest to tzw. animacja domyślna. Wiele widżetów Fluttera będzie płynnie interpolować wartości, aby interfejs nie „przeskakiwał” między stanami.

Przycisk pod kartą również zmienia kolor. To właśnie zaleta korzystania z elementu Theme w całej aplikacji zamiast zakodowanych wartości.

TextTheme

Karta nadal ma problem: tekst jest za mały i ma kolor, który trudno odczytać. Aby rozwiązać ten problem, wprowadź te zmiany w metodie BigCard build().

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 spowodowało tę zmianę:

  • Korzystając z funkcji theme.textTheme,, możesz uzyskać dostęp do motywu czcionki aplikacji. Ta klasa obejmuje elementy takie jak bodyMedium (standardowy tekst o średnim rozmiarze), caption (napis pod obrazem) czy headlineLarge (duże nagłówki).
  • Właściwość displayMedium to duży styl przeznaczony do wyświetlania tekstu. Słowo display jest tu używane w sposób typograficzny, np. czcionka wyświetleniowa. Dokumentacja displayMedium podaje, że „style wyświetlania są zarezerwowane dla krótkich, ważnych tekstów” – dokładnie nasz przypadek użycia.
  • Właściwość displayMedium motywu może teoretycznie mieć wartość null. Dart, czyli język programowania, w którym piszesz tę aplikację, jest bezpieczny pod względem null, więc nie pozwala wywoływać metod obiektów, które mogą być null. W tym przypadku możesz użyć operatora ! („operatora wykrzyknika”), aby poinformować Darta, że wiesz, co robisz. (displayMedium jest w tym przypadku nie null). (sposób, w jaki to wiemy, wykracza poza zakres tego ćwiczenia z programowania).
  • Wywołanie funkcji copyWith() w przypadku obiektu displayMedium zwraca kopię stylu tekstu z zdefiniowanymi 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 głównego koloru aplikacji.

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

2405e9342d28c193.png

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

  • copyWith() pozwala zmienić znacznie więcej elementów stylu tekstu niż tylko kolor. Aby zobaczyć 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.
  • Spróbuj eksperymentować z kolorami. Oprócz theme.colorScheme.primary są też .secondary, .surface i wiele innych. Wszystkie te kolory mają swoje odpowiedniki w skali onPrimary.

Ulepszanie ułatwień dostępu

Flutter zapewnia dostępność aplikacji domyślnie. Na przykład każda aplikacja Flutter poprawnie wyświetla tekst i interaktywne elementy aplikacji dla czytników ekranu, takich jak TalkBack i VoiceOver.

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. Człowiek nie ma problemów z rozpoznaniem 2 słów w słowie cheaphead, ale czytnik ekranu może wymawiać ph w środku słowa jako f.

Prostym rozwiązaniem jest zastąpienie pair.asLowerCase wartością "${pair.first} ${pair.second}". Drugi z nich używa interpolacji ciągu znaków, aby utworzyć ciąg znaków (taki jak "cheap head") z 2 słów zawartych w elementach pair. Użycie dwóch oddzielnych słów zamiast wyrazu złożonego zapewnia czytnikom ekranu odpowiednie ich rozpoznawanie i ułatwia korzystanie z aplikacji przez osoby niedowidzące.

Możesz jednak zachować wizualną prostotę pair.asLowerCase. Użyj właściwości semanticsLabel atrybutu semanticsLabel, aby zastąpić wizualne treści widżetu tekstowego treściami semantycznymi, które są bardziej odpowiednie dla czytników ekranu:Text

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 poprawnie wymawiają każdą wygenerowaną parę słów, a interfejs pozostaje taki sam. Aby zobaczyć, jak to działa, użyj czytnika ekranu na urządzeniu.

Wyśrodkowanie interfejsu

Teraz, gdy losowa para słów jest już odpowiednio wyświetlana, możesz umieścić ją na środku okna lub ekranu aplikacji.

Najpierw pamiętaj, że BigCard jest częścią Column. Domyślnie kolumny umieszczają swoje elementy podrzędne u góry, ale możemy to łatwo zmienić. Otwórz metodę build() klasy 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'),
          ),
        ],
      ),
    );
  }
}

// ...

Dzięki temu elementy podrzędne są wyśrodkowane w elementach Column wzdłuż głównej (pionowej) osi.

b555d4c7f5000edf.png

Dzieci są już wyśrodkowane wzdłuż osi krzyżowania kolumny (czyli są już wyśrodkowane poziomo). Column nie jest jednak wyśrodkowana w Scaffold. Możemy to sprawdzić za pomocą Inspektora widżetów.

Inspekcja widżetów wykracza poza zakres tego Codelab, ale możesz zauważyć, że gdy Column jest podświetlone, nie zajmuje całej szerokości aplikacji. Zajmuje tylko tyle miejsca, ile potrzebują jego elementy podrzędne.

Możesz po prostu wyśrodkować samą kolumnę. Najedź kursorem na Column, wywołaj menu Refactor (za pomocą Ctrl+. lub Cmd+.) i wybierz Wrap with Center (Owiń do środka).

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

455688d93c30d154.png

Jeśli chcesz, możesz jeszcze bardziej dostosować te ustawienia.

  • Możesz usunąć widżet Text nad widżetem BigCard. Można argumentować, że tekst opisowy („Losowa świetna idea”) nie jest już potrzebny, ponieważ interfejs jest zrozumiały bez niego. I w ten sposób jest czystsze.
  • Możesz też dodać widżet SizedBox(height: 10) między BigCardElevatedButton. Dzięki temu oba widżety są od siebie nieco oddalone. Widget SizedBox zajmuje tylko miejsce i sam z siebie niczego nie renderuje. Jest on często używany do tworzenia wizualnych „luk”.

Opcjonalne zmiany w elementach MyHomePage:

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ść dodawania słów wygenerowanych przez model do ulubionych (lub „polubienia” ich).

6. Dodawanie funkcji

Aplikacja działa i czasem nawet podaje interesujące pary słów. Jednak gdy użytkownik kliknie Dalej, wszystkie pary słów znikną na zawsze. Lepiej byłoby mieć sposób na „zapamiętanie” najlepszych sugestii, np. przycisk „Lubię to”.

e6b01a8c90df8ffa.png

Dodawanie logiki biznesowej

Przewiń do 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śliliśmy też, że lista może zawierać tylko pary słów: <WordPair>[], używając generycznych. Dzięki temu aplikacja będzie bardziej niezawodna – jeśli spróbujesz dodać do niej coś innego niż WordPair, Dart nawet jej nie uruchomi. Możesz też użyć listy favorites, wiedząc, że nie ma na niej żadnych niechcianych obiektów (takich jak 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 dodaje ją (jeśli jej tam jeszcze nie ma). W obu przypadkach kod wywołuje później funkcję notifyListeners();.

Dodawanie przycisku

Po zakończeniu pracy nad „logiką biznesową” możesz wrócić do pracy 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 owiń istniejący przycisk w element Row. Otwórz metodę MyHomePage build(), umieść kursor na ElevatedButton, wywołaj menu Refactor za pomocą Ctrl+. lub Cmd+. i wybierz Zawij do wiersza.

Po zapisaniu zauważysz, że Row działa podobnie do Column – domyślnie elementy podrzędne są grupowane po lewej stronie. (Column umieścił swoje elementy podrzędne na górze). Aby to naprawić, możesz użyć tego samego podejścia co wcześniej, ale z wartością mainAxisAlignment. Jednak w celach dydaktycznych (edukacyjnych) używaj mainAxisSize. To polecenie mówi Row, aby nie zajmować całej dostępnej przestrzeni poziomej.

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 Lubię i połącz go z toggleFavorite(). Aby sprawdzić się, spróbuj najpierw wykonać to zadanie samodzielnie, bez patrzenia na blok kodu poniżej.

e6b01a8c90df8ffa.png

Nie musisz wykonywać tych czynności w taki sam sposób jak poniżej. Nie przejmuj się ikoną serca, chyba że naprawdę chcesz się zmierzyć z poważnym wyzwaniem.

Nieudane próby też są w porządku – to przecież Twoja pierwsza godzina z Flutterem.

252f7c4a212c94d2.png

Oto jeden ze sposobów dodawania 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 funkcji SizedBox, aby obie te opcje były nieco od siebie oddalone.

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 aplikacji osobny ekran. Do zobaczenia w następnej sekcji.

7. Dodawanie paska nawigacyjnego

Większość aplikacji nie mieści się na jednym ekranie. Ta konkretna aplikacja prawdopodobnie mogłaby to zrobić, ale na potrzeby dydaktyczne utworzysz osobny ekran z ulubionymi pozycjami użytkownika. Aby przełączać się między tymi ekranami, zaimplementujesz pierwszy StatefulWidget.

f62c54f5401a187.png

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

Zaznacz cały fragment MyHomePage, usuń go i zastąp go 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 interfejs jest gotowy pod względem wizualnym, ale nie działa. Kliknięcie ♥︎ (serce) na pasku nawigacyjnym nie powoduje żadnego działania.

388bc25fe198c54a.png

Sprawdź zmiany.

  • Najpierw zwróć uwagę, że wszystkie treści z poziomu MyHomePage zostały wyodrębnione 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 2 dziećmi. Pierwszy widżet to SafeArea, a drugi to widżet Expanded.
  • SafeArea zapewnia, że element podrzędny nie jest zasłonięty przez notch lub pasek stanu. W tej aplikacji widget jest dopasowywany do NavigationRail, aby nie zasłaniać przycisków nawigacyjnych, np. paska stanu na urządzeniu mobilnym.
  • W NavigationRail możesz zmienić linię extended: false na true. Wyświetla etykiety obok ikon. W następnym kroku dowiesz się, jak zrobić to automatycznie, gdy aplikacja ma wystarczającą ilość miejsca poziomego.
  • Pasek nawigacyjny zawiera 2 miejsca docelowe (Strona głównaUlubione) z odpowiednimi ikonami i etykietami. Określa ona też bieżącą wartość selectedIndex. Wybrany indeks 0 wybiera pierwsze miejsce docelowe, indeks 1 drugie miejsce docelowe itd. Na razie jest to zakodowane na zero.
  • Pasek nawigacyjny określa też, co się stanie, gdy użytkownik wybierze jeden z miejsc docelowych za pomocą onDestinationSelected. Obecnie aplikacja wyświetla tylko żądaną wartość indeksu z wartością print().
  • Drugim elementem podrzędnym Row jest widget Expanded. Rozwinięte widżety są bardzo przydatne w wierszach i kolumnach – umożliwiają tworzenie układów, w których niektóre elementy podrzędne zajmują tylko tyle miejsca, ile potrzebują (w tym przypadku SafeArea), a inne widżety powinny zajmować jak najwięcej miejsca (w tym przypadku Expanded). Expanded można nazwać „żarłocznymi”. Jeśli chcesz lepiej poznać rolę tego widżetu, spróbuj owinąć widżet SafeArea innym widżetem Expanded. Wygenerowany układ będzie wyglądał mniej więcej tak:

6bbda6c1835a1ae.png

  • Dwa widżety Expanded zajmują całą dostępną przestrzeń poziomą, mimo że pasek nawigacji potrzebuje tylko niewielkiej przestrzeni po lewej stronie.
  • W widżecie Expanded znajduje się kolorowy element Container, a w kontenerze element GeneratorPage.

Widgety bezstanowe a stanowe

Do tej pory MyAppState spełniał wszystkie Twoje potrzeby związane z stanem. Dlatego wszystkie utworzone do tej pory widżety są bez stanu. Nie zawierają żadnych zmiennych stanów. Żaden z widżetów nie może sam zmienić ustawień. Musi to zrobić MyAppState.

To się wkrótce zmieni.

Musisz w jakiś sposób przechować wartość właściwości selectedIndex szyny nawigacyjnej. Chcesz też mieć możliwość zmiany tej wartości w ramach wywołania zwrotnego onDestinationSelected.

Możesz dodać selectedIndex jako kolejną właściwość MyAppState. I to zadziała. Możesz sobie jednak wyobrazić, że stan aplikacji szybko przekroczyłby wszelkie rozsądne granice, gdyby każdy widżet przechowywał w niej swoje wartości.

e52d9c0937cc0823.jpeg

Niektóre stany dotyczą tylko 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 stanowy.

Umieść kursor na pierwszym wierszu MyHomePage (tym, który zaczyna się od class MyHomePage...), i uruchom menu Refactor, używając Ctrl+. lub Cmd+.. Następnie kliknij Przekształc w StatefulWidget.

IDE utworzy dla Ciebie nową klasę _MyHomePageState. Ta klasa rozszerza klasę State, dzięki czemu może zarządzać własnymi wartościami. (może sam siebie zmienić). Zwróć też uwagę, że metoda build ze starego widżetu bezstanowego została przeniesiona do metody _MyHomePageState (zamiast pozostawać w widżecie). Został przeniesiony bez zmian – nic w metodie build się nie zmieniło. Teraz po prostu znajduje się gdzieś indziej.

setState

Nowy widżet stanowy musi śledzić tylko 1 zmienną: selectedIndex. Wprowadź te 3 zmiany w _MyHomePageState:

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 inicjalizujesz ją wartością 0.
  2. Używasz tej nowej zmiennej w definicji NavigationRail zamiast zakodowanej na stałe zmiennej 0, która była używana do tej pory.
  3. Gdy wywoływana jest funkcja zwracająca wartość onDestinationSelected, zamiast po prostu wydrukowywania nowej wartości w konsoli przypisujesz ją do selectedIndex w ramach wywołania setState(). To wywołanie jest podobne do metody notifyListeners() użytej wcześniej. Zapewnia ono aktualizację interfejsu.

Pasek nawigacyjny reaguje teraz na interakcje użytkownika. Rozwinięty obszar po prawej stronie pozostaje bez zmian. Dzieje się tak, ponieważ kod nie używa funkcji selectedIndex do określania, który ekran ma się wyświetlić.

Używanie selectedIndex

Umieść ten kod na początku metody _MyHomePageState build, tuż przed instrukcją 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 page na podstawie bieżącej wartości w selectedIndex.
  3. Ponieważ nie ma jeszcze FavoritesPage, użyj widgeta Placeholder. Jest to przydatny element, który rysuje przekreślony prostokąt w miejscu, w którym go umieścisz, oznaczając tę część interfejsu jako niedokończoną.

5685cf886047f6ec.png

  1. Zgodnie z zasadą szybkiego niepowodzenia instrukcja switch powoduje też wygenerowanie błędu, jeśli selectedIndex nie jest równe 0 ani 1. Pomoże to uniknąć błędów w przyszłości. Jeśli dodasz nowe miejsce docelowe do paska nawigacyjnego i zapomnisz zaktualizować kodu, program ulegnie awarii podczas tworzenia (nie będziesz musiał/a zgadywać, dlaczego coś nie działa, ani publikować w produkcji kodu z błędami).

Teraz, gdy page zawiera widżet, który chcesz wyświetlić po prawej stronie, prawdopodobnie wiesz, jaką jeszcze zmianę należy wprowadzić.

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 GeneratorPage a miejscem substytucyjnym, które wkrótce stanie się stroną Ulubione.

Reagowanie

Następnie spraw, aby pasek nawigacyjny był elastyczny. Oznacza to, że etykiety będą automatycznie wyświetlane (za pomocą extended: true), gdy będzie na nie wystarczająco dużo miejsca.

a8873894c32e0d0b.png

Flutter udostępnia kilka widżetów, które pomagają automatycznie dostosowywać aplikacje do ekranu. Na przykład Wrap to widżet podobny do Row lub Column, który automatycznie przenosi elementy podrzędne na następną „linię” (tzw. „bieg”), gdy nie ma wystarczającej ilości miejsca w poziomie lub w pionie. Jest to widget FittedBox, który automatycznie dopasowuje swoje dziecko do dostępnej przestrzeni zgodnie z Twoimi wytycznymi.

Ale NavigationRail nie automatycznie wyświetla etykiet, gdy jest wystarczająco dużo miejsca, ponieważ nie może wiedzieć, ile to w każdym kontekście. To Ty, jako deweloper, decydujesz, czy chcesz to zrobić.

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

W tym przypadku należy użyć widżetu LayoutBuilder. Dzięki temu możesz zmieniać drzewo widżetów w zależności od ilości dostępnego miejsca.

Aby wprowadzić wymagane zmiany, ponownie użyj menu Refactor w Flutterze w VS Code. 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 Zakończ 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).

Funkcja LayoutBuilder builder jest wywoływana za każdym razem, gdy zmieniają się ograniczenia. Dzieje się tak, gdy na przykład:

  • Użytkownik zmienia rozmiar okna aplikacji
  • Użytkownik obraca telefon z orientacji pionowej na poziomą lub odwrotnie.
  • Niektóre widżety obok MyHomePage powiększają swój rozmiar, przez co ograniczenia MyHomePage są mniejsze
  • itd.

Teraz Twój kod może zdecydować, czy wyświetlić etykietę, wysyłając zapytanie do bieżącego elementu constraints. Wprowadź tę jednowierszową zmianę w metodzie build w klasie build:_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,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

Aplikacja reaguje teraz na środowisko, np. rozmiar ekranu, orientację i platformę. Innymi słowy, jest responsywna.

Musisz tylko zastąpić Placeholder rzeczywistym ekranem Ulubione. Omówimy to w następnej sekcji.

8. Dodaj nową stronę

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

Czas to naprawić.

Jeśli masz ochotę na przygodę, spróbuj 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 widżetu Placeholder.

Oto kilka wskazówek:

  • Jeśli chcesz użyć Column, który się przewija, użyj widżetu ListView.
  • Pamiętaj, że możesz uzyskać dostęp do instancji MyAppState z dowolnego widżetu, używając context.watch<MyAppState>().
  • Jeśli chcesz też wypróbować nowy widżet, ListTile ma właściwości takie jak title (zazwyczaj do tekstu), leading (do ikon lub awatarów) i onTap (do interakcji). Możesz jednak uzyskać podobne efekty za pomocą znanych Ci już widżetów.
  • W języku Dart można używać pętli for w ramach zbiorów literackich. Jeśli np. messages zawiera listę ciągów znaków, kod może wyglądać tak:

f0444bba08f205aa.png

Jeśli natomiast bardziej znasz programowanie funkcyjne, możesz też pisać kod w Dart, np. messages.map((m) => Text(m)).toList(). Oczywiście zawsze możesz utworzyć listę widgetów i dodać do niej elementy w ramach metody build.

Zaletą samodzielnego dodawania strony Ulubione jest to, że więcej się uczysz, podejmując własne decyzje. Wadą jest to, że możesz napotkać problemy, których nie uda Ci się rozwiązać samodzielnie. Pamiętaj, że porażki są normalne i stanowią jeden z najważniejszych elementów nauki. Nikt nie oczekuje, że już w pierwszej godzinie opanujesz programowanie w Flutterze. Ty też nie powinieneś.

252f7c4a212c94d2.png

Poniżej przedstawiamy jedną z możliwych metod implementacji strony ulubionych. Sposób jego implementacji (miejmy nadzieję) zainspiruje Cię do eksperymentowania z kodem, aby ulepszyć interfejs i dostosować go do własnych 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),
          ),
      ],
    );
  }
}

Oto, co robi ten widget:

  • Pobiera aktualny stan aplikacji.
  • Jeśli lista ulubionych jest pusta, wyświetla się wyśrodkowany komunikat: Brak ulubionych*.
  • W przeciwnym razie wyświetla się lista (przewijalna).
  • Lista zaczyna się od podsumowania (np. Masz 5 ulubionych*).
  • Następnie kod przechodzi przez wszystkie ulubione i tworzy dla każdego z nich widżet ListTile.

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

Gotowy kod tej aplikacji znajdziesz w repozytorium kodu na GitHubie.

9. Dalsze kroki

Gratulacje!

Gratulacje! Za pomocą niedziałającego szablonu z widżetami Column i 2 widżetami Text stworzyłeś/-aś responsywną, uroczą małą aplikację.

d6e3d5f736411f13.png

Omówione zagadnienia

  • Podstawy działania Fluttera
  • Tworzenie układów w Flutterze
  • Powiązanie interakcji użytkowników (np. naciśnięć przycisków) z zachowaniem w aplikacji
  • Zarządzanie kodem Flutter
  • Sprawianie, aby aplikacja była responsywna
  • Utrzymywanie spójnego wyglądu i działania aplikacji

Co dalej?

  • Przetestuj aplikację napisaną podczas tego laboratorium.
  • Aby dowiedzieć się, jak dodać animowane listy, gradienty, przejścia łagodne i inne elementy, przejrzyj kod tej zaawansowanej wersji tej samej aplikacji.