07 Gibt es ein praktisches Beispiel?

Aber natürlich.

Im Folgenden soll eine Funktion implementiert werden, welche eine Zahl um eins erhöht.

Die Schritte hierfür sind:

  • Erzeugen des Backends – die Logik
  • Erzeugen des Frontends – des User-Interfaces
  • Erzeugen der Internationalisierung
  • Registrieren der Funktion
  • Testen der Funktion

Erzeugen des Backends – die Logik

Die Logik befindet sich in \lib\logic. Für die Krypto-Tools solltest Du den Pfad \lib\logic\tools\crypto_and_encodings wählen.
Erstelle eine sinnvoll benannte neue dart-Datei. Angenommen, Dein Verschlüsselungsalgorithmus heißt “IncreasedN”, solltest Du Deine Datei increased_n.dart nennen. Alles in Kleinbuchstaben und mit Unterstrichen. Wenn Du eine komplexe Funktion mit einigen Unterfunktionen planst, denke an ein Unterverzeichnis, z. B. für die Koordinaten-Funktionen oder die math/primes-Funktionen.

Wenngleich die Dart-Coderichtlinien empfehlen, keine Klasse zu erstellen, wenn Du nur statische Funktionen schreiben möchtest, weichen wir hiervon ab.
Du brauchst für jede eine Funktion – die Verschlüsselung und die Entschlüsselung – eine statische Methode. Diese Einstiegsfunktionen, die später aufgerufen werden, sollten encryptX/decryptX heißen,
im Falle von Verschlüsselungen, encodeX/decodeX heißen.

Bitte achte darauf, dass einige Sonderfälle wie leere Eingaben abfangen und entweder null oder einen leeren String zurückgeben oder was auch immer in diesem Fall passt.

encryptIncreasedN (int number ) {
if ( number == null )
return null ;

return number + 1;
}

decryptIncreasedN ( int number ) {
if ( number == null )
return null ;

return number - 1;
}

Erzeugen des Frontends – des User-Interfaces

In Flutter ist alles ein Widget. Widgets sind kleine Teile der Benutzeroberfläche, die Du kombinieren kannst, um eine komplette App zu erstellen. Der Aufbau einer App in Flutter ist wie das Bauen eines Lego-Sets – Stück für Stück. Widgets werden ineinander verschachtelt um die App zu bauen. Sogar die Wurzel der App ist nur ein Widget. Es sind Widgets den ganzen Weg nach unten.

Der wichtigste Teil der effektiven Nutzung von Flutter Widgets ist es, die Widgets auf der untersten Ebene so zu gestalten, dass sie wiederverwendbar sind.

Widget-Klassen haben (normalerweise) nur eine Anforderung: Sie müssen eine build-Methode haben, die andere Widgets zurückgibt. Die einzige Ausnahme von dieser Regel sind Low-Level-Widgets wie Text, die primitive Typen wie Strings oder Zahlen zurückgeben.

Flutter-Widgets müssen eine Handvoll von Klassen aus der Flutter-Bibliothek erweitern. Die beiden Klassen, die Du fast immer verwenden wirst, sind StatelessWidget und StatefulWidget.
Der wesentliche Unterschied ist, dass Stateful Widgets auf ihren Zustand achten und ggf. Flutter anweisen, den Bildschirmaufbau neu zu zeichnen. Dies ist ein Schlüsselkonzept in Flutter. Daher hat StatefulWidget zwei Klassen: das Zustandsobjekt und das Widget.

Die GC Wizard-Widgets befinden sich in /lib/widgets/. Das Unterverzeichnis folgt wiederum der Struktur des Logik-Verzeichnisses. Das
bedeutet, Dein Widget sollte sich in /lib/widgets/tools/crypto_and_encodings/increased_n.dart befinden.

Die Widget-Struktur unterstützt Dich, indem sie den Boiler-Plate-Code kapselt für die einfache Widget-Erstellung: die Navigation zum Widget, das Scrollen, den Titel und so weiter.
Du musst lediglich ein Layout für Ein- und Ausgaben schreiben.

Alle Widgets sind vom Typ StatefulWidget. Die Zustandsverwaltung – wenn Sie so wollen, ihre gesamte Frontend-Logik – befindet sich in ihrer State-Klasse. Das Wichtigste ist die build()-Methode in der State-Klasse, die vom Framework aufgerufen wird, um Dein Frontend zu bauen.

Normalerweise – sofern niemand eine bessere Idee hat – wirst Du ein Column()-Widget erstellen, welches Deine Eingabefelder und Ausgabefelder und alles andere in vertikaler Reihenfolge anordnet.

Wichtig: importiere immer die Widgets der Material-Klasse aus material.dart und nicht aus cupertino.dart. cupertino.dart ist nur für iOS-Apps geeignet.

Nachfolgend ist das Grundgerüst:

import ’package:flutter/material.dart ’;

class IncreasedN extends StatefulWidget {
@override
IncreasedNState createState () => IncreasedNState ();
}

class IncreasedNState extends State < IncreasedN > {

@override
Widget build ( BuildContext context ) {
return Column (
children : <Widget >[

],
);
}
}

Die Struktur

Für das Beispiel wird benötigt:

  • ein Eingabefeld für eine Zahl
  • ein Schalter für Ver- oder Entschlüsselung
  • ein Ausgabefeld

Die bereits vorhandenen Widgets für die Ein- und Ausgabe sind im Verzeichnis \lib\widgets\common.

Eingabe

Das GCWIntegerTextField-Widget besitzt einen Eingabe-Controller, der lediglich die Eingabe von Ziffern erlaubt und sowohl die Zahl als auch die Zeichenkette zurückgibt.

Ein weiteres Widget wäre GCWIntegerSpinner. Dieses Widget verfügt über zwei Knöpfe “+” und “-” neben dem Textfeld.

Ver-/Entschlüsselung

Der Modus – ver- oder entschlüsseln wird über einen Schalter gesteuert. Für den GC Wizard stehen zwei Schaltertypen zur Verfügung:

  • ein einfacher Ein/Aus-Schalter, der jeweils wahr oder falsch zurückgibt – GCWOnOffSwitch
  • sowie ein Optionen-Schalter, der lediglich die Position links oder rechts zurückgibt – GCWTwoOptionsSwitch

Ausgabe

Auch für die Ausgabe stehen verschiedene Widgets zur Verfügung. Das GCWOutputText-Widget zeichnet eine Trennlinie mit einem Standard-Titel sowie ein Textfeld für die Ausgabe, dem der auszugebende Text übergeben wird.

import ’package:flutter/material.dart’;
import ’package:gc_wizard/widgets/common/base/gcw_output_text.
dart’;
import ’package:gc_wizard/widgets/common/gcw_integer_textfield
.dart’;
import ’package:gc_wizard/widgets/common/gcw_twooptions_switch
.dart’;
import ’package:gc_wizard/logic/tools/crypto_and_encodings /
increased_n.dart’;

class IncreasedN extends StatefulWidget {
@override
IncreasedNState createState () => IncreasedNState ();
}

class IncreasedNState extends State < IncreasedN > {

@override
Widget build ( BuildContext context ) {
return Column (
children : <Widget >[
GCWIntegerTextField () ,
GCWTwoOptionsSwitch () ,
GCWOutputText (
text : // TODO
)
],
);
}
}

Speichern der Eingabe und Berechnen der Ausgabe

Jedes Widget gibt einen Wert zurück: Das Integer-Eingabefeld gibt eine ganze Zahl zurück und der Schalter gibt seine Position zurück.

Der Ausgabetext hingegen setzt den Ausgabetext.

Damit müssen also einige lokale Variablen definiert werden, um die Eingabewerte zu speichern und um das Ergebnis an die Ausgabe zu übergeben. Außerdem benötigst Du ein Ereignis, bei welchem Du die Daten holen oder setzen können.

Die meisten Widgets können einen Funktions-Callback für ihre “onChanged-Events” annehmen. Dies ist der Punkt, um die Werte zu erhalten.

class IncreasedNState extends State < IncreasedN > {

var _ currentIntegerValue = {’text ’: ’’, ’value ’: 0};
var _ currentMode = GCWSwitchPosition.left ;

@override
Widget build ( BuildContext context ) {
return Column (
children : <Widget >[
GCWIntegerTextField (
onChanged : ( ret) {
setState (() {
_ currentIntegerValue = ret ;
});
}
),
GCWTwoOptionsSwitch (
onChanged : ( value ) {
setState (() {
_ currentMode = value ; // set value

});
},
),
GCWOutputText (
text : // TODO
)
],
);

}
}

Berechnen der Ausgabe

Es gibt mehrere Möglichkeiten, dies zu tun. Sicherlich sind Dir die setState()-Aufrufe in den onChanged()-Aufrufen aufgefallen. Sie lösen einen Neuaufbau des Widgets aus. Also, in diesem Fall jedes Mal, wenn sich ein Wert geändert hat, wird das Widget neu aufgebaut und die Ausgabe wird neu berechnet.

Damit kann die Berechnung also direkt in das text-Attribut des GCWOutputTextes hinzugefügt werden.

Ist die Berechnung viel komplexer, ist es besser, diese Berechnung in eine Methode auszulagern, die in jedem Fall einen String zurückgeben sollte.

class IncreasedNState extends State < IncreasedN > {

var _ currentIntegerValue = defaultIntegerText ;
var _ currentMode = GCWSwitchPosition . left ;

@override
Widget build ( BuildContext context ) {
return Column (
children : <Widget >[
GCWIntegerTextField (
...
),
GCWTwoOptionsSwitch (
...
),
GCWOutputText (
text : _ buildOutput ()
)
],
);
}

_ buildOutput () {
if (_currentIntegerValue == null )
return ’’;

var calculated = _ currentMode == GCWSwitchPosition.
left;

? encryptIncreasedN (_currentIntegerValue [’value ’])
// Position.left == encrypt
// notice the [’value ’] part , which takes the parsed
integer value from the text field result
: decryptIncreasedN (_ currentIntegerValue [’value ’]);

// Position.right == decrypt

return calculated == null ? ’’ : calculated.toString ();
}
}

Erzeugen der Internationalisierung

Damit die Funktion in verschiedenen Sprachen genutzt werden kann, sind die Ausgaben des Frontends an die verschiedenen Sprachen anzupassen.

Im Verzeichnis /asstes/i18n/ werden die Sprachdateien abgelegt. Diese enthalten jeweils ein JSON-Objekt für jede Sprache mit entsprechenden Eigenschaften in der JSON-Notation.

/asstes/i18n/en.json

...
"increasedn_title" : "Increased N",
"increasedn_description" : "Increases an integer value by 1",
"increasedn_example" : "41 → 42" ,
...

/asstes/i18n/de.json

...
"increasedn_title" : " Inkrementiertes N",
"increasedn_description" : " Erhöht eine Ganzzahl um 1",
"increasedn_example" : "41 → 42

...

Registrieren der Funktion

Alle Funktionen werden zentral registriert in der Datei /widgets/registry.dart.

Diese Datei

  • importiert Dein Widget,
  • umhüllt Dein Layout mit einem echten Seiten-Widget,
  • fügt ein Schlüsselwort hinzu, welches die Einträge in den Sprachdateien referenziert.
  • kategorisiert die Funktion – normalerweise CRYPTOGRAPHY für Codes oder SCIENCE_AND_TECHNOLOGY für irgendwelche wissenschaftlichen Formelkram
  • enthält Schlüsselwörter für die Suchmaschine ohne Umlaute oder diakritische Zeichen.

import ’package:gc_wizard/widgets/tools/crypto_and_encodings/
increasedn.dart’;
...
GCWToolWidget (
tool : IncreasedN () ,
i18 nPrefix : ’increasedn ’,
category : ToolCategory . CRYPTOGRAPHY ,
searchStrings : ’increasedn ’
),
...

Wichtig: der Toolname ist ein Aufruf der i18n()-Methode für die Internationalisierung. Damit wird die korrekte Ausgabe in den verschiedenen Sprachen sichergestellt.

Abschließend wird die Funktion in die Listenstruktur der App eingefügt. Dies erfolgt in der Regel in der Datei /widgets/main_view.dart.

import ’package:gc_wizard/widgets/tools/crypto_and_encodings/
increased.dart ’;
...
className ( IncreasedN ()),
...

Die Sortierung erfolgt anhand der lokalisierten Namen.

Allerdings sind verschiedene Funktionen wie bspw. die Koordinatenberechnungen in eigenen Listen als eine Art Untermenü aufgelistet.

Testen der Funktion

Tests für die Logik sind unerlässlich: Erstens stellen sie sicher, dass die Logik wie erwartet funktioniert und sie stellen sicher, dass eine mögliche Änderung nicht die aktuelle Funktionalität beeinflusst. Zweitens dienen sie als Dokumentation. Wenn jemand wissen möchte, wie eine bestimmte Funktion funktioniert, kann er oder sie die Testfälle ansehen, die Parameter überprüfen und das erwartete Ergebnis sehen.
Das ist auch eines der ersten Dinge, die sich ein Prüfer für Pull Requests anschaut bevor er oder sie einen Blick auf den echten Code wirft.

Die Tests befinden sich im \test-Verzeichnis. Die Struktur folgt der Projekt-Struktur.

Somit sollte der Test gespeichert werden unter
\test\logic\tools\crypto_and_encodings\increased_n.dart

Struktur der Teste

Jeder Test besitzt Testgruppen. Jede Gruppe dient zum Testen einer bestimmten Methode. Normalerweise gibt es zwei Gruppen, eine für die Verschlüsselung/Kodierung und eine für die Entschlüsselung/Entkodierung.

Jede Gruppe erhält eine Liste von Eingabewerten, kombiniert mit der
spezifischen erwarteten Ausgabe. Jeder Listeneintrag ist eine Map, welche die Parameternamen und ihre Werte widerspiegelt. Anschließend wird diese Liste iteriert. Für jeden Listeneintrag wird die entsprechende Funktion aufgerufen.

Bitte füge einige gut durchdachte Testfälle, einige verrückte Werte und natürlich die typischen Fälle “null” und, falls relevant, leere Strings oder Listen hinzu.

Unser Test sieht wie folgt aus:

import ’package:flutter_test/flutter_test.dart ’;
import ’package:gc_wizard/logic/tools/crypto_and encodings/
increased_n.dart’;

void main () {
group (’ IncreasedN . encrypt :’, () {
List <Map <String , dynamic >> _ inputsToExpected = [
{’number ’ : null , ’expectedOutput ’ : null },
{’number ’ : -42, ’expectedOutput ’ : -41},
{’number ’ : -1, ’expectedOutput ’ : 0},
{’number ’ : 0, ’expectedOutput ’ : 1},
{’number ’ : 1, ’expectedOutput ’ : 2},
{’number ’ : 42, ’expectedOutput ’ : 43} ,
];

_ inputsToExpected . forEach (( elem ) {
test (’ number : ${ elem [’ number ’]} ’, () {
var _ actual = encryptIncreasedN ( elem [’number ’]);
expect (_ actual , elem [’ expectedOutput ’]) ;
});
});
});

group (’ IncreasedN . decrypt :’, () {
List <Map <String , dynamic >> _ inputsToExpected = [
{’number ’ : null , ’expectedOutput ’ : null },
{’number ’ : -42, ’expectedOutput ’ : -43},
{’number ’ : -1, ’expectedOutput ’ : -2},
{’number ’ : 0, ’expectedOutput ’ : -1},
{’number ’ : 1, ’expectedOutput ’ : 0},
{’number ’ : 42, ’expectedOutput ’ : 41} ,
];

_ inputsToExpected . forEach (( elem ) {
test (’ number : ${ elem [’ number ’]} ’, () {
var _ actual = decryptIncreasedN ( elem [’number ’]);
expect (_ actual , elem [’ expectedOutput ’]) ;
});
});
});
}