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()),
      ]),
    );
  }
}

image

  • 자동으로 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()),
      ]),
    );
  }

image

  • 일반적으로 class 안에 instance 에 toString() 실행하면 Instance of {클래스명} 이런식으로 정보가 return 됩니다. 이부분은 toString 메소드를 override 하면서 조금 더 중요한 정보들을 제공해주는 형태로 변경이 가능한데, code 가 많아 질수록 작업이 복잡하게 됩니다.

  • 그래서 freezed 를 사용하면 toString() 메소드가 자동으로 override 되어서 debugging 에 상당이 유용합니다

  • toJson 의 특이한 점은 json_serializablefromJsontoJson 2개를 모두 정의를 해줘야 하는데, freezedfromJson 하나만 정의를 해주면 알아서 자동으로 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()),
      ]),
    );
  }

image

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()),
        ]),
      ),
    );
  }
}

image

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()),
        ]),
      ),
    );
  }

image

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()),
          ]),
        ),
      ),
    );
  }
}

image

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)),
]),

image

  • 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/

}

Categories:

Updated:

Leave a comment