Flutter

Flutter'da BLoC ile Ölçeklenebilir Uygulamalar

Neden BLoC ve Temiz Mimari?

Flutter ile uygulama geliştirirken, projenin karmaşıklığı arttıkça kod tabanını yönetmek, yeni özellikler eklemek ve hataları ayıklamak giderek zorlaşır. Bu noktada, BLoC ve Temiz Mimari gibi yapılandırılmış bir yaklaşım benimsemek yalnızca bir tercih değil, stratejik bir gerekliliktir. Bu mimari desen, uygulamanın farklı bölümlerini net sınırlarla ayırarak ölçeklenebilir, sürdürülebilir ve kolay test edilebilir sistemler kurmanın temelini atar.

Bu yaklaşımın temel hedefleri üç başlıkta toplanabilir:

  • Sorumlulukların ayrılması: İş mantığını kullanıcı arayüzünden ve veri kaynaklarından soyutlar.
  • Test edilebilirlik: İş mantığını izole test etmeyi kolaylaştırır.
  • Sürdürülebilirlik ve ölçeklenebilirlik: Yeni özellikler eklerken karmaşıklığı kontrol altında tutar.

BLoC Mimarisi’nin Temel Kavramları

BLoC, sunum mantığını iş mantığından ayırmak için tasarlanmış olay odaklı bir durum yönetimi desenidir. UI katmanından gelen olayları dinler, gerekli işlemleri yapar ve UI’ın kendini güncellemesi için yeni durumlar yayınlar. Bu tek yönlü veri akışı, uygulamanın durumunu daha öngörülebilir hale getirir.

BLoC veri akışı

Temiz BLoC Mimarisi: Katmanların Ayrıştırılması

Temiz Mimari, yazılım sistemlerini katmanlara ayırarak sorumlulukların net şekilde ayrılmasını hedefler. Temel ilke, iş mantığının UI, veritabanı veya ağ istekleri gibi dış bağımlılıklardan izole olmasıdır. Bu rehberde bu ilkeyi BLoC ile birleştirerek uygulamayı sunum, iş mantığı ve veri katmanlarına ayırıyoruz.

Sunum Katmanı

Bu katman, kullanıcının gördüğü ve etkileşimde bulunduğu her şeyden sorumludur.

  • Flutter widget’ları ile arayüzü oluşturur.
  • BlocBuilder, BlocListener ve BlocConsumer gibi yapılarla BLoC durumlarını dinler.
  • Kullanıcı etkileşimlerini olay olarak BLoC’a iletir.

İş Mantığı Katmanı

Bu katman uygulamanın beynidir. Sunum katmanından gelen olayları işler, repository’ler üzerinden veri talep eder ve sonuç olarak yeni durumlar yayınlar.

Karmaşık ve yeniden kullanılabilir iş kuralları için mantığı domain/usecases içinde tutmak daha doğru bir tercihtir. Bu senaryoda BLoC, use case’leri çağıran bir aracı gibi davranır.

Veri Katmanı

Bu katman, uygulamanın ihtiyaç duyduğu tüm verileri sağlar ve yönetir.

  1. Repository’ler: İş mantığının veriyle nasıl konuşacağını tanımlayan soyut sözleşmelerdir.
  2. Veri kaynakları: API, yerel veritabanı veya önbellek gibi somut veri giriş noktalarıdır.
  3. Modeller: Ham veriyi Dart nesnelerine dönüştürür.
  4. Repository implementasyonları: Veri kaynaklarından gelen çıktıyı iş mantığının kullanacağı yapılara dönüştürür.

Örnek Proje Yapısı

lib/
├── presentation/
│   ├── bloc/
│   ├── screens/
│   └── widgets/
├── domain/
│   ├── entities/
│   └── repositories/
├── data/
│   ├── datasources/
│   ├── models/
│   └── repositories_impl/
└── main.dart

Etkili Olay ve Durum Yönetimi

İyi tanımlanmış olay ve durum sınıfları, öngörülebilir ve sürdürülebilir bir BLoC mimarisi için kritiktir.

part of 'stopwatch_bloc.dart';

abstract class StopwatchEvent extends Equatable {
  const StopwatchEvent();

  @override
  List<Object> get props => [];
}

class StopwatchStarted extends StopwatchEvent {
  const StopwatchStarted();
}

class StopwatchPaused extends StopwatchEvent {
  const StopwatchPaused();
}

class StopwatchReset extends StopwatchEvent {
  const StopwatchReset();
}

Durum tarafında Equatable, copyWith ve immutability kullanmak gereksiz yeniden çizimleri azaltır ve durum geçişlerini daha güvenilir hale getirir.

class StopwatchState extends Equatable {
  const StopwatchState({
    this.ticks = 0,
    this.status = StopwatchStatus.initial,
  });

  final int ticks;
  final StopwatchStatus status;

  @override
  List<Object> get props => [ticks, status];

  StopwatchState copyWith({
    int? ticks,
    StopwatchStatus? status,
  }) {
    return StopwatchState(
      ticks: ticks ?? this.ticks,
      status: status ?? this.status,
    );
  }
}

Hata Yönetimi

Hata durumları için özel state sınıfları oluşturmak en temiz yaklaşımdır. BLoC olay işleyicileri içinde try-catch kullanarak veri katmanından gelen hataları yakalayabilir ve UI’a anlamlı geri bildirim gösterebilirsiniz.

Bağımlılık Enjeksiyonu Stratejileri

Bağımlılık enjeksiyonu, katmanlar arasındaki gevşek bağlılığı korumak için kritik rol oynar.

  • RepositoryProvider repository örneklerini sağlar.
  • BlocProvider BLoC örneklerini widget ağacına verir.
  • MultiRepositoryProvider ve MultiBlocProvider daha temiz bir kurulum sunar.

UI Etkileşim Desenleri

flutter_bloc paketi üç temel yapı sunar:

İsimKullanım amacıÖnemli özellik
BlocBuilderDurum değiştikçe UI’ın ilgili kısmını yeniden oluşturur.buildWhen ile optimizasyon yapılabilir.
BlocListenerSnackBar, yönlendirme veya diyalog gibi tek seferlik yan etkiler için kullanılır.UI yeniden oluşturmaz.
BlocConsumerHem yeniden çizim hem de yan etki gerektiğinde kullanılır.BlocBuilder ve BlocListener davranışını birleştirir.

Küçük Bir Örnek

BLoC’un temel prensiplerini göstermek için küçük bir sayaç uygulaması:

BLoC sayaç uygulaması

  1. Yeni proje oluşturup flutter_bloc paketini ekleyin.
  2. Event ve state sınıflarını tanımlayın.
  3. CounterBloc sınıfını oluşturun.
  4. UI’ı BLoC ile bağlayın.
flutter create bloc_counter_app
cd block_counter_app
flutter pub add flutter_bloc
flutter pub get
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}
abstract class CounterState {
  final int counterValue;
  const CounterState(this.counterValue);
}

class CounterInitialState extends CounterState {
  CounterInitialState() : super(0);
}

class CounterUpdatedState extends CounterState {
  const CounterUpdatedState(int counterValue) : super(counterValue);
}
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitialState()) {
    on<IncrementEvent>((event, emit) {
      emit(CounterUpdatedState(state.counterValue + 1));
    });

    on<DecrementEvent>((event, emit) {
      emit(CounterUpdatedState(state.counterValue - 1));
    });

    on<ResetEvent>((event, emit) {
      emit(CounterInitialState());
    });
  }
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}

abstract class CounterState {
  final int counterValue;
  const CounterState(this.counterValue);
}

class CounterInitialState extends CounterState {
  CounterInitialState() : super(0);
}

class CounterUpdatedState extends CounterState {
  const CounterUpdatedState(int counterValue) : super(counterValue);
}

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitialState()) {
    on<IncrementEvent>((event, emit) {
      emit(CounterUpdatedState(state.counterValue + 1));
    });

    on<DecrementEvent>((event, emit) {
      emit(CounterUpdatedState(state.counterValue - 1));
    });

    on<ResetEvent>((event, emit) {
      emit(CounterInitialState());
    });
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterScreen(),
      ),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterBloc = BlocProvider.of<CounterBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('BLoC Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Text(
              '${state.counterValue}',
              style: TextStyle(fontSize: 50),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'increment',
            child: Icon(Icons.add),
            onPressed: () => counterBloc.add(IncrementEvent()),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'decrement',
            child: Icon(Icons.remove),
            onPressed: () => counterBloc.add(DecrementEvent()),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'reset',
            child: Icon(Icons.refresh),
            onPressed: () => counterBloc.add(ResetEvent()),
          ),
        ],
      ),
    );
  }
}

Test Stratejileri

bloc_test ve mocktail gibi araçlarla başarı ve hata senaryolarını kapsayan testler yazmak, iş mantığının güvenilirliğini ciddi biçimde artırır.

Sonuç

BLoC ve Temiz Mimari birlikte kullanıldığında, uygulamanın farklı katmanlarını net şekilde ayırabilir, iş mantığını UI’dan bağımsız tutabilir ve uzun vadede daha sürdürülebilir Flutter projeleri geliştirebilirsiniz.