Flutter Monorepo: Melos ile Çoklu Paket Yönetimi
# Flutter'da Multi-Package Monorepo ve Paylasilmis Kod Yonetimi
Birden fazla Flutter uygulamasinin ayni is mantigi, UI bilesenleri ve API istemcisini paylastigi projeleri yonetmek, basit gorunen ama uc ay sonra kopyala-yapistir kodlarin icinde bogulan bir probleme donusur. Paylasilan paketlerle yapilandirilan monorepo, her seyi tek bir depoda tutarken paketler arasinda net sinirlar koruyarak bu sorunu cozer. Yonettigim cok uygulamali bir projede monorepo yapisina gecis, kod tekrarina %60'in uzerinde azalma ve yeni bir ozelligi tum uygulamalara yayma suresinde iki haftadan uc gune dusus sagladi.
Flutter Projeleri Icin Neden Monorepo?
Tek bir uygulamaniz oldugunda standart Flutter proje yapisi gayet yeterlidir. Ikinci bir uygulama eklediginiz an -- belki bir musteri uygulamasi ve bir yonetim paneli, ya da farkli marka varyantlarina sahip bir white-label urun -- isler karisir.
Kopyala-yapistir tuzagi: Paylasilmis kodu depolar arasinda kopyalayarak baslarsiniz. Ilk basta hizli hissedilir. Sonra bir depodaki hata duzeltmesi digerlerine ulasmaz. Modeller birbirinden uzaklasir. A uygulamasindaki API istemcisi sayfalandirmayi B uygulamasindan farkli sekilde ele alir. Uc ay sonra ayni kodun uc farkli versiyonunu bakimda tutuyorsunuzdur.
Paket yayinlama yukunu: Paylasilan kodu yayinlanmis Dart paketlerine cikarabilirsiniz. Ama simdi ozel bir paket kayit defteri, versiyon disiplini ve her kucuk degisiklik icin bir yayin sureci gerekir. Cogu ekip icin bu gereksiz bir surtusmedir.
Monorepo cozumu: Her sey tek bir depoda yasayor. Paylasilan kod yerel paketler halinde organize ediliyor. Tum uygulamalar bu paketlere path bagimliliklari uzerinden referans veriyor. Paylasilan API istemcisinde yapilan bir degisiklik aninda her uygulamada mevcut oluyor. Tek PR, tek inceleme, tek merge.
Zamanla birikimli olarak artan faydalar:
Melos: Monorepo Orkestratoru
Melos, Dart ve Flutter monorepolari yonetmek icin fiili standart aractir. Bagimlilik cozumleme, betik orkestrasyon ve paketler arasi secici komut calistirma islerini halleder. Onu tum paketlerinizin uyum icinde calmasini saglayan bir orkestra sefi olarak dusunebilirsiniz.
Kurulum
# Kok dizinindeki pubspec.yaml'a ekleyin
dev_dependencies:
melos: ^6.1.0dart pub get
dart run melos bootstrapMelos Yapilandirmasi
Deponuzun kokunde bulunan `melos.yaml` dosyasi calisma alaninizi tanimladiginiz yerdir. Produksiyona hazir bir yapilandirma:
name: flutter_calisma_alani
repository: https:"code-comment">//github.com/kurum/flutter-calisma-alani
packages:
- apps/*
- packages/*
command:
bootstrap:
usePubspecOverrides: true
scripts:
analyze:
run: dart analyze --fatal-infos
exec:
concurrency: 5
description: Tum paketlerde statik analiz calistir.
test:
run: flutter test --coverage
exec:
concurrency: 3
failFast: true
packageFilters:
dirExists: test
description: Test dizini olan tum paketlerde test calistir.
format:
run: dart format --set-exit-if-changed .
description: Calisma alaninda formatlama kontrolu yap.
build:runner:
run: dart run build_runner build --delete-conflicting-outputs
exec:
concurrency: 1
failFast: true
packageFilters:
dependsOn: build_runner
description: build_runner'a bagimliligi olan paketlerde build_runner calistir.
clean:
run: flutter clean
exec:
concurrency: 5
description: Tum paketleri temizle.
pub:upgrade:
run: flutter pub upgrade
exec:
concurrency: 3
description: Tum paketlerde bagimliliklari guncelle.`packageFilters` secenegi guclu bir yetenektir. build_runner'i her pakette calistirmak yerine (cogu kullanmaz bile), Melos yalnizca gercekten ona bagimli olan paketleri hedef alir. Bu, CI suresinden onemli olcude tasarruf saglar.
Gercek Bir Monorepo Icin Klasor Yapisi
Produksiyonda kullandigim klasor yapisi asagidadir. Birden fazla projede evrilmis olup netlik ve pratiklik arasinda denge kurar:
flutter_calisma_alani/
melos.yaml
pubspec.yaml
apps/
musteri_app/
lib/
test/
pubspec.yaml
android/
ios/
yonetim_app/
lib/
test/
pubspec.yaml
android/
ios/
kurye_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/`packages/` altindaki her dizin, kendi `pubspec.yaml`, testleri ve barrel dosyasina sahip bagimsiz bir Dart veya Flutter paketidir. `apps/` dizini gercek Flutter uygulamalarini icerir. `tools/` dizini CI betiklerini ve yardimci araclari barindirir.
Paylasilan Paketler Detayli Inceleme
Core Paketi
Core paketi, diger tum paketlerin bagimli oldugu temel yardimci araclari icerir. Kararli, iyi test edilmis olmali ve seyrek degismelidir.
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,
});
}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 Paketi
Paylasilan modeller, bir monorepodaki en etkili pakettir. Tum uygulamalar ayni `User`, `Order` veya `Product` modelini kullandiginda, model kaymalarindan kaynaklanan tum bir hata sinifini ortadan kaldirirsiniz.
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 Paketi
Paylasilan bir API istemcisi, tum uygulamalarda tutarli kimlik dogrulama, hata yonetimi ve istek/yanit islemesini saglar.
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">'Kullanici bilgisi alinamadi',
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">'Siparisler alinamadi',
statusCode: e.response?.statusCode,
);
}
}
}UI Kit Paketi
UI kit, tum uygulamalarda tutarli bir gorsel dil saglar. Paylasilan tema, tasarim tokenlari ve yeniden kullanilabilir widget'lari icerir.
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),
);
}Paketler Arasi Bagimlilik Yonetimi
Monorepoda bagimliliklari dogru kurmak kritik oneme sahiptir. Paylasilan tum paketleri kullanan bir uygulamanin `pubspec.yaml` dosyasi:
# apps/musteri_app/pubspec.yaml
name: musteri_app
description: Musteriye yonelik mobil uygulama.
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
# Uygulamaya ozel bagimliliklar
flutter_bloc: ^8.1.0
go_router: ^14.0.0Paylasilan paketlerin birbirine nasil bagimli oldugu:
# packages/api_client/pubspec.yaml
name: api_client
description: Kimlik dogrulama ve hata yonetimli paylasilan HTTP istemcisi.
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.0Bagimlilik Kurali
Paketler yonlu dongusuz bir graf (DAG) olusturmalidir. A paketi B paketine bagimliysa, B asla A'ya bagimli olmamalidir. Pratikte bu su anlama gelir: `core` hicbir seye bagimli degildir, `models` `core`'a, `api_client` `core` ve `models`'a, uygulamalar ise her seye bagimlidir.
Dongusel bagimliliklar Dart'ta derleme zamani hatasidir, bu yuzden derleyici bunu zorlar. Ama daha ince bir nokta olarak, derin bagimlilik zincirlerinden de kacinmalisiniz. `core`'u degistirmek her paketin yeniden derlenmesini gerektiriyorsa, CI sureleriniz zarar gorur. `core`'u kucuk ve kararli tutun.
Monorepolar Icin CI/CD
Monoreponun en buyuk operasyonel avantaji secici CI'dir. Yalnizca bir dosya degistiginde her paketi yeniden derleyip test etmemelisiniz.
Etkilenen Paketleri Tespit Etmek
Melos hangi paketlerin bir degisiklikten etkilendigini belirleyebilir:
# Son release etiketinden bu yana degisen paketleri listele
melos list --since=origin/main --diff
# Yalnizca etkilenen paketlerde testleri calistir
melos run test --since=origin/mainGitHub Actions Is Akisi
# .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 Kur
run: dart pub global activate melos
- name: Bootstrap
run: melos bootstrap
- name: Etkilenen paketleri analiz et
run: melos run analyze --since=origin/main
- name: Etkilenen paketleri test et
run: melos run test --since=origin/main
- name: Formatlama kontrolu
run: melos run format
build-musteri-app:
needs: analyze-and-test
if: |
contains(github.event.pull_request.labels.*.name, 'build:musteri') ||
contains(github.event.pull_request.title, '[musteri]')
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/musteri_app && flutter build apk --release`--since=origin/main` bayragi kilit unsurdur. Melos mevcut dal ile main'i karsilastirir ve yalnizca degisen paketlerde komut calistirir. 15 paketli bir monorepoda bu, CI suresini 25 dakikadan 5 dakikanin altina dusurur.
Monorepoda Test Stratejisi
Her paketin kendi testleri olmalidir. Bu istege bagli degildir -- monorepo gelistirmeyi surdurulebilir kilan temeldir.
Paket Seviyesi Birim Testleri
Paylasilan paketler kapsamli birim testlerine ihtiyac duyar, cunku birden fazla uygulama onlara bagimlidir. `models` paketindeki bir hata her uygulamayi etkiler.
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 dogru sekilde User olusturur', () {
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">'Test Kullanici',
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 dogru map uretir', () {
final user = User(
id: class="code-string">'usr_123',
email: class="code-string">'test@example.com',
displayName: class="code-string">'Test Kullanici',
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);
});
});
}Uygulama Seviyesinde Entegrasyon Testleri
Uygulamalarin, paylasilan paketlerin birlikte nasil calistigini deneyimleyen entegrasyon testleri olmalidir.
class=class="code-string">"code-comment">// apps/musteri_app/test/integration/order_flow_test.dart
import class="code-string">'package:musteri_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">'API istemcisinden siparisleri yukler', () 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>(),
]),
);
});
}Paylasilan Paketleri Izole Test Etmek
Kritik bir kural: Paylasilan paket testleri asla uygulama koduna bagimli olmamalidir. `api_client` testiniz `musteri_app`'tan bir sey import ediyorsa, olusmayi bekleyen bir dongusel bagimlilikla karsi karsiyasinizdir. Paketin kendi icinde mock ve fake'ler kullanin.
Monorepo mu Multi-Repo mu: Ne Zaman Hangisini Secmeli
Bu bir inanc tartismasi degildir. Her yaklasim farkli ekip yapilarina ve proje gercekliklerine uygundur.
Monorepoyu secin:
Multi-repoyu secin:
Hibrit yaklasim: Bazi ekipler monorepo ile baslar ve zaman icinde kararli paketleri yayinlanmis paketlere terfi ettirir. Alti aydir degismemis `core` paketi versiyonlu bir bagimlilik haline gelebilir. Her API guncellemesiyle degisen `models` paketi monorepoda kalir.
Sik Yapilan Monorepo Hatalari
1. Tanri Paketi
Her seyi tek bir `shared` paketine koymak amaci bozar. `shared` icinde modeller, API istemcileri, yardimci araclar, tema verileri ve analitik varsa, her degisiklik her seyin yeniden derlenmesini tetikler. Kolayliga gore degil, alana gore bolumlendirin.
2. Uygulamalar Arasi Import
A uygulamasi asla B uygulamasindan kod import etmemelidir. Iki uygulamanin da ayni widget'a ihtiyaci varsa, onu `ui_kit` paketine tasiyiniz. Uygulamalar arasi import'lar gorunmez baglasma olusturur ve uygulamalari bagimsiz olarak derlemeyi imkansiz kilar.
3. Tutarsiz Bagimlilik Versiyonlari
`musteri_app` `dio: ^5.3.0` kullanirken `yonetim_app` `dio: ^5.4.0` kullaniyorsa, ince calisma zamani farkliliklari davet ediyorsunuz demektir. Versiyon kisitlamalarini merkezilestirin veya tutarliligi zorlamak icin Melos bagimlilik override'larini kullanin.
4. Bootstrap Adimini Atlamak
Yeni ekip uyeleri depoyu klonlayip uygulama dizininde `flutter pub get` calistiracaktir. Path bagimliliklari cozulmediigi icin bu basarisiz olur. `melos bootstrap`'un calistirilacak ilk komut oldugunu belgeleyin ve bir kurulum betigi ile zorlayin.
5. Paketleri Uygulamalar Uzerinden Test Etmek
`api_client` paketinizi musteri uygulamasini calistirip tiklayarak test etmek bir test stratejisi degildir. Her paket bagimsiz calisabilen kendi birim testlerine sahip olmalidir. CI pipeline'i bunu zorlamalidir.
6. Aracsiz Monorepo
Melos (veya benzer bir arac) olmadan bir monorepo, sadece birden fazla pubspec dosyasi olan bir klasordur. Tum avantajlari -- secici test, betik orkestrasyon, bagimlilik yonetimi -- kaybedersiniz. Arac yatirimini ilk gunden yapin.
Adim Adim Baslangic
Ikna oldunuz ve ilk Flutter monoreponuzu kurmak istiyorsaniz, minimal yol haritasi:
# Calisma alani olustur
mkdir calisma_alanim && cd calisma_alanim
dart pub init
# dev_dependencies'e melos ekleyin, ardindan:
dart pub get
# Yapiyi olustur
mkdir -p apps/uygulamam packages/core packages/models
# Paketleri baslat
cd packages/core && dart pub init && cd ../..
cd packages/models && dart pub init && cd ../..
# Kok dizinde melos.yaml olustur (yukaridaki yapilandirmaya bakin)
# Her seyi bootstrap et
dart run melos bootstrapBuradan itibaren paylasilan kodu kademeli olarak paketlere tasiyin. Her seyi bir anda cikarmayin. Modellerle baslayin -- cikarilmasi en kolay olan ve en aninda deger saglayan onlardir.
Deneyimlerime gore, iyi yapilandirilmis bir monorepo cok uygulamali bir Flutter projesinin belkemigi haline gelir. Baslangic kurulumu bir iki gun surer. Sonraki aylarda kazanilan zaman haftalarla olculur. Anahtar, disiplinli paket sinirlari ve CI'ya en basindan yatirim yapmaktir.
Cok uygulamali bir Flutter mimarisi planliyorsaniz, ekibinize en uygun yapiyi birlikte belirleyelim.
İlgili Makaleler
Flutter'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme
Flutter projelerinde Clean Architecture'ı uygulanabilir şekilde öğrenin. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için pratik bir rehber.
Dart Best Practices: Temiz ve Sürdürülebilir Kod Rehberi
Dart projelerinde okunabilir, test edilebilir ve sürdürülebilir kod yazmak için en etkili pratikleri öğrenin.
Flutter CI/CD: Otomatik Build, Test ve Release Süreci
Flutter projelerinde güvenli ve hızlı teslimat için CI/CD pipeline tasarlayın. Build, test ve release adımlarını otomatikleştirin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç