Flutter’da Freezed Kullanımı

Mirkan
Flutter İzmir
Published in
4 min readNov 13, 2020

--

Photo credit: Osman Rana

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ı, ç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 lazım olacak 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 bunu sizin için yapar.

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çeneklerimize 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-outputsvoid 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);
print(mirkan == User('Mirkan', 22); // hashCode ve == operatörü override edildiği için sonuç true.
// 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 kodta, 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ı veya ticket) veriyor. Zaten objenin içinden erişebildiğim için ben hep .map kullandım. maybeWhen, 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.

--

--