[Flutter] toString() 노가다 탈출하기

dev asdf·2025년 3월 3일
0

Flutter

목록 보기
1/1

print(객체.toString()); 할 때마다 나오는 지긋지긋한 Instance of 'YourModel' ...

자바처럼 알아서 객체 값들이 출력되면 얼마나 좋을까...하고, JSON 직렬화를 최대한 날로 먹고 싶었다.

냅다 dart에서도 refelction이 가능하게끔 구현해놓은 라이브러리를 찾아서 getter 필드 접근을 통한 JSON 직렬화를 구현 했었는데...

freezed 라는 간단하고 쌈@뽕한 방법이 있었다.....

어쨌든 바보같은 짓을 기록해본다.

1) 👍👍👍 freezed 쓰자...

https://github.com/rrousselGit/freezed/blob/master/resources/translations/ko_KR/README.md

2) 👎 json_serializable, build_runner, reflectable 사용해서 자바의 reflection 흉내내기

pubspec.yaml에 다음 라이브러리를 등록해준다.

dart pub add dev:json_serializable
dart pub add dev:build_runner
flutter pub add reflectable

  • ReflectableClass
import 'package:reflectable/reflectable.dart';


class ReflectableClass extends Reflectable {
  const ReflectableClass()
      : super(
    invokingCapability,// 객체의 메서드를 동적으로 호출 
    declarationsCapability, // 클래스에 선언된 필드와 메서드 조회
    typeCapability, // 객체의 타입 정보 조회
    typeRelationsCapability, // 클래스 계층 구조 조회
    reflectedTypeCapability, // 객체의 타입을 동적으로 탐지
    instanceInvokeCapability, // 인스턴스를 동적으로 호출
    superclassQuantifyCapability, // 상위 클래스(부모 클래스) 접근
  );
}

const reflectable = ReflectableClass();

const reflectable = ReflectableClass();를 통해 @reflactable 어노테이션을 사용할 수 있게 되었다. lombok처럼 모델 클래스에 사용한다.

  • StringUtil
import 'package:reflaction/reflection.dart';
import 'package:reflectable/mirrors.dart';

class StringUtil{
  static const Set<Type> primitiveTypes = {int, double, String, bool, Enum};

  static bool isPrimitive(dynamic value) {
    return value == null || primitiveTypes.contains(value.runtimeType);
  }

  static String putIndent(int indentSize) {
    StringBuffer sb = StringBuffer();
    for (int i = 0; i < indentSize; i++) {
      sb.write("\t");
    }
    return sb.toString();
  }

  static List<MethodMirror> getAllGetters(ClassMirror classMirror) {
    List<MethodMirror> getters = [];

    while (classMirror != null) {
      getters.addAll(classMirror.declarations.values
          .whereType<MethodMirror>()
          .where((method) => method.isGetter 
          && method.owner.simpleName.toString() != 'Object'));
      if (classMirror.superclass == null) break;
      classMirror = classMirror.superclass!;
    }
    return getters;
  }

  static String toJsonString(Object instance, {int indent=0}){
    StringBuffer sb = StringBuffer();
    InstanceMirror mirror = reflectable.reflect(instance);
    ClassMirror classMirror = mirror.type;

    sb.write(putIndent(0));
    sb.write("{");
    sb.write("\n");

    List<MethodMirror> getters = getAllGetters(classMirror);

    for(int i = 0; i < getters.length; i++){
      MethodMirror field = getters.elementAt(i);
      String fieldName = field.simpleName;
      sb.write(putIndent(indent + 1));
      sb.write("$fieldName: ");
      Object? fieldValue =  mirror.invokeGetter(fieldName);
      if(isPrimitive(fieldValue)){
        sb.write("$fieldValue");
      }
      else if(fieldValue is Map){
        sb.write("{\n");
        for (int j =0; j < fieldValue.entries.length; j++) {
          var item = fieldValue.entries.elementAt(j);
          sb.write(putIndent(indent + 2));
          sb.write("${item.key}:${item.value}");
          if (j != fieldValue.entries.length - 1) {
            sb.write(",");
          }
          sb.write("\n");
        }
        sb.write(putIndent(indent + 1));
        sb.write("}");
      }
      else if(fieldValue is List){
        sb.write("[");
        for(int j = 0; j < fieldValue.length; j++){
          var item = fieldValue.elementAt(j);
          if(isPrimitive(item)){
            sb.write("${fieldValue.elementAt(j)}");
          }
          else{
            sb.write(toJsonString(item,indent: indent));
          }
          if(j != fieldValue.length-1){
            sb.write(", ");
          }
        }
        sb.write("]");
      }
      else{
        sb.write(toJsonString(fieldValue!,indent: indent + 1));
      }
      if(i != getters.length -1){
        sb.write(",");
      }
      sb.write("\n");
    }
    sb.write(putIndent(indent));
    sb.write("}");
    return sb.toString();
  }

}

static으로 구현한 toJsonString 메서드를 각 Model의 toString() 내부에서 호출한다.

  • Model
@reflectable
abstract class Mob{
  late String _name;
  late int _hp;
  late int _atk;
  late int _level;

  Mob({required String name,required int hp, required int atk, int level = 1}):
    _name = name,
    _hp = hp,
    _atk = atk,
    _level = level;

  int get atk => _atk;
  set atk(int value) => _atk = value;
  int get hp => _hp;
  set hp(int value) => _hp = value;
  int get level => _level;
  set level(int level) => _level = level;
  String get name => _name;

  void attack(Mob mob);
  bool dead();

  @override
  String toString(){
    return StringUtil.toJsonString(this);
  }
}

@reflectable
class Enemy extends Mob{

  Enemy({required super.name, required super.hp, required super.atk, required super.level});

  @override
  void attack(Mob mob) {
    print('$name: attack to ${mob.name}');
    mob.hp -= (atk * level * 0.01).round();
  }
  
  @override
  bool dead() {
    if(hp <= 0){
      return true;
    }
    return false;
  }
}

@reflectable
class Player extends Mob{
  int _exp = 0;
  int get exp => _exp;

  Player({required super.name, required super.hp, required super.atk, super.level});

  void killed(Mob mob){
    _exp += (10 * mob.level * 0.1).round();
  }

  void levelUp(){
    level ++;
  }

  @override
  void attack(Mob mob) {
    print('attack to ${mob.name}');
    mob.hp -= (atk * level * 0.05).round();
  }

  @override
  bool dead() {
    if(hp <= 0){
      print('$name was dead');
      return true;
    }
    return false;
  }
}

모델 객체별로 @reflectable를 적용해 주었다면 터미널에서 다음 명령어를 수행한다.

dart run build_runner build ./

명령어가 성공적으로 수행되었다면, main.reflectable.dart 파일이 생성 된 것을 확인할 수 있을 것이다.

만약 명령 실행 이후 새로운 모델 클래스를 생성하고 @reflectable를 적용했다면, 해당 명령어를 다시 수행해야 한다.

  • Main
import 'main.reflectable.dart';

void main() {
  initializeReflectable();
  Player player = Player(name: "player",hp: 100, atk: 120, level: 1);
  Enemy enemy1 = Enemy(name: "bunny1", hp: 100, atk: 50, level: 1);
  Enemy enemy2 = Enemy(name: "bunny2",hp: 100, atk: 50, level: 1);
  print(player.toString());
  print(enemy1.toString());
  print(enemy2.toString());
  runApp(const MyApp());
}

Flutter 앱 예제도 간단하게 만들어 보았다...

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

import 'main.reflectable.dart';
import 'model/enemy.dart';
import 'model/player.dart';

void main() {
  initializeReflectable();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Player player;
  late Enemy enemy1;
  late Enemy enemy2;
  bool _playerTurn = true;
  Timer? _enemyTimer;

  @override
  void initState() {
    super.initState();
    player = Player(name: "player",hp: 100, atk: 120, level: 1);
    enemy1 = Enemy(name: "bunny1", hp: 100, atk: 50, level: 1);
    enemy2 = Enemy(name: "bunny2",hp: 100, atk: 50, level: 1);
    print(player.toString());
    print(enemy1.toString());
    print(enemy2.toString());
    _enemyTimer = Timer.periodic(Duration(seconds: 2), (timer){
      if(_playerTurn == false){
        if(enemy1.dead() == false){
          enemy1.attack(player);
        }
        if(enemy2.dead() == false){
          enemy2.attack(player);
        }
        _playerTurn = true;
        setState(() {});
      }
      if(enemy1.dead() && enemy2.dead()){
        print('player win');
        player.killed(enemy1);
        player.killed(enemy2);
        player.levelUp();
        print('player level up');
        print(player.toString());
        _playerTurn = false;
        setState(() {});
        _enemyTimer!.cancel();
      }
      if(player.dead()){
        print('player lose');
        _playerTurn = false;
        setState(() {});
        _enemyTimer!.cancel();
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _enemyTimer!.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
               Text('LV${player.level}. ${player.name}\nHP. ${player.hp}',),
               SizedBox(height: 10),
               Icon(Icons.directions_walk_sharp, size: 72,),
               SizedBox(height: 10),
               ElevatedButton(onPressed: _playerTurn ? ()
               {
                 int rand = Random().nextInt(2);
                 if(rand == 0){
                   if(enemy1.dead()){
                     player.attack(enemy2);
                   }
                   else{
                     player.attack(enemy1);
                   }
                 }
                 if(rand == 1){
                   if(enemy2.dead()){
                     player.attack(enemy1);
                   }
                   else{
                     player.attack(enemy2);
                   }
                 }
                 _playerTurn = false;
                 setState(() {});
               } : null, child: Text("공격")),
            ],
          ),
          SizedBox(width: 32,),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              if(!enemy1.dead())...[
                Text('LV${enemy1.level}. ${enemy1.name}\nHP. ${enemy1.hp}'),
                SizedBox(height: 10),
                Icon(Icons.cruelty_free_outlined, size: 48,),
              ]
            ],
          ),
          SizedBox(width: 32,),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              if(!enemy2.dead())...[
                Text('LV${enemy2.level}. ${enemy2.name}\nHP. ${enemy2.hp}'),
                SizedBox(height: 10),
                Icon(Icons.cruelty_free_outlined, size: 48,),
              ]
            ],
          )
        ],
      ),
    );
  }
}

결과


무야호~ toString()이 잘 적용되어 나온다.

https://github.com/DEV-asdf-516/flutter-reflection-exam

0개의 댓글

관련 채용 정보