Boehrsi.de - Blog

Flutter App Development - Teil 5 - RSS Feed Formular

Erstellt am event Uhr von account_circle Boehrsi in label Development
Flutter App Development - Teil 5 - RSS Feed Formular Bild

Nachdem es durch die Neuinstallation des Servers und diverse andere Themen mal wieder ein paar Verzögerungen gab, geht es nun weiter mit meiner kleinen Futter Tutorialreihe. Heute gehe ich auf das Formular ein, welches das Hinzufügen und Bearbeiten von RSS Feeds erlaubt.
Ich werde heute lediglich eine Klasse beleuchten, da ich das Gefühl habe in anderen Beiträgen wurde etwas viel auf einmal erläutert. So hoffe ich etwas nachvollziehbarere Erklärungen liefern zu können. Wie gesagt ist auch für mich diese umfangreiche Tutorialreihe etwas Neues und zugleich ist sie sehr aufwendiges. Ich versuche entsprechend direkt während der Erstellung und wenn ich mir meine eigenen Beiträge später noch einmal anschaue, Dinge zu optimieren.
Doch nun zum eigentlichen Thema, dem Verwalten der RSS Feeds über ein Formular in Flutter, inklusive simpler Validierung der Eingaben und Übergabe der Daten an unseren BloC.

lib/feed_list/feed_list_change.dart (Code auf GitHub open_in_new)

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../constants/dimensions.dart';
import '../constants/strings_user_visible.dart';
import '../feed_list/feed_list_barrel.dart';
import '../types/rss_feed.dart';

enum Type {
  add,
  edit,
  editId,
  delete,
}

enum _DialogResult {
  yes,
  no,
}

class FeedListChange extends StatefulWidget {
  final RssFeed feed;

  const FeedListChange({Key key, this.feed}) : super(key: key);

  @override
  _FeedListChangeState createState() => _FeedListChangeState();
}

class _FeedListChangeState extends State<FeedListChange> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _urlController = TextEditingController();
  bool _isEditMode;

  @override
  void initState() {
    super.initState();
    final feed = widget.feed;
    _isEditMode = feed != null;
    if (_isEditMode) {
      _nameController.text = feed.name;
      _urlController.text = feed.url;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isEditMode ? kFeedChangeScreenTitleEdit : kFeedChangeScreenTitleAdd),
        actions: <Widget>[
          Visibility(
            visible: _isEditMode,
            child: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () => showDeleteDialog(context),
            ),
          )
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: kDefault8dp, horizontal: kDefault16dp),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              TextFormField(
                decoration: InputDecoration(labelText: kFeedChangeNameLabel),
                controller: _nameController,
                validator: (value) => value.isEmpty ? kFeedChangeNameErrorHint : null,
              ),
              TextFormField(
                decoration: InputDecoration(labelText: kFeedChangeUrlLabel),
                controller: _urlController,
                validator: (value) => value.isEmpty ? kFeedChangeNameErrorHint : null,
              ),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: kDefault16dp),
                child: Center(
                  child: RaisedButton(
                    child: Text(kFeedChangeSubmitButton),
                    onPressed: () => setFeed(context),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }

  void showDeleteDialog(BuildContext context) {
    showDialog(
        context: context,
        builder: (_) {
          return AlertDialog(
            title: Text(kFeedChangeDeleteDialogTitle),
            content: Text('$kFeedChangeDeleteDialogText: ${widget.feed.name}?'),
            actions: <Widget>[
              FlatButton(child: Text(kNo), onPressed: () => Navigator.pop(context, _DialogResult.no)),
              FlatButton(
                child: Text(kYes),
                onPressed: () {
                  BlocProvider.of<FeedListBloc>(context).add(DeleteFeed(feed: widget?.feed));
                  Navigator.pop(context, _DialogResult.yes);
                },
              ),
            ],
          );
        }).then((result) {
      if (result == _DialogResult.yes) {
        Navigator.pop(context, Type.delete);
      }
    });
  }

  void setFeed(BuildContext context) {
    if (_formKey.currentState.validate()) {
      final newUrl = _urlController.text;
      final feed = RssFeed(
        id: newUrl.hashCode,
        url: newUrl,
        name: _nameController.text,
      );
      Type type;
      if (widget?.feed == null) {
        type = Type.add;
      } else if (widget.feed.url == newUrl) {
        type = Type.edit;
      } else {
        type = Type.editId;
      }
      BlocProvider.of<FeedListBloc>(context).add(SetFeed(newFeed: feed, oldFeed: widget?.feed));
      Navigator.pop(context, type);
    }
  }
}

Wir beginnen mit zwei Enums (Type und _DialogResult), welche uns helfen sollen Typen und Rückgabewerte sicher zu bestimmen. Weiter geht es mit dem eigentlichen Widget, welches an dieser Stelle ein Stateful Widget open_in_new ist. Dies ist relevant, um den State des Formulars erfassen und verarbeiten zu können. Das Formular wird dabei über den _formKey (GlobalKey open_in_new) identifiziert und der Inhalt wird über die zwei folgenden TextEditingController open_in_new verarbeitet. Außerdem unterscheide ich zwischen dem Hinzufügen und dem Bearbeiten von einem neuen RSS Feed.
Die UI passt sich je nach Zustand (hinzufügen oder bearbeiten) an und hat entsprechend einen anderen Titel und zeigt in der bearbeiten Ansicht ebenfalls einen löschen Button in der AppBar open_in_new. Die beiden TextFormFields open_in_new werden mit den oben genannten Controllern ausgestattet und erhalten einen Validator. Selbiger wird relevant sobald das Formular abgeschickt wird. Denn nur wenn Name und URL gesetzt sind kann das Formular bestätigt werden.
Dies geschieht über die setFeed() Methode, während die showDeleteDialog() Methode den löschen Dialog anzeigt. Beide interagieren mit unserem FeedListBloc, welcher über die Provider Library verfügbar gemacht wird.
In dieser Klasse ist vor allem interessant, dass wir einen GlobalKey nutzen. Selbige müssen im gesamten App Kontext eindeutig sein und erlauben es euch Widgets von überall zu identifizieren. Dies gesagt sei erwähnt das ihr, ebenso wie in anderen Sprachen mit globalen Variablen oder ähnlichem, dieses Konzept nicht als Standardweg für den Zugriff auf Daten oder die Kommunikation des States nutzen solltet. Für Formulare ist dieser Ansatz allerdings Best-Practice, aber ansonsten sollten passende Lösungen genutzt werden.
Zusätzlich sehen wir in der showDeleteDialog() Methode eine mögliche Art asynchrone Abfrage ohne das await Keyword zu behandeln (Futures, Async und Await via Dart Docs open_in_new). Die vom Flutter Framework gestellte showDialog() open_in_new Methode erlaubt es uns einen Dialog mit eine definierten UI zu bauen. In dieser nutze ich den Navigator open_in_new, um den Dialog zu schließen, sobald ein Button betätigt wurde. Je nachdem welche Entscheidung der Nutzer trifft möchte ich allerdings auch auf dem eigentlichen Screen darauf reagieren. Dafür wird im Navigator.pop open_in_new Aufruf der Optionale Rückgabe Parameter gesetzt. Abschließend überprüfe ich dann mit dem then() Aufruf am Ende der Dialog Nutzung, ob der Eintrag gelöscht wurde und schließe den gesamten Screen falls ja. then() ist hier die besagte Alternative zum await Handling. Die Methode wird ausgelöst sobald der Dialog geschlossen wird, was möglich ist da der Dialog selbst ein Future ist.
Abschließend haben wir die setFeed() Methode. Das Überprüfen des Form States via _formKey.currentState.validate() löst alle validator der enthaltenen FormFields open_in_new aus. Bei uns werden also beide TextFormFields überprüft, ob sie nicht leer sind. Sofern die Überprüfung erfolgreich ist wird dem BloC mitgeteilt welche Aktion durchgeführt werden muss.
Mehr Informationen zum BloC findet ihr im Teil 4.2 der Reihe. Dort wird die eigentliche Logik behandelt. Dies war die letzte Klasse aus dem feed_list Package und im nächsten Teil geht es weiter mit den entry_list Klassen.

Related Links
Kommentare  
Kommentar erstellen
Mit dem Abschicken des Kommentars erklären sie sich mit der in der Datenschutzerklärung dargelegten Datenerhebung für Kommentare einverstanden. Spam, unangebrachte Werbung und andere unerwünschte Inhalte werden entfernt. Das Abonnieren via E-Mail ist nur für E-Mail Adressen erlaubt die Sie rechtmäßig administrieren. Widerrechtliche Abonnements werden entfernt.