Flutter ve AngularDart'ta Kod Paylaşımı için BLoC Nasıl Kullanılır?
Yayınlanan: 2022-03-11Geçen yılın ortalarında, bir Android uygulamasını iOS ve web'e taşımak istedim. Mobil platformlar için tercih Flutter oldu ve ben de web tarafı için ne seçeceğimi düşünüyordum.
Flutter'a ilk görüşte aşık olsam da, hala bazı çekincelerim vardı: Durumu widget ağacında yayarken, Flutter'ın InheritedWidget
veya Redux - tüm çeşitleriyle - işi yapacak, ancak Flutter gibi yeni bir çerçeve ile, görünüm katmanının biraz daha reaktif olmasını bekleyin, yani widget'ların kendileri durumsuz olacak ve dışarıdan beslendikleri duruma göre değişecekler, ancak değiller. Aslo, Flutter sadece Android ve iOS'u destekliyor ama ben web'de yayınlamak istedim. Uygulamamda zaten bir sürü iş mantığı var ve onu mümkün olduğunca yeniden kullanmak istedim ve iş mantığında tek bir değişiklik için kodu en az iki yerde değiştirme fikri kabul edilemezdi.
Bunu nasıl aşacağımı araştırmaya başladım ve BLoC ile karşılaştım. Hızlı bir giriş için, vaktiniz olduğunda Flutter/AngularDart – Kod paylaşımı, birlikte daha iyi (DartConf 2018) izlemenizi tavsiye ederim.
BLOK Modeli
BLoC , Google tarafından "iş mantığı bileşenleri " anlamına gelen süslü bir kelimedir. BLoC modelinin fikri, diğer platformlar tarafından yeniden kullanılabilmesi için iş mantığınızın mümkün olduğunca çoğunu saf Dart kodunda depolamaktır. Bunu başarmak için uymanız gereken kurallar vardır:
- Katmanlar halinde iletişim kurun. Görünümler, havuzlarla iletişim kuran BLoC katmanıyla iletişim kurar ve havuzlar veri katmanıyla konuşur. İletişim kurarken katmanları atlamayın.
- Arayüzler üzerinden iletişim kurun. Arayüzler saf, platformdan bağımsız Dart koduyla yazılmalıdır. Daha fazla bilgi için örtük arabirimlerle ilgili belgelere bakın.
- BLoC'ler yalnızca akışları ve havuzları ortaya çıkarır. Bir BLoC'nin G/Ç'si daha sonra tartışılacaktır.
- Görünümleri basit tutun. İş mantığını görüşlerden uzak tutun. Yalnızca verileri görüntülemeli ve kullanıcı etkileşimine yanıt vermelidirler.
- BLoC platformunu agnostik yapın. BLoC'ler saf Dart kodudur ve bu nedenle platforma özel mantık veya bağımlılık içermemelidirler. Platform koşullu koduna dalmayın. BLoC'ler, saf Dart'ta uygulanan mantıktır ve temel platformla ilgilidir.
- Platforma özgü bağımlılıkları enjekte edin. Bu, yukarıdaki kurala aykırı gelebilir, ama beni iyi dinleyin. BLoC'lerin kendileri platformdan bağımsızdır, ancak ya platforma özel bir havuzla iletişim kurmaları gerekiyorsa? Enjekte et. Arayüzler üzerinden iletişimi sağlayarak ve bu depoları enjekte ederek, deponuzun Flutter veya AngularDart için yazılmış olup olmadığına bakılmaksızın BLoC'nin umursamadığından emin olabiliriz.
Akılda tutulması gereken son bir şey, çıktı bir akıştan geçerken bir BLoC girişinin bir havuz olması gerektiğidir. Bunların ikisi de StreamController
parçasıdır.
Web (veya mobil!) uygulamanızı yazarken bu kurallara sıkı sıkıya bağlı kalırsanız, mobil (veya web!) bir sürüm oluşturmak, görünümleri ve platforma özel arayüzleri oluşturmak kadar basit olabilir. AngularDart veya Flutter'ı yeni kullanmaya başlamış olsanız bile, temel platform bilgisi ile görüş oluşturmak yine de kolaydır. Kod tabanınızın yarısından fazlasını yeniden kullanabilirsiniz. BLoC modeli, her şeyi yapılandırılmış ve bakımı kolay tutar.
AngularDart ve Flutter BLoC Todo Uygulaması Oluşturma
Flutter ve AngularDart'ta basit bir yapılacaklar uygulaması yaptım. Uygulama, arka uç olarak Firecloud'u ve oluşturmayı görüntülemek için reaktif bir yaklaşım kullanır. Uygulamanın üç bölümü vardır:
-
bloc
-
todo_app_flutter
-
todoapp_dart_angular
Daha fazla parçaya sahip olmayı seçebilirsiniz - örneğin, veri arayüzü, yerelleştirme arayüzü vb. Hatırlanması gereken şey, her katmanın bir arayüz üzerinden diğeriyle iletişim kurması gerektiğidir.
BLoC Kodu
bloc/
dizinde:
-
lib/src/bloc
: BloC modülleri burada iş mantığını içeren saf Dart kitaplıkları olarak depolanır. -
lib/src/repository
: Veri arayüzleri dizinde saklanır. -
lib/src/repository/firestore
: Depo, modeliyle birlikte verilere FireCloud arabirimini içerir ve bu örnek bir uygulama olduğundan, yalnızca bir veri modelimiztodo.dart
ve veri için bir arabirimtodo_repository.dart
; ancak gerçek dünyadaki bir uygulamada daha fazla model ve veri havuzu arabirimi olacaktır. -
lib/src/repository/preferences
preferences_interface.dart
başarıyla oturum açmış kullanıcı adlarını web'de yerel depolamaya veya mobil cihazlarda paylaşılan tercihlere depolayan basit bir arabirim olan tercihler_interface.dart'ı içerir.
//BLOC abstract class PreferencesInterface{ //Preferences final DEFAULT_USERNAME = "DEFAULT_USERNAME"; Future initPreferences(); String get defaultUsername; void setDefaultUsername(String username); }
Web ve mobil uygulamalar bunu mağazaya uygulamalı ve yerel depolama/tercihlerden varsayılan kullanıcı adını almalıdır. Bunun AngularDart uygulaması şöyle görünür:
// ANGULAR DART class PreferencesInterfaceImpl extends PreferencesInterface { SharedPreferences _prefs; @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance(); @override void setDefaultUsername(String username) => _prefs.setString(DEFAULT_USERNAME, username); @override String get defaultUsername => _prefs.getString(DEFAULT_USERNAME); }
Burada muhteşem bir şey yok - ihtiyacı olanı uygular. null
değerini döndüren initPreferences()
async yöntemini fark edebilirsiniz. SharedPreferences
örneğini mobil cihazda almak zaman uyumsuz olduğundan, bu yöntemin Flutter tarafında uygulanması gerekir.
//FLUTTER @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance();
Biraz lib/src/bloc dizini üzerinde duralım. Bazı iş mantığını işleyen herhangi bir görünüm, BLoC bileşenine sahip olmalıdır. Bu dizinde, BLoC base_bloc.dart
, endpoints.dart
ve session.dart
görünümlerini göreceksiniz. Sonuncusu, kullanıcının oturum açıp kapatmasından ve depo arayüzleri için uç noktalar sağlamaktan sorumludur. Oturum arayüzünün var olmasının nedeni, firebase
ve firecloud
paketlerinin web ve mobil için aynı olmaması ve platform bazında uygulanması gerektiğidir.
// BLOC abstract class Session implements Endpoints { //Collections. @protected final String userCollectionName = "users"; @protected final String todoCollectionName = "todos"; String userId; Session(){ _isSignedIn.stream.listen((signedIn) { if(!signedIn) _logout(); }); } final BehaviorSubject<bool> _isSignedIn = BehaviorSubject<bool>(); Stream<bool> get isSignedIn => _isSignedIn.stream; Sink<bool> get signedIn => _isSignedIn.sink; Future<String> signIn(String username, String password); @protected void logout(); void _logout() { logout(); userId = null; } }
Buradaki fikir, oturum sınıfını global (singleton) tutmaktır. _isSignedIn.stream
bağlı olarak, oturum açma/yapılacaklar listesi görünümü arasındaki uygulama geçişini yönetir ve userId varsa (yani kullanıcı oturum açmışsa) depo uygulamalarına uç noktalar sağlar.
base_bloc.dart
, tüm BLoC'ların temelidir. Bu örnekte, gerektiği gibi yük göstergesini ve hata iletişim kutusunu yönetir.
İş mantığı örneği için todo_add_edit_bloc.dart
bir göz atacağız. Dosyanın uzun adı amacını açıklar. Özel bir geçersiz yöntemi _addUpdateTodo(bool addUpdate)
.
// BLOC void _addUpdateTodo(bool addUpdate) { if(!addUpdate) return; //Check required. if(_title.value.isEmpty) _todoError.sink.add(0); else if(_description.value.isEmpty) _todoError.sink.add(1); else _todoError.sink.add(-1); if(_todoError.value >= 0) return; final TodoBloc todoBloc = _todo.value == null ? TodoBloc("", false, DateTime.now(), null, null, null) : _todo.value; todoBloc.title = _title.value; todoBloc.description = _description.value; showProgress.add(true); _toDoRepository.addUpdateToDo(todoBloc) .doOnDone( () => showProgress.add(false) ) .listen((_) => _closeDetail.add(true) , onError: (err) => error.add( err.toString()) ); }
Bu yöntemin girdisi bool addUpdate
ve final BehaviorSubject<bool> _addUpdate = BehaviorSubject<bool>()
dinleyicisidir. Bir kullanıcı uygulamada kaydet düğmesine tıkladığında, olay bu konu havuzu gerçek değerini gönderir ve bu BLoC işlevini tetikler. Bu çarpıntı kodu parçası, görünüm tarafında sihri yapar.
// FLUTTER IconButton(icon: Icon(Icons.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true),),
_addUpdateTodo
, hem başlığın hem de açıklamanın boş olmadığını kontrol eder ve bu koşula bağlı olarak _todoError
BehaviorSubject değerini değiştirir. _todoError
hatası, herhangi bir değer sağlanmadığında giriş alanlarında görüntüleme hatası görüntüsünün tetiklenmesinden sorumludur. Her şey yolundaysa, TodoBloc
oluşturmayı veya güncellemeyi kontrol eder ve son olarak _toDoRepository
, FireCloud'a yazmayı yapar.
İş mantığı burada ancak dikkat edin:
- BLoC'da yalnızca akışlar ve lavabolar halka açıktır.
_addUpdateTodo
ve görünümden erişilemez. -
_title.value
ve_description.value
, metin girişine değeri giren kullanıcı tarafından doldurulur. Metin değiştirme olayındaki metin girişi, değerini ilgili havuzlara gönderir. Bu şekilde, BLoC'da reaktif bir değer değişikliği ve bunların görünümde gösterilmesini sağlıyoruz. -
_toDoRepository
, platforma bağlıdır ve enjeksiyonla sağlanır.
todo_list.dart
BLoC _getTodos()
yönteminin kodunu kontrol edin. Yapılacaklar koleksiyonunun anlık görüntüsünü dinler ve koleksiyon verilerini kendi görünümünde listelemek için akar. Görünüm listesi, koleksiyon akışı değişikliğine göre yeniden çizilir.

// BLOC void _getTodos(){ showProgress.add(true); _toDoRepository.getToDos() .listen((todosList) { todosSink.add(todosList); showProgress.add(false); }, onError: (err) { showProgress.add(false); error.add(err.toString()); }); }
Akışlar veya rx eşdeğeri kullanırken dikkat edilmesi gereken önemli şey, akışların kapalı olması gerektiğidir. Bunu her bir BLoC'nin Dispose dispose()
yönteminde yapıyoruz. Her görünümün BLoC'sini bertaraf/yok etme yönteminde atın.
// FLUTTER @override void dispose() { widget.baseBloc.dispose(); super.dispose(); }
Veya bir AngularDart projesinde:
// ANGULAR DART @override void ngOnDestroy() { todoListBloc.dispose(); }
Platforma Özgü Depoları Enjekte Etme
Daha önce bir BLoC'da gelen her şeyin basit Dart olması ve platforma bağlı olmaması gerektiğini söylemiştik. TodoAddEditBloc
Firestore'a yazabilmesi için ToDoRepository
ihtiyacı var. Firebase, platforma bağlı paketlere sahiptir ve ToDoRepository
arabiriminin ayrı uygulamalarına sahip olmamız gerekir. Bu uygulamalar uygulamalara enjekte edilir. Flutter için flutter_simple_dependency_injection
paketini kullandım ve şuna benziyor:
// FLUTTER class Injection { static Firestore _firestore = Firestore.instance; static FirebaseAuth _auth = FirebaseAuth.instance; static PreferencesInterface _preferencesInterface = PreferencesInterfaceImpl(); static Injector injector; static Future initInjection() async { await _preferencesInterface.initPreferences(); injector = Injector.getInjector(); //Session injector.map<Session>((i) => SessionImpl(_auth, _firestore), isSingleton: true); //Repository injector.map<ToDoRepository>((i) => ToDoRepositoryImpl(injector.get<Session>()), isSingleton: false); //Bloc injector.map<LoginBloc>((i) => LoginBloc(_preferencesInterface, injector.get<Session>()), isSingleton: false); injector.map<TodoListBloc>((i) => TodoListBloc(injector.get<ToDoRepository>(), injector.get<Session>()), isSingleton: false); injector.map<TodoAddEditBloc>((i) => TodoAddEditBloc(injector.get<ToDoRepository>()), isSingleton: false); } }
Bunu şuna benzer bir widget'ta kullanın:
// FLUTTER TodoAddEditBloc _todoAddEditBloc = Injection.injector.get<TodoAddEditBloc>();
AngularDart, sağlayıcılar aracılığıyla yerleşik enjeksiyona sahiptir.
// ANGULAR DART @GenerateInjector([ ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl), ClassProvider(Session, useClass: SessionImpl), ExistingProvider(Endpoints, Session) ])
Ve bir bileşende:
// ANGULAR DART providers: [ overlayBindings, ClassProvider(ToDoRepository, useClass: ToDoRepositoryImpl), ClassProvider(TodoAddEditBloc), ExistingProvider(BaseBloc, TodoAddEditBloc) ],
Session
global olduğunu görebiliriz. ToDoRepository
ve BLoC'lerde kullanılan oturum açma/kapama işlevselliğini ve bitiş noktalarını sağlar. ToDoRepository
, SessionImpl
ve benzerlerinde uygulanan bir uç nokta arabirimine ihtiyaç duyar. Görünüm yalnızca BLoC'sini görmeli ve başka bir şey görmemelidir.
Görüntüleme
Görünümler mümkün olduğunca basit olmalıdır. Yalnızca BLoC'dan gelenleri görüntüler ve kullanıcının girişini BLoC'a gönderir. Flutter'dan TodoAddEdit
widget'ı ve web eşdeğeri TodoDetailComponent
ile bunun üzerinden geçeceğiz. Seçilen yapılacaklar başlığını ve açıklamasını görüntülerler ve kullanıcı bir yapılacakları ekleyebilir veya güncelleyebilir.
çarpıntı:
// FLUTTER _todoAddEditBloc.todoStream.first.then((todo) { _titleController.text = todo.title; _descriptionController.text = todo.description; });
Ve daha sonra kodda…
// FLUTTER StreamBuilder<int>( stream: _todoAddEditBloc.todoErrorStream, builder: (BuildContext context, AsyncSnapshot errorSnapshot) { return TextField( onChanged: (text) => _todoAddEditBloc.titleSink.add(text), decoration: InputDecoration(hintText: Localization.of(context).title, labelText: Localization.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null), controller: _titleController, ); }, ),
StreamBuilder
pencere öğesi, bir hata varsa (hiçbir şey eklenmemişse) kendini yeniden oluşturur. Bu, _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink
_todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink
, başlığı tutan ve kullanıcının metin alanına metin girmesiyle güncellenen BLoC'daki bir havuzdur.
Bu giriş alanının başlangıç değeri (bir yapılacaklar seçiliyse), seçilen yapılacakları tutan _todoAddEditBloc.todoStream
veya yeni bir yapılacaklar eklersek boş olanı dinleyerek doldurulur.
Bir metin alanına değer atanması, denetleyicisi tarafından yapılır _titleController.text = todo.title;
.
Kullanıcı yapılacakları kaydetmeye karar verdiğinde, uygulama çubuğundaki onay simgesine basar ve _todoAddEditBloc.addUpdateSink.add(true)
tetikler. Bu, önceki BLoC bölümünde bahsettiğimiz _addUpdateTodo(bool addUpdate)
çağırır ve hatayı kullanıcıya geri ekleme, güncelleme veya görüntüleme iş mantığını yapar.
Her şey reaktiftir ve widget durumunu işlemeye gerek yoktur.
AngularDart kodu daha da basittir. Sağlayıcıları kullanarak bileşene kendi BLoC'sini sağladıktan sonra, todo_detail.html
dosya kodu, verileri görüntüleme ve kullanıcı etkileşimini BLoC'a geri gönderme bölümünü yapar.
// AngularDart <material-input #title label="{{titleStr}}" ngModel="{{(todoAddEditBloc.titleStream | async) == null ? '' : (todoAddEditBloc.titleStream | async)}}" (inputKeyPress)="todoAddEditBloc.titleSink.add($event)" [error]="(todoAddEditBloc.todoErrorStream | async) == 0 ? titleErrString : ''" autoFocus floatingLabel type="text" useNativeValidation="false" autocomplete="off"> </material-input> <material-input #description label="{{descriptionStr}}" ngModel="{{(todoAddEditBloc.descriptionStream | async) == null ? '' : (todoAddEditBloc.descriptionStream | async)}}" (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)" [error]="(todoAddEditBloc.todoErrorStream | async) == 1 ? descriptionErrString : ''" autoFocus floatingLabel type="text" useNativeValidation="false" autocomplete="off"> </material-input> <material-button animated raised role="button" class="blue" (trigger)="todoAddEditBloc.addUpdateSink.add(true)"> {{saveStr}} </material-button> <base-bloc></base-bloc>
Flutter'a benzer şekilde, ngModel=
başlık akışından başlangıç değeri olan değeri atadık.
// AngularDart (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)"
inputKeyPress
çıkış olayı, kullanıcının metin girişine yazdığı karakterleri BLoC'un açıklamasına geri gönderir. Malzeme düğmesi (trigger)="todoAddEditBloc.addUpdateSink.add(true)"
olayı, BLoC'da aynı _addUpdateTodo(bool addUpdate)
işlevini tekrar tetikleyen BLoC ekleme/update olayını gönderir. todo_detail.dart
koduna bakarsanız, görünümde görüntülenen dizeler dışında neredeyse hiçbir şey olmadığını göreceksiniz. Burada yapılabilecek olası yerelleştirme nedeniyle onları HTML'ye değil oraya yerleştirdim.
Aynısı diğer tüm bileşenler için de geçerlidir; bileşenler ve widget'lar sıfır iş mantığına sahiptir.
Bir senaryodan daha bahsetmeye değer. Karmaşık veri sunum mantığına sahip bir görünümünüz veya biçimlendirilmesi gereken değerlere (tarihler, para birimleri vb.) sahip bir tablo gibi bir şeye sahip olduğunuzu hayal edin. Birisi, değerleri BLoC'den almak ve bunları bir görünümde biçimlendirmek için cazip olabilir. Bu yanlış! Görünümde görüntülenen değerler, önceden biçimlendirilmiş (dizeler) görünüme gelmelidir. Bunun nedeni, biçimlendirmenin kendisinin de iş mantığı olmasıdır. Bir başka örnek de, görüntüleme değerinin biçimlendirmesinin çalışma zamanında değiştirilebilen bazı uygulama parametrelerine bağlı olmasıdır. Bu parametreyi BLoC'a sağlayarak ve görüntülemeyi görüntülemek için reaktif bir yaklaşım kullanarak, iş mantığı değeri biçimlendirecek ve yalnızca ihtiyaç duyulan parçaları yeniden çizecektir. Bu örnekte sahip olduğumuz BLoC modeli TodoBloc
, çok basittir. FireCloud modelinden BLoC modeline dönüştürme depoda yapılır, ancak gerekirse model değerlerinin görüntülenmeye hazır olması için BLoC'da yapılabilir.
Toplama
Bu kısa makale, BLoC model uygulaması ana kavramlarını kapsar. Flutter ve AngularDart arasında kod paylaşımının mümkün olduğunu ve yerel ve platformlar arası geliştirmeye olanak tanıdığının çalışan bir kanıtı.
Örneği inceleyerek, doğru uygulandığında BLoC'nin mobil/web uygulamaları oluşturma süresini önemli ölçüde kısalttığını göreceksiniz. Bir örnek ToDoRepository
ve uygulamasıdır. Uygulama kodu hemen hemen aynıdır ve görünüm oluşturma mantığı bile benzerdir. Birkaç widget/bileşenden sonra hızlı bir şekilde seri üretime başlayabilirsiniz.
Umarım bu makale size Flutter/AngularDart ve BLoC modelini kullanarak web/mobil uygulamalar yaparken sahip olduğum eğlence ve coşku hakkında bir fikir verir. JavaScript'te platformlar arası masaüstü uygulamaları oluşturmak istiyorsanız, Toptaler'den Stephane P. Pericat'in yazdığı Electron: Platformlar Arası Masaüstü Uygulamalarını Kolaylaştırın başlıklı makaleyi okuyun.