Mobilutveckling med Flutter · Lektion

Gränser mellan domän-, data- och presentationslager

Separera ansvar i entiteter, repositories och use cases med strikta beroenderegler.

Lektion 1 av 413 steg

Gränser mellan domän-, data- och presentationslager är en gratis lektion i Mobilutveckling med Flutter på CoddyKit. Detta är lektion 1 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Mobilutveckling med Flutter, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Mobilutveckling med Flutter innehåller totalt 4 lektioner.

Varför lagergränser är viktiga

I en Flutter-app av någon verklig storlek innebär det att blanda användargränssnitt, affärsregler och dataåtkomst i samma filer att varje ändring medför risk för regressioner. Clean Architecture delar upp appen i tre lager:

  • Domain — rena affärsregler: entiteter och användningsfall. Ingen Flutter, ingen HTTP och inga Dart-paket bundna till I/O.
  • Data — implementerar repositories: API-klienter, databaser och DTO-mappning.
  • Presentation — widgets, tillståndshantering (Bloc/Riverpod) och vy-modeller.

Den viktigaste regeln är: beroenden pekar inåt. Presentation och Data beror på Domain. Domain beror inte på något.

Beroenderegeln

Domain-lagret är den stabila kärnan. Det får aldrig importera från Data eller Presentation. Föreställ dig det som en cirkel: beroendepilarna pekar alltid mot mitten.

  • Presentation -> Domain (tillåtet)
  • Data -> Domain (tillåtet)
  • Domain -> Data (förbjudet)
  • Domain -> Presentation (förbjudet)

Eftersom Domain äger repository-gränssnitten (abstraktionerna) beror Data-lagret, som implementerar dem, inåt. Detta är Dependency Inversion Principle i praktiken.

Domänentiteter

En entitet är ett vanligt Dart-objekt som modellerar ett centralt affärsbegrepp. Det innehåller ingen JSON-tolkning, ingen fromMap och inga ramverksanteckningar. Håll det oföränderligt och fritt från I/O-relaterade frågor.

Lägg märke till att den här klassen inte importerar något från Flutter eller något nätverksbibliotek. Den är ren Dart och skulle kunna kompileras i en konsolapp.

class User {
  final String id;
  final String name;
  final String email;

  const User({
    required this.id,
    required this.name,
    required this.email,
  });

  User copyWith({String? name, String? email}) => User(
        id: id,
        name: name ?? this.name,
        email: email ?? this.email,
      );
}

void main() {
  const user = User(id: '1', name: 'Ada', email: 'ada@example.com');
  final renamed = user.copyWith(name: 'Ada L.');
  print('${renamed.id}: ${renamed.name} <${renamed.email}>');
}

Repository-gränssnitt hör hemma i Domain

Domain-lagret deklarerar vilka dataoperationer som finns, inte hur de utförs. Det görs med en abstrakt klass (ett gränssnitt). Data-lagret tillhandahåller den konkreta implementationen senare.

Denna abstraktion är den kopplingspunkt som gör att Domain kan förbli ovetande om HTTP, SQLite och Firebase. Returtyperna använder endast entiteter från Domain.

abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> saveUser(User user);
}

class User {
  final String id;
  final String name;
  final String email;
  const User({required this.id, required this.name, required this.email});
}

Användningsfall orkestrerar affärsregler

Ett användningsfall (även kallat interactor) representerar en enhet av programmets beteende, till exempel ”hämta profilen för den inloggade användaren”. Det beror endast på repository-gränssnitt från Domain.

Om varje användningsfall hålls som en klass med ett enda ansvar blir affärsavsikten tydlig och den kan enkelt testas med ett fejkat repository.

class GetUser {
  final UserRepository repository;
  const GetUser(this.repository);

  Future<User> call(String id) => repository.getUser(id);
}

abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> saveUser(User user);
}

class User {
  final String id;
  final String name;
  final String email;
  const User({required this.id, required this.name, required this.email});
}

Datalager: DTO:er och modeller

Data-lagret introducerar modeller (DTO:er) som vet hur data ska serialiseras. Ett vanligt mönster är att utöka eller mappa från domänentiteten, så att JSON-logik hålls helt utanför Domain.

Här hanterar UserModel fromJson/toJson och tillhandahåller en toEntity()-konvertering, så att resten av appen endast ser den rena User.

class UserModel {
  final String id;
  final String name;
  final String email;

  const UserModel({required this.id, required this.name, required this.email});

  factory UserModel.fromJson(Map<String, dynamic> json) => UserModel(
        id: json['id'] as String,
        name: json['name'] as String,
        email: json['email'] as String,
      );

  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};

  User toEntity() => User(id: id, name: name, email: email);
}

class User {
  final String id;
  final String name;
  final String email;
  const User({required this.id, required this.name, required this.email});
}

void main() {
  final model = UserModel.fromJson({'id': '7', 'name': 'Grace', 'email': 'g@x.io'});
  final entity = model.toEntity();
  print('Entity name: ${entity.name}');
}

Datalager: repository-implementation

Det konkreta repositoryt finns i Data och implementerar Domain-gränssnittet. Det kopplar samman fjärrdatakällor, lokala cacher och DTO-mappning. Lägg märke till satsen implements UserRepository — beroendet pekar inåt mot Domain.

Det returnerar User-entiteter, aldrig UserModel, så att serialiseringsdetaljer aldrig läcker uppåt.

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remote;
  const UserRepositoryImpl(this.remote);

  @override
  Future<User> getUser(String id) async {
    final model = await remote.fetchUser(id);
    return model.toEntity();
  }

  @override
  Future<void> saveUser(User user) {
    final model = UserModel(id: user.id, name: user.name, email: user.email);
    return remote.putUser(model);
  }
}

abstract class UserRemoteDataSource {
  Future<UserModel> fetchUser(String id);
  Future<void> putUser(UserModel model);
}

Presentation beror endast på användningsfall

Presentation-lagret (en Bloc, Cubit eller Riverpod notifier) innehåller referenser till användningsfall, inte till repositories eller datakällor. Det håller widgets frikopplade från hur data hämtas.

Nedan anropar en Cubit användningsfallet GetUser och skickar ut vytillstånd. Den vet ingenting om JSON eller HTTP.

class UserCubit extends Cubit<UserState> {
  final GetUser getUser;
  UserCubit(this.getUser) : super(UserInitial());

  Future<void> load(String id) async {
    emit(UserLoading());
    try {
      final user = await getUser(id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

Mappa mappstrukturen

En feature-first-struktur gör gränserna synliga på disken. Varje feature äger sina tre lager:

  • lib/features/user/domain/ — entiteter, repositories (gränssnitt), use cases
  • lib/features/user/data/ — modeller, datakällor, repositories (implementationer)
  • lib/features/user/presentation/ — cubit/bloc, sidor, widgets

När en fil i domain/ importerar från data/ har ni brutit mot en lagergräns. Lint-regler (t.ex. import_lint eller anpassade förbud i analysis_options.yaml) kan upprätthålla detta automatiskt.

Felhantering över lagergränser

Lågnivåundantag (ett SocketException, ett 404-fel) hör hemma i Data-lagret. Låt dem inte bubbla upp oförändrade till Domain eller Presentation. Omvandla dem till domänspecifika typer av Failure, så att gränssnittet resonerar om betydelse i stället för transport.

Ett vanligt idiom är att returnera Either<Failure, T> (via paketet dartz) från repositories, eller att kasta typade domänundantag som fångas vid use case-gränsen.

sealed class Failure {
  const Failure(this.message);
  final String message;
}

class NetworkFailure extends Failure {
  const NetworkFailure() : super('No connection');
}

class NotFoundFailure extends Failure {
  const NotFoundFailure() : super('Resource not found');
}

String describe(Failure f) => switch (f) {
      NetworkFailure() => 'Check your internet.',
      NotFoundFailure() => 'We could not find that.',
      _ => 'Something went wrong.',
    };

void main() {
  print(describe(const NetworkFailure()));
  print(describe(const NotFoundFailure()));
}

Testa varje lager isolerat

Strikta gränser lönar sig i tester. Eftersom ett use case är beroende av ett gränssnitt kan ni injicera ett fejk-repository utan nätverk eller ett Flutter-testharness.

Det här testet körs i vanlig Dart — ingen WidgetTester, ingen fejkat HTTP-server — vilket visar att Domain-lagret verkligen är frikopplat.

abstract class UserRepository {
  Future<User> getUser(String id);
}

class User {
  final String id;
  final String name;
  const User({required this.id, required this.name});
}

class GetUser {
  final UserRepository repo;
  const GetUser(this.repo);
  Future<User> call(String id) => repo.getUser(id);
}

class FakeUserRepository implements UserRepository {
  @override
  Future<User> getUser(String id) async => User(id: id, name: 'Test User');
}

void main() async {
  final useCase = GetUser(FakeUserRepository());
  final user = await useCase('42');
  print(user.id == '42' && user.name == 'Test User'
      ? 'PASS'
      : 'FAIL');
}

Kontrollpunkt: Beroenderiktningen

Föreställ er en Flutter-feature som använder Clean Architecture. Var bör det abstrakta gränssnittet UserRepository deklareras, och vilket lager implementerar det?

Sammanfattning

Ni har delat upp en Flutter-feature i tre lager med strikta beroenden som pekar inåt:

  • Domain — rena entiteter, repository-gränssnitt och use cases; är inte beroende av något.
  • Data — DTO-modeller med JSON-logik, datakällor och repository-implementationer som mappar modeller till entiteter.
  • Presentation — Blocs/Cubits som anropar use cases och aldrig hanterar serialisering eller HTTP.

Viktiga lärdomar: deklarera repository-gränssnitt i Domain (Dependency Inversion), omvandla transportfel till domänspecifika Failure-typer vid gränsen, håll entiteter fria från framework-importer och upprätthåll reglerna med mappstruktur samt lint-förbud. Resultatet blir en kodbas där varje lager kan testas separat och ändringar förblir lokala.

Gratis att börja

Lär dig Dart med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
88

Vanliga frågor

Är lektionen ”Gränser mellan domän-, data- och presentationslager” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Mobilutveckling med Flutter, inklusive ”Gränser mellan domän-, data- och presentationslager”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Mobilutveckling med Flutter innehåller totalt 4 lektioner.

Vad lär jag mig i ”Gränser mellan domän-, data- och presentationslager”?

Separera ansvar i entiteter, repositories och use cases med strikta beroenderegler. Ni övar på Mobilutveckling med Flutter med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Mobilutveckling med Flutter?

Du behöver inga förkunskaper. Utbildningen i Mobilutveckling med Flutter på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 1 av 4.

Hur lång tid tar lektionen ”Gränser mellan domän-, data- och presentationslager”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Mobilutveckling med Flutter-lektionen?

Ja. Varje Mobilutveckling med Flutter-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Gränser mellan domän-, data- och presentationslager
  2. Beroendeinjektion med get_it och injectable
  3. Funktionsbaserad mappstruktur och Melos-monorepon
  4. Either, feltyper och funktionell felhantering
← Tillbaka till Mobilutveckling med Flutter