Deine erste Flutter-App

1. Einführung

Flutter ist das UI-Toolkit von Google zum Erstellen von Anwendungen für Mobilgeräte, das Web und Computer auf einer gemeinsamen Codebasis. In diesem Codelab erstellen Sie die folgende Flutter-Anwendung:

Die Anwendung generiert klingende Namen wie „newstay“, „lightstream“, „mainbrake“ oder „graypine“. Der Nutzer kann den nächsten Namen anfordern, den aktuellen Namen als Favoriten markieren und die Liste der Favoritennamen auf einer separaten Seite aufrufen. Die App ist für verschiedene Bildschirmgrößen optimiert.

Lerninhalte

  • Grundlagen der Funktionsweise von Flutter
  • Layouts in Flutter erstellen
  • Nutzerinteraktionen (z. B. Tastendrücke) mit dem App-Verhalten verknüpfen
  • Flutter-Code organisieren
  • App responsiv gestalten (für verschiedene Bildschirme)
  • Einheitliches Erscheinungsbild Ihrer App

Sie beginnen mit einem einfachen Gerüst, damit Sie direkt zu den interessanten Teilen springen können.

e9c6b402cd8003fd.png

Und hier ist Filip, der Sie durch das gesamte Codelab führt.

Klicken Sie auf „Weiter“, um das Lab zu starten.

2. Flutter-Umgebung einrichten

Editor

Damit dieses Codelab so einfach wie möglich ist, gehen wir davon aus, dass Sie Visual Studio Code (VS Code) als Entwicklungsumgebung verwenden. Die App ist kostenlos und funktioniert auf allen wichtigen Plattformen.

Sie können natürlich einen beliebigen Editor verwenden: Android Studio, andere IntelliJ-IDEs, Emacs, Vim oder Notepad++. Sie alle funktionieren mit Flutter.

Wir empfehlen, für dieses Codelab VS Code zu verwenden, da in der Anleitung standardmäßig VS Code-spezifische Tastenkombinationen verwendet werden. Es ist einfacher, Dinge wie „Klicken Sie hier“ oder „Drücken Sie diese Taste“ zu sagen, anstatt „Führen Sie die entsprechende Aktion in Ihrem Editor aus, um X zu tun“.

228c71510a8e868.png

Entwicklungsziel auswählen

Flutter ist ein plattformübergreifendes Toolkit. Ihre App kann auf einem der folgenden Betriebssysteme ausgeführt werden:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • web

Es ist jedoch üblich, ein einzelnes Betriebssystem auszuwählen, für das Sie hauptsächlich entwickeln. Das ist dann Ihr Entwicklungsziel – das Betriebssystem, auf dem Ihre Anwendung während der Entwicklungsphase ausgeführt wird.

16695777c07f18e5.png

Angenommen, Sie verwenden einen Windows-Laptop, um eine Flutter-App zu entwickeln. Wenn Sie Android als Entwicklungsziel auswählen, schließen Sie in der Regel ein Android-Gerät über ein USB-Kabel an Ihren Windows-Laptop an. Ihre App in der Entwicklung wird dann auf diesem angeschlossenen Android-Gerät ausgeführt. Sie können aber auch Windows als Entwicklungsziel auswählen. In diesem Fall wird Ihre App in der Entwicklung als Windows-App neben dem Editor ausgeführt.

Es kann verlockend sein, das Web als Entwicklungsziel auszuwählen. Der Nachteil dieser Option ist, dass Sie eine der nützlichsten Entwicklungsfunktionen von Flutter verlieren: Stateful Hot Reload. Flutter kann Webanwendungen nicht per Hot Reload neu laden.

Treffen Sie jetzt Ihre Auswahl. Hinweis: Sie können Ihre App später jederzeit auf anderen Betriebssystemen ausführen. Mit einem klaren Entwicklungsziel vor Augen lässt sich der nächste Schritt leichter umsetzen.

Flutter installieren

Die aktuellste Anleitung zur Installation des Flutter SDK finden Sie immer unter docs.flutter.dev.

Die Anleitung auf der Flutter-Website umfasst nicht nur die Installation des SDK selbst, sondern auch die Entwicklungstools und die Editor-Plug-ins. Denken Sie daran, dass Sie für dieses Codelab nur Folgendes installieren müssen:

  1. Flutter SDK
  2. Visual Studio Code mit dem Flutter-Plug-in
  3. Die Software, die für das von Ihnen gewählte Entwicklungsziel erforderlich ist (z. B. Visual Studio für Windows oder Xcode für macOS)

Im nächsten Abschnitt erstellen Sie Ihr erstes Flutter-Projekt.

Wenn Sie bisher Probleme hatten, finden Sie möglicherweise in einigen dieser Fragen und Antworten (von Stack Overflow) hilfreiche Informationen zur Fehlerbehebung.

FAQ

3. Projekt erstellen

Erstes Flutter-Projekt erstellen

Starten Sie Visual Studio Code und öffnen Sie die Befehlspalette (mit F1, Ctrl+Shift+P oder Shift+Cmd+P). Beginnen Sie mit der Eingabe von „flutter new“. Wählen Sie den Befehl Flutter: New Project aus.

Wählen Sie als Nächstes Application (Anwendung) und dann einen Ordner aus, in dem Sie das Projekt erstellen möchten. Das kann Ihr Basisverzeichnis oder ein Verzeichnis wie C:\src\ sein.

Geben Sie Ihrem Projekt einen Namen. Beispiel: namer_app oder my_awesome_namer.

260a7d97f9678005.png

Flutter erstellt nun den Projektordner und VS Code öffnet ihn.

Sie überschreiben jetzt den Inhalt von drei Dateien mit einem einfachen Gerüst der App.

Kopieren und Einfügen der ursprünglichen App

Achten Sie darauf, dass im linken Bereich von VS Code Explorer ausgewählt ist, und öffnen Sie die Datei pubspec.yaml.

e2a5bab0be07f4f7.png

Ersetzen Sie den Inhalt dieser Datei durch Folgendes:

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

In der Datei pubspec.yaml werden grundlegende Informationen zu Ihrer App angegeben, z. B. die aktuelle Version, die Abhängigkeiten und die Assets, die mit der App ausgeliefert werden.

Öffnen Sie als Nächstes eine andere Konfigurationsdatei im Projekt, analysis_options.yaml.

a781f218093be8e0.png

Ersetzen Sie den Inhalt durch Folgendes:

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

Diese Datei bestimmt, wie streng Flutter bei der Analyse Ihres Codes sein soll. Da dies Ihr erster Ausflug in Flutter ist, weisen Sie den Analyzer an, es langsam angehen zu lassen. Sie können diese Einstellung später jederzeit anpassen. Wenn Sie eine Produktions-App veröffentlichen möchten, sollten Sie den Analyzer sogar noch strenger einstellen.

Öffnen Sie schließlich die Datei main.dart im Verzeichnis lib/.

e54c671c9bb4d23d.png

Ersetzen Sie den Inhalt dieser Datei durch Folgendes:

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

Diese 50 Zeilen Code sind die gesamte App bis jetzt.

Im nächsten Abschnitt führen Sie die Anwendung im Debug-Modus aus und beginnen mit der Entwicklung.

4. Schaltfläche hinzufügen

In diesem Schritt wird die Schaltfläche Weiter hinzugefügt, mit der ein neues Wortpaar generiert werden kann.

App starten

Öffnen Sie zuerst lib/main.dart und prüfen Sie, ob Ihr Zielgerät ausgewählt ist. Rechts unten in VS Code sehen Sie eine Schaltfläche mit dem aktuellen Zielgerät. Klicken Sie darauf, um sie zu ändern.

Suchen Sie in lib/main.dart oben rechts im Fenster von VS Code nach der Schaltfläche „Wiedergabe“ b0a5d0200af5985d.png und klicken Sie darauf.

Nach etwa einer Minute wird Ihre App im Debug-Modus gestartet. Bisher ist noch nicht viel zu sehen:

f96e7dfb0937d7f4.png

Erster Hot Reload

Fügen Sie unten in lib/main.dart dem String im ersten Text-Objekt etwas hinzu und speichern Sie die Datei (mit Ctrl+S oder Cmd+S). Beispiel:

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

Beachten Sie, dass sich die App sofort ändert, das zufällige Wort jedoch gleich bleibt. Das ist der berühmte zustandsorientierte Hot Reload von Flutter. Hot Reload wird ausgelöst, wenn Sie Änderungen an einer Quelldatei speichern.

Häufig gestellte Fragen

Schaltfläche hinzufügen

Fügen Sie als Nächstes unten im Column, direkt unter der zweiten Text-Instanz, eine Schaltfläche hinzu.

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'),
          ),

        ],
      ),
    );

// ...

Wenn Sie die Änderung speichern, wird die App noch einmal aktualisiert: Es wird eine Schaltfläche angezeigt. Wenn Sie darauf klicken, wird in der Debug Console in VS Code die Meldung button pressed! angezeigt.

Flutter-Crashkurs in 5 Minuten

Die Debug Console ist zwar ganz interessant, aber Sie möchten, dass die Schaltfläche etwas Sinnvolleres bewirkt. Sehen Sie sich aber zuerst den Code in lib/main.dart an, um zu verstehen, wie er funktioniert.

lib/main.dart

// ...

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

// ...

Ganz oben in der Datei finden Sie die Funktion main(). In der aktuellen Form wird Flutter nur angewiesen, die in MyApp definierte App auszuführen.

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

// ...

Die Klasse MyApp erweitert StatelessWidget. Widgets sind die Elemente, aus denen Sie jede Flutter-App erstellen. Wie Sie sehen, ist sogar die App selbst ein Widget.

Der Code in MyApp richtet die gesamte App ein. Er erstellt den appweiten Status (mehr dazu später), benennt die App, definiert das visuelle Design und legt das „Home“-Widget fest, den Ausgangspunkt Ihrer App.

lib/main.dart

// ...

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

// ...

Als Nächstes wird in der Klasse MyAppState der Status der App definiert. Dies ist Ihr erster Ausflug in Flutter. Daher ist dieses Codelab einfach und fokussiert gehalten. Es gibt viele leistungsstarke Möglichkeiten, den App-Status in Flutter zu verwalten. Einer der einfachsten Ansätze ist ChangeNotifier, der von dieser App verwendet wird.

  • MyAppState definiert die Daten, die für die Funktion der App erforderlich sind. Derzeit enthält sie nur eine Variable mit dem aktuellen zufälligen Wortpaar. Sie fügen später weitere hinzu.
  • Die Statusklasse erweitert ChangeNotifier. Das bedeutet, dass sie andere über ihre eigenen Änderungen benachrichtigen kann. Wenn sich beispielsweise das aktuelle Wortpaar ändert, müssen einige Widgets in der App darüber informiert werden.
  • Der Status wird mit einem ChangeNotifierProvider erstellt und für die gesamte App bereitgestellt (siehe Code oben in MyApp). So kann jedes Widget in der App auf den Status zugreifen.

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

// ...

Schließlich gibt es noch MyHomePage, das Widget, das Sie bereits geändert haben. Jede nummerierte Zeile unten entspricht einem Zeilennummer-Kommentar im Code oben:

  1. Jedes Widget definiert eine build()-Methode, die automatisch aufgerufen wird, wenn sich die Umstände des Widgets ändern, damit das Widget immer auf dem neuesten Stand ist.
  2. MyHomePage verfolgt Änderungen am aktuellen Status der App mithilfe der Methode watch.
  3. Jede build-Methode muss ein Widget oder (in der Regel) einen verschachtelten Baum von Widgets zurückgeben. In diesem Fall ist das Widget der obersten Ebene Scaffold. In diesem Codelab arbeiten Sie nicht mit Scaffold, aber es ist ein nützliches Widget, das in den meisten Flutter-Apps in der Praxis verwendet wird.
  4. Column ist eines der grundlegendsten Layout-Widgets in Flutter. Es werden beliebig viele untergeordnete Elemente in einer Spalte von oben nach unten angeordnet. Standardmäßig werden die untergeordneten Elemente der Spalte oben platziert. Das werden Sie aber gleich ändern, damit die Spalte zentriert wird.
  5. Sie haben dieses Text-Widget im ersten Schritt geändert.
  6. Dieses zweite Text-Widget verwendet appState und greift auf das einzige Element dieser Klasse zu, current (ein WordPair). WordPair bietet mehrere hilfreiche Getter wie asPascalCase oder asSnakeCase. Hier verwenden wir asLowerCase. Sie können dies jedoch jetzt ändern, wenn Sie eine der Alternativen bevorzugen.
  7. Beachten Sie, dass in Flutter-Code häufig nachgestellte Kommas verwendet werden. Dieses Komma ist nicht erforderlich, da children das letzte (und auch einzige) Element dieser Column-Parameterliste ist. Es ist jedoch im Allgemeinen eine gute Idee, nachgestellte Kommas zu verwenden: Sie erleichtern das Hinzufügen weiterer Elemente und dienen auch als Hinweis für den automatischen Formatierer von Dart, dort eine neue Zeile einzufügen. Weitere Informationen finden Sie unter Code formatieren.

Als Nächstes verbinden Sie die Schaltfläche mit dem Status.

Ihr erstes Verhalten

Scrollen Sie zu MyAppState und fügen Sie eine getNext-Methode hinzu.

lib/main.dart

// ...

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

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

// ...

Mit der neuen Methode getNext() wird current mit einer neuen zufälligen WordPair neu zugewiesen. Außerdem wird notifyListeners() aufgerufen, eine Methode von ChangeNotifier), die dafür sorgt, dass alle, die MyAppState beobachten, benachrichtigt werden.

Jetzt müssen Sie nur noch die Methode getNext über den Callback der Schaltfläche aufrufen.

lib/main.dart

// ...

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

// ...

Speichern Sie die App und probieren Sie sie jetzt aus. Bei jedem Drücken der Schaltfläche Weiter sollte ein neues zufälliges Wortpaar generiert werden.

Im nächsten Abschnitt gestalten Sie die Benutzeroberfläche ansprechender.

5. App ansprechender gestalten

So sieht die App derzeit aus.

3dd8a9d8653bdc56.png

Nicht gut. Das Herzstück der App, das zufällig generierte Wortpaar, sollte besser sichtbar sein. Das ist schließlich der Hauptgrund, warum unsere Nutzer diese App verwenden. Außerdem sind die App-Inhalte seltsam zentriert und die gesamte App ist langweilig schwarz-weiß.

In diesem Abschnitt werden diese Probleme durch die Überarbeitung des App-Designs behoben. Das Endziel für diesen Abschnitt ist etwa Folgendes:

2bbee054d81a3127.png

Widget extrahieren

Die Zeile, die für die Anzeige des aktuellen Wortpaars verantwortlich ist, sieht jetzt so aus: Text(appState.current.asLowerCase). Wenn Sie sie in etwas Komplexeres ändern möchten, ist es eine gute Idee, diese Zeile in ein separates Widget zu extrahieren. Separate Widgets für separate logische Teile der Benutzeroberfläche sind eine wichtige Möglichkeit, die Komplexität in Flutter zu verringern.

Flutter bietet eine Refactoring-Hilfe zum Extrahieren von Widgets. Bevor Sie sie verwenden, sollten Sie jedoch darauf achten, dass die extrahierte Zeile nur auf das zugreift, was sie benötigt. Derzeit greift die Zeile auf appState zu, muss aber eigentlich nur wissen, welches Wortpaar gerade verwendet wird.

Schreiben Sie das MyHomePage-Widget daher so um:

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

// ...

Schön. Das Text-Widget bezieht sich nicht mehr auf die gesamte appState.

Rufen Sie nun das Menü Refactor auf. In VS Code haben Sie dafür zwei Möglichkeiten:

  1. Klicken Sie mit der rechten Maustaste auf den Code, den Sie umgestalten möchten (in diesem Fall Text), und wählen Sie im Drop-down-Menü Umgestalten… aus.

ODER

  1. Bewegen Sie den Cursor auf den Codeabschnitt, den Sie umgestalten möchten (in diesem Fall Text), und drücken Sie Ctrl+. (Windows/Linux) oder Cmd+. (Mac).

Wählen Sie im Menü Refactor (Umgestalten) die Option Extract Widget (Widget extrahieren) aus. Weisen Sie einen Namen wie BigCard zu und klicken Sie auf Enter.

Dadurch wird automatisch eine neue Klasse, BigCard, am Ende der aktuellen Datei erstellt. Die Klasse sieht in etwa so aus:

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

// ...

Beachten Sie, dass die App auch während dieses Refactorings weiter funktioniert.

Hinzufügen einer Karte

Jetzt ist es an der Zeit, dieses neue Widget in das auffällige UI-Element zu verwandeln, das wir uns am Anfang dieses Abschnitts vorgestellt haben.

Suchen Sie die Klasse BigCard und die Methode build() darin. Rufen Sie wie bisher das Menü Refactor (Umgestalten) im Text-Widget auf. Diesmal extrahieren Sie das Widget jedoch nicht.

Wählen Sie stattdessen Mit Padding umbrechen aus. Dadurch wird ein neues übergeordnetes Widget namens Padding um das Widget Text erstellt. Nach dem Speichern hat das zufällige Wort mehr Platz.

Erhöhen Sie den Abstand vom Standardwert 8.0. Verwenden Sie beispielsweise 20 für einen größeren Abstand.

Gehen Sie als Nächstes eine Ebene höher. Bewegen Sie den Mauszeiger auf das Padding-Widget, rufen Sie das Menü Refactor auf und wählen Sie Wrap with widget... aus.

So können Sie das übergeordnete Widget angeben. Geben Sie „Card“ ein und drücken Sie die Eingabetaste.

Dadurch wird das Padding-Widget und damit auch das Text-Widget in ein Card-Widget eingeschlossen.

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

// ...

Die App sieht nun in etwa so aus:

6031adbc0a11e16b.png

Design und Stil

Um die Karte besser hervorzuheben, können Sie sie mit einer kräftigeren Farbe bemalen. Da es immer eine gute Idee ist, ein einheitliches Farbschema zu verwenden, können Sie die Farbe über das Theme der App auswählen.

Nehmen Sie die folgenden Änderungen an der build()-Methode von BigCard vor.

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

// ...

Diese beiden neuen Zeilen erledigen viel Arbeit:

  • Zuerst wird mit Theme.of(context) das aktuelle Design der App angefordert.
  • Anschließend wird die Farbe der Karte auf dieselbe Farbe wie die colorScheme-Eigenschaft des Designs festgelegt. Das Farbschema enthält viele Farben und primary ist die wichtigste Farbe der App.

Die Karte wird jetzt in der Primärfarbe der App dargestellt:

a136f7682c204ea1.png

Sie können diese Farbe und das Farbschema der gesamten App ändern, indem Sie nach oben zu MyApp scrollen und dort die Ausgangsfarbe für ColorScheme ändern.

Die Farbe wird flüssig animiert. Das wird als implizite Animation bezeichnet. Viele Flutter-Widgets interpolieren Werte fließend, sodass die Benutzeroberfläche nicht einfach zwischen Zuständen „springt“.

Die Farbe der erhöhten Schaltfläche unter der Karte ändert sich ebenfalls. Das ist der Vorteil der Verwendung eines app-weiten Theme im Gegensatz zur Hartcodierung von Werten.

TextTheme

Die Karte hat immer noch ein Problem: Der Text ist zu klein und seine Farbe ist schwer zu lesen. Nehmen Sie dazu die folgenden Änderungen an der build()-Methode von BigCard vor.

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

// ...

Hintergrund dieser Änderung:

  • Mit theme.textTheme, greifen Sie auf das Schriftartdesign der App zu. Diese Klasse umfasst Elemente wie bodyMedium (für Standardtext mittlerer Größe), caption (für Bildunterschriften) oder headlineLarge (für große Überschriften).
  • Das Attribut displayMedium ist ein großer Stil, der für Anzeigetext vorgesehen ist. Das Wort display wird hier im typografischen Sinne verwendet, z. B. in display typeface (Schriftart für Überschriften). In der Dokumentation für displayMedium heißt es, dass „Anzeigestile für kurzen, wichtigen Text reserviert sind“ – genau unser Anwendungsfall.
  • Die displayMedium-Eigenschaft des Designs könnte theoretisch null sein. Dart, die Programmiersprache, in der Sie diese App schreiben, ist null-safe. Sie können also keine Methoden von Objekten aufrufen, die möglicherweise null sind. In diesem Fall können Sie jedoch den !-Operator („Bang-Operator“) verwenden, um Dart zu versichern, dass Sie wissen, was Sie tun. displayMedium ist in diesem Fall definitiv nicht null. Warum das so ist, wird in diesem Codelab nicht behandelt.
  • Wenn Sie copyWith() für displayMedium aufrufen, wird eine Kopie des Textstils mit den von Ihnen definierten Änderungen zurückgegeben. In diesem Fall ändern Sie nur die Farbe des Textes.
  • Um die neue Farbe zu erhalten, greifen Sie noch einmal auf das Design der App zu. Die onPrimary-Property des Farbschemas definiert eine Farbe, die sich gut für die Verwendung auf der primären Farbe der App eignet.

Die App sollte jetzt in etwa so aussehen:

2405e9342d28c193.png

Wenn Sie möchten, können Sie die Karte weiter bearbeiten. Hier einige Tipps:

  • Mit copyWith() können Sie viel mehr als nur die Farbe des Textes ändern. Wenn Sie die vollständige Liste der Eigenschaften aufrufen möchten, die Sie ändern können, setzen Sie den Cursor an eine beliebige Stelle in die Klammern von copyWith() und drücken Sie Ctrl+Shift+Space (Windows/Linux) oder Cmd+Shift+Space (Mac).
  • Ebenso können Sie weitere Einstellungen für das Card-Widget ändern. Sie können beispielsweise den Schatten der Karte vergrößern, indem Sie den Wert des Parameters elevation erhöhen.
  • Experimentieren Sie mit Farben. Neben theme.colorScheme.primary gibt es auch .secondary, .surface und viele andere. Für alle diese Farben gibt es onPrimary-Entsprechungen.

Inhalte besser zugänglich machen

Flutter macht Apps standardmäßig barrierefrei. So werden beispielsweise alle Text- und interaktiven Elemente in jeder Flutter-App korrekt für Screenreader wie TalkBack und VoiceOver angezeigt.

d1fad7944fb890ea.png

Manchmal ist jedoch etwas Arbeit erforderlich. Bei dieser App hat der Screenreader möglicherweise Probleme, einige generierte Wortpaare auszusprechen. Während Menschen keine Probleme haben, die beiden Wörter in cheaphead zu identifizieren, spricht ein Screenreader das ph in der Mitte des Wortes möglicherweise als f aus.

Eine Lösung besteht darin, pair.asLowerCase durch "${pair.first} ${pair.second}" zu ersetzen. Bei der zweiten Methode wird die String-Interpolation verwendet, um aus den beiden Wörtern in pair einen String (z. B. "cheap head") zu erstellen. Wenn Sie zwei separate Wörter anstelle eines zusammengesetzten Worts verwenden, können Screenreader sie richtig erkennen. Das ist für sehbehinderte Nutzer hilfreich.

Möglicherweise möchten Sie jedoch die visuelle Einfachheit von pair.asLowerCase beibehalten. Verwenden Sie die semanticsLabel-Eigenschaft von Text, um den visuellen Inhalt des Text-Widgets mit einem semantischen Inhalt zu überschreiben, der für Screenreader besser geeignet ist:

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

// ...

Screenreader sprechen jetzt jedes generierte Wortpaar richtig aus, die Benutzeroberfläche bleibt jedoch unverändert. Probieren Sie es auf Ihrem Gerät aus.

Benutzeroberfläche zentrieren

Nachdem das zufällige Wortpaar nun ansprechend präsentiert wird, ist es an der Zeit, es in der Mitte des App-Fensters bzw. des Bildschirms zu platzieren.

BigCard ist Teil eines Column. Standardmäßig werden untergeordnete Elemente in Spalten oben zusammengefasst. Das lässt sich jedoch überschreiben. Rufen Sie die Methode build() von MyHomePage auf und nehmen Sie die folgende Änderung vor:

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

// ...

Dadurch werden die untergeordneten Elemente innerhalb des Column entlang der Hauptachse (vertikal) zentriert.

b555d4c7f5000edf.png

Die untergeordneten Elemente sind bereits entlang der Querachse der Spalte zentriert (d. h. sie sind bereits horizontal zentriert). Das Column selbst ist jedoch nicht in der Mitte des Scaffold zentriert. Wir können dies mit dem Widget Inspector überprüfen.

Der Widget Inspector selbst geht über den Rahmen dieses Codelabs hinaus. Sie können jedoch sehen, dass das Column nicht die gesamte Breite der App einnimmt, sondern nur so viel horizontalen Platz, wie seine untergeordneten Elemente benötigen.

Sie können die Spalte einfach zentrieren. Bewegen Sie den Cursor auf Column, rufen Sie das Menü Refactor (mit Ctrl+. oder Cmd+.) auf und wählen Sie Wrap with Center aus.

Die App sollte jetzt in etwa so aussehen:

455688d93c30d154.png

Wenn Sie möchten, können Sie diese Einstellungen noch weiter anpassen.

  • Sie können das Text-Widget über BigCard entfernen. Man könnte argumentieren, dass der beschreibende Text („Eine zufällige TOLLE Idee:“) nicht mehr erforderlich ist, da die Benutzeroberfläche auch ohne ihn verständlich ist. So ist es übersichtlicher.
  • Sie können auch ein SizedBox(height: 10)-Widget zwischen BigCard und ElevatedButton einfügen. So ist etwas mehr Abstand zwischen den beiden Widgets. Das SizedBox-Widget nimmt nur Platz ein und rendert nichts. Sie wird häufig verwendet, um visuelle „Lücken“ zu erstellen.

Mit den optionalen Änderungen enthält MyHomePage diesen Code:

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

// ...

Die App sieht so aus:

3d53d2b071e2f372.png

Im nächsten Abschnitt fügen Sie die Möglichkeit hinzu, generierte Wörter zu favorisieren („liken“).

6. Funktionen hinzufügen

Die App funktioniert und liefert gelegentlich sogar interessante Wortpaare. Wenn der Nutzer jedoch auf Weiter klickt, verschwindet jedes Wortpaar für immer. Es wäre besser, wenn es eine Möglichkeit gäbe, die besten Vorschläge zu „merken“, z. B. durch eine Schaltfläche „Gefällt mir“.

e6b01a8c90df8ffa.png

Geschäftslogik hinzufügen

Scrollen Sie zu MyAppState und fügen Sie den folgenden Code hinzu:

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

// ...

Prüfen Sie die Änderungen:

  • Sie haben der Property MyAppState eine neue Property namens favorites hinzugefügt. Diese Property wird mit einer leeren Liste initialisiert: [].
  • Sie haben außerdem angegeben, dass die Liste nur Wortpaare enthalten darf: <WordPair>[], indem Sie Generics verwendet haben. Dadurch wird Ihre App robuster. Dart weigert sich sogar, Ihre App auszuführen, wenn Sie versuchen, etwas anderes als WordPair hinzuzufügen. Sie können die favorites-Liste verwenden, da Sie wissen, dass sich darin keine unerwünschten Objekte wie null befinden können.
  • Außerdem haben Sie eine neue Methode, toggleFavorite(), hinzugefügt, die das aktuelle Wortpaar entweder aus der Liste der Favoriten entfernt (falls es bereits vorhanden ist) oder es hinzufügt (falls es noch nicht vorhanden ist). In beiden Fällen wird danach notifyListeners(); aufgerufen.

Schaltfläche hinzufügen

Nachdem die Geschäftslogik erledigt ist, ist es an der Zeit, sich wieder der Benutzeroberfläche zu widmen. Wenn die Schaltfläche „Gefällt mir“ links neben der Schaltfläche „Weiter“ platziert werden soll, ist eine Row erforderlich. Das Row-Widget ist das horizontale Äquivalent von Column, das Sie bereits gesehen haben.

Schließen Sie die vorhandene Schaltfläche zuerst in ein Row ein. Gehen Sie zur Methode MyHomePage von build(), bewegen Sie den Cursor auf ElevatedButton, rufen Sie das Menü Refactor mit Ctrl+. oder Cmd+. auf und wählen Sie Wrap with Row aus.

Wenn Sie die Änderungen speichern, sehen Sie, dass sich Row ähnlich wie Column verhält. Standardmäßig werden die untergeordneten Elemente links zusammengefasst. (Column hat die untergeordneten Elemente nach oben verschoben.) Um das Problem zu beheben, können Sie denselben Ansatz wie zuvor verwenden, aber mit mainAxisAlignment. Verwenden Sie jedoch für didaktische Zwecke mainAxisSize. Dadurch wird Row angewiesen, nicht den gesamten verfügbaren horizontalen Platz einzunehmen.

Nehmen Sie die folgende Änderung vor:

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

// ...

Die Benutzeroberfläche ist wieder so, wie sie vorher war.

3d53d2b071e2f372.png

Fügen Sie als Nächstes die Schaltfläche Gefällt mir hinzu und verbinden Sie sie mit toggleFavorite(). Versuchen Sie es zuerst selbst, ohne sich den Codeblock unten anzusehen.

e6b01a8c90df8ffa.png

Es ist in Ordnung, wenn Sie nicht genau so vorgehen wie unten beschrieben. Tatsächlich musst du dich nicht um das Herzsymbol kümmern, es sei denn, du möchtest eine große Herausforderung.

Es ist auch völlig in Ordnung, wenn etwas nicht klappt – schließlich ist das deine erste Stunde mit Flutter.

252f7c4a212c94d2.png

Hier ist eine Möglichkeit, die zweite Schaltfläche zu MyHomePage hinzuzufügen. Verwenden Sie dieses Mal den ElevatedButton.icon()-Konstruktor, um eine Schaltfläche mit einem Symbol zu erstellen. Wählen Sie oben in der build-Methode das entsprechende Symbol aus, je nachdem, ob das aktuelle Wortpaar bereits in den Favoriten enthalten ist. Beachten Sie auch hier die Verwendung von SizedBox, um die beiden Schaltflächen etwas voneinander zu trennen.

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

// ...

Die App sollte so aussehen:

Leider kann der Nutzer die Favoriten nicht sehen. Es ist an der Zeit, unserer App einen ganz neuen Bildschirm hinzuzufügen. Wir sehen uns im nächsten Abschnitt!

7. Navigationsstreifen hinzufügen

Die meisten Apps können nicht alle Inhalte auf einem einzigen Bildschirm darstellen. Diese spezielle App könnte das wahrscheinlich, aber aus didaktischen Gründen erstellen Sie einen separaten Bildschirm für die Favoriten des Nutzers. Um zwischen den beiden Bildschirmen zu wechseln, implementieren Sie Ihr erstes StatefulWidget.

f62c54f5401a187.png

Um so schnell wie möglich zum Kern dieses Schritts zu gelangen, teilen Sie MyHomePage in zwei separate Widgets auf.

Wählen Sie den gesamten Code in MyHomePage aus, löschen Sie ihn und ersetzen Sie ihn durch den folgenden Code:

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

// ...

Nach dem Speichern sehen Sie, dass die visuelle Seite der Benutzeroberfläche fertig ist, sie funktioniert aber nicht. Wenn Sie in der Navigationsleiste auf ♥︎ (das Herz) klicken, passiert nichts.

388bc25fe198c54a.png

Sehen Sie sich die Änderungen an.

  • Der gesamte Inhalt von MyHomePage wird in ein neues Widget, GeneratorPage, extrahiert. Der einzige Teil des alten MyHomePage-Widgets, der nicht extrahiert wurde, ist Scaffold.
  • Das neue MyHomePage enthält ein Row mit zwei untergeordneten Elementen. Das erste Widget ist SafeArea und das zweite ein Expanded-Widget.
  • Das SafeArea sorgt dafür, dass das untergeordnete Element nicht durch eine Hardware-Aussparung oder eine Statusleiste verdeckt wird. In dieser App wird das Widget um NavigationRail gewrappt, um zu verhindern, dass die Navigationsschaltflächen beispielsweise durch eine mobile Statusleiste verdeckt werden.
  • Sie können die Zeile extended: false in NavigationRail in true ändern. So werden die Labels neben den Symbolen angezeigt. In einem späteren Schritt erfahren Sie, wie Sie das automatisch erledigen lassen können, wenn die App genügend horizontalen Platz hat.
  • Die Navigationsleiste hat zwei Ziele (Startseite und Favoriten) mit den entsprechenden Symbolen und Labels. Außerdem wird die aktuelle selectedIndex definiert. Bei einem ausgewählten Index von null wird das erste Ziel ausgewählt, bei einem ausgewählten Index von eins das zweite Ziel usw. Derzeit ist der Wert fest auf null codiert.
  • Außerdem wird festgelegt, was passiert, wenn der Nutzer eines der Ziele mit onDestinationSelected auswählt. Derzeit gibt die App nur den angeforderten Indexwert mit print() aus.
  • Das zweite untergeordnete Element von Row ist das Expanded-Widget. Erweiterte Widgets sind in Zeilen und Spalten äußerst nützlich. Sie ermöglichen es Ihnen, Layouts zu erstellen, in denen einige untergeordnete Elemente nur so viel Platz einnehmen, wie sie benötigen (in diesem Fall SafeArea), und andere Widgets so viel Platz wie möglich einnehmen sollen (in diesem Fall Expanded). Expanded-Widgets sind in gewisser Weise „gierig“. Wenn Sie ein besseres Gefühl für die Rolle dieses Widgets bekommen möchten, können Sie das SafeArea-Widget in ein anderes Expanded-Widget einfügen. Das resultierende Layout sieht in etwa so aus:

6bbda6c1835a1ae.png

  • Zwei Expanded-Widgets teilen den gesamten verfügbaren horizontalen Platz untereinander auf, obwohl die Navigationsleiste nur einen kleinen Bereich auf der linken Seite benötigt.
  • Im Expanded-Widget befindet sich ein farbiges Container und im Container das GeneratorPage.

Zustandslose und zustandsorientierte Widgets

Bisher hat MyAppState alle Ihre Anforderungen erfüllt. Deshalb sind alle Widgets, die Sie bisher geschrieben haben, zustandslos. Sie enthalten keinen eigenen veränderlichen Status. Keines der Widgets kann sich selbst ändern – Änderungen müssen über MyAppState erfolgen.

Das ändert sich jetzt.

Sie benötigen eine Möglichkeit, den Wert von selectedIndex der Navigationsleiste zu speichern. Sie möchten diesen Wert auch über den onDestinationSelected-Callback ändern können.

Sie könnten selectedIndex als weitere Property von MyAppState hinzufügen. Und es würde funktionieren. Stellen Sie sich aber vor, wie schnell der App-Status unübersichtlich würde, wenn jedes Widget seine Werte darin speicherte.

e52d9c0937cc0823.jpeg

Einige Status sind nur für ein einzelnes Widget relevant und sollten daher bei diesem Widget bleiben.

Geben Sie die StatefulWidget ein, eine Art von Widget mit State. Wandeln Sie MyHomePage zuerst in ein zustandsorientiertes Widget um.

Bewegen Sie den Cursor auf die erste Zeile von MyHomePage (die mit class MyHomePage... beginnt) und rufen Sie das Menü Refactor (Umgestalten) mit Ctrl+. oder Cmd+. auf. Wählen Sie dann Convert to StatefulWidget (In StatefulWidget konvertieren) aus.

Die IDE erstellt eine neue Klasse für Sie: _MyHomePageState. Diese Klasse erweitert State und kann daher ihre eigenen Werte verwalten. (Es kann sich selbst ändern.) Die Methode build aus dem alten, zustandslosen Widget wurde in _MyHomePageState verschoben (anstatt im Widget zu bleiben). Sie wurde unverändert verschoben – es hat sich nichts an der build-Methode geändert. Sie befindet sich jetzt nur an einem anderen Ort.

setState

Das neue zustandsorientierte Widget muss nur eine Variable erfassen: selectedIndex. Nehmen Sie die folgenden drei Änderungen an _MyHomePageState vor:

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

// ...

Prüfen Sie die Änderungen:

  1. Sie führen eine neue Variable selectedIndex ein und initialisieren sie mit 0.
  2. Sie verwenden diese neue Variable in der NavigationRail-Definition anstelle des bisherigen hartcodierten 0.
  3. Wenn der onDestinationSelected-Callback aufgerufen wird, weisen Sie den neuen Wert nicht nur der Konsole zu, sondern weisen ihn selectedIndex innerhalb eines setState()-Aufrufs zu. Dieser Aufruf ähnelt der zuvor verwendeten Methode notifyListeners(). Er sorgt dafür, dass die Benutzeroberfläche aktualisiert wird.

Die Navigationsleiste reagiert jetzt auf Nutzerinteraktionen. Der erweiterte Bereich auf der rechten Seite bleibt jedoch unverändert. Das liegt daran, dass im Code nicht selectedIndex verwendet wird, um zu bestimmen, welcher Bildschirm angezeigt wird.

selectedIndex verwenden

Fügen Sie den folgenden Code oben in die build-Methode von _MyHomePageState ein, direkt vor 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');
}

// ...

Sehen Sie sich diesen Code an:

  1. Im Code wird eine neue Variable page vom Typ Widget deklariert.
  2. Anschließend wird mit einer Switch-Anweisung page ein Bildschirm zugewiesen, je nach dem aktuellen Wert in selectedIndex.
  3. Da es noch kein FavoritesPage gibt, verwenden Sie Placeholder. Dieses praktische Widget zeichnet ein durchgestrichenes Rechteck an der Stelle, an der Sie es platzieren, und kennzeichnet diesen Teil der Benutzeroberfläche als unfertig.

5685cf886047f6ec.png

  1. Gemäß dem Fail-Fast-Prinzip wird mit der switch-Anweisung auch ein Fehler ausgegeben, wenn selectedIndex weder 0 noch 1 ist. So lassen sich Fehler von vornherein vermeiden. Wenn Sie der Navigationsleiste ein neues Ziel hinzufügen und vergessen, diesen Code zu aktualisieren, stürzt das Programm in der Entwicklung ab. So müssen Sie nicht raten, warum etwas nicht funktioniert, und Sie können keinen fehlerhaften Code in der Produktion veröffentlichen.

Da page das Widget enthält, das Sie rechts anzeigen möchten, können Sie wahrscheinlich erraten, welche andere Änderung erforderlich ist.

So sieht _MyHomePageState nach dieser einen verbleibenden Änderung aus:

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

// ...

Die App wechselt jetzt zwischen unserem GeneratorPage und dem Platzhalter, der bald zur Seite Favoriten wird.

Ansprechbarkeit

Als Nächstes machen Sie die Navigationsleiste responsiv. Das bedeutet, dass die Labels automatisch (mit extended: true) angezeigt werden, wenn genügend Platz dafür vorhanden ist.

a8873894c32e0d0b.png

Flutter bietet mehrere Widgets, mit denen Sie Ihre Apps automatisch responsiv gestalten können. Wrap ist beispielsweise ein Widget, das Row oder Column ähnelt und untergeordnete Elemente automatisch in die nächste „Zeile“ (als „Run“ bezeichnet) umbricht, wenn nicht genügend vertikaler oder horizontaler Platz vorhanden ist. Es gibt FittedBox, ein Widget, das sein untergeordnetes Element automatisch an den verfügbaren Platz anpasst.

NavigationRail zeigt Labels jedoch nicht automatisch an, wenn genügend Platz vorhanden ist, da nicht bekannt ist, was in jedem Kontext genügend Platz ist. Es liegt an Ihnen als Entwickler, diesen Aufruf zu starten.

Angenommen, Sie möchten Labels nur anzeigen, wenn MyHomePage mindestens 600 Pixel breit ist.

Das zu verwendende Widget ist in diesem Fall LayoutBuilder. Damit können Sie die Struktur Ihres Widgets je nach verfügbarem Platz ändern.

Verwenden Sie noch einmal das Menü Refactor von Flutter in VS Code, um die erforderlichen Änderungen vorzunehmen. Dieses Mal ist es jedoch etwas komplizierter:

  1. Setzen Sie den Cursor in der Methode build von _MyHomePageState auf Scaffold.
  2. Rufen Sie das Menü Refactor mit Ctrl+. (Windows/Linux) oder Cmd+. (Mac) auf.
  3. Wählen Sie Mit Builder umschließen aus und drücken Sie die Eingabetaste.
  4. Ändern Sie den Namen des neu hinzugefügten Builder in LayoutBuilder.
  5. Ändern Sie die Liste der Callback-Parameter von (context) in (context, constraints).

Der builder-Callback von LayoutBuilder wird jedes Mal aufgerufen, wenn sich die Einschränkungen ändern. Das passiert beispielsweise in folgenden Fällen:

  • Der Nutzer ändert die Größe des App-Fensters.
  • Der Nutzer dreht sein Smartphone vom Hochformat ins Querformat oder umgekehrt.
  • Einige Widgets neben MyHomePage werden größer, wodurch die Einschränkungen von MyHomePage kleiner werden.

Ihr Code kann jetzt entscheiden, ob das Label angezeigt werden soll, indem er die aktuelle constraints abfragt. Nehmen Sie die folgende einzeilige Änderung an der build-Methode von _MyHomePageState vor:

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


// ...

Ihre App reagiert jetzt auf die Umgebung, z. B. Bildschirmgröße, Ausrichtung und Plattform. Mit anderen Worten: Es ist responsiv.

Die einzige Aufgabe, die noch aussteht, ist, Placeholder durch einen tatsächlichen Favoriten-Bildschirm zu ersetzen. Das wird im nächsten Abschnitt behandelt.

8. Neue Seite hinzufügen

Erinnerst du dich an das Placeholder-Widget, das wir anstelle der Seite Favoriten verwendet haben?

Es ist an der Zeit, das zu ändern.

Wenn Sie sich das zutrauen, können Sie diesen Schritt auch selbst ausführen. Ihr Ziel ist es, die Liste der favorites in einem neuen zustandslosen Widget, FavoritesPage, anzuzeigen und dieses Widget dann anstelle von Placeholder zu verwenden.

Hier einige Hinweise:

  • Wenn Sie ein Column mit Scrollfunktion wünschen, verwenden Sie das ListView-Widget.
  • Denken Sie daran, dass Sie mit context.watch<MyAppState>() von jedem Widget aus auf die MyAppState-Instanz zugreifen können.
  • Wenn Sie auch ein neues Widget ausprobieren möchten, hat ListTile Eigenschaften wie title (im Allgemeinen für Text), leading (für Symbole oder Avatare) und onTap (für Interaktionen). Mit den bekannten Widgets lassen sich jedoch ähnliche Effekte erzielen.
  • In Dart können for-Schleifen in Collection-Literalen verwendet werden. Wenn messages beispielsweise eine Liste von Strings enthält, können Sie Code wie den folgenden verwenden:

f0444bba08f205aa.png

Wenn Sie sich mit funktionaler Programmierung besser auskennen, können Sie in Dart auch Code wie messages.map((m) => Text(m)).toList() schreiben. Sie können natürlich auch jederzeit eine Liste von Widgets erstellen und sie in der Methode build imperativ ergänzen.

Der Vorteil, die Seite Favoriten selbst hinzuzufügen, besteht darin, dass Sie mehr lernen, wenn Sie Ihre eigenen Entscheidungen treffen. Der Nachteil ist, dass Sie möglicherweise auf Probleme stoßen, die Sie noch nicht selbst lösen können. Denken Sie daran: Scheitern ist in Ordnung und einer der wichtigsten Bestandteile des Lernens. Niemand erwartet, dass Sie Flutter-Entwicklung in der ersten Stunde beherrschen, und Sie sollten das auch nicht tun.

252f7c4a212c94d2.png

Im Folgenden wird eine Möglichkeit zur Implementierung der Seite „Favoriten“ beschrieben. Die Implementierung soll Sie (hoffentlich) dazu anregen, mit dem Code zu experimentieren, die Benutzeroberfläche zu verbessern und sie an Ihre Bedürfnisse anzupassen.

Hier ist die neue Klasse 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),
          ),
      ],
    );
  }
}

Das kann das Widget:

  • Ruft den aktuellen Status der App ab.
  • Wenn die Favoritenliste leer ist, wird die zentrierte Meldung Noch keine Favoriten angezeigt.
  • Andernfalls wird eine (scrollbare) Liste angezeigt.
  • Die Liste beginnt mit einer Zusammenfassung (z. B. Du hast 5 Favoriten.).
  • Der Code durchläuft dann alle Favoriten und erstellt für jeden ein ListTile-Widget.

Jetzt muss nur noch das Placeholder-Widget durch ein FavoritesPage-Widget ersetzt werden. Und voilà!

Den endgültigen Code dieser App finden Sie im Codelab-Repository auf GitHub.

9. Nächste Schritte

Glückwunsch!

Sieh mal einer an! Sie haben ein nicht funktionales Scaffold mit einem Column- und zwei Text-Widgets genommen und daraus eine responsive, ansprechende kleine App gemacht.

d6e3d5f736411f13.png

Behandelte Themen

  • Grundlagen der Funktionsweise von Flutter
  • Layouts in Flutter erstellen
  • Nutzerinteraktionen (z. B. Tastendrücke) mit dem App-Verhalten verknüpfen
  • Flutter-Code organisieren
  • App responsiv gestalten
  • Einheitliches Erscheinungsbild Ihrer App

Nächste Schritte

  • Experimentieren Sie weiter mit der App, die Sie in diesem Lab geschrieben haben.
  • Im Code dieser erweiterten Version derselben App sehen Sie, wie Sie animierte Listen, Verläufe, Überblendungen und mehr hinzufügen können.