Flutter'da Freezed Kullanımı



Neyi çözmek istiyoruz?


Bir sınıfın görevinin sadece veri taşımak olduğu durumlarda, mesela BLoC'ta kullandığınız bir event sınıfı veya kullanıcı giriş yaptığında verilerini tutacağımız user sınıfı gibi, çok fazla tekrar eden sıkıcı kodlar yazmak durumunda kalıyoruz, Constructor yazıyoruz. Bu sınıfın iki objesi eşit mi diye bakmak için equals() yazıyoruz. 1-2 field değiştirip yeni bir obje yaratmak istediğimizde ihtiyaç duyulacak olan copyWith() metodunu yazıyoruz, güzel güzel loglamak istiyorsak toString() yazıyoruz.


Bunları yazdırmak çok büyük bir sıkıntı değil bana göre, bir VSCode eklentisi de bunları sizin için yapabilir. Fakat tek görevi x, y ve z değerlerini taşımak olan point diye bir sınıf düşünelim. İçinde tonla metod var ve elinizde de 200-300 satır bir point.dart dosyanız var.

  • Bunun okunabilirliği ne olurdu?

  • Bakar bakmaz "Tamam bu x, y, z değerlerini tutan bir sınıfmış" diyebilsek ve üstte saydığım güzel özellikleri alabilseydik nasıl olurdu?

Bence müthiş olurdu!


Başka dillerden örnekler: case classes in Scala, data classes in Kotlin, and record classes in C#


Seçeneklere bakalım


Bunun için built_value kullanabiliriz, fakat kanımca freezed çok daha iyi bir syntax sunuyor.


Data Class


Data Class, field'ları immutable olan, değer eşitliği sağlayan ve copy metodları barındıran basit sınıflardır. Kotlin'den örnek:

data class User(val name: String, val age: Int)

Dart ile ise bu kadar kod yazmamız gerekiyor 😅 Bu basit sınıf için bile bu kadar kod varken, iç içe bir yapınız varsa ne kadar kod yazmanız gerektiğini az çok tahmin edersiniz.

@immutable
class User {
   final String name;
   final int age;    
   
   User(this.name, this.age);
   
   User copyWith({
        String name,
        int age,
   }) {
     return User(
        name ?? this.name,
        age ?? this.age,
     );
   }
            
   @override
   bool operator ==(Object o) {
     if (identical(this, o)) return true;
     return o is User && o.name == name && o.age == age;
   }
        
   @override
   int get hashCode => name.hashCode ^ age.hashCode;
 }

Freezed kullanırsak ise bu şekilde:

import 'package:meta/meta.dart';
part 'freezed_classes.freezed.dart';

@freezed
abstract class User with _$User {
   const factory User(String name, int age) = _User;
}

Daha sonrasında terminal'de alttaki kodu çalıştırarak, dosyaismi.freezed.dart isimli dosyanın yaratılmasını sağlıyoruz. Bu da ihtiyacımız olan metod ve özellikleri bizim için barındırıyor.


flutter pub run build_runner watch --delete-conflicting-outputs

void main() {
  final user = User('Mirkan', 22);
  // user.name = 'Can'; // Immutable olduğu için böyle değiştiremeyiz
  // ve hata alırız   
  final anotherUser = user.copyWith(name: 'Can');
  // copyWith ile kopyabiliriz, yeni bir objemiz olur
  
  final mirkan = User('Mirkan', 22);
  // hashCode ve == operatörü override edildiği için sonuç true.
  print(mirkan == User('Mirkan', 22); 
  // toString metodu override edildiği için güzelce loglayabiliriz.   
  print(user); // User(name: Mirkan, age: 22) }


Unions/Sealed Sınıflar


Union'ları limitli olasılıkları göstermek için kullanabiliriz. Success - Failure, User - Pro User gibi. Aslında kulağa enum gibi geliyor, benziyor da, fakat enum'ların yetmediği durumlar oluyor.


enum State { Success, Error }

Mesela üstteki kodda, Error'un içinde hata mesajını tutabilsek güzel olurdu. Fakat enum ile bunu yapamıyoruz. Abstract class kullanabiliriz, fakat bu da enum'ın getirdiği limitli olasılık özelliğini kaybetmemize sebep oluyor.


Union kullandığımızda hep enum gibi kısıtlı/limitli durumları güzelce gösterebiliyoruz, hem de normal bir class gibi veri taşıyabiliyoruz. İki tarafın da iyi yanlarını alıyoruz özetle.


sealed class Result<out T : Any> {
     data class Success<out T : Any>(val data: T) : Result<T>()          
     data class Error(val exception: Exception) : Result<Nothing>()
}  

fun main() {
  ...   
  // exhaustive "switch", bize burada kapsamadığımız
  // bir branch olduğunda hata veriyor
  when(result) {
    is Result.Success -> { }
    is Result.Error -> { }
  }
 }

Freezed ile Dart'ta kullanımı:


@freezed
abstract class Result with _$Result {
   // Eğer nested yapmak istiyorsak, yani Success değil de
   // Result.success olarak kullanmak istiyorsak,
   // sağ tarafı private yapmamız yeterli.
   // const factory Result.success(int value) = _Success; şeklinde   
   const factory Result.success(int value) = Success;
   const factory Result.error(int value) = Error;
 }

Sonrasında when, map, maybeWhen, maybeMap gibi metodlar ile bu durumları tek tek kapsayabiliyoruz.


Union'ları BLoC pattern ile kullanmak inanılmaz keyifli. Kendi yazdığım bir projede, kullanıcının kendi promo kodunu paylaşabildiği bir sayfa var. Bunun için Ticket bloc isimli bir bloc yazdım.


Event'leri şu şekilde:


part of 'ticket_bloc.dart';

@freezed
abstract class TicketEvent with _$TicketEvent {
  const factory TicketEvent.fetch() = _Fetch;
  const factory TicketEvent.share() = _Share;
}

if-else ile hangi event geldiğine bakmadan ve en güzeli de hiç bir event'i unutmadan, switch tarzı bir yapı ile böyle kullanabiliyorum.


class TicketBloc extends Bloc<TicketEvent, TicketState> {
  final _promotionService = locator<PromotionService>();
  
  @override
  TicketState get initialState => TicketState.loading();
  
  @override
  Stream<TicketState> mapEventToState(TicketEvent event) async* {
    yield* event.map(
      fetch: _handleFetch,
      share: _handleShare,
     );
   }

Kullandığım state'ler ise şu şekilde;


part of 'ticket_bloc.dart';

@freezed
abstract class TicketState with _$TicketState {
   const factory TicketState.error() = _Error;
   const factory TicketState.loading() = _Loading;
   const factory TicketState.ready({@required Ticket ticket}) = _Ready; }

UI'da ise her bir state'in karşısına hangi widget geleceğini belirliyorum. If(state is Loading) yaklaşımına göre hem daha okunur, hem de daha güvenli. Çünkü herhangi bir state'i yazmadığınızda, required olduğu için uyarı veriyor. linting ayarları ile bu uyarıyı, daha büyük bir hataya dönüştürmek de mümkün


body: BlocBuilder<TicketBloc, TicketState>(
        builder: (context, state) {
          return state.map(
            loading: (state) => LoadingFlare(),
            ready: (state) => TicketQR(ticket: state.ticket),					 
            error: (state) => ErrorWidget(), 					
          );
         }
)

map bize direkt olarak objeyi (örnekte State) verirken, when o objenin field'larını (mesela State'in içindeki hata mesajını veya ticket'ı) veriyor. Zaten objenin içinden erişebildiğim için ben hep .map kullandım. maybeMap'te ise her durumu karşılamamız gerekmiyor, onun yerine orElse'in karşısına kapsamadığımız durumlarda ne olacağını yazıyoruz.


#flutter #Freezed #Unionsclasses #Sealedclasses


0 yorum

Komünite

Platform

Mobiler.dev Anasayfa
  • Twitter
  • Instagram
  • development_düzenlendi_düzenlendi
  • Youtube
  • slack-icon-black_edited_edited_edited
  • Gri LinkedIn Simge
imageedit_2_9667998092.png
JetBrains Hakkında Detaylı Bilgi Alın

© 2020 by mobiler.dev

Kurumsal Yazar Hesapları

adesso.png
mobilerdevLogo.jpg
Yazarlık Başvurusu Hakkında Bilgi Alın, Başvuru Yapın.
Topluluk Yazarlarını Tanıyın