09.09.2024
Wie Flutter die mobile App-Entwicklung revolutioniert: Ein Blick auf die wichtigsten Funktionen.
Hands on: Eine Einführung in Flutter.
Heute nehmen wir euch mit auf eine spannende Reise in die Welt der mobilen App-Entwicklung mit Flutter. Wir stellen euch „blue_todo“ vor, eine kleine aber elegante ToDo-App, die zeigt, wie leistungsstark und vielseitig Flutter sein kann. In diesem Artikel bekommt ihr nicht nur einen Einblick in die Basics von Flutter sondern auch einen kleinen Deep Dive in Best Practices. Lasst uns gemeinsam einen Blick auf die Technik und das Design hinter „blue_todo“ werfen!
Inhaltsverzeichnis
Einführung
Hattest du schon einmal den Drang, Code zu schreiben und ihn überall auszuführen? Wenn ja, dann hast du wahrscheinlich schon Frameworks wie React Native und Flutter ausprobiert. Und wenn nicht, dann ist dieser Beitrag genau das Richtige für dich!
In diesem Artikel werden wir uns mit der erstaunlichen und sich schnell entwickelnden Welt (fast so schnell wie die Entwicklung von JS-Frameworks) der Multiplattform-Frameworks beschäftigen. Das Vorzeigebeispiel heute: Flutter.
Ich beginne mit einer kurzen Zusammenfassung der Geschichte von Flutter und den Vorteilen. Dann leite ich über zu einem kleinen App-Beispiel, das nativ auf Linux läuft und beende den Artikel mit einer kleinen Anmerkung, warum du für dein nächstes App-Projekt zu uns kommen solltest.
Geschichte und Vorteile
Nach ersten Entwürfen bereits im Jahr 2015 wurde Flutter 2017 mit Unterstützung für Android und iOS veröffentlicht. Im Jahr 2019 kündigte das Flutter-Team Unterstützung für Web- und Desktop-Plattformen an. Seitdem sind das Flutter-Framework selbst und sein Ökosystem gewachsen und haben sich zu einer wirklich einheitlichen Erfahrung auf allen Plattformen entwickelt. Einige der jüngsten Verbesserungen sind die neue Impeller-Rendering-Engine und Webassembly-Unterstützung für Web.
Flutter ist eine Neuheit in der Welt der UI-Frameworks! Es liefert Anwendungen mit einer eigenen Rendering-Engine, die Pixeldaten direkt auf dem Bildschirm ausgibt. Dies steht im Gegensatz zu vielen anderen Frameworks, die sich auf die Zielplattform verlassen, um eine Rendering-Engine bereitzustellen. Native Android-Apps sind auf das Android-SDK auf Geräteebene angewiesen, während React Native dynamisch den integrierten UI-Stack der Zielplattform nutzt. Die Kontrolle über die Rendering-Pipeline von Flutter ist ein entscheidender Faktor für die Unterstützung mehrerer Plattformen. Sie ermöglicht es Entwicklern, identischen UI-Code für alle Zielplattformen zu verwenden, was die Erstellung plattformübergreifender Anwendungen einfacher denn je macht!
Darüber hinaus bietet das Framework mit einem JIT-Compiler, der Codeänderungen in Sekundenschnelle neu verarbeitet, eine hervorragende DX. Dies ermöglicht schnelles Prototyping und einfaches Ausprobieren von Funktionalitäten.
Ein weiterer Vorteil ist, dass Flutter auf der Sprache Dart aufbaut. Diese Sprache bietet eine typsichere Umgebung. Sie nutzt eine Mischung aus statischer Typprüfung und Typinferenz, wobei die Typannotationen optional sind. Der Dart-Compiler verfügt außerdem über eine eingebaute Null-Prüfung, die alle lästigen Null-/None-Fehler direkt in der IDE abfängt.
Und nicht zuletzt: Flutter hat eine großartige Community mit einer Vielzahl von Plugins, die über den eingebauten Paketmanager pub einfach genutzt werden können.
Flutter Hands-On
Für diesen Blogartikel möchte ich eine kleine ToDo-App (was denn sonst?) bauen, die nativ unter Linux läuft. Flutter hat eine sehr gute Integration mit Firebase (Googles Plattform zur einfachen Entwicklung von Apps) oder anderen ähnlichen Cloud Infrastructure as a Service (CIaaS) Produkten wie Supabase. Aber für dieses Beispiel möchte ich, dass die App eine lokale Datenbank verwendet. Nämlich eine meiner Lieblings-NoSQL-Datenbanken: ObjectBox. Sie unterstützt Dart / Flutter und ist gebaut für Geschwindigkeit.
Flutter-Einrichtung
Bevor du mit der Entwicklung deiner Apps beginnen kannst, muss das Flutter-SDK installiert werden. Dies ist von Plattform zu Plattform unterschiedlich, aber ein guter Startpunkt ist die SDK Installations Seite des Flutter Projekts. Falls du so faul bist wie ich, empfehle ich version-fox. Das ist ein SDK-Versionsmanager geschrieben in GoLang. Nach der Installation und Konfiguration von version-fox, kannst du folgende Befehle ausführen um das Flutter Plugin hinzuzufügen und das SDK zu installieren:
vfox add flutter
vfox search flutter // this will open a selection with all available flutter versions
vfox use -g flutter@{use your version here}
Nach dem Ausführen dieser Befehle kann es schon losgehen! Um zu überprüfen ob alles geklappt hat kannst du folgenden Befehl ausführen:
flutter doctor -v
Dieser Befehl wird einige Informationen über deine aktuelle Flutter Installation ausgeben. Abhängig von deiner Plattform musst du eventuell einige Entwicklungspakete installieren. Bitte konsultiere die SDK Installationsseite, um die richtigen Pakete zu finden.
Projekt Start
Der gesamte, für dieses Projekt erstellte, Code kann in diesem Github Repository gefunden werden.
Flutter hat (wie Django) Management Befehle, um schnell Projekte und andere Templates, wie Plugins zu erstellen. Folgender Befehl kann genutzt werden um ein neues Projekt mit dem Base Template zu erstellen:
flutter create --org de.blueshoe blue_todo
Dieser Befehl kreiert ein neues Projekt mit der folgenden Ordnerstruktur in blue_todo:
Wie du sehen kannst, hat Flutter automatisch Verzeichnisse für alle unterstützten Plattformen erstellt. Wir werden nur das lib- und das linux-Verzeichnis verwenden. lib enthält den dart- und plattform unspezifischen Code, während linux den plattformspezifischen Linux Code enthält. Flutter bietet die Kommunikation mit nativen Funktionen und Bibliotheken über sogenannte „Platform-Channels“. Diese sind ein mächtiges Werkzeug, um native Funktionalität und Leistung zu nutzen. Eine Erklärung dieser würde allerdings den Rahmen dieses Artikels sprengen. Aber vielleicht kommt ja bald ein neuer Artikel dazu?
Als nächstes installieren wir ein paar Abhängigkeiten! Du kannst auf pub.dev nach diesen suchen.
flutter pub add objectbox objectbox_flutter_libs:any flutter_bloc gap intl
flutter pub add --dev build_runner objectbox_generator:any
Dadurch werden die erforderlichen ObjectBox
Abhängigkeiten sowie build_runner
hinzugefügt, mit dem ObjectBox Code generieren kann. Außerdem wird flutter_bloc
installiert, eine Business Logic Controller (BLoC) Implementierung für Flutter. BLoC ermöglicht eine einfacheres und besseres State Management (wir kommen später darauf zu sprechen, was State Mangement genau ist).
Außerdem werden die Gap
- und Intl
-Abhängigkeiten installiert. Gap ist ein sehr hilfreiches Widget zum Anordnen von Widgets in einer Spalte oder Zeile. Und intl
ermöglicht das Formatieren eines Datums.
Projekt Implementierung
Tauchen wir nun in den Code ein! Die Standarddatei main.dart
enthält die folgende Implementierung einer Counter-App:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Dieser Code deckt alle grundlegenden Prinzipien ab, wie man eine Flutter-Anwendung erstellt. Flutter basiert auf Widgets. Widgets sind die Bausteine der Benutzeroberfläche, und alles andere (Ihre Business-Logik) wird über den State gesteuert. State macht deine Anwendung reaktiv und interaktiv. Im Flutter-Beispiel haben wir einen FloatingActionButton (FAB) (ein Widget aus der MaterialDesign-Bibliothek). Der, wenn er gedrückt wird, eine Zählervariable inkrementiert. Das Framework ist dafür verantwortlich, seinen State neu zu generieren, wenn setState aufgerufen wird, und zeigt den inkrementierten Zähler an.
Dann lass uns doch mal flutter run -d linux
ausführen und schauen wie das Beispiel aussieht:
Wie im Screenshot zu sehen, habe ich den FAB 5 Mal gedrückt und die Benutzeroberfläche wurde aktualisiert, um dies anzuzeigen. Flutter zeigt auch ein schönes Debug-Banner an, damit ich weiß, dass ich die Anwendung im Debug-Modus gestartet habe.
Beginnen wir nun mit der Implementierung unserer ToDo-App! Zuerst werde ich ein neues Verzeichnis namens business_logic erstellen. Dieses Verzeichnis wird unseren Flutter BLoC-Code beherbergen. Es folgen zwei weitere Verzeichnisse namens models und components. Bei einer größeren App sieht die Verzeichnisstruktur natürlich anders aus. Aber für dieses kleine Single Page Application Projekt ist das ausreichend. Nach den Verzeichnissen fügen wir den Business Logic Code hinzu: Wir werden in diesem Beispiel ein sogenanntes Cubit verwenden. Du kannst dir ein Cubit als einen weniger komplexen BLoC vorstellen. Zuerst müssen wir die States des Cubits definieren:
// todo_state.dart
part of 'todo_cubit.bloc.dart';
sealed class TodoState extends Equatable {
const TodoState();
@override
List<Object> get props => [];
}
final class TodoInitial extends TodoState {}
final class TodoLoading extends TodoState {}
final class TodoDone extends TodoState {
final List<Todo> todos;
const TodoDone(this.todos);
@override
List<Object> get props => [
todos,
];
}
// in case anything goes wrong
final class TodoFailed extends TodoState {}
States sind Klassen, die vom Cubit verwendet werden, um zwischen, nun ja, seinem aktuellen State zu unterscheiden. Das Ändern des States im Cubit wird sich in der Benutzeroberfläche widerspiegeln, und du solltest für verschiedene States unterschiedliche Dinge anzeigen.
Danach können wir unser Cubit hinzufügen:
// todo_cubit.bloc.dart
import 'package:blue_todo/models/todo.dart';
import 'package:blue_todo/objectbox.g.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'todo_state.dart';
class TodoCubit extends Cubit<TodoState> {
final Box todoBox;
TodoCubit({required this.todoBox}) : super(TodoInitial()) {
loadTodos();
}
loadTodos() async {
emit(TodoLoading());
try {
final todos = List<Todo>.from(await todoBox.getAllAsync());
emit(TodoDone(todos));
} catch (ex) {
emit(TodoFailed());
}
}
addTodo(Todo todo) async {
final state = this.state;
if (state is TodoDone) {
final id = await todoBox.putAsync(todo);
todo.id = id;
emit(
TodoDone(
[
todo,
...state.todos,
],
),
);
}
}
deleteTodo(Todo toDelete) async {
final state = this.state;
if (state is TodoDone) {
await todoBox.removeAsync(toDelete.id);
emit(
TodoDone(
state.todos.where((t) => t.id != toDelete.id).toList(),
),
);
}
}
}
Es gibt eine Menge zu sehen, also schauen wir uns den Code Schritt für Schritt an. Beginnen wir mit dem Cubit-Konstruktor. Er wird mit der Datenbank und dem Anfangs-State TodoInitial initiert. Dies kann ein beliebiger State sein, den Du definieren kannst. Er ruft auch loadTodos auf, sobald er initialisiert ist.
loadTodos “emitted” zuerst TodoLoading, was signalisieren soll, dass wir auf Daten warten oder etwas anderes im Hintergrund tun. emit ist die Methode, um States in einem BLoC/Cubit zu ändern. Dann holt loadTodos alle Todos aus der Datenbank und gibt den State TodoDone aus. Dieser State ist besonders, weil er die Todos speichert, die wir anzeigen wollen.
Es gibt zwei weitere Methoden im Cubit: addTodo und deleteTodo. Diese Methoden dienen zum Hinzufügen bzw. Entfernen eines Todos. Beide prüfen, ob wir uns im richtigen State befinden, bevor sie ihre jeweiligen Aktionen ausführen. Wenn der State nicht stimmt, wäre es klug, eine Fehlerbehandlung hinzuzufügen oder dem Benutzer einen Hinweis zu geben, dass etwas schief gelaufen ist.
Damit ist die gesamte Logik, die wir brauchen, fertig!
Jetzt können wir den UI-Code hinzufügen. Der komplette UI Code kann hier eingesehen werden.
Ansonsten ist es am wichtigsten zu verstehen, wie wir BlocProvider und BlocBuilder benutzen können, um das UI, abhängig vom TodoState, zu verändern.
Lass uns deswegen den folgenden Code-Abschnitt ansehen:
BlocBuilder<TodoCubit, TodoState>(builder: (context, state) {
if (state is TodoInitial || state is TodoLoading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
if (state is TodoFailed) {
return const Center(
child: Text("Failed to load ToDo's :("),
);
}
final doneState = state as TodoDone;
if (doneState.todos.isEmpty) {
return const Text(
"No ToDo's added yet. Add some via the bottom right FloatingActionButton!",
);
}
...
}
Wie du sehen kannst, wird das BlocBuilder Widget verwendet, um zwischen den TodoStates zu unterscheiden. Für jeden State geben wir etwas anderes und kontextbezogenes zurück. Du kannst auch auf andere Dinge als den State prüfen, z.B. ob die Todos leer sind.
Wenn keine der vorherigen Überprüfungen erfüllt ist, werden alle Todos mit dem folgenden Codeschnipsel angezeigt:
return Expanded(
child: ListView.separated(
itemCount: doneState.todos.length,
itemBuilder: (context, index) => Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text(doneState.todos[index].title!),
subtitle: Text(doneState.todos[index].description!),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"due at: ${DateFormat('dd.MM.yyyy').format(doneState.todos[index].dueDate!)}",
),
const Gap(8.0),
IconButton(
onPressed: () => _deleteTodo(
context,
doneState.todos[index],
),
icon: Icon(MdiIcons.trashCanOutline))
],
),
),
),
),
separatorBuilder: (context, index) => const Divider(),
),
);
Expanded ist ein Widget, das dem untergeordneten Widget mitteilt, dass es den gesamten verbleibenden Platz in der Hauptachse des übergeordneten Widgets einnehmen soll. In diesem Fall sollte die zugrunde liegende ListView den gesamten verbleibenden vertikalen Platz in der übergeordneten Column einnehmen.
Die ListView hat eine Builder-Eigenschaft, die für die Erstellung von Kindern der ListView und deren Anzeige verantwortlich ist. Die Kinder bestehen im Wesentlichen aus einer ListTile, einem Widget, das eine bestimmte Struktur für seine Kinder vorgibt. Auf diese Weise kannst Du einen Titel und einen Untertitel hinzufügen, die sich automatisch und immer untereinander auf der linken Seite des Widgets befinden. Die Eigenschaft Trailing dient zum Hinzufügen von Widgets am Ende der ListTile. Hier haben wir das Fälligkeitsdatum und einen Button zum Löschen hinzugefügt. Der Lösch-Button ruft _deleteTodo auf, was wiederum die deleteTodo-Methode des Cubits aufruft, wodurch der State geändert und die Benutzeroberfläche neu gezeichnet wird.
Ein weiterer interessanter Teil des Codes ist das Hinzufügen von ToDos. Wenn Du den FloatingActionButton drückst, erscheint ein Dialog, der einige Eingaben abfragt (z.B. Titel und Beschreibung) und wenn Du auf Speichern klickst, wird das ToDo an den Cubit übertragen und in der Datenbank gespeichert.
Die speziellen Widgets BlueshoeTextfield und BlueshoeDatefield sind im Git Repository auf GitHub zu finden.
Und wie sieht das alles aus? Hier einige Screenshots:
Ohne ToDo’s
Ein ToDo hinzufügen
Sobald ein ToDo hinzugefügt ist
Ich hoffe, dieses kurze Beispiel hat Dir gezeigt, dass man mit Flutter sehr schnell vielseitige und schöne Apps bauen kann!
Blueshoe x Flutter
Bei Blueshoe streben wir nach höchster Performance und Usability in unseren Anwendungen. Mit Flutter können wir dieses Versprechen halten und alle Erwartungen unserer Kunden erfüllen.
Wenn Du jemanden für Deine nächste App suchst, dann zögere nicht, kontakt mit uns aufzunehmen!