Mobil Geliştirme
🐦

Flutter ile
Çapraz Platform
Mobil Geliştirme

Flutter ile tek kod tabanından hem iOS hem Android'e native performansta uygulamalar geliştiriyoruz. Kurumsal projelerde BLoC pattern ve Riverpod ile state management, Dio ile tip-güvenli HTTP katmanı ve GetIt ile bağımlılık enjeksiyonu kullanıyoruz.

Versiyon Flutter 3.x
Dil Dart 3
Platform iOS & Android
Destek Web & Desktop
order_bloc.dart
DART
// BLoC ile sipariş yönetimi
class OrderBloc extends Bloc<OrderEvent, OrderState> {
  final OrderRepository _repo;

  OrderBloc(this._repo) : super(OrderInitial()) {
    on<FetchOrders>(_onFetchOrders);
    on<RefreshOrders>(_onRefreshOrders);
    on<CancelOrder>(_onCancelOrder);
  }

  Future<void> _onFetchOrders(
    FetchOrders event,
    Emitter<OrderState> emit,
  ) async {
    emit(OrderLoading());
    final result = await _repo.getOrders(
      page: event.page,
      status: event.status,
    );
    result.fold(
      (failure) => emit(OrderError(failure.message)),
      (orders)  => emit(OrderLoaded(orders)),
    );
  }
}

Tek Codebase, Çift Platform

Google tarafından geliştirilen Flutter, Dart programlama dili ile yazılmış widget tabanlı bir UI framework'üdür. Skia/Impeller render motoru sayesinde 60-120 fps native performans sunarken iOS ve Android için aynı kod tabanı kullanılmasına olanak tanır. Cofreex olarak Flutter'ı kurumsal projelerde BLoC veya Riverpod state management, clean architecture katmanları ve kapsamlı test stratejisiyle kullanıyoruz.

  • Native performans — Skia/Impeller render motoru ile 120fps
  • Tek codebase — iOS, Android, Web ve Desktop
  • Hot reload ile hızlı iterasyon ve geliştirme
  • Dart'ın null safety ve tip sistemi ile güvenli kod
  • BLoC / Riverpod ile ölçeklenebilir state yönetimi
  • Clean Architecture ile bakımı kolay katmanlı yapı
order_bloc.dart
DART
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../domain/repositories/order_repository.dart';
import '../domain/entities/order.dart';

part 'order_bloc.freezed.dart';

// ── Events ────────────────────────────────────────────────────
@freezed
sealed class OrderEvent with _$OrderEvent {
  const factory OrderEvent.fetch({
    @Default(1) int page,
    String? status,
  }) = FetchOrders;

  const factory OrderEvent.refresh() = RefreshOrders;

  const factory OrderEvent.cancel(String orderId) = CancelOrder;

  const factory OrderEvent.updateFilter(OrderFilter filter) = UpdateFilter;
}

// ── States ────────────────────────────────────────────────────
@freezed
sealed class OrderState with _$OrderState {
  const factory OrderState.initial()                           = OrderInitial;
  const factory OrderState.loading()                          = OrderLoading;
  const factory OrderState.loaded(
    List<Order> orders, {
    @Default(false) bool hasMore,
    @Default(1)     int currentPage,
  })                                                           = OrderLoaded;
  const factory OrderState.refreshing(List<Order> current)   = OrderRefreshing;
  const factory OrderState.error(String message, {int? code}) = OrderError;
}

// ── BLoC ──────────────────────────────────────────────────────
class OrderBloc extends Bloc<OrderEvent, OrderState> {
  final OrderRepository _repository;

  OrderBloc({required OrderRepository repository})
      : _repository = repository,
        super(const OrderState.initial()) {
    on<FetchOrders>(_onFetch,
        transformer: sequential()); // Eş zamanlı event'ları sıraya al
    on<RefreshOrders>(_onRefresh);
    on<CancelOrder>(_onCancel,
        transformer: droppable()); // Çift-tıklamayı engelle
    on<UpdateFilter>(_onUpdateFilter);
  }

  Future<void> _onFetch(
    FetchOrders event,
    Emitter<OrderState> emit,
  ) async {
    emit(const OrderState.loading());

    final result = await _repository.getOrders(
      page:   event.page,
      status: event.status,
    );

    result.fold(
      (failure) => emit(OrderState.error(
        failure.message,
        code: failure.statusCode,
      )),
      (paginatedOrders) => emit(OrderState.loaded(
        paginatedOrders.items,
        hasMore:     paginatedOrders.hasMore,
        currentPage: event.page,
      )),
    );
  }

  Future<void> _onRefresh(
    RefreshOrders event,
    Emitter<OrderState> emit,
  ) async {
    final currentOrders = state is OrderLoaded
        ? (state as OrderLoaded).orders
        : <Order>[];

    emit(OrderState.refreshing(currentOrders));

    final result = await _repository.getOrders(page: 1);
    result.fold(
      (failure) => emit(OrderState.error(failure.message)),
      (data)    => emit(OrderState.loaded(data.items, hasMore: data.hasMore)),
    );
  }

  Future<void> _onCancel(
    CancelOrder event,
    Emitter<OrderState> emit,
  ) async {
    final result = await _repository.cancelOrder(event.orderId);
    result.fold(
      (failure) => emit(OrderState.error(failure.message)),
      (_)       => add(const OrderEvent.refresh()),
    );
  }

  void _onUpdateFilter(
    UpdateFilter event,
    Emitter<OrderState> emit,
  ) {
    add(OrderEvent.fetch(status: event.filter.status));
  }
}
order_providers.dart
DART
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../data/datasources/remote/order_remote_datasource.dart';
import '../data/repositories/order_repository_impl.dart';
import '../domain/entities/order.dart';

part 'order_providers.g.dart';

// ── Repository Provider ───────────────────────────────────────
@riverpod
OrderRepository orderRepository(OrderRepositoryRef ref) {
  final dio       = ref.watch(dioProvider);
  final dataSource = OrderRemoteDataSource(dio);
  final cache      = ref.watch(localCacheProvider);
  return OrderRepositoryImpl(remote: dataSource, cache: cache);
}

// ── Orders List — AsyncNotifier ───────────────────────────────
@riverpod
class Orders extends _$Orders {
  int _page = 1;

  @override
  Future<List<Order>> build() async {
    _page = 1;
    return _fetchPage(1);
  }

  Future<List<Order>> _fetchPage(int page) async {
    final repo   = ref.read(orderRepositoryProvider);
    final result = await repo.getOrders(page: page);
    return result.fold(
      (failure) => throw failure,
      (data)    => data.items,
    );
  }

  /// Sayfayı yenile (pull-to-refresh)
  Future<void> refresh() async {
    ref.invalidateSelf();
    await future;
  }

  /// Sonraki sayfayı yükle (infinite scroll)
  Future<void> loadNextPage() async {
    final current = await future;
    _page++;
    final next = await _fetchPage(_page);
    state = AsyncData([...current, ...next]);
  }

  /// Sipariş iptal et
  Future<void> cancelOrder(String id) async {
    final repo   = ref.read(orderRepositoryProvider);
    final result = await repo.cancelOrder(id);
    result.fold(
      (f) => throw f,
      (_) => refresh(),
    );
  }
}

// ── Tekil Sipariş — family provider ──────────────────────────
@riverpod
Future<Order> orderDetail(OrderDetailRef ref, String orderId) async {
  final repo   = ref.watch(orderRepositoryProvider);
  final result = await repo.getOrderById(orderId);
  return result.fold(
    (failure) => throw failure,
    (order)   => order,
  );
}

// ── Filtreli Siparişler ───────────────────────────────────────
@riverpod
AsyncValue<List<Order>> filteredOrders(
  FilteredOrdersRef ref,
  OrderFilter filter,
) {
  final allOrders = ref.watch(ordersProvider);
  return allOrders.whenData(
    (orders) => orders.where((o) {
      if (filter.status != null && o.status != filter.status) return false;
      if (filter.minAmount != null && o.total < filter.minAmount!) return false;
      if (filter.maxAmount != null && o.total > filter.maxAmount!) return false;
      return true;
    }).toList(),
  );
}

// ── UI'da Kullanım ────────────────────────────────────────────
// class OrdersPage extends ConsumerWidget {
//   @override
//   Widget build(BuildContext context, WidgetRef ref) {
//     final ordersAsync = ref.watch(ordersProvider);
//
//     return ordersAsync.when(
//       data:    (orders) => OrderList(orders: orders),
//       loading: () => const CenteredLoader(),
//       error:   (e, _)  => ErrorView(message: e.toString()),
//     );
//   }
// }
api_service.dart
DART
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../core/constants/api_constants.dart';
import '../core/errors/api_exception.dart';

class ApiService {
  late final Dio _dio;
  final FlutterSecureStorage _storage;

  ApiService({FlutterSecureStorage? storage})
      : _storage = storage ?? const FlutterSecureStorage() {
    _dio = Dio(BaseOptions(
      baseUrl:        ApiConstants.baseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 30),
      headers: {
        'Accept':       'application/json',
        'Content-Type': 'application/json',
        'X-App-Version': ApiConstants.appVersion,
      },
    ));

    _dio.interceptors.addAll([
      _AuthInterceptor(_storage, _dio),
      _LoggingInterceptor(),
      RetryInterceptor(
        dio:        _dio,
        logPrint:   print,
        retries:    3,
        retryDelays: const [
          Duration(seconds: 1),
          Duration(seconds: 2),
          Duration(seconds: 3),
        ],
        retryEvaluator: (error, attempt) async =>
            error.type != DioExceptionType.badResponse,
      ),
    ]);
  }

  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? queryParams,
    required T Function(dynamic json) fromJson,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParams,
      );
      return fromJson(response.data);
    } on DioException catch (e) {
      throw _mapException(e);
    }
  }

  Future<T> post<T>(
    String path, {
    required Map<String, dynamic> body,
    required T Function(dynamic json) fromJson,
  }) async {
    try {
      final response = await _dio.post(path, data: body);
      return fromJson(response.data);
    } on DioException catch (e) {
      throw _mapException(e);
    }
  }

  ApiException _mapException(DioException e) {
    return switch (e.type) {
      DioExceptionType.connectionTimeout ||
      DioExceptionType.sendTimeout     ||
      DioExceptionType.receiveTimeout  => const ApiException.timeout(),
      DioExceptionType.badResponse     => ApiException.serverError(
          statusCode: e.response?.statusCode ?? 0,
          message:    e.response?.data?['message'] ?? 'Sunucu hatası',
        ),
      DioExceptionType.connectionError => const ApiException.noInternet(),
      _                                => const ApiException.unknown(),
    };
  }
}

// ── Auth Interceptor ──────────────────────────────────────────
class _AuthInterceptor extends Interceptor {
  final FlutterSecureStorage _storage;
  final Dio _dio;

  const _AuthInterceptor(this._storage, this._dio);

  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final token = await _storage.read(key: 'access_token');
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    return handler.next(options);
  }

  @override
  Future<void> onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    if (err.response?.statusCode == 401) {
      // Token yenile
      final refreshed = await _refreshToken();
      if (refreshed) {
        // İsteği tekrar gönder
        final opts = err.requestOptions;
        final token = await _storage.read(key: 'access_token');
        opts.headers['Authorization'] = 'Bearer $token';
        final response = await _dio.fetch(opts);
        return handler.resolve(response);
      }
    }
    return handler.next(err);
  }

  Future<bool> _refreshToken() async {
    try {
      final refresh = await _storage.read(key: 'refresh_token');
      if (refresh == null) return false;
      final resp = await _dio.post('/auth/refresh', data: {
        'refresh_token': refresh,
      });
      await _storage.write(
        key:   'access_token',
        value: resp.data['access_token'],
      );
      return true;
    } catch (_) {
      await _storage.deleteAll();
      return false;
    }
  }
}
app_router.dart
DART
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/auth/presentation/pages/login_page.dart';
import '../features/auth/presentation/pages/splash_page.dart';
import '../features/home/presentation/pages/home_page.dart';
import '../features/orders/presentation/pages/orders_page.dart';
import '../features/orders/presentation/pages/order_detail_page.dart';
import '../features/profile/presentation/pages/profile_page.dart';
import '../features/shell/presentation/widgets/main_shell.dart';

// ── Route Paths ───────────────────────────────────────────────
class AppRoutes {
  static const splash      = '/';
  static const login       = '/login';
  static const home        = '/home';
  static const orders      = '/orders';
  static const orderDetail = '/orders/:orderId';
  static const profile     = '/profile';
}

// ── Router Provider ───────────────────────────────────────────
final appRouterProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authStateProvider);

  return GoRouter(
    initialLocation: AppRoutes.splash,
    debugLogDiagnostics: true,
    refreshListenable: GoRouterRefreshStream(authState.stream),

    // ── Auth Guard ─────────────────────────────────────────
    redirect: (context, state) {
      final isLoggedIn  = authState.valueOrNull?.isAuthenticated ?? false;
      final isSplash    = state.matchedLocation == AppRoutes.splash;
      final isLoginPage = state.matchedLocation == AppRoutes.login;

      if (isSplash) return null; // Splash kendi yönlendirmesini yapar

      if (!isLoggedIn && !isLoginPage) return AppRoutes.login;
      if (isLoggedIn  &&  isLoginPage) return AppRoutes.home;
      return null;
    },

    routes: [
      // Splash
      GoRoute(
        path:        AppRoutes.splash,
        pageBuilder: (ctx, state) => NoTransitionPage(
          child: const SplashPage(),
        ),
      ),

      // Login
      GoRoute(
        path:        AppRoutes.login,
        pageBuilder: (ctx, state) => CustomTransitionPage(
          child: const LoginPage(),
          transitionsBuilder: (ctx, anim, sec, child) =>
              FadeTransition(opacity: anim, child: child),
        ),
      ),

      // ── Shell Route (Bottom Nav) ──────────────────────────
      ShellRoute(
        builder: (ctx, state, child) => MainShell(child: child),
        routes: [
          GoRoute(
            path:    AppRoutes.home,
            builder: (ctx, state) => const HomePage(),
          ),

          GoRoute(
            path:    AppRoutes.orders,
            builder: (ctx, state) => const OrdersPage(),
            routes: [
              // Nested: Sipariş Detay
              GoRoute(
                path: ':orderId',
                builder: (ctx, state) {
                  final orderId = state.pathParameters['orderId']!;
                  final extra   = state.extra as OrderExtra?;
                  return OrderDetailPage(
                    orderId:      orderId,
                    preloadOrder: extra?.order,
                  );
                },
              ),
            ],
          ),

          GoRoute(
            path:    AppRoutes.profile,
            builder: (ctx, state) => const ProfilePage(),
          ),
        ],
      ),
    ],

    // ── Hata Sayfası ──────────────────────────────────────
    errorBuilder: (ctx, state) => ErrorPage(
      error: state.error?.toString() ?? 'Sayfa bulunamadı',
    ),
  );
});

// ── Kullanım: Tip-güvenli Navigasyon ─────────────────────────
// context.push(
//   '/orders/42',
//   extra: OrderExtra(order: cachedOrder),
// );
//
// context.go(AppRoutes.home);       // Tab değiştir
// context.pushNamed('orderDetail',  // İsimli rota
//   pathParameters: {'orderId': id},
// );

Kullandığımız Flutter Paketleri

flutter_bloc State

Kurumsal projelerde tercih ettiğimiz BLoC state management kütüphanesi. Event-driven mimari ve sealed class'lar ile tip-güvenli durum yönetimi sağlar.

riverpod / hooks_riverpod State

Provider'ın kompile-time güvenli ve test edilebilir halefi. AsyncNotifier ile asenkron state, family provider ile parametrik veri erişimi sağlar.

dio HTTP

İnterceptor zinciri, retry mekanizması, multipart upload ve cancel token desteğiyle kurumsal kalite HTTP istemci. Auth token yönetimi interceptor katmanında yapılır.

go_router Routing

Google destekli declarative routing çözümü. ShellRoute ile bottom navigation, deep link ve auth guard desteği ile URL tabanlı navigasyon sağlar.

get_it DI

Service locator pattern ile bağımlılık enjeksiyonu. Injectable anotasyonlarıyla otomatik kayıt, singleton ve factory scope yönetimi sunar.

hive / isar Yerel DB

Hive hafif key-value store; Isar ise indexli sorgular için Flutter'a özel yüksek performanslı NoSQL veritabanı. Offline-first mimaride önbellek katmanı olarak kullanılır.

flutter_secure_storage Güvenlik

iOS Keychain ve Android Keystore altyapısını kullanarak token, şifre ve hassas verilerin şifrelenmiş şekilde saklanmasını sağlar.

freezed Codegen

Dart'ta immutable data class'lar, union type'lar ve copyWith desteği için code generation aracı. BLoC event/state ve entity tanımlarında kullanılır.

Flutter ile Hangi Projeleri Yapıyoruz?

🛒

E-ticaret Uygulaması

Ürün kataloğu, sepet yönetimi, ödeme entegrasyonu (iyzico, Stripe), sipariş takibi ve push notification ile tam özellikli alışveriş deneyimi.

🏢

Kurumsal Mobil App

Çalışan yönetimi, izin / puantaj takibi, belge onay akışları ve ERP/CRM entegrasyonları ile kurumsal iç süreçleri mobil'e taşıma.

📍

Takip & Lojistik

Gerçek zamanlı harita takibi (google_maps_flutter), rota optimizasyonu, kargo durum bildirimleri ve sürücü uygulama yönetimi.

💬

Sosyal Platform

Anlık mesajlaşma (WebSocket), medya paylaşımı, hikaye / feed akışı, bildirim altyapısı ve içerik moderasyon araçları.

💳

Finans & Ödeme

Bütçe takibi, fatura yönetimi, biometric kimlik doğrulama, PCI DSS uyumlu ödeme akışları ve gerçek zamanlı varlık dashboard'u.

🔌

IoT Kontrol Arayüzü

Bluetooth/BLE cihaz bağlantısı, MQTT protokolü, sensör verisi grafikleştirme ve uzaktan cihaz kontrol paneli uygulamaları.

Clean Architecture ile Ölçeklenebilir Yapı

Kurumsal Flutter projelerinde Clean Architecture prensiplerini uyguluyoruz. Presentation, Domain ve Data katmanlarına bölünmüş yapı sayesinde iş mantığı UI'dan bağımsızdır; birim testleri mock repository'ler ile kolayca yazılır. Feature-first klasör yapısı ile her özellik kendi state, domain ve data alt katmanlarını barındırır.

proje yapısı
TREE
lib/
├── core/
│   ├── constants/          # API URL, uygulama sabitleri
│   ├── di/                 # GetIt servis kaydı (injection.dart)
│   ├── errors/             # Failure sınıfları, ApiException
│   ├── network/            # ApiService, Dio konfigürasyonu
│   └── utils/              # Extensions, validators, formatters
│
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── datasources/    # AuthRemoteDataSource
│   │   │   ├── models/         # UserModel (json_serializable)
│   │   │   └── repositories/   # AuthRepositoryImpl
│   │   ├── domain/
│   │   │   ├── entities/       # User entity
│   │   │   ├── repositories/   # AuthRepository (abstract)
│   │   │   └── usecases/       # LoginUseCase, LogoutUseCase
│   │   └── presentation/
│   │       ├── bloc/           # AuthBloc, AuthEvent, AuthState
│   │       ├── pages/          # LoginPage, RegisterPage
│   │       └── widgets/        # LoginForm, SocialLoginButtons
│   │
│   └── orders/             # Aynı yapı: data / domain / presentation
│
├── shared/
│   ├── widgets/            # AppButton, AppTextField, LoadingOverlay
│   └── theme/              # AppTheme, AppColors, AppTextStyles
│
└── app/
    ├── app.dart            # MaterialApp.router tanımı
    └── router/             # GoRouter konfigürasyonu

Flutter ile Mobil Projenizi Hayata Geçirelim

iOS ve Android için tek codebase, native performans. Ücretsiz teknik danışmanlık için bize ulaşın.

🐦