[Flutter] - Freezed 스터디
이번 신규 프로젝트에 사용하기 위해 Freezed를 정리해보도록 하자.
Freezed는 코드 생성 라이브러리로 model 클래스를 기본 틀만 작성하면 자동으로 생성해준다.
지원해주는 기능을 확인해보자.
지원해주는 기능
- fromJson , toJson
- 변수 생성 (생성자만 만들어주면 변수 만들어줌)
- == equals
- toString
- Assert (생성자에서)
- 커스텀 함수 추가
- 복사
- 깊은 복사 (내부 객체들까지 깊은 복사 지원)
- 불변 객체 (내부 List, Map과 같은 Collection 객체도 불변으로 만들어줌, 해제 가능)
- 기본값 설정
- 데코레이터 및 코멘트 작성
- Union 타입 , Sealed 클래스
기본 사용법을 알아보자.
기본 사용법
import 'package:freezed_annotation/freezed_annotation.dart';
part 'news_model.freezed.dart';
part 'news_model.g.dart';
@freezed
class NewsModel with _$NewsModel {
const factory NewsModel({
required int id,
required String title,
required String type,
required String url,
}) = _NewsModel;
factory NewsModel.fromJson(Map<String, dynamic> json) =>
_$NewsModelFromJson(json);
}
위와 같이 작성하고 dart run build_runner build
를 실행해주면 news_model.g.dart
와
news_model.freezed.dart
가 생성된다.
1. fromJson, toJson
여기서 조금 독특했던건 fromJson만 설정해주고 생성하면 toJson까지 만들어졌다.
@freezed
class NewsModel with _$NewsModel {
const factory NewsModel({
required int id,
required String title,
required String type,
required String url,
}) = _NewsModel;
factory NewsModel.fromJson(Map<String, dynamic> json) =>
_$NewsModelFromJson(json);
}
NewsModel model = NewsModel(id: 1, title: 'test', type: 'test', url: 'test');
model.toJson(); // 사용가능
2. == equals
NewsModel model1 = NewsModel(id: 1, title: 'test', type: 'test', url: 'test');
NewsModel model2 = NewsModel(id: 1, title: 'test', type: 'test', url: 'test');
print(model1 == model2); //true
//news_model.freezed.dart
...
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _NewsModel &&
const DeepCollectionEquality().equals(other.id, id) &&
const DeepCollectionEquality().equals(other.title, title) &&
const DeepCollectionEquality().equals(other.type, type) &&
const DeepCollectionEquality().equals(other.url, url));
}
...
위와 같이 내부 변수 값 비교를 통해 equals 연산을 해준다.
잘 보면 DeepCollectionEquality객체를 통해 내부 객체 혹은 Collection들도 Deep Equals 비교를 진행한다.
3. toString()
NewsModel model = NewsModel(id: 1, title: 'test', type: 'test', url: 'test');
print(model); // NewsModel(id: 1, name: test, type: test, url: test)
4. Assert
@freezed
class NewsModel with _$NewsModel {
@Assert('title.length < 5', '제목은 5자 이하만 입력 가능합니다.')
const factory NewsModel({
required int id,
required String title,
required String type,
required String url,
}) = _NewsModel;
factory NewsModel.fromJson(Map<String, dynamic> json) =>
_$NewsModelFromJson(json);
}
NewsModel model = NewsModel(id: 1, title: 'testttt', type: 'test', url: 'test');
// 에러 -> 제목은 5자 이하만 입력 가능합니다.
생성자위에 Annotation으로 Assert를 설정가능하다.
String으로 해야한다.
5. 커스텀 함수 추가
freezed로 자동으로 생성되는 class지만 내부에 내가 원하는 method를 추가 할 수 있다.
그럴려면 internal constructor를 필수
로 추가해야한다.
@freezed
class NewsModel with _$NewsModel {
const factory NewsModel({
required int id,
required String title,
required String type,
required String url,
}) = _NewsModel;
factory NewsModel.fromJson(Map<String, dynamic> json) =>
_$NewsModelFromJson(json);
const NewsModel._(); //무조건 추가 해줘야 한다.
//커스텀 method
String getId() => id.toString();
}
6. 복사
일반적으로 불변객체를 복사하듯이 copyWith를 사용해 복사하면 된다.
NewsModel model1 = NewsModel(id: 1, title: 'test', type: 'test', url: 'test');
NewsModel model2 = model1.copyWith(id: 2);
7. 깊은 복사
객체 안에 객체들이 있다면 일반적인 복사로는 깊은복사를 지원하지 않는다
그래서 일반적으로 아래와 같이 작성한다.
Company company;
Company newCompany = company.copyWith(
director: company.director.copyWith(
assistant: company.director.assistant.copyWith(
name: 'John Smith',
),
),
);
하지만 freezed는 깊은복사 또한 지원한다.
Company company;
Company newCompany = company.copyWith.director.assistant(name: 'John Smith');
훨씬 간단하고 직관적이다.
8. 불변 객체
freezed는 기본적으로 immutable이다.
여기서 중요한 점은 내부 객체와 Collection또한 immutable이다.
_newsModel = const NewsModel(
id: 3,
title: 'test',
type: 'test2',
url: 'url',
person: PersonModel(id: 3, name: 'person'),
subtitles: ['subtitle1', 'subtitle2'],
ids: [3, 5, 6]);
_newsModel.person = PersonModel(id: 3, name: 'news'); //컴파일 에러
_newsModel.subtitles.add('test3'); //throw Unsupported operation
_newsModel.ids.add(3); //throw Unsupported operation
여기서 주의점은 객체는 컴파일에서 에러가 뜨는데 Collection들은 러닝타임에서 throw가 일어난다는 점이다.
아래와 같이 Annoation에 옵션을 걸어주면 Collection 객체의 불변성을 끌 수 있다.
@Freezed(makeCollectionsUnmodifiable: false)
class Example with _$Example {
factory Example(List<int> list) = _Example;
}
void main() {
var example = Example([]);
example.list.add(42); // OK
}
9. 기본값 설정
만약 변수의 기본값을 설정하고 싶다면 아래처럼 Annotation을 활용하면 된다.
class Example with _$Example {
const factory Example([@Default(42) int value]) = _Example;
}
10. 데코레이터 및 코멘트 작성
vscode에 해당 변수에 마우스를 올리면 ide에서 띄워주는 커맨트 작성을 매우 쉽게 할 수 있다.
@freezed
class Person with _$Person {
const factory Person({
/// The name of the user.
///
/// Must not be null
String? name,
int? age,
Gender? gender,
}) = _Person;
}
위와 같이 해당 변수 위에 /// 을 통해 주석을 달아주면 된다.
@freezed
class Person with _$Person {
const factory Person({
String? name,
int? age,
@deprecated Gender? gender,
}) = _Person;
}
deprecated도 있다.
11. 유니온 타입과 Selaed 클래스
이게 어떻게 보면 freezed만의 특징인것 같다.
Bloc과 같이 상태를 관리해야할때 아주 유용하다.
@freezed
class Union with _$Union {
const factory Union.data(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = Error;
}
해당 데이터 클래스 내부에 factory method로 Loading, Data, Error 등을 설정해 줄 수 있다.
주의할 점은 이렇게 되면 공통 속성이 아닌것들은 참조가 불가능해진다.
void main() {
Union union = Union.data(42);
print(union.value); // compilation error: property value does not exist
}
이럴때 유용한게 Map과 When이다.
var union = Union(42);
print(
union.when(
(int value) => 'Data $value',
loading: () => 'loading',
error: (String? message) => 'Error: $message',
),
); // Data 42
print(
union.map(
data: (Data value) => 'Data $value',
loading: (Loading value) => 'loading',
error: (Error value) => 'Error: ${value.message}',
),
); // Data 42
혹은 as / is로 처리도 가능하다.
void main() {
Example value;
if (value is Person) {
// By using `is`, this allows the compiler to know that "value" is a Person instance
// and therefore allows us to read all of its properties.
print(value.age);
value = value.copyWith(age: 42);
}
// Alternatively we can use `as` if we are certain of type of an object:
Person person = value as Person;
print(person.age);
}
그래서 공통된 내용을 mixin으로 처리가능하다.
abstract class GeographicArea {}
abstract class House {}
abstract class Shop {}
abstract class AdministrativeArea<T> {}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@With<AdministrativeArea<House>>()
const factory Example.street(String name) = Street;
@With<House>()
@Implements<Shop>()
@Implements<GeographicArea>()
const factory Example.city(String name, int population) = City;
}
위와 같이 공통통된 내용을 추상클래스로 만들어 @With @Implements Annotation으로 mixin이 가능하다.
With와 Implement의 사용차이를 잘 모르겠다…
결론
확실히 freezed를 공부해보니 왜 사용하는지 알것같았다.
솔직히 toJson, fromJson은 json_serializer로도 충분하고 vscode extension으로도 충분하다.
아래 내용이 잘 활용할것 같았다.
- 불변객체 (내부까지 불변)
- 깊은 복사
- 유니온 타입
특히 유니온타입은 bloc등 내부 아키텍쳐와 잘 설계하면 매번 쉽게 잘 쓸 수 있을것 같다.
이번 프로젝트에서 사용해보면서 나중에 피드백을 달아보도록 하겠다.