Mobilutvikling med Flutter · leksjon

Funksjonsbasert mappestruktur og Melos-monorepoer

Organiser funksjoner som uavhengige pakker, og administrer dem med et Melos-monorepo.

Leksjon 3 av 413 trinn

Funksjonsbasert mappestruktur og Melos-monorepoer er en gratis leksjon i Mobilutvikling med Flutter på CoddyKit. Dette er leksjon 3 av 4. Du kan lese valgfritt 3 leksjoner fra denne læringsstien gratis i sin helhet – deretter låser CoddyKit PRO opp alle leksjoner, samt praktisk øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Mobilutvikling med Flutter, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Mobilutvikling med Flutter inneholder totalt 4 leksjoner.

Hvorfor funksjonsorientert?

Etter hvert som en Flutter-app vokser, begynner den klassiske lagorienterte strukturen (mapper på toppnivå som screens/, widgets/, models/ og services/) å skape problemer. For å endre én funksjon må De hoppe mellom fem mapper, og team som jobber med uavhengige deler, kommer i konflikt i de samme filene.

Funksjonsorientering snur dette: Toppnivået organiseres etter forretningsfunksjon (auth, cart, profile), og hver funksjon eier sine egne lag internt.

  • Høy kohesjon — alt som gjelder en funksjon, ligger samlet.
  • Lav kobling — funksjoner avhenger av kontrakter, ikke av hverandres interne detaljer.
  • Skalerbarhet — en funksjon kan senere trekkes ut i sin egen pakke med nesten ingen omstrukturering.

En funksjonsmappes anatomi

Inne i lib/features/<feature>/ gjenspeiler vi Clean Architectures tre lag. domain-laget er ren Dart uten Flutter- eller plugin-importer, og nettopp derfor er det enkelt å trekke ut en funksjon senere.

En typisk auth-funksjon:

  • data/ — DTO-er, datakilder og repository-implementasjoner.
  • domain/ — entiteter, repository-grensesnitt og use cases.
  • presentation/ — widgets, sider og tilstand (BLoC/Riverpod).
lib/
├── core/                  // shared cross-cutting code
│   ├── error/
│   └── network/
└── features/
    └── auth/
        ├── data/
        │   ├── models/
        │   ├── datasources/
        │   └── repositories/
        ├── domain/
        │   ├── entities/
        │   ├── repositories/
        │   └── usecases/
        └── presentation/
            ├── bloc/
            ├── pages/
            └── widgets/

Domain-laget er ren Dart

Den viktigste regelen som gjør en funksjon portabel: domain-laget importerer ingenting fra Flutter eller plugins. Entiteter er vanlige, uforanderlige Dart-klasser, og repository-kontrakter er abstrakte grensesnitt.

Fordi denne koden ikke har avhengigheter til et framework, kan den enkelt enhetstestes og kjøres på enhver Dart-VM — også i en nettbasert kodearena.

// domain/entities/user.dart
class User {
  final String id;
  final String email;
  const User({required this.id, required this.email});
}

// domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Future<User> signIn(String email, String password);
}

void main() {
  const user = User(id: '1', email: 'ada@example.com');
  print('Loaded ${user.email}');
}

Use cases uttrykker forretningsregler

Et use case er et objekt med ett ansvarsområde som orkestrerer én handling i applikasjonen. Det avhenger bare av repository-grensesnittet, aldri av implementasjonen. Dette holder presentasjonslaget enkelt og reglene testbare.

Den vanlige Flutter-konvensjonen er å gi hvert use case en call-metode, slik at det kan kalles som en funksjon.

// domain/usecases/sign_in.dart
class User {
  final String email;
  const User(this.email);
}

abstract class AuthRepository {
  Future<User> signIn(String email, String password);
}

class SignIn {
  final AuthRepository repository;
  const SignIn(this.repository);

  Future<User> call(String email, String password) {
    if (!email.contains('@')) {
      throw ArgumentError('Invalid email');
    }
    return repository.signIn(email, password);
  }
}

class FakeRepo implements AuthRepository {
  @override
  Future<User> signIn(String email, String password) async => User(email);
}

void main() async {
  final signIn = SignIn(FakeRepo());
  final user = await signIn('ada@example.com', 'pw');
  print('Signed in: ${user.email}');
}

Fra mapper til pakker

Feature-fokuserte mapper er et godt første steg, men mapper håndhever ikke grenser — alle filer kan fortsatt importere alle andre filer. Neste nivå er å gjøre hver feature til en egen Dart-pakke med sin egen pubspec.yaml.

Nå håndhever kompilatoren isolasjon: En pakke kan bare bruke det den uttrykkelig deklarerer som en avhengighet. Det er her en monorepo kommer inn — mange pakker, ett repositorium.

  • packages/feature_auth/
  • packages/feature_cart/
  • packages/core/
  • apps/mobile/ (den kjørbare Flutter-appen som kobler sammen features)

Hva er Melos?

Melos er et CLI-verktøy for å administrere Dart-/Flutter-monorepoer med flere pakker. Det løser problemet med å kjøre pub get, tester og versjonsoppdateringer på tvers av dusinvis av pakker.

  • Bootstrap — kobler sammen lokale pakker og løser alle avhengigheter med én kommando.
  • Skripting — definer navngitte skript som kjøres på tvers av alle pakker.
  • Versjonering — versjonsoppdateringer og endringslogger basert på konvensjonelle commits.

Du installerer det én gang globalt og konfigurerer det per repositorium med en melos.yaml.

dart pub global activate melos

# from the monorepo root
melos bootstrap   # or: melos bs

Konfigurere melos.yaml

I roten av repositoriet deklarerer melos.yaml arbeidsområdets navn og hvor pakkene ligger (glob-mønstre). Moderne Melos (3+) støtter også å deklarere arbeidsområdet i roten sin pubspec.yaml, men en dedikert fil er fortsatt vanlig.

packages-globene forteller Melos hvilke mapper som er administrerte medlemmer av monorepoet.

# melos.yaml
name: shop_workspace

packages:
  - apps/**
  - packages/**

command:
  bootstrap:
    usePubspecOverrides: true

Koble sammen lokale baneavhengigheter

Inne i appen er du avhengig av lokale feature-pakker via navnet. Med usePubspecOverrides: true legger Melos automatisk inn lokale path:-overstyringer under bootstrap, slik at den innsendte pubspec.yaml kan referere til versjoner, mens lokal utvikling fortsatt bruker kildekoden.

Resultatet er at endringer i en fil i feature_auth blir synlige i appen med én gang — ingen publisering eller kopiering og innliming.

# apps/mobile/pubspec.yaml
name: mobile
dependencies:
  flutter:
    sdk: flutter
  feature_auth: ^1.0.0
  feature_cart: ^1.0.0
  core: ^1.0.0

# Melos generates pubspec_overrides.yaml on bootstrap:
# dependency_overrides:
#   feature_auth:
#     path: ../../packages/feature_auth

Definere skript for arbeidsområdet

Den største gevinsten i hverdagen er skript. Du definerer en kommando én gang og kjører den på tvers av alle pakker som samsvarer med et filter. Dette erstatter sårbare shell-løkker.

Vanlige filtre omfatter --dir-exists=test (bare pakker som har tester) og --depends-on=core (bare pakker som er avhengige av en gitt pakke).

# melos.yaml
scripts:
  analyze:
    run: melos exec -- dart analyze .
    description: Analyze every package.

  test:
    run: melos exec --dir-exists=test -- flutter test
    description: Run tests only where a test/ dir exists.

# usage:
#   melos run analyze
#   melos run test

Holde features frikoblet

Pakker håndhever grenser, men du må fortsatt utforme kontraktene. En god tommelfingerregel er:

  • Features importerer aldri hverandre direkte.
  • Delte kontrakter og primitiver ligger i en lavnivåpakken core som alt kan være avhengig av.
  • Navigering på tvers av features går gjennom en abstraksjon (en ruter eller en event-buss) som injiseres på app-nivå.

Denne avhengighetsretningen — features → core, app → features — holder grafen asyklisk. En syklus mellom to feature-pakker vil ikke kunne løses, noe som er et nyttig tidlig varsel.

// packages/core/lib/src/app_route.dart  (shared contract)
abstract class AppRouter {
  void goToCart();
  void goToProfile();
}

// feature_auth depends on `core`, never on feature_cart.
class LoginController {
  final AppRouter router;
  const LoginController(this.router);

  void onLoginSuccess() => router.goToCart();
}

void main() => print('Auth depends on core, not on cart.');

En typisk arbeidsflyt for utviklere

Sett under ett er den daglige sløyfen i et Melos-monorepo kort og ensartet, uansett hvor mange pakker du har:

  • melos bootstrap etter at du har hentet nye endringer, for å koble sammen pakkene på nytt.
  • melos run analyze og melos run test i CI og lokalt.
  • melos version for å oppdatere versjonen på endrede pakker basert på konvensjonelle commits.

Fordi appen er det eneste Flutter-kjørbare målet, og features er vanlige pakker, kan CI teste domenelogikk på den rene Dart-VM-en (raskt) og bare starte Flutter for presentasjonstester.

# clone + first run
melos bootstrap
melos run analyze
melos run test

# ship a release
melos version          # bumps + changelogs from commits
cd apps/mobile && flutter build apk

Kort kontroll

Du har et Melos-monorepo med pakkene feature_auth og feature_cart. Etter innlogging må auth navigere brukeren til handlekurvskjermen. Hva er den ryddigste måten å holde avhengighetsgrafen asyklisk på?

Oppsummering

Du har lært hvordan du kan skalere en Flutter-kodebase fra mapper til pakker:

  • En feature-fokusert struktur grupperer kode etter forretningsfunksjon, der hver feature gjenspeiler data-, domene- og presentasjonslagene.
  • Domenelaget forblir ren Dart, noe som gjør det portabelt og enkelt å trekke ut.
  • Når features gjøres om til Dart-pakker, kan kompilatoren håndheve grenser som mapper ikke kan håndheve.
  • Melos administrerer det resulterende monorepoet: bootstrap kobler sammen pakker, scripts kjører kommandoer på tvers av dem alle, og version automatiserer utgivelser.
  • Hold grafen asyklisk: features er avhengige av core, appen er avhengig av features, og funksjonalitet på tvers av features går gjennom delte kontrakter.
Gratis å komme i gang

Lær deg Dart med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
88

Ofte stilte spørsmål

Er leksjonen «Funksjonsbasert mappestruktur og Melos-monorepoer» gratis?

Ja – du kan lese valgfritt 3 av leksjonene i læringsstien Mobilutvikling med Flutter, inkludert «Funksjonsbasert mappestruktur og Melos-monorepoer», gratis i sin helhet her på nettet. Deretter låser CoddyKit PRO opp alle leksjoner, samt interaktiv øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Kurset i Mobilutvikling med Flutter inneholder totalt 4 leksjoner.

Hva lærer jeg i «Funksjonsbasert mappestruktur og Melos-monorepoer»?

Organiser funksjoner som uavhengige pakker, og administrer dem med et Melos-monorepo. Du øver på Mobilutvikling med Flutter med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Mobilutvikling med Flutter?

Ingen tidligere erfaring er nødvendig. Mobilutvikling med Flutter på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 3 av 4.

Hvor lang tid tar leksjonen «Funksjonsbasert mappestruktur og Melos-monorepoer»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Mobilutvikling med Flutter-leksjonen?

Ja. Alle Mobilutvikling med Flutter-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Grenser mellom domene-, data- og presentasjonslag
  2. Dependency injection med get_it og injectable
  3. Funksjonsbasert mappestruktur og Melos-monorepoer
  4. Either, feiltyper og funksjonell feilhåndtering
← Tilbake til Mobilutvikling med Flutter