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 ćwiczeniu 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ć obecną do ulubionych i sprawdzić 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
  • Zapewnianie spójnego wyglądu i działania aplikacji

Zaczniesz od podstawowego szkieletu, dzięki czemu od razu przejdziesz do interesujących części.

e9c6b402cd8003fd.png

A tutaj Filip przeprowadzi Cię przez cały codelab.

Aby rozpocząć moduł, kliknij Dalej.

2. Konfigurowanie środowiska Flutter

Edytor

Aby ułatwić Ci wykonanie tych ćwiczeń z programowania, założymy, że jako środowiska programistycznego używasz 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 IDE IntelliJ, Emacsa, Vima lub Notepad++. Wszystkie działają z Flutterem.

W tym samouczku zalecamy używanie VS Code, ponieważ instrukcje zawierają domyślne skróty klawiszowe 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, w którym 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, ż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 programowania 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 Windowsa obok edytora.

Może Cię kusić, aby wybrać sieć jako cel programowania. 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. Pakiet Flutter SDK
  2. Visual Studio Code z wtyczką Flutter
  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 początkowej aplikacji

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 publikacji rzeczywistej aplikacji produkcyjnej prawie na pewno zechcesz ustawić analizator na bardziej rygorystyczny poziom.

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. Gorące przeładowanie 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 gdy go klikniesz, w konsoli debugowania w VS Code pojawi się komunikat button pressed!.

5-minutowy kurs wprowadzający do Fluttera

Konsola debugowania może być interesująca, ale chcesz, aby przycisk wykonywał bardziej przydatne działanie. 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 polecenie to 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. Później dodasz do niego kolejne informacje.
  • Klasa stanu rozszerza 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 kodzie powyżej:

  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 widżetem Scaffold, ale jest to przydatny widżet, który występuje w większości rzeczywistych aplikacji Flutter.
  4. Column to jeden z najbardziej podstawowych widżetów układu w Flutterze. Umieszcza dowolną liczbę elementów podrzędnych 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. 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.

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 current nowy losowy 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ę już teraz. Za każdym razem, gdy naciśniesz przycisk 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

Kiepskie. 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 przesunięta względem środka, a cała aplikacja jest nudna, bo czarno-biała.

W tej sekcji zajmiemy się tymi problemami, pracując nad projektem aplikacji. Ostateczny cel tej sekcji to np.:

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. Posiadanie osobnych widżetów dla poszczególnych logicznych części interfejsu użytkownika 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 tego, czego potrzebuje. 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 refaktoryzować (w tym przypadku Text), i z menu wybierz Refaktoryzuj….

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 nadal nawet po 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 (Refaktoryzacja) w widżecie Text. Tym razem jednak nie wyodrębnisz widżetu.

Zamiast tego wybierz Zawijaj z dopełnieniem. Spowoduje to utworzenie nowego widżetu nadrzędnego wokół widżetu Text o nazwie Padding. Po zapisaniu 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 jeden 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.

Obejmuje to widżet Padding, a tym samym widżet 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 też zachować spójny schemat kolorów, dlatego użyj 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 czynności:

  • 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 właściwość 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ę zmienia. 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.

Przycisk pod kartą również zmieni kolor. To zaleta używania zmiennej Theme w całej aplikacji 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. W dokumentacji displayMedium czytamy, ż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 może teoretycznie 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. (Dlaczego tak jest, wykracza poza zakres tego ćwiczenia).
  • Wywołanie funkcji copyWith() na displayMedium zwraca kopię stylu tekstu z wprowadzonymi 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 głównym 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ć znacznie więcej niż tylko kolor tekstu. 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 napisana we Flutterze prawidłowo wyświetla wszystkie teksty i elementy interaktywne w aplikacji czytnikom ekranu, takim 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. 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 poprawia komfort użytkowników z wadami wzroku.

Możesz jednak zachować prostotę wizualną symbolu 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'),
          ),
        ],
      ),
    );
  }
}

// ...

Centruje elementy podrzędne wewnątrz elementu Column wzdłuż jego 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 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 bardziej 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. Jest to też bardziej przejrzyste.
  • 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 on 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 nawet nie uruchomi aplikacji, jeśli spróbujesz dodać do niej coś innego niż WordPair. Z kolei możesz używać listy favorites, wiedząc, że nie ma na niej żadnych 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 dodaje ją (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 przesuwa elementy podrzędne na lewą stronę. (Column przesunięto 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. Informuje to Row, że nie ma 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(). Aby utrudnić sobie zadanie, najpierw spróbuj 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 zachować odstęp między przyciskami.

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 tymi 2 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.

  • Najpierw zauważ, ż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.
  • SafeArea zapewnia, że jego 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, 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 wyświetla tylko żądaną wartość indeksu z symbolem print().
  • Drugim elementem podrzędnym tagu Row jest widżet Expanded. Rozszerzone widżety są bardzo 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 powinny zajmować jak najwięcej pozostałego miejsca (w tym przypadku Expanded). Można powiedzieć, że Expanded widżety są „zachłanne”. Jeśli chcesz lepiej poznać rolę tego widżetu, spróbuj otoczyć widżet SafeArea innym widżetem Expanded. Wynikowy układ będzie wyglądać mniej więcej tak:

6bbda6c1835a1ae.png

  • 2 widżety Expanded zajmują 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 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, dlatego 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 _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. Użyj tej nowej zmiennej 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 być wyświetlany.

Użyj właściwości selectedIndex

Umieść ten kod na początku metody _MyHomePageStatebuild, 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 jest równa 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 programowania (zamiast pozwolić Ci zgadywać, dlaczego coś nie działa, lub opublikować wadliwy kod w wersji produkcyjnej).

Skoro w page znajduje się 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 kolumnę nawigacji 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ć aplikacje automatycznie reagujące na zmiany. 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 użyj 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 w Flutterze 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 za pomocą klawisza 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. Zmieniono 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 Column można było przewijać, użyj widżetu ListView.
  • Pamiętaj, że możesz uzyskać dostęp do instancji MyAppState 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

Z drugiej strony, jeśli lepiej znasz programowanie funkcyjne, w Dart możesz też pisać kod w ten sposób: messages.map((m) => Text(m)).toList(). Oczywiście zawsze możesz utworzyć listę widżetó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, że 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 – ulepszania interfejsu i dostosowywania 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),
          ),
      ],
    );
  }
}

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 ćwiczeń na GitHubie.

9. Dalsze kroki

Gratulacje!

Gratulacje! Z niefunkcjonalnego szkieletu z widżetem Column i 2 widżetami Text udało Ci się stworzyć responsywną, atrakcyjną 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ń
  • Zapewnianie spójnego wyglądu i 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.