Flutter Freezed 플러그인
Updated:
1.Code Generation 필요성
-
code 를 작성하다보면 제일 피해야 될것은 코드 duplicate 과 boilerplate code 작성입니다. 왜냐하면 계속 반복되는 code 의 재사용은 code 의 양을 길게하고 최적화에 좋지 않기 때문에, 나중에 refactoring 하게 될때 boilerplate code 를 수정해야 되는 경우가 많이 생김니다.
-
그래서
code generation
을 많이 사용하게 되는데 하게 됩니다 -
Code Generation 은 개발자가 program code 를 직접 작성하지 않고 자동으로 code를 생성해내는 것을 말합니다. 자동으로 code 생성하는 compiler 를 통해 보다 직관적이고 보기 쉬운 script 가 사용되는 것입니다
-
그래서 Flutter 는 code generation 기능이 많이 활성화 되어 있고 라이브러리도 이를 사용하기 위한 라이브러리도 다양합니다(예, json_serializable, retrofit, chopper 등)
-
Freezed 라이브러리도 또한 데이터 클래스에 편의 기능을 제공해주는 code generation 라이브러리 입니다
-
. Freezed 라이브러리는 데이터 클래스에서 흔히 필요한 기능들을 한번에 제공해주는 라이브러리입니다. 이런 비슷한 계얼에
jsonserializable
이 있는데jsonserializable
과 혼합해서 freezed 는 copy 기능, toString override, union 클래스 등 필요한 편의성 기능들을 추가로 사용할 수 있게 해줍니다.
2.Freezed 생성
Freezed 를 사용하기 위해서 아래 dependencies 를 추가 합니다
dependencies:
freezed_annotation:
json_annotation: ^4.3.0
dev_dependencies:
build_runner:
freezed:
json_serializable:
- toJson , fromJson 기능을 사용하기 위해서 json_serializable 도 추가해 줍니다
import 'package:freezed_annotation/freezed_annotation.dart';
part 'person.freezed.dart';
part 'person.g.dart';
@freezed
class Person with _$Person {
factory Person({
required int id,
required String name,
required int age,
}) = _Person;
// json_serializable
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}
/*
위의 code @ freezed 부분을 풀이하면 아래와 같습니다
class Person {
final int id;
final String name;
final int age;
Person({
required int id,
required String name,
required int age,
});
}
*/
- terminal 에서
flutter pub run build_runner build
을 싱행해주면 code generation 을 실행하게 되면person.freezed.dart
라는 파일이 생성 됩니다.
3.Freezed 사용
Constructor 및 property 자동 생성
lass MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
renderText(String title, String text) {
return Column(
children: [
Row(
children: [
Text(
title,
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
],
),
Row(
children: [
Text(
text,
style: TextStyle(fontSize: 20.0),
),
],
),
Divider(),
],
);
}
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'Jacob KO', age: 30);
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
]),
);
}
}
- 자동으로 class 의 property 들을 제작해줘서 작성해야 할 코드가 자동으로 생성해 줍니다
toString 및 toJson
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'Jacob KO', age: 30);
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
renderText('toString()', person1.toString()),
renderText('toJson()', person1.toJson().toString()),
]),
);
}
-
일반적으로 class 안에 instance 에
toString()
실행하면Instance of {클래스명}
이런식으로 정보가 return 됩니다. 이부분은 toString 메소드를 override 하면서 조금 더 중요한 정보들을 제공해주는 형태로 변경이 가능한데, code 가 많아 질수록 작업이 복잡하게 됩니다. -
그래서 freezed 를 사용하면 toString() 메소드가 자동으로 override 되어서 debugging 에 상당이 유용합니다
-
toJson 의 특이한 점은
json_serializable
은fromJson
과toJson
2개를 모두 정의를 해줘야 하는데,freezed
는fromJson
하나만 정의를 해주면 알아서 자동으로toJson
은 생성해 줍니다
== 및 hashCode override
@override
Widget build(BuildContext context) {
// 변수의 포인트가 다르기 때문에 두 변수의 == 는 false 여야 함
final person1 = Person(id: 1, name: 'Jacob KO', age: 30);
final person2 = Person(id: 1, name: 'Jacob KO', age: 30);
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
renderText('toString()', person1.toString()),
renderText('toJson()', person1.toJson().toString()),
// 두개의 변수의 value 값이 같기 때문에 freezed 에서 자동으로 override 되서 true 가 됨
renderText('==', (person1 == person2).toString()),
]),
);
}
freezed 는 == 함수 및 hasCode 함수 또한 자동으로 override 합니다. 글래스 인스턴스를 특별한 override 없이 서로 비교하게되면 메모리 위치를 서로 비교하게 됩니다. 결과적으로 같은 클래스의 인스턴스고 모든 필드가 다 같더라도 비교는 false 가 나오게되죠. 하지만 freezed 를 사용하면 자동으로 클래스의 모든 property 의 조합으로 == 및 hashCode 함수가 override 되어서 상식적인 비교를 진행할 수 있습니다.
Assert 하기
- class 컨스트럭터를 제작할때 assert 를 통해서 변수 값을 제한하고싶을때가 있습니다. freezed 패키지도 assert 기능을 사용할 수 있도록 annotation 을 따로 제공해주고 있습니다.
// in person.dart
@freezed
class Person with _$Person {
// 자동으로 assert 를 생성해줍니다 (조건) : 단, string 으로 작성으로 해야되서 스펠링이나 오류 체크 잘 해야됨
@Assert('name.length < 5', 'name 의 길이는 5보가 작아야 합니다')
factory Person({
required int id,
required String name,
required int age,
}) = _Person;
// json_serializable
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}
// 자동으로 생성된 person.freezed.dart
// assert 부분이 자동으로 생성 되었습니다
@JsonSerializable()
class _$_Person implements _Person {
_$_Person({required this.id, required this.name, required this.age})
: assert(name.length < 5, 'name 의 길이는 5보가 작아야 합니다');
custom method 및 getter 작성하기
- custom method 또는 getter 를 freezed 패키지로 작성할 수 있습니다. 하지만, class 에 한줄의 internal constructor 를 필수적으로 추가 해줘야 합니다.
// in person.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'person.freezed.dart';
part 'person.g.dart';
@freezed
class Person with _$Person {
@Assert('name.length < 5', 'name 의 길이는 5보가 작아야 합니다')
factory Person({
required int id,
required String name,
required int age,
}) = _Person;
// json_serializable
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
// internal constructor
Person._();
// getter 생성
get nameLength => this.name.length;
// method 생성
void hello() {
print('Hello World!');
}
}
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'Jacb', age: 30);
final person2 = Person(id: 1, name: 'Emma', age: 30);
// method 불러오기
person1.hello();
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
renderText('toString()', person1.toString()),
renderText('toJson()', person1.toJson().toString()),
renderText('==', (person1 == person2).toString()),
// getter 사용해서 nameLength 불어오기
renderText('nameLength', person1.nameLength.toString()),
]),
),
);
}
}
Copy
- freezed 는 기본적으로 class 를 immutables 하게 사용하는걸 목적으로 하기 때문에 setter 를 설정하는건 불가능 하지만 일반적으로 copy 메소드를 정의해서 사용하게 되는데 freezed 가 자동으로 생성해 줍니다
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'Jacb', age: 30);
final person2 = Person(id: 1, name: 'Emma', age: 30);
// final 값이기때문에 만약에 person1 에서 age 값만 40으로 바꾸려고 할때, person1.age = 40 이런식으로 변수를 바꿀수 없음
// 방법1로 값을 일일히 불러와서 작성하면 되긴하는데 아래와 같이 코드가 길어지고 복잡해짐
final person3 = Person(id: person1.id, name: person1.name, age: 50);
// 방법2 copyWith method 사용하기 변경하고 싶은 값만 변경해주면, 나머지의 값은 자동으로 person1의 값을 그대로 가져와서 사용함
final person4 = person1.copyWith(age: 40);
person1.hello();
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
renderText('toString()', person1.toString()),
renderText('toJson()', person1.toJson().toString()),
renderText('==', (person1 == person2).toString()),
renderText('nameLength', person1.nameLength.toString()),
// Copy 해서 원하는 값만 변경하기 방법1 출력
renderText('person3.toString()', person3.toString()),
// 방법2 출력
renderText('person4.toString()', person4.toString()),
]),
),
);
}
DeepCopy
- freezed 패키지는 deep copy 기능 또한 간단하게 제공합니다. 예) 클래스를 nesting 하기
// in prson.dart (model)
@freezed
class Person with _$Person {
// 자동으로 assert 를 생성해줍니다 (조건) : 단, string 으로 작성으로 해야되서 스펠링이나 오류 체크 잘 해야됨
@Assert('name.length < 5', 'name 의 길이는 5보가 작아야 합니다')
factory Person({
required int id,
required String name,
required int age,
required Group group,
}) = _Person;
// internal constructor
Person._();
// getter 생성
get nameLength => this.name.length;
// method 생성
void hello() {
print('Hello World!');
}
}
// Class nesting
@freezed
class Group with _$Group {
factory Group({
required int id,
required String name,
required School scholl,
}) = _Group;
}
@freezed
class School with _$School {
factory School({
required int id,
required String name,
}) = _School;
}
// in main.dart
@override
Widget build(BuildContext context) {
final school1 = School(id: 3, name: 'Harvard');
final group1 = Group(id: 2, name: 'Computer Science', scholl: school1);
final person1 = Person(id: 1, name: 'Jacb', age: 30, group: group1);
final person2 = Person(id: 1, name: 'Emma', age: 30, group: group1);
final person3 =
Person(id: person1.id, name: person1.name, age: 50, group: group1);
final person4 = person1.copyWith(age: 40);
// 만약 person1에서 scholl 의 name 을 'Harvard' 에서 'Stanford' 로 바꾸는 법
// deepCopy
final person1New = person1.copyWith.group.scholl(name: 'Stanford');
person1.hello();
return Scaffold(
appBar: AppBar(
title: Text('Freezed Demo'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
renderText('person1.id', person1.id.toString()),
renderText('person1.name', person1.name.toString()),
renderText('person1.age', person1.age.toString()),
renderText('toString()', person1.toString()),
renderText('==', (person1 == person2).toString()),
renderText('nameLength', person1.nameLength.toString()),
renderText('person3.toString()', person3.toString()),
renderText('person4.toString()', person4.toString()),
// DeepCopy 출력
renderText('person1New.toString()', person1New.toString()),
]),
),
),
);
}
}
Union
- freezed 의 union 기능을 사용하면 내부 클래스들을 정의하고, constructor 별로 다른 class instance 들을 돌려주는것도 가능합니다.
// person.dart
@freezed
class Person with _$Person {
@Assert('name.length < 5', 'name 의 길이는 5보가 작아야 합니다')
factory Person({
required int id,
required String name,
required int age,
required Group group,
int? statusCode,
}) = _Person;
Person._();
void hello() {
print('Hello World!');
}
// Person 이 또다른 internal class 를 return 하기
factory Person.loading({int? statusCode}) = _Loading;
factory Person.error(String message, {int? statusCode}) = _Error;
}
//
@override
Widget build(BuildContext context) {
final school1 = School(id: 3, name: 'Harvard');
final group1 = Group(id: 2, name: 'Computer Science', scholl: school1);
final person1 = Person(id: 1, name: 'Jacb', age: 30, group: group1);
final person2 = Person(id: 1, name: 'Emma', age: 30, group: group1);
final person =
Person(id: 1, name: 'Emma', age: 30, group: group1, statusCode: 200);
final personLoding = Person.loading();
final personError = Person.error('accessToken 이 잘못됬습니다.', statusCode: 401);
// id만 가지고 올때 정의가 안되있다고 함 왜냐하면 가져올수 있는값은 공동된 값인 statusCode 밖에 없기 때문임
// renderText('person', person.id.toString()),
// statusCode 는 됨
renderText('person', person.statusCode.toString()),
// 그래서 case 별로 값을 mapping 해주기 위해서 when 이라는 mapping 함수를 만들어줘서 각각의 값을 지정해줌
renderText('person.when', mapWhen(person)),
renderText('person.loading', mapWhen(personLoding)),
renderText('person.error', mapWhen(personError)),
]),
- Union 을 사용할때는 모든 컨스트럭터에서 공통으로 제공하는 변수만 직접 가져올 수 있습니다. 각각 특화된 컨스트럭터에서 제공하는 파라미터는 when, maybeWhen, map, maybeMap 등을 사용해 불러올 수 있습니다.
🔶 🔷 📌 🔑
Reference
Freezed in Flutter - https://dev.to/kodega2016/freezed-in-flutter-b1n
소프트웨어 이야기 - http://jamestic.egloos.com/60818
코드팩토리 - https://blog.codefactory.ai/flutter/freezed/
}
Leave a comment