Flutter Monorepo: Multi-Paket-Verwaltung mit Melos

11 Min. Lesezeit9. März 2026
Flutter monorepoMelos FlutterFlutter multi-packageFlutter shared codeFlutter workspaceFlutter modular architectureMelos CI/CDFlutter code sharing

# Multi-Package Flutter Monorepo mit geteiltem Code

Mehrere Flutter-Apps zu verwalten, die sich Geschaeftslogik, UI-Komponenten und API-Clients teilen, wirkt auf den ersten Blick einfach. Drei Monate spaeter ertrinkt man jedoch in kopiertem Code. Ein Monorepo mit geteilten Packages loest dieses Problem, indem alles in einem einzigen Repository lebt und gleichzeitig klare Grenzen zwischen den Packages bestehen bleiben. In einem Multi-App-Projekt, das ich verantwortet habe, hat der Wechsel zum Monorepo die Code-Duplizierung um ueber 60% reduziert und die Zeit, ein neues Feature in allen Apps auszurollen, von zwei Wochen auf drei Tage verkuerzt.

Warum Monorepo fuer Flutter-Projekte?

Solange man nur eine App hat, funktioniert die Standard-Flutter-Projektstruktur bestens. Sobald eine zweite App hinzukommt -- etwa eine Kunden-App und ein Admin-Panel oder ein White-Label-Produkt mit mehreren Markenvarianten -- wird es kompliziert.

Die Copy-Paste-Falle: Man beginnt damit, geteilten Code zwischen Repositories zu kopieren. Anfangs fuehlt es sich schnell an. Dann erreicht ein Bugfix in einem Repo die anderen nicht. Modelle driften auseinander. Der API-Client in App A behandelt Paginierung anders als in App B. Drei Monate spaeter pflegt man drei leicht unterschiedliche Versionen desselben Codes.

Der Package-Publishing-Aufwand: Man koennte geteilten Code in veroeffentlichte Dart-Packages auslagern. Aber dann braucht man eine private Package-Registry, Versionierungsdisziplin und einen Release-Prozess fuer jede kleine Aenderung. Fuer die meisten Teams ist das unnoetige Reibung.

Die Monorepo-Loesung: Alles lebt in einem Repository. Geteilter Code ist in lokalen Packages organisiert. Alle Apps referenzieren diese Packages ueber Path-Dependencies. Eine Aenderung am geteilten API-Client steht sofort in jeder App zur Verfuegung. Ein PR, ein Review, ein Merge.

Vorteile, die sich ueber die Zeit summieren:

  • **Atomare Aenderungen**: Ein geteiltes Modell aktualisieren und alle betroffenen Apps in einem einzigen Commit reparieren.
  • **Konsistentes Tooling**: Eine Lint-Konfiguration, eine CI-Pipeline, ein Satz Codequalitaetsregeln.
  • **Einfaches Onboarding**: Neue Entwickler klonen ein Repo und haben alles, was sie brauchen.
  • **Refactoring-Vertrauen**: Eine Klasse in einem geteilten Package umbenennen und jeden Bruch sofort sehen.
  • Melos: Der Monorepo-Orchestrator

    Melos ist das De-facto-Tool zur Verwaltung von Dart- und Flutter-Monorepos. Es uebernimmt Dependency-Resolution, Skript-Orchestrierung und selektive Befehlsausfuehrung ueber Packages hinweg. Man kann es sich als den Dirigenten vorstellen, der alle Packages im Einklang haelt.

    Installation

    yaml
    # In die Root-pubspec.yaml einfuegen
    dev_dependencies:
      melos: ^6.1.0
    bash
    dart pub get
    dart run melos bootstrap

    Melos-Konfiguration

    Die Datei `melos.yaml` im Wurzelverzeichnis des Repositories definiert den Workspace. Hier eine produktionsreife Konfiguration:

    yaml
    name: flutter_workspace
    repository: https:"code-comment">//github.com/meinorg/flutter-workspace
    
    packages:
      - apps/*
      - packages/*
    
    command:
      bootstrap:
        usePubspecOverrides: true
    
    scripts:
      analyze:
        run: dart analyze --fatal-infos
        exec:
          concurrency: 5
        description: Statische Analyse in allen Packages ausfuehren.
    
      test:
        run: flutter test --coverage
        exec:
          concurrency: 3
          failFast: true
        packageFilters:
          dirExists: test
        description: Tests in allen Packages mit Test-Verzeichnis ausfuehren.
    
      format:
        run: dart format --set-exit-if-changed .
        description: Formatierung im gesamten Workspace pruefen.
    
      build:runner:
        run: dart run build_runner build --delete-conflicting-outputs
        exec:
          concurrency: 1
          failFast: true
        packageFilters:
          dependsOn: build_runner
        description: build_runner in Packages mit build_runner-Abhaengigkeit ausfuehren.
    
      clean:
        run: flutter clean
        exec:
          concurrency: 5
        description: Alle Packages bereinigen.
    
      pub:upgrade:
        run: flutter pub upgrade
        exec:
          concurrency: 3
        description: Abhaengigkeiten in allen Packages aktualisieren.

    Die Option `packageFilters` ist maechtig. Statt build_runner in jedem Package auszufuehren (die meisten verwenden es gar nicht), zielt Melos nur auf Packages ab, die tatsaechlich davon abhaengen. Das spart erheblich CI-Zeit.

    Ordnerstruktur fuer ein echtes Monorepo

    Hier die Ordnerstruktur, die ich in der Produktion verwende. Sie hat sich ueber mehrere Projekte hinweg entwickelt und vereint Klarheit mit Praktikabilitaet:

    flutter_workspace/
      melos.yaml
      pubspec.yaml
      apps/
        kunden_app/
          lib/
          test/
          pubspec.yaml
          android/
          ios/
        admin_app/
          lib/
          test/
          pubspec.yaml
          android/
          ios/
        fahrer_app/
          lib/
          test/
          pubspec.yaml
          android/
          ios/
      packages/
        core/
          lib/
            src/
              constants/
              extensions/
              errors/
              utils/
            core.dart
          test/
          pubspec.yaml
        models/
          lib/
            src/
              user/
              order/
              product/
            models.dart
          test/
          pubspec.yaml
        api_client/
          lib/
            src/
              interceptors/
              endpoints/
              client.dart
            api_client.dart
          test/
          pubspec.yaml
        ui_kit/
          lib/
            src/
              theme/
              widgets/
              tokens/
            ui_kit.dart
          test/
          pubspec.yaml
        analytics/
          lib/
            src/
              providers/
              events/
            analytics.dart
          test/
          pubspec.yaml
      tools/
        ci/
        scripts/

    Jedes Verzeichnis unter `packages/` ist ein eigenstaendiges Dart- oder Flutter-Package mit eigener `pubspec.yaml`, Tests und Barrel-Datei. Das `apps/`-Verzeichnis enthaelt die eigentlichen Flutter-Anwendungen. Das `tools/`-Verzeichnis beherbergt CI-Skripte und Hilfstools.

    Geteilte Packages im Detail

    Core-Package

    Das Core-Package enthaelt grundlegende Hilfsmittel, von denen jedes andere Package abhaengt. Es sollte stabil, gut getestet und selten veraendert werden.

    dart
    class=class="code-string">"code-comment">// packages/core/lib/src/errors/app_exception.dart
    abstract class AppException implements Exception {
      final String message;
      final String? code;
      final StackTrace? stackTrace;
    
      const AppException({
        required this.message,
        this.code,
        this.stackTrace,
      });
    
      @override
      String toString() => class="code-string">'AppException($code): $message';
    }
    
    class NetworkException extends AppException {
      final int? statusCode;
    
      const NetworkException({
        required super.message,
        this.statusCode,
        super.code,
        super.stackTrace,
      });
    }
    
    class CacheException extends AppException {
      const CacheException({
        required super.message,
        super.code,
        super.stackTrace,
      });
    }
    dart
    class=class="code-string">"code-comment">// packages/core/lib/src/extensions/date_extensions.dart
    extension DateTimeX on DateTime {
      String get formatted => class="code-string">'$day/$month/$year';
    
      bool get isToday {
        final now = DateTime.now();
        return year == now.year && month == now.month && day == now.day;
      }
    
      bool get isPast => isBefore(DateTime.now());
    }

    Models-Package

    Geteilte Modelle sind das wirkungsvollste Package in einem Monorepo. Wenn alle Apps dasselbe `User`-, `Order`- oder `Product`-Modell verwenden, eliminiert man eine ganze Klasse von Bugs, die durch Model-Drift entstehen.

    dart
    class=class="code-string">"code-comment">// packages/models/lib/src/user/user.dart
    import class="code-string">'package:freezed_annotation/freezed_annotation.dart';
    
    part class="code-string">'user.freezed.dart';
    part class="code-string">'user.g.dart';
    
    @freezed
    class User with _$User {
      const factory User({
        required String id,
        required String email,
        required String displayName,
        required UserRole role,
        required DateTime createdAt,
        String? avatarUrl,
        @Default(false) bool isVerified,
      }) = _User;
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    }
    
    enum UserRole {
      @JsonValue(class="code-string">'customer')
      customer,
      @JsonValue(class="code-string">'admin')
      admin,
      @JsonValue(class="code-string">'driver')
      driver,
    }

    API-Client-Package

    Ein geteilter API-Client stellt konsistente Authentifizierung, Fehlerbehandlung und Request-/Response-Verarbeitung ueber alle Apps hinweg sicher.

    dart
    class=class="code-string">"code-comment">// packages/api_client/lib/src/client.dart
    import class="code-string">'package:dio/dio.dart';
    import class="code-string">'package:core/core.dart';
    import class="code-string">'package:models/models.dart';
    
    class ApiClient {
      final Dio _dio;
    
      ApiClient({required String baseUrl, required TokenProvider tokenProvider})
          : _dio = Dio(BaseOptions(baseUrl: baseUrl)) {
        _dio.interceptors.addAll([
          AuthInterceptor(tokenProvider: tokenProvider),
          LoggingInterceptor(),
          RetryInterceptor(maxRetries: class="code-number">3),
        ]);
      }
    
      Future<User> getCurrentUser() async {
        try {
          final response = await _dio.get(class="code-string">'/users/me');
          return User.fromJson(response.data);
        } on DioException catch (e) {
          throw NetworkException(
            message: e.message ?? class="code-string">'Benutzer konnte nicht geladen werden',
            statusCode: e.response?.statusCode,
          );
        }
      }
    
      Future<List<Order>> getOrders({int page = class="code-number">1, int limit = class="code-number">20}) async {
        try {
          final response = await _dio.get(
            class="code-string">'/orders',
            queryParameters: {class="code-string">'page': page, class="code-string">'limit': limit},
          );
          return (response.data as List)
              .map((json) => Order.fromJson(json))
              .toList();
        } on DioException catch (e) {
          throw NetworkException(
            message: e.message ?? class="code-string">'Bestellungen konnten nicht geladen werden',
            statusCode: e.response?.statusCode,
          );
        }
      }
    }

    UI-Kit-Package

    Das UI-Kit sorgt fuer eine konsistente visuelle Sprache ueber alle Apps hinweg. Es enthaelt das geteilte Theme, Design-Tokens und wiederverwendbare Widgets.

    dart
    class=class="code-string">"code-comment">// packages/ui_kit/lib/src/theme/app_theme.dart
    import class="code-string">'package:flutter/material.dart';
    
    class AppTheme {
      static ThemeData light({Color? primaryColor}) {
        final primary = primaryColor ?? const Color(0xFF2563EB);
        return ThemeData(
          useMaterial3: true,
          colorSchemeSeed: primary,
          brightness: Brightness.light,
          textTheme: _textTheme,
          inputDecorationTheme: _inputTheme,
          elevatedButtonTheme: _buttonTheme(primary),
        );
      }
    
      static const _textTheme = TextTheme(
        headlineLarge: TextStyle(fontWeight: FontWeight.w700, fontSize: class="code-number">28),
        titleMedium: TextStyle(fontWeight: FontWeight.w600, fontSize: class="code-number">16),
        bodyMedium: TextStyle(fontWeight: FontWeight.w400, fontSize: class="code-number">14),
      );
    }

    Abhaengigkeitsverwaltung zwischen Packages

    Die Abhaengigkeiten in einem Monorepo richtig aufzusetzen ist entscheidend. So sieht die `pubspec.yaml` einer App aus, die alle geteilten Packages nutzt:

    yaml
    # apps/kunden_app/pubspec.yaml
    name: kunden_app
    description: Mobile Anwendung fuer Endkunden.
    
    dependencies:
      flutter:
        sdk: flutter
      core:
        path: ../../packages/core
      models:
        path: ../../packages/models
      api_client:
        path: ../../packages/api_client
      ui_kit:
        path: ../../packages/ui_kit
      analytics:
        path: ../../packages/analytics
    
      # App-spezifische Abhaengigkeiten
      flutter_bloc: ^8.1.0
      go_router: ^14.0.0

    Und so haengen die geteilten Packages voneinander ab:

    yaml
    # packages/api_client/pubspec.yaml
    name: api_client
    description: Geteilter HTTP-Client mit Auth und Fehlerbehandlung.
    
    dependencies:
      dio: ^5.4.0
      core:
        path: ../core
      models:
        path: ../models
    
    dev_dependencies:
      test: ^1.25.0
      mockito: ^5.4.0
      build_runner: ^2.4.0

    Die Abhaengigkeitsregel

    Packages muessen einen gerichteten azyklischen Graphen (DAG) bilden. Wenn Package A von Package B abhaengt, darf B niemals von A abhaengen. Praktisch bedeutet das: `core` haengt von nichts ab, `models` haengt von `core` ab, `api_client` haengt von `core` und `models` ab, und Apps haengen von allem ab.

    Zirkulaere Abhaengigkeiten sind ein Compile-Time-Fehler in Dart -- der Compiler erzwingt das. Aber subtiler sollte man auch tiefe Abhaengigkeitsketten vermeiden. Wenn eine Aenderung an `core` jedes Package neu bauen laesst, leiden die CI-Zeiten. Halten Sie `core` klein und stabil.

    CI/CD fuer Monorepos

    Der groesste operationelle Vorteil eines Monorepos ist selektive CI. Man sollte nicht jedes Package neu bauen und testen, wenn sich nur eine Datei geaendert hat.

    Betroffene Packages erkennen

    Melos kann identifizieren, welche Packages von einer Aenderung betroffen sind:

    bash
    # Packages auflisten, die sich seit dem letzten Release-Tag geaendert haben
    melos list --since=origin/main --diff
    
    # Tests nur in betroffenen Packages ausfuehren
    melos run test --since=origin/main

    GitHub-Actions-Workflow

    yaml
    # .github/workflows/ci.yml
    name: CI
    
    on:
      pull_request:
        branches: [main]
    
    jobs:
      analyze-and-test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: '3.24.0'
              cache: true
    
          - name: Melos installieren
            run: dart pub global activate melos
    
          - name: Bootstrap
            run: melos bootstrap
    
          - name: Betroffene Packages analysieren
            run: melos run analyze --since=origin/main
    
          - name: Betroffene Packages testen
            run: melos run test --since=origin/main
    
          - name: Formatierung pruefen
            run: melos run format
    
      build-kunden-app:
        needs: analyze-and-test
        if: |
          contains(github.event.pull_request.labels.*.name, 'build:kunden') ||
          contains(github.event.pull_request.title, '[kunden]')
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: '3.24.0'
          - run: melos bootstrap
          - run: cd apps/kunden_app && flutter build apk --release

    Das `--since=origin/main`-Flag ist der Schluessel. Melos vergleicht den aktuellen Branch mit main und fuehrt Befehle nur in geaenderten Packages aus. Bei einem Monorepo mit 15 Packages kann das die CI-Zeit von 25 Minuten auf unter 5 Minuten reduzieren.

    Teststrategie in Monorepos

    Jedes Package sollte eigene Tests haben. Das ist keine Option -- es ist das Fundament, das Monorepo-Entwicklung tragfaehig macht.

    Package-Level Unit Tests

    Geteilte Packages brauchen gruendliche Unit-Tests, weil mehrere Apps von ihnen abhaengen. Ein Bug im `models`-Package betrifft jede App.

    dart
    class=class="code-string">"code-comment">// packages/models/test/user_test.dart
    import class="code-string">'package:models/models.dart';
    import class="code-string">'package:test/test.dart';
    
    void main() {
      group(class="code-string">'User', () {
        test(class="code-string">'fromJson erzeugt User korrekt', () {
          final json = {
            class="code-string">'id': class="code-string">'usr_123',
            class="code-string">'email': class="code-string">'test@example.com',
            class="code-string">'display_name': class="code-string">'Testbenutzer',
            class="code-string">'role': class="code-string">'customer',
            class="code-string">'created_at': class="code-string">'class="code-number">2025-class="code-number">01-15T10:class="code-number">30:00Z',
            class="code-string">'is_verified': true,
          };
    
          final user = User.fromJson(json);
    
          expect(user.id, class="code-string">'usr_123');
          expect(user.email, class="code-string">'test@example.com');
          expect(user.role, UserRole.customer);
          expect(user.isVerified, isTrue);
        });
    
        test(class="code-string">'toJson erzeugt korrekte Map', () {
          final user = User(
            id: class="code-string">'usr_123',
            email: class="code-string">'test@example.com',
            displayName: class="code-string">'Testbenutzer',
            role: UserRole.admin,
            createdAt: DateTime(class="code-number">2025, class="code-number">1, class="code-number">15),
          );
    
          final json = user.toJson();
    
          expect(json[class="code-string">'role'], class="code-string">'admin');
          expect(json[class="code-string">'is_verified'], false);
        });
      });
    }

    Integrationstests auf App-Ebene

    Apps sollten Integrationstests haben, die den vollstaendigen Stack durchspielen, einschliesslich der geteilten Packages in ihrer Zusammenschaltung.

    dart
    class=class="code-string">"code-comment">// apps/kunden_app/test/integration/order_flow_test.dart
    import class="code-string">'package:kunden_app/features/orders/order_bloc.dart';
    import class="code-string">'package:api_client/api_client.dart';
    import class="code-string">'package:models/models.dart';
    import class="code-string">'package:flutter_test/flutter_test.dart';
    import class="code-string">'package:mockito/mockito.dart';
    
    class MockApiClient extends Mock implements ApiClient {}
    
    void main() {
      late OrderBloc bloc;
      late MockApiClient mockApi;
    
      setUp(() {
        mockApi = MockApiClient();
        bloc = OrderBloc(apiClient: mockApi);
      });
    
      test(class="code-string">'laedt Bestellungen vom API-Client', () async {
        when(mockApi.getOrders()).thenAnswer(
          (_) async => [
            Order(id: class="code-string">'class="code-number">1', status: OrderStatus.pending, total: class="code-number">29.99),
          ],
        );
    
        bloc.add(LoadOrders());
    
        await expectLater(
          bloc.stream,
          emitsInOrder([
            isA<OrdersLoading>(),
            isA<OrdersLoaded>(),
          ]),
        );
      });
    }

    Geteilte Packages isoliert testen

    Eine entscheidende Regel: Tests geteilter Packages duerfen niemals von App-Code abhaengen. Wenn der `api_client`-Test etwas aus `kunden_app` importiert, hat man eine zirkulaere Abhaengigkeit in der Entstehung. Verwenden Sie Mocks und Fakes innerhalb des Packages selbst.

    Monorepo vs. Multi-Repo: Wann was waehlen

    Das ist keine Glaubensfrage. Jeder Ansatz passt zu unterschiedlichen Teamstrukturen und Projektrealitaeten.

    Monorepo waehlen, wenn:

  • Man 2-5 Apps hat, die signifikante Geschaeftslogik teilen.
  • Ein kleines bis mittleres Team (2-15 Entwickler) appuebergreifend arbeitet.
  • Geteilter Code sich haeufig aendert und sofort verfuegbar sein muss.
  • Man eine einzige CI/CD-Pipeline und einen einheitlichen Code-Review-Prozess moechte.
  • Apps in aehnlicher Kadenz versioniert und released werden.
  • Multi-Repo waehlen, wenn:

  • Apps von voellig unabhaengigen Teams mit unterschiedlichen Release-Zyklen verantwortet werden.
  • Geteilter Code stabil ist und sich selten aendert (als versioniertes Package veroeffentlichen).
  • Die Organisation 50+ Entwickler hat und das Monorepo zum Engpass wuerde.
  • Apps unterschiedliche Tech-Stacks verwenden (eine Flutter, eine React Native).
  • Regulatorische Anforderungen getrennte Repositories fuer Audit-Trails verlangen.
  • Der hybride Ansatz: Manche Teams starten mit einem Monorepo und befoerdern stabile Packages mit der Zeit zu veroeffentlichten Packages. Das `core`-Package, das sich seit sechs Monaten nicht geaendert hat, kann eine versionierte Abhaengigkeit werden. Das `models`-Package, das sich mit jedem API-Update aendert, bleibt im Monorepo.

    Haeufige Monorepo-Fehler

    1. Gott-Package

    Alles in ein einziges `shared`-Package zu packen verfehlt den Zweck. Wenn `shared` Modelle, API-Clients, Hilfsmittel, Theme-Daten und Analytics enthaelt, loest jede Aenderung einen Rebuild von allem aus. Gliedern Sie nach Domaene, nicht nach Bequemlichkeit.

    2. Cross-App-Imports

    App A sollte niemals Code aus App B importieren. Wenn beide Apps dasselbe Widget brauchen, verschieben Sie es ins `ui_kit`-Package. Cross-App-Imports erzeugen unsichtbare Kopplung und machen es unmoeglich, Apps unabhaengig zu bauen.

    3. Inkonsistente Abhaengigkeitsversionen

    Wenn `kunden_app` `dio: ^5.3.0` verwendet und `admin_app` `dio: ^5.4.0`, laden Sie subtile Laufzeitunterschiede ein. Zentralisieren Sie Versionseinschraenkungen oder erzwingen Sie Konsistenz ueber Melos-Dependency-Overrides.

    4. Den Bootstrap-Schritt ueberspringen

    Neue Teammitglieder werden das Repo klonen und `flutter pub get` im App-Verzeichnis ausfuehren. Das schlaegt fehl, weil Path-Dependencies nicht aufgeloest werden. Dokumentieren Sie, dass `melos bootstrap` der erste auszufuehrende Befehl ist, und erzwingen Sie es mit einem Setup-Skript.

    5. Packages ueber Apps testen

    Das `api_client`-Package zu testen, indem man die Kunden-App startet und herumklickt, ist keine Teststrategie. Jedes Package muss eigene Unit-Tests haben, die unabhaengig laufen. Die CI-Pipeline sollte das erzwingen.

    6. Monorepo ohne Tooling

    Ein Monorepo ohne Melos (oder vergleichbares Tooling) ist nur ein Ordner mit mehreren Pubspec-Dateien. Man verliert alle Vorteile -- selektives Testen, Skript-Orchestrierung, Abhaengigkeitsverwaltung. Investieren Sie vom ersten Tag an in das Tooling.

    Schritt fuer Schritt loslegen

    Wenn Sie ueberzeugt sind und Ihr erstes Flutter-Monorepo aufsetzen moechten, hier der minimale Weg:

    bash
    # Workspace erstellen
    mkdir mein_workspace && cd mein_workspace
    dart pub init
    # melos zu dev_dependencies hinzufuegen, dann:
    dart pub get
    
    # Struktur anlegen
    mkdir -p apps/meine_app packages/core packages/models
    
    # Packages initialisieren
    cd packages/core && dart pub init && cd ../..
    cd packages/models && dart pub init && cd ../..
    
    # melos.yaml im Root erstellen (siehe Konfiguration oben)
    
    # Alles bootstrappen
    dart run melos bootstrap

    Von hier aus verschieben Sie geteilten Code schrittweise in Packages. Versuchen Sie nicht, alles auf einmal zu extrahieren. Beginnen Sie mit Modellen -- sie sind am einfachsten zu extrahieren und bieten den unmittelbarsten Nutzen.

    Meiner Erfahrung nach wird ein gut strukturiertes Monorepo zum Rueckgrat eines Multi-App-Flutter-Projekts. Die initiale Einrichtung dauert ein bis zwei Tage. Die in den folgenden Monaten eingesparte Zeit misst sich in Wochen. Der Schluessel sind disziplinierte Package-Grenzen und die Investition in CI von Anfang an.

    Wenn Sie eine Multi-App-Flutter-Architektur planen, lassen Sie uns gemeinsam besprechen, welche Struktur am besten zu Ihrem Team passt.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen