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 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. Es ist nur so, dass ein klares Entwicklungsziel den nächsten Schritt einfacher macht.

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.

Häufig gestellte Fragen

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 bilden 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 ein Weiter-Button hinzugefügt, mit dem 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. Noch ist 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 aber 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

Button hinzufügen

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

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 ein Button 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 interessant, aber die Schaltfläche soll natürlich etwas Sinnvolleres tun. 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 jede Flutter-App besteht. 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 Startpunkt 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. Da dies Ihr erster Ausflug in Flutter ist, wird dieses Codelab einfach und fokussiert gehalten. Es gibt viele leistungsstarke Möglichkeiten, den App-Status in Flutter zu verwalten. Am einfachsten lässt sich ChangeNotifier erklären, der Ansatz, 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 Informationen 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 mit 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 kann eine beliebige Anzahl von untergeordneten Elementen enthalten, die von oben nach unten in einer Spalte angeordnet werden. 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 aber jetzt ändern, wenn Sie eine der Alternativen bevorzugen.
  7. Beachten Sie, dass im 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. Jedes Mal, wenn Sie auf die Schaltfläche Weiter klicken, sollte ein neues zufälliges Wortpaar generiert werden.

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

5. App schöner gestalten

So sieht die App derzeit aus.

3dd8a9d8653bdc56.png

Alles andere als großartig. 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 sinnvoll, 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 verwalten.

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 zuvor 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 Text-Widget erstellt. Nach dem Speichern ist das zufällige Wort nicht mehr so nah am Rand.

Erhöhen Sie den Abstand vom Standardwert 8.0. Verwenden Sie beispielsweise 20 für eine größere Innenpolsterung.

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 etwa so aus:

6031adbc0a11e16b.png

Thema und Stil

Um die Karte besser hervorzuheben, können Sie sie mit einer kräftigeren Farbe gestalten. 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. 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 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 reibungslos zwischen Werten, sodass die Benutzeroberfläche nicht einfach zwischen den Status „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 die folgenden Änderungen an der build()-Methode von BigCard vor, um das Problem zu beheben.

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ßes Format, das 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 kurze, wichtige Texte 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 Textformats mit den von Ihnen definierten Änderungen zurückgegeben. In diesem Fall ändern Sie nur die Farbe des Textes.
  • Um die neue Farbe zu erhalten, müssen Sie noch einmal auf das Design der App zugreifen. Die onPrimary-Eigenschaft 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 Texte 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 die beiden Wörter in cheaphead problemlos erkennen, 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 einen String (z. B. "cheap head") aus den beiden Wörtern in pair zu erstellen. Wenn Sie zwei separate Wörter anstelle eines zusammengesetzten Worts verwenden, können Screenreader sie richtig identifizieren. 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 durch einen 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 korrekt aus, die Benutzeroberfläche bleibt jedoch unverändert. Probieren Sie es auf Ihrem Gerät aus.

Benutzeroberfläche zentrieren

Nachdem das zufällige Wortpaar ansprechend präsentiert wurde, kann es in der Mitte des App-Fensters bzw. des Bildschirms platziert werden.

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 im Column entlang der Hauptachse (vertikal) zentriert.

b555d4c7f5000edf.png

Die Kinder sind bereits entlang der Querachse der Spalte zentriert (d. h. sie sind bereits horizontal zentriert). Das Column selbst ist jedoch nicht mittig im Scaffold platziert. 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, wenn es hervorgehoben ist, nicht die gesamte Breite der App einnimmt. Es nimmt nur so viel horizontalen Platz ein, wie seine untergeordneten Elemente benötigen.

Sie können einfach die Spalte selbst 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 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 benötigt wird, 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 oder zu 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();
  }
}

// ...

Änderungen prüfen:

  • Sie haben der Property MyAppState eine neue Property namens favorites hinzugefügt. Diese Eigenschaft wird mit einer leeren Liste initialisiert: [].
  • Sie haben außerdem angegeben, dass die Liste nur Wortpaare enthalten darf: <WordPair>[], indem Sie Generics verwendet haben. So wird Ihre App robuster. Dart weigert sich, 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, mit der das aktuelle Wortpaar entweder aus der Liste der Favoriten entfernt (falls es bereits vorhanden ist) oder hinzugefügt wird (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, wieder an der Benutzeroberfläche zu arbeiten. Wenn Sie die Schaltfläche „Gefällt mir“ links neben der Schaltfläche „Weiter“ platzieren möchten, 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-Tag ein. Gehen Sie zur Methode build() von MyHomePage, 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. Untergeordnete Knoten werden standardmäßig 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 (Lernen) 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 den Like-Button hinzu und verbinden Sie ihn mit toggleFavorite(). Versuchen Sie, das selbst zu tun, 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 dir keine Gedanken über das Herzsymbol machen, es sei denn, du möchtest wirklich 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 zu den Favoriten gehört. 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. Navigationsleiste 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 diesen Schritt so schnell wie möglich zu durchlaufen, 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'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

Wenn Sie die Datei 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.

  • Zuerst wird der gesamte Inhalt von MyHomePage 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-Notch 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. Dadurch 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.
  • Die Navigationsleiste definiert auch, was passiert, wenn der Nutzer eines der Ziele mit onDestinationSelected auswählt. Derzeit gibt die App lediglich 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, 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 Eigenschaft 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 zustandsbehaftetes 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. (Sie kann sich selbst ändern.) Beachten Sie auch, dass die Methode build aus dem alten, zustandslosen Widget in die _MyHomePageState verschoben wurde (anstatt im Widget zu bleiben). Sie wurde unverändert verschoben – nichts in der build-Methode hat sich geändert. Sie befindet sich jetzt nur an einem anderen Ort.

setState

Das neue zustandsorientierte Widget muss nur eine Variable verfolgen: 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(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Änderungen prüfen:

  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 selectedIndex innerhalb eines setState()-Aufrufs zu, sondern geben ihn auch in der Konsole aus. 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 am Anfang der 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 ein Bildschirm für page 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 jetzt 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 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 angezeigt werden (mit extended: true), wenn genügend Platz dafür vorhanden ist.

a8873894c32e0d0b.png

Flutter bietet mehrere Widgets, mit denen Sie Ihre Apps automatisch responsiv machen 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 seine untergeordneten Elemente 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, dem Entwickler, diesen Aufruf zu tätigen.

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 den Widget-Baum je nach verfügbarem Speicherplatz ä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 umbrechen 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 kann beispielsweise folgende Gründe haben:

  • 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 für MyHomePage kleiner werden.

Ihr Code kann jetzt entscheiden, ob das Label angezeigt werden soll, indem er den aktuellen 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 ihre Umgebung, z. B. auf Bildschirmgröße, Ausrichtung und Plattform. Mit anderen Worten: Es ist responsiv.

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

8. Neue Seite hinzufügen

Erinnern Sie sich 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 sind einige Hinweise:

  • Wenn Sie ein Column mit Scrollfunktion benötigen, 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 (in der Regel 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. Natürlich können Sie auch jederzeit eine Liste von Widgets erstellen und sie in der Methode build imperativ ergänzen.

Der Vorteil, wenn Sie die Seite Favoriten selbst hinzufü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 gehört zu den wichtigsten Elementen des Lernens. Niemand erwartet, dass Sie Flutter-Entwicklung in der ersten Stunde beherrschen, und das sollten Sie auch nicht.

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 No favorites yet. (Noch keine Favoriten) angezeigt.
  • Andernfalls wird eine (scrollbare) Liste angezeigt.
  • Die Liste beginnt mit einer Zusammenfassung (z. B. Sie haben 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

Das wars!

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.