flutter_clean_architecture paketi ve mimari kullanimi

Umut Barış Çoşkun
7 min readOct 23, 2022

--

Selamlar, bugün size kullanmaktan büyük keyif aldığım, oldukça clean uygulamaları çok hızlı bir şekilde çıkarmanıza yardımcı olabilecek bir paketten bahsedeceğim. Bu paket sayesinde koca bir mimariyi çok hızlı bir şekilde kurabilir, aynı zamanda ek bi state management paketine ihtiyaç duymadan barındırdığı provider ile state’lerinizi kontrol edebilirsiniz.

Bu paketin en sevdiğim kısmı da Clean Architecture’ a yeni başlayan Flutter developerlar için bu state management çözümünü de birlikte sunması. Bu sayede ikinci bir state management kütüphanesi öğrenmeden temiz uygulamalar yazabilirsiniz.

Bahsi geçen Flutter paketi:

Makalemde görüntüler aldığım ve bir başka örnek repository:

Kısaca Clean Architecture değinip, paketimizin bize sağladığı kolaylıklara geçebiliriz. Kısaca Clean Architecture ve katmanlara göre paketin uyarlanışı:

1-Clean Architecture Nedir?

Öncelikle Clean Architecture, sadece Flutter’a veya mobil uygulamaya özel çıkarılmış bir mimari değildir.Sanırım Uzun yıllardır hayatımızda olan Clean Code prensiplerini baz almış bir mimaridir demek yanlış olmayacaktır. Uncle Bob Clean Code kitabını okumanız bu konuda ufkunuzu daha da genişletecektir.

2- Clean Architecture Katmanları

Şekil 1 Clean Architecture Katman ve Flow

Şekil 1'de de gördüğünüz üzere 3 ana katmanımız var ve yapımız bu 3 ana katman etrafında şekilleniyor. Normalde Data katmanı altında da Entity’ den ayrı olarak Modeller kullanıyoruz. Fakat biz özellikle StreamController kullanacağımızdan dolayı daha anlaşılır olması için tüm Flow’da entity kullanacağız. Siz sonradan Data katmanı için Ayrı Modeller yazarak kullanabilirsiniz. Yapıları kullanımınıza göre yorumlayabilirsiniz. Ben Su an Clean Architecture’ dan ziyade flutter_clean_architecture paketini anlatacağım için daha sade ve anlaşılır bir flowda clean anlatmak istedim.

Domain

Entity

Entity’ ler data kısmı dışındaki katmanlarımızdaki modelin karşılığı diyebiliriz. Normalde Burada from.JSON() gibi data metodlarımız olmaz sadece arayüzde kullancağımız özellikleri yazarız. Bu sekilde kodumuzu data kısmındaki gibi karışık Hive modellerinden soyutlar, daha sade şekilde kullanabiliriz.

Şekil 2. Entiy

Şekil 2. de model olmadan sadece Entity ile yazılmış bir Flow için kullanılan bir Entity örneği var. Normalde Entity’de fromjson gibi işlemleri yapmak yerine Model kullanmayı tercih edebiliriz.

AbstractRepository

Kodu yazma sırasına göre önce Domain katmanından baslarız. Bu katman en soyut katmanımızdır ve eğer yeni başlıyorsanız, Bu katman size “Neden bunları yazıyorum ki?” dedirtebilir defalarca. Bu katman sayesinde Asıl metodlarımızın işlevlerini doldurduğumuz DataRepository’ leri kullanmadan abstraction yaparak tüm flowda abstract Repository ile işlem yapabiliyoruz. Bu sayede DataRepo’muza da olur olmadık her yerden erişmek zorunda kalmıyoruz. Ayrıca UnitTest yazarken de abstractRepository’ ler üzerinden mocklama yapabiliyoruz.

Şekil 3. AbstractRepository

UseCase

Usecaseler, Kodumuzu singleton pattern’ e uygun olarak yazmamızı sağlar. UnitTest yazarken bu usecaseler sayesinde, uygulamamızın büyük kısmını kolaylıkla test edebiliriz. Ayrıca fonksiyonları kod ile takip etmek çok daha kolay olur. Nerede olduğunu dahi bilmediğiniz fonksiyonların bırakın testini yazmayı; bakımı yapmak, bir şeyler ekleyip çıkarmak dahi oldukça zor olacaktır.

flutter_clean_architecture paketinde ise bir base usecase extend ederiz. Bu usecase ve Stream’ler sayesinde usecaselerimizi dinleyebiliriz.

Şekil 4. Base UseCase

Şekil 4. deki base UseCase’ i tüm usecaselerimizde extend ederiz. build UsecaseStream’i override ederiz. Bu base usecase paketimiz ile birlikte geliyor.

STREAM

UseCase kısmına geldiğimizde karşımıza Stream’ler çıkıyor. Stream kullanmamızın başlıca nedeni daha sonra değineceğimiz observerlar ile usecaseleri dinleyebilmektir.

Yapımızda kullanacağımız iki farklı Stream çeşidi var:

1- Açık Stream

Tüm Flowda, abstract repo dahil Future<Stream> tipte döndüren fonksiyon ile kullanırız, UseCase içinde bu streami kapatmayız ve DataRepository’de de veriyi streamController a ekleyerek; return streamController.stream şeklinde Stream olarak döndeririz.

Bunun bize sağladığı avantaj ise, sürekli dinlememiz gereken realTime mantığıyla çalışan durumlarda tekrar get atmadan değişikliğin arayüzde güncellenmesini sağlarız. Örneğin sepete bir ürün eklediniz ve aynı sayfada bir sepet ikonu var. Bu sepetin üstünde ürün sayısını belirten bir ibare var ve siz sepete ekler eklemez bunun da güncellenmesini istiyorsanız, açık streamleri kullanabilirsiniz.

2- Kapalı Stream

Usecase içinde StreamController’ a veriyi ekleriz ve bu controller’ı geri kapatırız bunun tek nedeni sonradan execute ettiğimizde subscribe olacağımız bir stream açmaktır. Tüm flowda Stream döndermemize gerek yoktur. AbstractRepoda(Şekil 3) da Future<void> fonksyionlarda göreceğimiz üzere, Stream tipi ile işimiz usecase içinde bitmektedir.

Observer

Şekil 5. Observer

Şekil 5.te de görebileceğiniz üzere observerımızın üç ana durumu var.

Eğer usecase bir şey dönderiyorsa parametresi ne olursa olsun onNext durumu ile takip ederiz. Bu şekilde gelen response’ ı kullanabiliriz.

Eğer usecase void return tipine sahipse, yani hiçbir şey döndermiyorsa bu useCase’yi onComplete ile takip ederiz.

OnError durumu ise her iki durumda da geçerli olabilecek bir durum olduğundan her halükarda yazar ve kullanırız, eğer hataya düşerse buradan hatayı dinleyebiliriz. Presenter sayfasında Bu observeri extend ederek Bir observer yazarız ve usecaseleri execute ettiğimizde bu observerleri execute içine parametre olarak veririz. Bu base Observer da paketimiz ile birlikte gelecek, sizin yazmanıza gerek yok.

Şekil 5. AddToDo UseCase

DATA LAYER

Data Repository veya RepositoryImpl

Abstract Repository’leri implement ederek kullanırız. Bildiğiniz üzere Dart’ta implement ettiğimiz classların tüm metotlarını override etmek zorundayız. AbstractRepository’ lerde return type’ları, paramsları belirtip usecaseleri yazdıktan sonra bu metotların işlevlerini doldurmak zorundayız. Burada tercih ettiğiniz yapıya göre değişebilmekle beraber; bu metotları dataRepositorylerde doldurabilir veya DataSource’lar kullanarak orada doldurduğunuz metotları burada döndürebilirsiniz.

// ignore_for_file: avoid_printimport 'dart:async';import 'package:squamobi_to_do/data/repositories/data_locale_db_repository.dart';import 'package:squamobi_to_do/domain/entities/to_do_card.dart';import 'package:squamobi_to_do/domain/repositories/locale_db_repository.dart';import 'package:squamobi_to_do/domain/repositories/to_do_repository.dart';class DataToDoRepository implements ToDoRepository {static final _instance = DataToDoRepository._internal();DataToDoRepository._internal(): _localeDBRepository = DataLocalDBRepository();factory DataToDoRepository() => _instance;final LocaleDbRepository _localeDBRepository;List<ToDoCard> _toDos = [];final StreamController<List<ToDoCard>> _streamController =StreamController.broadcast();@overrideFuture<void> addToDo(ToDoCard toDoCard) async {try {_toDos.add(toDoCard);await _localeDBRepository.setDatabase("toDo", toDoCard.toJson());_streamController.add(_toDos);} catch (e, st) {print(e);print(st);rethrow;}}@overrideFuture<void> removeToDo(String toDoId) async {try {_toDos.remove(_toDos.firstWhere((toDo) => toDo.id == toDoId));await _localeDBRepository.deleteRowFromDatabase("toDo",toDoId,);_streamController.add(_toDos);} catch (e, st) {print(e);print(st);rethrow;}}@overrideStream<List<ToDoCard>> get toDoCards {try {_initToDos();return _streamController.stream;} catch (e, st) {print(e);print(st);rethrow;}}void _initToDos() async {try {final result = await _localeDBRepository.getDatabase("toDo");_toDos = result.map((json) => ToDoCard.fromJson(json)).toList();Future.delayed(Duration.zero).then((_) => _streamController.add(_toDos),);} catch (e, st) {print(e);print(st);rethrow;}}}

DataModel

DataModel’ de apiden parse ettiğimiz özelliklerin Modellenmiş halleridir. Entityden farklı özellikler içerebilir. FromJson, ToJson, GetProps gibi metotları içerebilir. Aynı zamanda kullandığınız Cache yönetimine göre bu modeller birer HiveModel dahi olabilir. Adından da anlaşabileceği üzere kısaca Entitylerimizin Data Layerde kullandığımız halleridir.

PRESENTATION

Bu katmanda kullanıcının muhatap olacağı arayüz katmanımız, widgetlarımız ve bunların yanı sıra kullandığımız paket sayesinde; controller ve presenter’larımız var.

Presenter

Bu classta ilk olarak observer içinde kullanacağımız onNext, onComplete, onError gibi fonksiyonları late olarak belirtiriz. Çünkü bunları daha sonradan observer içinde initilaize ederiz.

Daha sonra privete olarak UseCaselerimizi tanımlar ve içinde Daha sonradan Controllerda constructor ile alacağımız Abstract Olan Repositoryimizi veririz.

Tüm Flowda View katmanına kadar Abstract Repository kullanıyoruz. Bu nokta çok önemlidir.

Daha sonrasında ise returnType ne olursa olsun void olarak birer metot yazıp bu private usecaseleri burada execute ederiz. Tabiki birer observer da lazım olacağından öncesinde observerlarımızı yazarız.

Controller

Bu sayfada ise Constructor’da presenterımızı alırız ve Presenter’ ımızında bir repoya ihtiyaç duydugu için bir de dolaylı constructor kullanarak repository alırız. Bu aldığımız Repository’ i ise View’dan alırız. View’da kullandığımız Repository artık son katman oldugu için DataRepository’dir, abstract olan değil.

View’ a kadar kullandığımız tüm repository’ler abstracttır. Viewda implementedDataRepository kullanırız.

Bunun sebebi ise aynı response, params tipine sahip fonksiyonlarda farklı işlemler yapmak isteyebilirsiniz. Örneğin Facebook girişi yapan kullanıcı’da da auth metodu olabilir. Google girişi yapan kullanıcıda da. Fakat implement ettiğiniz dataRepositoryler farklıdır (örneğin FacebookAuthRepo, GoogleAuthRepo) ve işlem auth olsa dahi fonksiyonların içi farklı şekilde dolacaktır. Bu da abstraction’ ın mükemmel bir kullanımıdır ve neden yaptığımızı sorgularsanız bu örneği aklınıza getirebilirsiniz.

override ettiğimiz metotlara bakarsak, initState zaten aşina oldugumuz sayfa ilk açıldığında çalışacak metottur. initListeners sayesinde ise observerların içinde initialize ettiğimiz fonksiyonları dinleyebiliriz ve response’ a ulaşabiliriz, hata durumlarında kullanıcıya uyarılar gösterebiliriz, veya controller’da context kullanmak istemezsek bu durumları yine initListeners içinde kontrol edebiliriz.

Ayrıca bu classta viewda kullanacağımız değişkenleri, ve fonksiyonları tutarız. Örneğin Butona basıldığında çalışacak addToDoCard useCase’ ini tetikleyecek bir fonksiyonu burada yazarız ve onun içinde presenter classına erişiriz.

VIEW, STATE MANAGEMENT

View yapımız bu şekilde oluşmaktadır ve state’ i kontrol edeceğimiz ControlledWidgetBuilder için ViewState’ e ihtiyacımız vardır. Bu ViewState içine super Constructor ile Controller classımızı veririz ve ardından ControlledWidgetBuilder kullanabiliriz.

Ayrıca bu paket ile birlikte key:globalKey’i her Scaffold içinde yazmalıyız, aksi takdirde navigate sırasında hatalarla karşılaşabilirsiniz. Küçük harflerle yazılan globalKeyi herhangi bir yerde tanımlamanıza gerek yok, bu key paketten geliyor.

ControlledWidgetBuilder & State Management

State yönetme sihri de burdan geliyor, ControlledWidgetBuilder sayesinde tüm UI yerine sadece bu widget ile sarmaladığını kısımı yenilersiniz ve dikkatli bir kullanımla da beraber( tüm sayfayı sarmalamadan ihtiyaç duyulan yeri sarmalamak) çok performanslı uygulamalar yazabilirsiniz.

--

--