• Keine Ergebnisse gefunden

6.1 Backend

6.3.2 Stores

Ein Store ist grundlegend wie ein normales Modell aufgebaut. Es gibt aber ein paar Regeln, die zu beachten sind. Alle Variablen, auf deren Änderung die Benutzeroberfläche reagieren soll, müssen mit einer @observable-Annotation versehen werden. Außerdem sollten Varia-blen nicht direkt, sondern nur über Methoden im Store mit der Annotation@actiongeändert werden. Zudem gibt es noch@computed. Diese Annotation wird üblicherweise für Getter ge-nutzt, welche ihren Wert bei einer Änderung von Observables ebenfalls ändern. [Pod+19a]

Im Laufe der Entwicklung hat sich gezeigt, dass bestimmte Aktionen in den Stores häufig zu bestimmten Zeitpunkten ausgeführt werden müssen. Daten können beispielsweise erst nach einem erfolgreichen Login geladen werden. Oft ist es auch ausreichend, wenn dies geschieht, sobald ein Modul das erste Mal genutzt wird. Deshalb wurde entschieden, eine Basisklasse für Stores einzuführen, welche Lifecycle-Methoden einführt. [Hec19]

Stores in der ganzen App verfügbar machen Ein Store ist zunächst lediglich ein Objekt, welches nur dort verfügbar ist, wo es instanziiert wurde. Dies ist allerdings nicht ausreichend, um den App-State zu verwalten. Deshalb ist ein Mechanismus notwendig, um die Stores in der ganzen App abrufen zu können. Die Dokumentation empfiehlt einen Main-Store zu führen, welcher weitere Stores beinhaltet. Dieser Main-Store wird per Provider in der App verteilt.

Jeder Store kann nun überall in der App, wo ein context verfügbar ist, abgerufen werden.

[PBR19]

BaseStore Der BaseStore, welcher im Sourcecode 12 abgebildet ist, stellt eine Reihe von Lifecycle-Methoden bereit. Diese sind im Folgenden in der Reihenfolge ihres Auftretens kurz erklärt.

Konstruktor / initStore

Bei dem Konstruktor handelt es sich um den normalen Konstruktor, welchen jede Klasse besitzt. Dieser hat allerdings den Nachteil, dass er nicht asynchron sein darf. Deshalb wird nach Ausführung des Konstruktors die Methode initStore ausgeführt, welche asyn-chronen Code erlaubt.

initData

Diese Methode wird nicht automatisch, sondern nur händisch durch den Nutzer aufge-rufen. Sie ist gedacht für Aktionen, welche nach der Instanziierung des Stores, aber vor dem Login ausgeführt werden sollen.

onLoginSuccess

Wird direkt nach einem erfolgreichen Login aufgerufen.

onFirstModuleUse

Diese Methode wird einmalig, beim ersten Aufruf vononModuleUseaufgerufen.

onModuleUse

Diese Methode wird jedes Mal aufgerufen, wenn der Store abgerufen wird.

onLogout

Nach einem Logout wird diese Methode aufgerufen. Sie ermöglicht beispielsweise das Löschen von benutzerbezogenen Daten.

1 abstract class BaseStore {

2 static List<BaseStore> _storeRegistry = List<BaseStore>();

3 Future _onLoginSuccessFuture, _onFirstModuleUseFuture;

4 bool _isFirstUsage = true;

5

6 static callLoginSuccessFunctions() => _storeRegistry.forEach((BaseStore store) =>

store._onLoginSuccessFuture = store.onLoginSuccess());

Quellcode 12: Auszug aus der BaseStore-Klasse

Es wird eine Registry geführt, in welcher sich jeder Store automatisch registriert. Dies erfolgt im Konstruktor desBaseStore. So ist es möglich in jedem Store simultan eine Funktion aufzuru-fen, ohne dass eine händische Implementierung notwendig ist. Diese Registry ist eine statische Variable und ist somit einmalig im BaseStoreverfügbar. Findet nun ein erfolgreicher Login

statt, muss in derBaseStore-Klasse die Funktion callLoginSuccessFunctions() aufge-rufen werden. Diese Funktion ruft nun in jedem Store die MethodeonLoginSuccess() auf.

Selbiges gilt auch für dieonLogout()-Funktionalität.

Die Umsetzung vononModuleUsekommt nicht ganz ohne extra Code im übergeordneten Store aus. Der Store wird als private Variable geführt und über einen Getter nach außen verfügbar gemacht, wie es die Abbildung 13 zeigt. Der Getter ruft zuerstonModuleUse()im jeweiligen Store auf und gibt dann diesen zurück.

1 final EquipmentStore _equipmentStore = EquipmentStore();

2

3 EquipmentStore get equipmentStore {

4 _equipmentStore.onModuleUse();

5 return _equipmentStore;

6 }

Quellcode 13: Umsetzung von onModuleUse

Die Umsetzung vononFirstModuleUse()ist hingegen sehr einfach. Die Methode wird beim ersten Aufruf von onModuleUse() aufgerufen. Außerdem wird ein Flag gesetzt, welches bei künftigen Aufrufen vononModuleUse() die Ausführung vononFirstModuleUse() deakti-viert.

EquipmentStore Für die Implementierung eines konkreten Stores sind nur, wie dem Sour-cecode 14 zu entnehmen ist, wenige Zeilen Code notwendig.

1 abstract class _EquipmentStore extends BaseStore with Store {

2 @observable

3 List<Equipment> data = List<Equipment>();

4

5 @override

6 onFirstModuleUse() => EquipmentService().cacheWithInit.then((serviceData) =>

this.data = serviceData);

,→

7

8 @action

9 refresh() => EquipmentService().get().then((BaseServiceResponse sr) => data = sr.data);

,→

10 }

Quellcode 14: Auszug aus EquipmentStore

Es wird eine normale Variable, in diesem Fall eine Liste vom Typ Equipment, angelegt und diese mit der@observable-Annotation versehen. Um die Liste mit Daten zu füllen, wird die

Methode onFirstModuleUse() des BaseStoreüberschrieben. Sie ist nun für das Abrufen des Datensatzes vom EquipmentServicezuständig. Außerdem gibt es die Action refresh, welche die Liste bei Aufruf aktualisiert.

6.3.3 Benutzeroberfläche

Für die Benutzeroberfläche war die Erstellung vieler neuer Komponenten notwendig. Diese lassen sich grob in zwei Kategorien einteilen. Zum einen globale Elemente wie das unter 5.3.1 behandelte, angepasste Scaffold oder eine Komponente, welche sich um Übersetzungen in ver-schiedene Sprachen kümmert. Diese Elemente werden in fast jedem Teil der App genutzt. Zum anderen gibt es Komponenten, welche in der normalen Gestaltung der Benutzeroberfläche be-nötigt, aber nur in einigen Komponenten eingesetzt werden. Dazu zählen List-Tiles und alle weiteren Widgets, welche eine Seite in der App bilden.

NiftyScaffold Das NiftyScaffold ist grundlegend ein Wrapper um das Scaffold, welches vom Framework mitgeliefert wird und einige Vorgaben macht sowie Funktionalitäten einfügt. Im Codeausschnitt 15 ist dies Beispielhaft zu sehen. Parameter wiefloatingActionButton wer-den einfach durchgereicht. Dadurch ist das NiftyScaffold weitestgehend kompatibel mit einem normalen Scaffold. Der Wechsel ist somit einfach möglich. Auch derbody-Parameter existiert im regulären Scaffold, wird hier aber zum einen mit einem Padding versehen und zum anderen besteht die Option, einen Button für ein Debug-Menü darüber zu legen.

1 class NiftyScaffold extends Scaffold {

2 NiftyScaffold({

3 padding = const EdgeInsets.all(defaultEdgeInsetsValue),

4 niftyOptions = const NiftyOptions(),

5 appBarOptions = const AppBarOptions(),

6 Widget body,

7 Widget floatingActionButton,

8 }) : super(

9 appBar: (appBarOptions is AppBarOptions) ? NiftyAppBar(niftyOptions, appBarOptions) : null,

,

10 body: ConfigHelper.debugMode

11 ? Stack(

12 children: <Widget>[

13 AppBody(appBarOptions, padding, body, niftyOptions),

14 Positioned(

20 )

21 : AppBody(appBarOptions, padding, body, niftyOptions),

22 drawer: niftyOptions.showMenu ? NiftyDrawer() : null,

23 floatingActionButton: floatingActionButton,

24 );

25 }

Quellcode 15: Symbolhafter Ausschnitt aus NiftyScaffold

Im NiftyScaffold ist auch die AppBar vorgegeben. Die Parameter hierfür werden im AppBarOptions-Objekt übergeben. Das NiftyOptions-Objekt beinhaltet weitere angepass-te Optionen, welche so nicht in den Standard-Widgets vorhanden sind. Ein Beispiel hier-für ist das SearchBar-Widget, welches in die AppBar des NiftyScaffold integriert ist. Das NiftyOptions-Objekt hält hierfür drei Variablen. Die VariableappBarShowSearchaktiviert das Suchfeld. Der aktuelle Wert wird an die Callback-FunktionappBarHandleSearch über-geben und über appBarSearchBarValue kann ein initialer Wert für das Suchfeld festgelegt werden.

Eine weitere, wichtige Aufgabe des NiftyScaffold ist es, das Hauptmenü anzuzeigen. Hierzu wird eine FunktionbuildMainMenudefiniert, welche den Inhalt des Menü-Widgets zurückgibt.

Um das Einfügen eines Menü-Eintrages möglichst einfach zu halten, wurde ein MenuEntry-List-Tile erstellt, welches als Parameter einen Titel und einen Routennamen für die Navigation entgegennimmt. Das List-Tile kümmert sich dann selbständig um die Navigation und farbliche Hervorhebung, wenn es sich bei einem List-Tile um die aktuell aktive Route handelt. Codeaus-schnitt 16 zeigt den einfachsten Fall eines Hauptmenüs. Da der Inhalt des Menüs eine Liste von Widgets ist, kann in dieses Menü jedes beliebige Widget eingefügt werden.

1 buildMainMenu(BuildContext context) {

2 final Translation translation = Translation.of(context);

3 return ListView(

4 children: <Widget>[

5 MenuEntry(translation.t('menu.equipment'), EquipmentPage.routeName),

6 MenuEntry(translation.t('menu.material'), MaterialPage.routeName),

7 MenuEntry(translation.t('menu.vehicle'), VehiclePage.routeName)

8 ],

9 );

10 }

Quellcode 16: Einfaches Beispiel eines Hauptmenüs

Lokalisierung Dart stellt mit dem intl-Paket eine Möglichkeit zur Umsetzung mehrerer Sprachen bereit. Dieser Ansatz hat allerdings eine hohe Komplexität. Um beispielsweise die

Nachrichten der Übersetzungen in eine weitere Sprache zu übersetzen, ist es notwendig, die zuvor in Form von Dart Code erstellten Funktionen zur Übersetzung über ein Script in eine ARB-Datei zu konvertieren. In dieser Datei können die Übersetzungen nun eingepflegt wer-den. Anschließend müssen die Dateien wieder per Script in Dart-Code umgewandelt werwer-den.

[Kni19]

Die Flutter-Dokumentation schlägt als Alternative eine eigene Implementierung vor. Bei die-sem Ansatz kann der Entwickler selbst entscheiden, wie das Problem gelöst werden soll [Cha+19a]. Für diese Arbeit wurde dieses Konzept gewählt, da er es erlaubt, die Übersetzun-gen in JSON Dateien nach dem Key-Value-Ansatz zu lösen. Für jede Sprache wird eine Datei angelegt. Jede Übersetzung bekommt einen frei wählbaren Bezeichner. Anhand dieses Bezeich-ners kann im Programmcode die gewünschte Übersetzung abgerufen werden. Dem Bezeichner wird als Wert die Übersetzung in der jeweiligen Sprache zugewiesen. Ein Beispiel einer Über-setzungsdatei ist im Codeausschnitt 17 zu sehen. Abschließend muss die Sprache noch in der supported_locales.json-Datei eingetragen werden. Nun steht die Sprache in der App zur Verfügung.

Quellcode 17: Beispiel einer Übersetzungsdatei

Die KlasseTranslationstellt alle relevanten Funktionen zum Laden und Ausgeben von Über-setzungen, sowie zum Formatieren von Datum und Uhrzeit bereit. Zur Einbindung der Klasse in Flutter ist ein Delegate nötig, welches unter anderem dieTranslation-Klasse zurückgibt. Au-ßerdem gibt es dieload-Methode, welche anhand der aktuellen Sprache ein Objekt vom Typ Translationerstellt. Hierzu wird die JSON Datei mit der entsprechenden Sprache geladen und in eine Map übertragen, aus welcher die Übersetzungen abgerufen werden können.

1 String formatTime(DateTime dateTime, {String language}) {

2 if (dateTime == null) throw ParameterException('dateTime should not be null');

3 if (language == null) language = locale.languageCode;

4 else if (_timeFormat[language] == null)

5 _timeFormat[language] = DateFormat(t('timeFormat', language: language));

6 return _timeFormat[language].format(dateTime);

7 }

8

9 String t(String key, {String language, Map<String, String> variables}) {

10 if (language != null && _localizedValues[language] == null) return key;

11 String localizedString = _localizedValues[language != null ? language : locale.languageCode][key];

,→

12 if (localizedString != null) {

13 if (variables != null)

14 localizedString = _insertVariables(localizedString, variables);

15 return localizedString;

21 static Future<Translation> load(Locale locale) async {

22 Translation niftyLocalizations = Translation(locale);

23 if (_localizedValues[locale.languageCode].isEmpty) {

24 String jsonContent = await

rootBundle.loadString("assets/i18n/${locale.languageCode}.json");

,

25 Map<String, dynamic> jsonMap;

26 jsonMap = json.decode(jsonContent);

27 _localizedValues[locale.languageCode] = jsonMap.map((key, value) => MapEntry(key, value.toString()));

,

28 _dateFormat[locale.languageCode] =

DateFormat(_localizedValues[locale.languageCode]['dateFormat']);

,

29 _timeFormat[locale.languageCode] =

DateFormat(_localizedValues[locale.languageCode]['timeFormat']);

,

30 }

31 return niftyLocalizations;

32 }

Quellcode 18: Funktionen zum Laden der Übersetzungen und zum Lokalisieren von Nach-richten und Daten

Um eine Übersetzung abzurufen, wird die Methode t genutzt. Ihr wird der Bezeichner der gewünschten Übersetzung übergeben. Wird dieser nicht in der Liste der Übersetzungen gefun-den, wird der Bezeichner zur Anzeige zurückgegeben. Ansonsten wird der entsprechende Text geladen. In diesen Text können noch Variablen eingefügt werden. Dazu wird im Text der Über-setzung ein als Variable markierter Text, wie beispielsweise${name}, eingefügt. Der Methode t muss in diesem Fall eine Map übergeben werden, welche einen Wert mit dem Namen der Variablen beinhaltet.

Zur Formatierung von Daten und Uhrzeiten im Format der jeweiligen Sprache, gibt es die Funk-tionen formatDate und formatTime. Um das Format festzulegen, werden in den Überset-zungsdateien aller Sprachen zwei Felder mit den BezeichnerndateFormat undtimeFormat angelegt. Diese Einträge in der Map mit den Übersetzungen werden in den beiden Methoden zum Formatieren von Datum und Uhrzeit geladen und der DateFormat-Funktion des

intl-Pakets übergeben, welche sich um die eigentliche Formatierung kümmert.

Übersichtsseite eines Ressourcentyps Da sich die Übersichtsseiten, abgesehen von den angezeigten Datensätzen, kaum voneinander unterscheiden, wird bei allen drei Ressourcen-typen dieselbe Liste zur Darstellung genutzt. Lediglich die Daten und das Menü werden an-gepasst. Um diese Wiederverwendung umzusetzen, wird der Seiteninhalt von der Seite selbst getrennt.

1 class EquipmentPage extends StatelessWidget {

2 static const String routeName = '/equipment';

3 @override

4 Widget build(BuildContext context) {

5 final EquipmentStore equipmentStore =

Provider.of<MainStore>(context).resourceStore.equipmentStore;

,

6 return Observer(

7 builder: (BuildContext context) => NiftyScaffold(

8 body: ResourceListView(ResourceType.equipment),

9 niftyOptions: NiftyOptions(

10 appBarTitle: Translation.of(context).t('resource.overview.equipment.title'),

11 appBarShowButtonFilter: true,

12 appBarFilterDialog: FilterResourcesDialog(ResourceType.equipment),

13 appBarShowSearch: true,

14 appBarHandleSearch: (value) => equipmentStore.setFilterSearch(value),

15 appBarSearchBarValue: equipmentStore.filterSearch,

16 ),),);}}

Quellcode 19: Übersichtsseite Equipment

Ressourcen-Seite Eine Seite ist eine Klasse, welche wie jedes normale Widget auch, mit einemStatefulWidgetoder einemStatelessWidgeterweitert wird. Der Unterschied zwi-schen den beiden Klassen liegt darin, dass einStatefulWidgeteinen State verwalten kann. Es kann somit Daten beinhalten, welche durch diesetState()-Methode geändert werden kön-nen. Auslöser kann beispielsweise die Aktivierung eines Buttons sein. Wird eine Änderung durch einsetState()ausgelöst, wird die Benutzeroberfläche automatisch aktualisiert, um die neuen Daten anzuzeigen. EinStatelessWidgetkann seine Daten hingegen nicht ändern. Um Daten in einem Widget dieses Typs zu ändern, muss die Änderung durch ein darüberliegendes Widgets ausgelöst werden. EinStatefullWidgetist somit flexibler. Es entsteht dadurch aber ein Overhead, welcher sich negativ auf die Performance auswirkt und auch die Komplexität des Codes erhöht. In diesem Projekt wird dieses State-Management nur für Ephemeral-State (UI-State) genutzt. Für den App-State wird hingegen MobX eingesetzt.

Für die Navigation in der App werden Seiten anhand eines Bezeichners aufgerufen. Dieser Be-zeichner muss über alle Seiten hinweg einmalig sein, kann aber ansonsten ein beliebiger String sein. Um die fehleranfällige, händische Eingabe des Strings zu vermeiden, hat jede Seite ein AttributrouteName. Mit dieser Variable wird die Seite im obersten Widget, derMaterialApp, im Navigator registriert. Später kann dann dem Navigator als Navigationsziel ebenfalls diese Variable übergeben werden.

Abbildung 7: Equipment Übersicht mit Fil-tern

Die build-Methode wird beim Rendering auf-gerufen. Ihr Rückgabewert wird dem Benutzer angezeigt. Da es sich bei dieser Seite um ein sehr simples Widget handelt, sind keine weite-ren Methoden notwendig. Als erstes wird über Provider der EquipmentStore abgerufen. Da sich der Inhalt dieser Seite anpassen soll, wenn sich Daten im Equipment-Store ändern, wird das NiftyScaffold, also das Hauptwidget dieser Seite, in einenObserver eingebaut. Ändert sich ein Oberservable oder Computed in einem Store und es wird innerhalb eines Observers verwendet, so wird ein Rerender ausgelöst, um die Benutze-roberfläche auf den neuen Stand der Daten zu ak-tualisieren.

Dem NiftyScaffold wird als body-Attribut die ResourceListView, also das Widget, welches den eigentlichen Seiteninhalt darstellt, mit dem Parameter ResourceType.equipment überge-ben. Dieser Parameter ist notwendig, da zwar al-le, der Seite nachfolgenden, Widgets für alle Res-sourcentypen dieselben sind, aber nicht immer denselben Inhalt anzeigen.

Über das nitftyOptions-Attribut des NiftyS-caffold werden noch der Seitentitel gesetzt und die Suchleiste sowie der Filterdialog aktiviert.

Ressourcenliste DieResourceListViewbesteht aus einerListView, welche alle Res-sourcen eines Typs, gegebenenfalls gefiltert, kurz auflistet. In dieser kurzen Ansicht werden die wichtigsten Informationen zusammengefasst. Neben dem Titel und der Kategorie wird, falls vorhanden, ein Bild der Ressource angezeigt. Zudem wird die wichtigste Defektmeldung

dar-gestellt und farblich visualisiert. Hierzu wird das ResourceListTile genutzt, welches ein Wrapper um einDefaultListTile bildet. Es wurde explizit zur Nutzung in diesem Widget entwickelt. Die Seite ist in Abbildung 7 zu sehen.

Vor den Ressourcen wird noch ein weiteres List-Tile eingefügt, welches den aktuellen Filtersta-tus anzeigt. Diese Zeile zeigt die Anzahl der gefundenen Ergebnisse, sowie alle aktiven Filter an. Ein aktiver Filter kann hier direkt entfernt werden.

Equipment-Details In der Detailansicht, welche in Abbildung 5 zu sehen ist, werden sämt-liche Infos zur gewählten Ressource dargestellt. Wie alle komplexeren Seiten ist auch die De-tailseite in mehrere Widgets aufgeteilt.

Detail-Seite Die Seite beinhaltet, genauso wie die Übersichtsseite, ebenfalls nur das Nif-tyScaffold. Alles darunterliegende ist Teil desResourceDetailsView-Widget. Die Detailseite ist allerdings erheblich komplexer als die Übersichtsseite, da das Scaffold auf dieser Seite mehr Aufgaben übernimmt.

In Flutter gibt es zwei Varianten der AppBar. Neben der Standard-AppBar gibt es die SliverAppBar. Im Gegensatz zur normalen AppBar erlaubt diese das Einbetten eines Wid-gets, wie einem Bild, in die AppBar. Das NiftyScaffold integriert dieSliverAppBarund setzt diese ein, wenn ihm alsappBarOptionsein Objekt vom TypSliverAppBarOptions überge-ben wird.

Um ein Bild anzuzeigen, muss in denSliverAppBarOptionsder OptionflexibleSpacedas spezielle WidgetFlexibleSpaceBarzugewiesen werden. Diesem Widget kann alschildein beliebiges Widget zugewiesen werden. Theoretisch kann hier direkt das Bild genutzt werden.

Da der Titel über dem Bild angezeigt wird, kann es zu Problemen bei der Lesbarkeit kommen, wenn Bild und Text einen ähnlichen Farbton haben. Deshalb wird das Bild in einen Stack eingefügt, sodass über das Bild ein Schatten gelegt werden kann, welcher die Lesbarkeit des Textes sicherstellt.

12 image: AdvancedNetworkImage(...),

Quellcode 20: AppBar mit Bild

Eine weitere Aufgabe des NiftyScaffold an dieser Stelle ist das Anzeigen eines Buttons für ein Menü. Dieses erlaubt weitere Aktionen, wie das Melden von Defekten oder die Anzeige der Problemgeschichte.

DetailsListView Dieses Widget ist für die Anzeige der Details zuständig. Da dieses Wid-get für alle Ressourcentypen verwendet wird, aber sich die verschiedenen Ressourcentypen in ihren Feldern unterscheiden, müssen manche Felder bei bestimmen Typen ausgeblendet wer-den. Außerdem müssen die Daten, je nach Typ, aus unterschiedlichen Stores bezogen werwer-den.

Da die Liste relativ lang ist, muss diese scrollbar sein. Deshalb ist das äußerste Widget ein SingleChildScrollView, welches das Scrollen des Inhaltes ermöglicht. Als nächstes wird ein Observer verwendet, da der Inhalt dieses Widgets sich beispielsweise beim Hinzufügen eines Bildes oder einer Defektmeldung aktualisieren soll. Der nächste Schritt ist das Auswählen des richtigen Stores für den aktuellen Ressourcentyp, um im Anschluss den korrekten Datensatz anhand seiner Id laden zu können.

Nun wird eine lange Liste von allen Attributen der Ressource erstellt. Als erstes werden alle gemeldeten und nicht gelösten Defekte angezeigt. Diese werden farblich anhand ihrer Dring-lichkeit hervorgehoben und können dort direkt bearbeitet oder als gelöst markiert werden. Dies ist nicht im Codeausschnitt zu sehen.

Das nächste List-Tile ist für die Bilder der Ressource verantwortlich. Ihm wird eine Liste von Bildern übergeben. Diese werden von dem ImagesListTileangezeigt und können von dort aus als Primärbild markiert oder gelöscht werden. Auch das Aufnehmen oder Hochladen von Bildern aus der Galerie ist möglich. Diese Aktionen werden an den Store weitergereicht, wel-cher sich um die Durchführung der nötigen Aktionen kümmert und dann die entsprechenden Observables aktualisiert. Da sich damit der Inhalt der, in diesem Widget genutzten, Observa-bles ändert, wird der Inhalt des Observers in diesem Widget automatisch mit den neuen Daten neu gerendert und zeigt somit den aktualisierten Bilddatensatz an.

Das nächste Attribut ist der Standort der Ressource. Um die Darstellung kümmert sich das MapTile, welches auf der linken Seite die Adresse und auf der rechten, sofern möglich, ei-ne Karte von Google Maps darstellt. Über diese Karte kann direkt in die Google Maps App gewechselt und eine Navigation gestartet werden.

Ab hier folgen alle Attribute, welche als Key-Value-Paar dargestellt werden. Einige dieser At-tribute sind in einem ausklappbaren Widget versteckt, um die Liste übersichtlicher zu gestalten.

Das im Codeausschnitt 21 zu sehende Attribut Fahrzeugtyp gibt es nur für Fahrzeuge. Deshalb wird dieses Widget über eine If-Abfrage nur gerendert, wenn derresTypeim aktuellen Kontext dem WertResourceType.vehicleentspricht. Handelt es sich um den korrektenresType, so wird einDetailsListTilemit dem jeweiligen Key-Value-Paar gerendert.

1 return SingleChildScrollView(

15 images: (resource.resource as Resource).images,

16 title: translation.t('resource.details.images'),

17 onImageDeletion: (int imageId) => store.deleteImage(imageId, resource.id),

18 onSetPrimaryImage: (Map<String, dynamic> data) {

19 data['resourceId'] = resource.id;

20 store.setPrimaryImage(data);

21 },

22 onImageTaken: (File image) => store.newImage(image, resource.id)

23 ),

29 translation.t('resource.details.vehicleType'),

30 resourceStore.vehicleStore.getVehicleType(resource.vehicleTypeId)?.type,

31 ),],);},),);

Quellcode 21: Ausschnitt aus der build Funktion des DetailsListView-Widgets

7 Ergebnisse und Ausblick

Ziel dieser Arbeit war es, ein Modul für eine Software zu entwickeln, welches die Verwaltung von Ressourcen in Handwerksbetrieben über Computer und Handys ermöglicht. Dies wurde

Ziel dieser Arbeit war es, ein Modul für eine Software zu entwickeln, welches die Verwaltung von Ressourcen in Handwerksbetrieben über Computer und Handys ermöglicht. Dies wurde