Flutter 2 - Sadece Mobil Uygulama Olmaktan Çıkıyor
-
Hım, sanırım fw içinde bu işi sağlayan mekanizma var ama konfor sağlamıyor anladığım.
Kısaca şöyle diyebilir miyiz?
Takibini yapmak istediğim tüm verileri singleton jenerik bir koleksiyon içinde sarmalıyorum. Ayrıca değişiklik taleplerini de doğrudan buraya yapıyorum. Değişiklikleri de listenerlara duyuruyorum.
Peki solid prensipleri vs. hiç olmuyor mu? Performansa nasıl yansıyor?
-
@YeniHarman her soruna cevap gelecek abi. Bu arada dart ffi ile c bile kodlayabiliyorsun. JITCompiler hallediyor. Direkt donanıma da hitap edebiliyorsun Flutter ile. Anlatacağım 3 4 gündür mobilim :/
-
YeniHarman bunu yazdı
Hım, sanırım fw içinde bu işi sağlayan mekanizma var ama konfor sağlamıyor anladığım.
Kısaca şöyle diyebilir miyiz?
Takibini yapmak istediğim tüm verileri singleton jenerik bir koleksiyon içinde sarmalıyorum. Ayrıca değişiklik taleplerini de doğrudan buraya yapıyorum. Değişiklikleri de listenerlara duyuruyorum.
Peki solid prensipleri vs. hiç olmuyor mu? Performansa nasıl yansıyor?
Solid prensiplerini uygulayabilirsin, state güncellemesinden tüm sayfayı değil sadece istediğin widgeti render edebilirsin. test driven development, MVVM tercih edebilirsin. MhmdAlmz'ın bahsettiği dartffi i unutmuşum. Flutter 2.0 ile gündemde. Ne kadar efektif, kullandıkça forumda tecrübelerimi paylaşırım. Ben MVVM ve state yönetimi için uzun süredir stacked paketini kullanıyorum. Memnunum. Bu konuda farklı çözümler var. Bakalım MhmdAlmz hangi paket(ler)i tercih ediyor (:
-
Merhaba herkese. Bildiğiniz üzere yaklaşık 3 senedir Flutter yazıyorum aktif olarak. İş hayatımda 1 sene öncesine kadar React-Native yazıyordum. Flutter için istihdam yoktu malesef. Gerçi şu an ki işime başladığımda da RN yazıyordum proje yöneticisini kafalayıp tüm şirketi Flutter'a çevirdik :D Neyse..
State Management Nedir?
State Management belli bir widget'i yada bir class'ı istediğiniz anda güncelleme işine yarar. Şöyle ki State Management olmadan bir şekilde (Singleton Pattern, static değişken vb.) yine de global de değişkenler yada yapılar tanımlayabilirsiniz. Child to parent widget ilişkilerinde çok zorlanıldığından dolayı ve sadece istenilen ekran bölümünü güncellediğimizden dolayı State Management'a ihtiyaç duyarız. Bunun için bir çok kütüphane duyabilirsiniz örnek olarak; Redux, MobX, GetX, Provider vs. vs. Aslında bu State Managementlar şu işe yarıyor. kısaca bir sınıf yazarsak.
class TestWidget extends StatefulWidget { @override _TestWidgetState createState() => _TestWidgetState(); } class _TestWidgetState extends State { @override void initState() { ObserverClass().listen(() { this.setState(() {}); }); super.initState(); } @override Widget build(BuildContext context) { return Text("test"); } }
Burada belirtilen ObserverClass sadece dinleme işini görür ve herhangi bir method ile listen methodu çalışır sizin belirttiğiniz widget tekrar render olur hepsi bu. (Kaba taslak arada bazı kontroller var bir önceki state ile şu an ki aynı mı vs diye). Bu bir sınıf olduğundan da her yerde erişebiliyorsunuz.
Dart FFI
Dart ffi ile OpenCV projesi geliştirdik. Yani JAVA yada Objective-C kullanmadık direkt Dart FFI üzerinde C++ kodlarını çalıştırarak daha performanslı uygulama geliştirdik. Şu an da çatır çatır çalışıyor. Bir adam da burda yapmış 2 dk google yazdım detaylı incelemedim. https://github.com/westracer/flutter_native_opencv yani direkt olarak popüler kütüphaneleri bu şekilde kullanabiliyoruz. Sadece C++ değil Python vs de yazabiliyorsunuz.
Ben ne kullanıyorum ?
Ben sadece RxDart kullanarak observer yapımı kurdum ve onun üzerine geliştirmeler yapıyorum. İstediğim yerden istediğim zaman erişebiliyorum. Bloc mantığında ama tam olarak bloc da değil bloc üzerine kendim bir repository yazarak store, listener, request/response datalarımı alıyorum. Her şey modeller üzerine gerçekleşiyor. Elim alıştı şu an şirketteki herkes bu mimari üzerine ilerliyor. Şu an CI/CD süreçleri için boiler plate ve popüler firebase kütüphanelerini de tek bir json dosyasından kontrolünü gerçekleştirecek proje geliştiriyorum. Bitince sizinle de paylaşırım.
Repository
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; abstract class BlocRepository<ResultObject, RequestObject> { RequestObject _requestObject; bool _isBlocHandling = false; ResultObject _store; final _fetcher = PublishSubject(); Uuid _uuidGenerator = new Uuid(); String _lastRequestUniqueId = ""; Function _listener; PublishSubject get fetcher => this._fetcher; Observable get stream => _fetcher.stream; ResultObject get store => this._store; // String get lastRequestUniqueId => this._lastRequestUniqueId; bool get isBlocHandling => this._isBlocHandling; RequestObject get requestObject => this._requestObject; void setStore(store) => this._store = store; void clearStore() { this._store = null; this.fetcher.sink.add(null); } Future prevProcess() async { this._lastRequestUniqueId = this._uuidGenerator.v4(); await process(this._lastRequestUniqueId); this._isBlocHandling = false; } Future process(String lastRequestUniqueId); Future call({RequestObject requestObject, bool sinkNullObject}) { this._isBlocHandling = true; this._requestObject = requestObject ?? null; if (sinkNullObject != null && sinkNullObject) this.fetcher.sink.add(null); this.prevProcess(); } void fetcherSink(ResultObject resultObject, {bool forceSink, @required lastRequestUniqueId}) { if ((forceSink != null && forceSink) || lastRequestUniqueId == this._lastRequestUniqueId) { this._store = resultObject; this.fetcher.sink.add(resultObject); if (this._listener != null) this._listener(resultObject); } else { // print("this request is old request" + ResultObject.toString()); } } void setListener(Function listener) => this._listener = listener; dispose() { this.fetcher.close(); } }
Örnek Kullanım
class TestRequest { final int count; TestRequest({@required this.count}); } class TestResponse { List list; TestResponse({@required this.list}); } // Result store model // Request username , password class TestBloc extends BlocRepository<TestResponse, TestRequest> { @override Future process(String lastRequestUniqueId) async { TestRequest request = this.requestObject; this.fetcherSink( this.store, lastRequestUniqueId: null, forceSink: true, ); print("istek atıldı ${request.count}"); await Future.delayed(Duration(seconds: request.count)); print("Request Obj ${request.count}"); List responseList = []; for (int i = 0; i < request.count; i++) { responseList.add(i * i); } this.fetcherSink( TestResponse(list: responseList), lastRequestUniqueId: null, forceSink: true, ); } } TestBloc testBloc = TestBloc(); class TestScreen extends StatefulWidget { @override _TestScreenState createState() => _TestScreenState(); } class _TestScreenState extends State { @override void initState() { testBloc.setListener((TestResponse response) { if (response == null) return; print("Listener -> ${response.list}"); }); super.initState(); } void _onPress() async { testBloc.call(requestObject: TestRequest(count: 2)); testBloc.call(requestObject: TestRequest(count: 3)); testBloc.call(requestObject: TestRequest(count: 4)); testBloc.call(requestObject: TestRequest(count: 1)); // testBloc.store; } @override Widget build(BuildContext context) { return Scaffold( body: StreamBuilder( stream: testBloc.stream, builder: (_, __) { Widget body; if (testBloc.isBlocHandling) body = SpinKitRing(color: Colors.red); if (testBloc.store == null) body = Text("BOŞ"); body = ListView.builder( itemCount: testBloc.store.list.length, itemBuilder: (_, index) { return Text("i'ni Karesi: ${testBloc.store.list[index]}"); }, ); return Container( alignment: Alignment.center, padding: EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded(child: body, flex: 1), MaterialButton( onPressed: _onPress, minWidth: 300, child: testBloc.isBlocHandling ? SpinKitCircle(color: Colors.white) : Text("Test Button"), color: Colors.red, ), ], ), ); }, ), ); } }
Kütüphaneler
- flutter_spinkit
- uuid
- rxdart
Örnekte Modeli Bloc'u vs hepsini tek dosyada tutabilirsiniz yada dosyalarınızı yapılayabilirsiniz. Bir kıyak daha yapayım. HttpClient sınıfı paylaşayım. Kullanırsınız belki.
HttpClient Response Repository
class HttpResponseRepository { bool hasError; int statusCode; String errorCode; String errorMessage; T response; HttpResponseRepository({ this.hasError = false, this.statusCode = 200, this.errorCode = "", this.errorMessage = "", @required this.response, }); }
HttpClient
class HttpClient { static HttpResponseRepository _errorModel() => HttpResponseRepository( response: null, hasError: true, errorMessage: "Üzgünüz, bir sorun oluştu.", statusCode: -1, errorCode: "", ); static Map<String, String> generateHeaders({@required bool withAuth, int page = 1, int pageSize = 1000}) { Map<String, String> headers = { "Accept-Language": "tr-TR", "User-Zone": "Europe/Istanbul", "User-Platform": "mobile", "X-Page-Size": "${pageSize}", "X-Page": "${page}", }; if (withAuth) { headers = {...headers, "Authorization": "Bearer ${userInformationBloc.store.token}"}; } return headers; } /* Http Error hata kodları 400 */ static Future _get({ Map<String, String> headers, String url, CancelToken cancelToken, }) async { return await Dio().get( url, options: Options(headers: headers), cancelToken: cancelToken, ); } static Future _post({ Map<String, String> headers, String url, dynamic data, CancelToken cancelToken, }) async { return await Dio().post( url, options: Options(headers: headers), cancelToken: cancelToken, data: data, ); } static Future _delete({ Map<String, String> headers, String url, dynamic data, CancelToken cancelToken, }) async { return await Dio().delete( url, options: Options(headers: headers), cancelToken: cancelToken, data: data, ); } static Future _patch({ Map<String, String> headers, String url, dynamic data, CancelToken cancelToken, }) async { return await Dio().patch( url, options: Options(headers: headers), cancelToken: cancelToken, data: data, ); } static _printError(apiUrl, headers, data, DioError dioError) { if (Application.appMode == AppMode.production) return; debugPrint(""" ====================ERROR REQUEST==================== URL: ${apiUrl} HEADERS: ${headers} REQUEST-DATA: ${jsonEncode(data)} RESPONSE-ERROR: ${dioError} RESPONSE-DATA: ${dioError?.response} ======================================================= """, wrapWidth: 1024); } static _printSuccess(apiUrl, headers, data, Response dioResponse) { if (Application.appMode == AppMode.production) return; debugPrint(""" ====================SUCCESS REQUEST==================== URL: ${apiUrl} HEADERS: ${headers} REQUEST-DATA: ${jsonEncode(data)} RESPONSE: ${dioResponse?.data} ======================================================= """, wrapWidth: 1024); } static Future _refreshToken() async { HttpResponseRepository responseRepository = await HttpClient.call( type: HttpCallType.get, apiUrl: HttpClientApiUrl.refreshToken, withAuth: false, data: {"AccessToken": "${userInformationBloc.store.token}", "RefreshToken": userInformationBloc.store.refreshToken}, dynamicResponse: true, ); if (responseRepository.hasError) { AppDialogs.showError("Oturumunuz Sonlandırılmıştır Lütfen Tekrar Giriş Yapınız.", onPress: () { Navigator.of(Application.context).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => LoginScreen()), (route) => false, ); }); return false; } await AppSharedPreferences.setUserInformationToken( DateTime.now().millisecondsSinceEpoch + (responseRepository.response["expiresIn"] * 1000), responseRepository.response["token"], responseRepository.response["refreshToken"], ); await userInformationBloc.refreshUserInformation(); return true; } static Future call({ @required HttpCallType type, T Function(dynamic) fromJSON, String apiUrlString, HttpClientApiUrl apiUrl = null, dynamic data, int page, int pageSize, bool dynamicResponse = false, bool withAuth = true, CancelToken cancelToken, }) async { if (fromJSON == null && dynamicResponse == false) throw ("Please set fromJSON response"); if (type == null) throw ("Please set HttpCallType"); Map<String, String> headers = generateHeaders(withAuth: withAuth, page: page, pageSize: pageSize); Response dioResponse; String url = apiUrlString ?? apiUrl.uri; String queryString = ""; try { if (type == HttpCallType.get) { if (data != null) { queryString = _getQueryString(data); queryString = queryString.substring(1, queryString.length); } url = "${url}?${queryString}"; dioResponse = await _get( url: url, cancelToken: cancelToken, headers: headers, ); } else if (type == HttpCallType.patch) { dioResponse = await _patch( data: data, url: url, cancelToken: cancelToken, headers: headers, ); } else if (type == HttpCallType.post) { dioResponse = await _post( data: data, url: url, cancelToken: cancelToken, headers: headers, ); } else if (type == HttpCallType.delete) { dioResponse = await _delete( data: data, url: url, cancelToken: cancelToken, headers: headers, ); } _printSuccess(url, headers, data, dioResponse); return HttpResponseRepository( response: dynamicResponse ? dioResponse.data : fromJSON(dioResponse.data), ); } on DioError catch (dioError) { _printError(url, headers, data, dioError); if (dioError.response.statusCode == 401) { bool reCall = await _refreshToken(); if (reCall) { return await call( type: type, fromJSON: fromJSON, apiUrlString: apiUrlString, apiUrl: apiUrl, data: data, dynamicResponse: dynamicResponse, withAuth: withAuth, cancelToken: cancelToken, ); } return null; } if (dioError.response.statusCode == 500) { return _errorModel(); } try { if (dioError.response.data["error_message"] == null && dioError.response.data["statusMessage"] == null) return _errorModel(); return HttpResponseRepository( response: null, hasError: true, errorMessage: dioError.response.data["error_message"] ?? dioError.response.data["statusMessage"], statusCode: dioError.response.data["status_code"] ?? dioError.response.data["statusCode"], errorCode: dioError.response.data["error_code"] ?? dioError.response.data["errorCode"], ); } catch (err) { return _errorModel(); } } catch (err) { // print(err); return _errorModel(); } } static String _getQueryString(Map params, {String prefix: '&', bool inRecursion: false}) { String query = ''; params.forEach((key, value) { if (inRecursion) { key = '[$key]'; } if (value is List) { value.forEach((element) { query += '$prefix$key=${Uri.encodeComponent("${element}")}'; }); } else if (value is String || value is int || value is double || value is bool) { query += '$prefix$key=${Uri.encodeComponent(value)}'; } else if (value is List || value is Map) { if (value is List) value = value.asMap(); value.forEach((k, v) { query += _getQueryString({k: v}, prefix: '$prefix$key', inRecursion: true); }); } }); return query; } }
Örnek Kullanım
class AddCardBloc extends BlocRepository<AddCardResponseBM, AddCardRequestBM> { @override Future process(String lastRequestUniqueId) async { HttpResponseRepository<AddCardResponseBM> responseRepository = await HttpClient.call<AddCardResponseBM>( type: HttpCallType.post, apiUrl: HttpClientApiUrl.cards, data: { "cardAlias": this.requestObject.alias, "cardHolderName": this.requestObject.name, "cardNumber": this.requestObject.number, "expireMonth": this.requestObject.date.split("/")[0], "expireYear": this.requestObject.date.split("/")[1], }, withAuth: true, fromJSON: (json) => AddCardResponseBM.fromJSON(json), ); if (responseRepository.hasError) { AppDialogs.showError(responseRepository.errorMessage); return this.fetcherSink(null, lastRequestUniqueId: lastRequestUniqueId); } this.fetcherSink(responseRepository.response, lastRequestUniqueId: null, forceSink: true); getCardsBloc.call(); Navigator.of(Application.context).pop(); } } AddCardBloc addCardBloc = AddCardBloc();
Sağlıcaklar :) Ek sorularınızı burdan sorarsanız sevinirim herkes faydalansın. Yakın zamanda (Takribi 3 ay) bu gibi özel kütüphanelerimi public bir repo da paylaşacağım github dan. Native den Flutter'a data gönderirken Channel kullanmamak, Native bir ekran tasarımını dart ile çağırmak. Örneğin Java ile bir kod buldunuz adam native yazmış ekran/component native açılıyor bunu dart da nasıl göstereceğiniz gibi gibi....
-
hocam ellerine sağlık, sorularım var :)
aynı anda future ve stream olan 2 ayrı state dinleyen bir widget için bu çözümü önerir misin
view ve model dışında bir classtan state'i takip edip istediğimiz widgetları render edebilir miyiz
Bunların testlerini yazdıysan onları da paylaşma imkanın var mı
-
makets bunu yazdı
hocam ellerine sağlık, sorularım var :)
aynı anda future ve stream olan 2 ayrı state dinleyen bir widget için bu çözümü önerir misin
view ve model dışında bir classtan state'i takip edip istediğimiz widgetları render edebilir miyiz
Bunların testlerini yazdıysan onları da paylaşma imkanın var mı
Evet hocam yapabiliyorsun. Repository de görürsün mesela örnekteki gibi testBloc.store dediğinde her yerden erişim sağlıyorsun. StreamBuilder response gerek yok istersen stream builder verdiği scaffold dan alabilirsin.
Bunun dışında aynı anda future ve stream olan 2 ayrı state de dinlersin hocam. testBloc.listener bu işe yarıyor. Mesela bende bir yapı var header de sepet var ve bir çok ekranda da sepet'i etkileyen işlemler var. Sepet ayrı bir widget ekranalrın altındaki işlemler ayrı bir widget ama ikisi arasında da köprü var.
Unit Test yazmadım hocam.
-
beyler flutteri öğrenecek bir acemi için, başlangıç olarak ne önerirsiniz acaba?
-
@MhmdAlmz Hocam ellerine sağlık. Kodlara bir göz attım. Bazı yapıları bilmediğimden tam oturmadı bende. Sanırım biraz Bloc yapısı hakkında bilgi edinmek gerekiyor.
-
MhmdAlmz bunu yazdımakets bunu yazdı
hocam ellerine sağlık, sorularım var :)
aynı anda future ve stream olan 2 ayrı state dinleyen bir widget için bu çözümü önerir misin
view ve model dışında bir classtan state'i takip edip istediğimiz widgetları render edebilir miyiz
Bunların testlerini yazdıysan onları da paylaşma imkanın var mı
Evet hocam yapabiliyorsun. Repository de görürsün mesela örnekteki gibi testBloc.store dediğinde her yerden erişim sağlıyorsun. StreamBuilder response gerek yok istersen stream builder verdiği scaffold dan alabilirsin.
Bunun dışında aynı anda future ve stream olan 2 ayrı state de dinlersin hocam. testBloc.listener bu işe yarıyor. Mesela bende bir yapı var header de sepet var ve bir çok ekranda da sepet'i etkileyen işlemler var. Sepet ayrı bir widget ekranalrın altındaki işlemler ayrı bir widget ama ikisi arasında da köprü var.
Unit Test yazmadım hocam.
eyvallah hocam teşekkür ederim paylaşımın için.
-
https://www.youtube.com/channel/UCdUaAKTLJrPZFStzEJnpQAg/playlists
Bu kanalda Flutter hakkinda farkli konulara deginilmis meragi olanlar bir goz atabilir.