7.3. Umsetzung¶
7.3.1. Installation von Flutter¶
Grundlegend ist Flutter einfach zu installieren. Flutter ist für die geläufigsten Package Manager verfügbar, wie z.B. Homebrew, Chocolatey, Scoop oder apt-get.
Nachdem der Installationsassistent unter https://docs.flutter.dev/get-started/install befolgt ist, kann mithilfe des Tools „Flutter Doctor“ überprüft werden, ob alle notwendigen Komponenten installiert sind.
Der Umfang des Installationsaufwands hängt davon ab, für welche Plattform man entwickeln und testen/debuggen möchte.
Für die Entwicklung von Android-Apps wird Android Studio benötigt. Hier kann der Android Device Manager genutzt werden, um ein Android-Gerät zu emulieren oder ein physisches Gerät verbunden werden.
Für die Entwicklung von iOS Apps wird MacOS und Xcode benötigt. Hier kann der iOS Simulator genutzt werden, um ein iOS-Gerät zu emulieren oder ein physisches Gerät verbunden werden.
Für die Entwicklung von Web-Apps wird ein Webbrowser wie Chrome oder Firefox benötigt.
Die nativen Apps für Windows, MacOS und Linux müssen auf den jeweiligen Plattformen getestet werden.
7.3.2. Erstellen eines neuen Flutter-Projekts¶
Nachdem Flutter erfolgreich installiert wurde, kann ein neues Flutter-Projekt mit dem Befehl „flutter create project_name“ erstellt werden. Dieser Befehl erstellt ein neues Flutter-Projekt mit dem angegebenen Namen im aktuellen Verzeichnis. Standardmäßig werden alle notwendigen Dateien und Ordner für ein Flutter-Projekt erstellt, einschließlich der Datei „main.dart“, die die Hauptklasse der Anwendung enthält.
Ordnerstruktur¶
Nach dm Erstellen eines neuen Flutter-Projekts wird eine Standardordnerstruktur erstellt, die die verschiedenen Dateien und Ordner für das Projekt enthält. Die wichtigsten Ordner und Dateien sind:
lib/: Enthält den Quellcode der Anwendung.
android/: Enthält den Quellcode für die Android-App.
ios/: Enthält den Quellcode für die iOS-App.
web/: Enthält den Quellcode für die Web-App.
windows/: Enthält den Quellcode für die Windows-App.
macos/: Enthält den Quellcode für die MacOS-App.
test/: Enthält die Tests für die Anwendung.
pubspec.yaml: Enthält die Konfigurationen und Abhängigkeiten des Projekts.
Shared Code wird in der lib/ Ordnerstruktur gespeichert, während plattformspezifischer Code in den entsprechenden Ordnern (android/, ios/, web/, windows/, macos/) gespeichert wird. Flutter wirbt damit, dass eine große Menge des Codes plattformübergreifend genutzt werden kann, was die Entwicklung von Apps für verschiedene Plattformen erleichtert. Die Prozentzahl des plattformübergreifenden Codes hängt jedoch von den spezifischen Anforderungen und Funktionen der App ab.
Pubspec.yaml¶
Die Datei „pubspec.yaml“ enthält die Konfigurationen und Abhängigkeiten des Projekts. In dieser Datei werden die Abhängigkeiten des Projekts definiert, einschließlich der Flutter- und Dart-Versionen sowie der externen Pakete, die im Projekt verwendet werden. Die Datei „pubspec.yaml“ enthält auch Informationen über den Namen, die Version und die Beschreibung des Projekts.
Beispiel für eine „pubspec.yaml“-Datei: .. code-block:
name: my_flutter_app
description: A new Flutter project
version: 1.0.0
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
pub.dev und Installieren von Paketen¶
Flutter-Pakete können über pub.dev gefunden und installiert werden. Pub ist der Standard-Paketmanager für Flutter und Dart.
Um ein Paket zu installieren, muss es in der Datei „pubspec.yaml“ unter „dependencies“ hinzugefügt werden. Alternativ kann die Installation auch über die Befehlszeile mit „flutter pub add package_name“ erfolgen.
Nachdem die Datei „pubspec.yaml“ aktualisiert wurde, kann das Paket mit dem Befehl „flutter pub get“ installiert werden. Pub get lädt die Abhängigkeiten des Projekts herunter und installiert sie.
Beispiel für die Installation eines Pakets: .. code-block:
dependencies:
flutter:
sdk: flutter
http: ^0.13.3
Debuggen der App¶
Flutter bietet verschiedene Möglichkeiten zum Debuggen von Apps, einschließlich der Verwendung von Entwicklertools wie dem Flutter Inspector und dem Dart DevTools.
Wenn ein Device angeschlossen ist, kann die App direkt auf dem Gerät oder Emulator ausgeführt werden. Der Befehl „flutter run“ startet die App auf dem angeschlossenen Gerät oder Emulator.
Im Android Emulator sieht das dann so aus:

Der Flutter Inspector ermöglicht es Entwicklern, die Widget-Hierarchie und den Zustand der App zu überprüfen, während das Dart DevTools detaillierte Informationen über den Code und die Leistung der App bereitstellt.

7.3.3. Erstellen der Chat-App¶
Unsere App besteht aus drei Hauptkomponenten, der Main.dart Datei, dem ChatGPTService und dem Chat-Widget.
Main.dart¶
In der Main.dart Datei wird die Hauptklasse der Anwendung definiert. Hier wird das Hauptwidget der Anwendung erstellt und gerendert. Es ist der Einstiegspunkt der Anwendung.
Ein Basis-Setup für die Main.dart Datei könnte wie folgt aussehen:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePage
Das MyApp Widget ist ein StatelessWidget, das das MaterialApp-Widget enthält, das die gesamte Anwendung umschließt. Das MaterialApp-Widget definiert das Aussehen und Verhalten der App, einschließlich des Farb-Themes und des Titels. Innerhalb des MyHomePage-Widgets wird der eigentliche Inhalt der App gerendert. In unserem Fall wird hier das im späteren Verlauf beschriebene Chat-Widget gerendert.
ChatGPTService¶
Der ChatGPTService ist eine Dart-basierte Klasse, die entwickelt wurde, um die Kommunikation mit dem OpenAI ChatGPT API zu ermöglichen. Er kümmert sich um das Senden von Nachrichten an die API, das Empfangen von Antworten und das lokale Speichern der Chat-Verläufe.
Features:
API-Integration: Abfragen der OpenAI ChatGPT API über einen API-Schlüssel.
Nachrichtenverwaltung: Der Chat-Verlauf wird lokal gespeichert und geladen, um die Konversation zu speichern.
Fehlerbehandlung: Netzwerk- und API-Fehler werden abgefangen und eine entsprechende Fehlermeldung wird zurückgegeben.
Beziehen eines API-Schlüssels¶
Um einen API-Key zu erhalten, muss ein Konto bei OpenAI erstellt werden.
Nach der Registrierung und Anmeldung kann im API Dashboard ein neuer API-Schlüssel erstellt werden. Dazu einfach im Abschnitt „API Keys“ auf „Create new secret key“ klicken, um einen neuen API-Schlüssel zu generieren.
Wir haben für das Testen 5 Euro Guthaben aufgeladen, um die API zu nutzen.
Ablauf einer Konversation¶
Folgender Ablauf muss im ChatGPTService implementiert werden:
Benutzer sendet eine Nachricht.
Die Nachricht wird zur Nachrichtenliste hinzugefügt und gespeichert.
Die letzten maxMessages Nachrichten werden an die ChatGPT API gesendet, um einen Kontext für die Antwort zu bieten.
Die Antwort der API wird empfangen und zur Nachrichtenliste hinzugefügt.
Die aktualisierte Nachrichtenliste wird gespeichert.
Code¶
Der ChatGPTService bietet Methoden zum Laden und Speichern von Nachrichten, zum Senden von Nachrichten an die API und zum Empfangen von Antworten.
Beim Erstellen eines ChatGPTService Objekts wird eine Nachrichtenliste initialisiert.
Beim Start der Anwendung oder beim Erstellen einer neuen Instanz des Dienstes werden vorher gespeicherte Nachrichten aus der lokalen Speicherung in die Nachrichtenliste geladen.
Nach jeder gesendeten Nachricht wird der aktuelle Nachrichtenverlauf gespeichert, um sicherzustellen, dass die Konversation auch nach dem Neustart der Anwendung erhalten bleibt.
Wir verwenden die Pakete „http“ und „shared_preferences“ für die Kommunikation mit der ChatGPT API und die lokale Speicherung der Nachrichten. Beide wurden über pub.dev installiert und in der Datei „pubspec.yaml“ hinzugefügt.
Es werden SharedPreferences zur Speicherung und Wiederherstellung von Nachrichtenverläufen verwendet. SharedPreferences sind ein einfacher Weg, um Daten lokal auf dem Gerät zu speichern und wiederherzustellen. Zur Anfrage an die ChatGPT API wird das Paket „http“ verwendet, das HTTP-Anfragen über die klassischen Methoden GET, POST, PUT, DELETE usw. ermöglicht. Der API-Key wird hierbei als Bearer-Token im Header der Anfrage übergeben.
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'package:flamingo/message.dart';
class ChatGPTService {
final String apiKey = 'XXXXXXXXXXXXXXXX';
final String url = 'https://api.openai.com/v1/chat/completions';
final int maxMessages = 10; // Maximale Anzahl der Nachrichten im Verlauf
List<Message> messages = [];
ChatGPTService();
Future<void> loadMessages() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? messagesString = prefs.getString('messages');
if (messagesString != null) {
List<dynamic> jsonMessages = jsonDecode(messagesString);
messages = jsonMessages
.map((e) => Message.fromJson(Map<String, String>.from(e)))
.toList();
}
}
Future<void> saveMessages() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String messagesString =
jsonEncode(messages.map((e) => e.toJson()).toList());
await prefs.setString('messages', messagesString);
}
Future<String> sendMessage(String message) async {
messages.add(Message(Issuer.user, message));
await saveMessages();
// Begrenzen der Anzahl der gesendeten Nachrichten
List<Message> recentMessages = messages.sublist(
messages.length > maxMessages ? messages.length - maxMessages : 0,
);
try {
var response = await http.post(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey'
},
body: jsonEncode({
'model': 'gpt-3.5-turbo',
'messages': recentMessages
.map((msg) => {
'role': msg.issuer == Issuer.user ? 'user' : 'assistant',
'content': msg.content
})
.toList()
}),
);
if (response.statusCode == 200) {
var data = jsonDecode(response.body);
String responseMessage = data['choices'][0]['message']['content'];
messages.add(Message(Issuer.chatgpt, responseMessage));
await saveMessages();
return responseMessage;
} else {
throw Exception('Failed to load data');
}
} catch (e) {
return 'Error: ${e.toString()}';
}
}
}
Folgende Konfiguration wird in der Klasse ChatGPTService verwendet:
apiKey: Der API-Schlüssel, der zur Authentifizierung bei der OpenAI API verwendet wird.
url: Die Endpoint-URL für die ChatGPT API.
maxMessages: Die maximale Anzahl von Nachrichten, die im Verlauf gespeichert werden sollen.
7.3.4. Chat Widget:¶
Um unser UI übersichtlicher zu machen, haben wir die Anzeige des Chats in ein eigenes Widget untergeordnet. Die Erstellung des Widgets übernimmt folgende Methode:
Widget createChatWidget() {
return Expanded(child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Row(
children: <Widget>[
SizedBox(
width: constraints.maxWidth / 2,
child: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
if (index % 2 == 0) {
return SizedBox(
height: 150,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: _messages[index],
),
);
} else {
return SizedBox(
height: 50,
child: Container(),
);
}
},
)),
SizedBox(
width: constraints.maxWidth / 2,
child: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
if (index % 2 == 1) {
return SizedBox(
height: 150,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: _messages[index],
),
);
} else {
return SizedBox(
height: 50,
child: Container(),
);
}
},
)),
],
);
}));
}
Dies erzeugt ein Widget welches ein Zeilen-Widget mit jeweils 2 SizedBox Widgets beinhaltet. Innerhalb der 2 SizedBox Widgets befinden sich die eigentlichen Chat Nachrichten. Hierzu filtern wir unser Nachrichten Array, in dem wir durch die Modulo Operation den SizedBoxen jeweils nur die zweite Nachricht zuweisen. Unser Nachrichten Array wird bei jedem Senden der Nutzer Nachricht befüllt durch folgende Funktion:
void _addMessageBox(Message message) {
late Align align;
late Container container;
late LinearGradient linearGradient;
if (message.issuer == Issuer.user) {
linearGradient = const LinearGradient(
colors: [Colors.deepPurple, Colors.deepPurpleAccent],
begin: Alignment.topCenter,
end: Alignment.bottomCenter);
} else {
linearGradient = const LinearGradient(
colors: [Colors.deepPurpleAccent, Colors.deepPurple],
begin: Alignment.topCenter,
end: Alignment.bottomCenter);
}
container = Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
gradient: linearGradient,
borderRadius: const BorderRadius.all(Radius.circular(10))),
child: Text(message.content, style: const TextStyle(color: Colors.white)),
);
align = Align(
alignment: message.issuer == Issuer.user
? Alignment.topRight
: Alignment.topLeft,
child: container);
setState(() {
_messages.add(align);
});
}
In der obigen Methode wird die eigentlich Chat Nachricht jeweils ummantelt mit verschiedenen Styling Komponent, wie LinearGradient, welches den Chatboxen einen Verlauf im Hintergrund verleiht.