개요

오늘은 Flutter로 도넛차트를 만들어보도록 하겠습니다.

 

 

디자인

 

 먼저 피그마로 도넛차트의 모양을 만들어보았습니다. 마음에 들어서 이 디자인을 사용하도록 하겠습니다.

 

 

 

 

DonutChart

class DonutChart extends StatefulWidget {
  
  final double radius;
  final double strokeWidth;
  final double total;
  final double value;
  Widget? child;

  DonutChart({
    super.key,
    this.radius = 100,
    this.strokeWidth = 20,
    required this.total,
    required this.value,
    this.child,
  });

  @override
  State<DonutChart> createState() => _DonutChartState();
}

class _DonutChartState extends State<DonutChart> {
  
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
  
}

 

먼저 도넛모양의 차트에 데이터는 위와같이 잡았습니다.

radius : 차트의 크기 default 100

strokeWidth : 차트의 width default 20

total : 전체 합

value : 표시할 값

 

 

다음은 애니메이션을 사용할것이기 때문에

SingleTickerProviderStateMixin

를 사용하고 AnimationController와 Animation<double> 을 만들어줍니다.

 

class _DonutChartState extends State<DonutChart> with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _valueAnimation;
  
  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1)
    );
    
    super.initState();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }

}

 

그리고  valueAnimation을 마저 구현해주고 AnimationController를 forward 해줍니다.

 

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1)
    );

    final curvedAnimation = CurvedAnimation(
        parent: _controller,
        curve: Curves.fastOutSlowIn
    );

    _valueAnimation = Tween<double>(
      begin: 0,
      end: (widget.value / widget.total) * 360
    ).animate(_controller);

    _controller.forward();

    super.initState();
}

 

 

 

CustomPainter

class _DonutChartProgressBar extends CustomPainter {

  final double strokeWidth;
  final double valueProgress;

  _DonutChartProgressBar({super.repaint, required this.strokeWidth, required this.valueProgress});

  
  @override
  void paint(Canvas canvas, Size size) {
    
    Paint defaultPaint = Paint()
      ..color = Color(0xFFE1E1E1)
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
    
    Paint valuePaint = Paint()
        ..color = Color(0xFF7373C9)
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke
        ..strokeCap = StrokeCap.round;
    
    Offset center = Offset(size.width / 2, size.height / 2);
    
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: size.width / 2), 
      math.radians(-90), 
      math.radians(360), 
      false, 
      defaultPaint
    );

    canvas.drawArc(
        Rect.fromCircle(center: center, radius: size.width / 2),
        math.radians(-90),
        math.radians(valueProgress),
        false,
        valuePaint
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

}

 

CustomPainter를 상속받아서 paint 해줍니다.

배경이 되는 defaultPaint 를 만들고, value 값을 표현하는 valuePaint를 만들어줍니다.

그리고 canvas에 그려주면 되는데 math는 

import 'package:vector_math/vector_math.dart' as math;

vector_math 를 임포트해주어야 사용할 수 있습니다.

 

drawArc의 첫번째 인자는 중심이 되는 지점을 표현한거고

두번째는 시작지점입니다. 3시방향이 0도이기때문에 12시 방향부터 시작하고싶어서 -90를 넣어주었습니다.

세번째는 마지막지점입니다. valueProgress 값을 사용해 외부에서 넣어주겠습니다.

네번째는 시작지점과 마지막지점을 중앙점과 이을건지 여부인데 true를 하게되면 Pie Chart가 됩니다. 저는 Donut Chart를 만들것이기 때문에 false를 해주었습니다.

마지막은 Paint를 넣어주면 됩니다.

 

shouldRepaint는 true로 하면됩니다.

 

자 이제 돌려보면

 

 

짜잔 완성했습니다. 어우 이거 만드는것보다 영상촬영하고 짤로 만드는게 더 어렵네요...

 

 

완성

DonutChart(
    radius: 50,
    strokeWidth: 10,
    total: 100,
    value: 85,
    child: Center(
      child: Text('85%',
        style: TextStyle(
          color: Colors.black54,
          fontWeight: FontWeight.w600,
          fontSize: 21
        ),
      ),
    ),
)

이렇게 완성했습니다. CustomPainter 나 Animation을 사용하는게 조금 쉽지 않았는데 만들고보니 뿌듯하네요

Stream API 개요

Java Stream API는 데이터 처리를 위한 강력한 도구이지만, 잘못 사용하면 오히려 성능이 저하될 수 있습니다. 이번 포스트에서는 Stream API의 효율적인 사용법과 병렬 스트림 활용 방법에 대해 알아보겠습니다.

 

 

 

병렬 스트림(Parallel Stream)

 

기본 사용법

// 순차 스트림
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                .mapToInt(n -> n)
                .sum();

// 병렬 스트림
int parallelSum = numbers.parallelStream()
                        .mapToInt(n -> n)
                        .sum();

// 기존 스트림을 병렬로 변환
int anotherSum = numbers.stream()
                       .parallel()
                       .mapToInt(n -> n)
                       .sum();

 

병렬 스트림이 효과적인 경우

// 1. 데이터가 많은 경우
List<Integer> largeList = new ArrayList<>(1000000);
// 리스트 초기화...

long count = largeList.parallelStream()
                     .filter(n -> n % 2 == 0)
                     .count();

// 2. 독립적인 연산이 많은 경우
double average = largeList.parallelStream()
                         .mapToDouble(this::complexCalculation)
                         .average()
                         .orElse(0.0);

private double complexCalculation(int number) {
    // CPU 집약적인 계산
    return Math.sqrt(Math.pow(number, 2));
}

 

 

 

성능 최적화 전략

 

적절한 데이터 구조 선택

// ArrayList - 병렬 처리에 좋음
List<Integer> arrayList = new ArrayList<>();
arrayList.parallelStream()...

// LinkedList - 병렬 처리에 비효율적
List<Integer> linkedList = new LinkedList<>();
linkedList.stream()...  // 순차 처리 권장

// Array - 가장 효율적
int[] array = {1, 2, 3, 4, 5};
Arrays.stream(array).parallel()...

 

 

Unboxing 오버헤드 방지

// 비효율적인 방법 (boxing/unboxing 발생)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                .mapToInt(n -> n)  // unboxing
                .sum();

// 효율적인 방법
int[] primitiveNumbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(primitiveNumbers)
               .sum();

 

 

적절한 중간 연산 사용

// 비효율적인 방법
List<String> result = strings.stream()
                           .filter(s -> s.length() > 3)
                           .sorted()  // 전체 정렬 후 필터링
                           .limit(5)
                           .collect(Collectors.toList());

// 효율적인 방법
List<String> betterResult = strings.stream()
                                 .filter(s -> s.length() > 3)
                                 .limit(5)  // 먼저 개수 제한
                                 .sorted()  // 필요한 요소만 정렬
                                 .collect(Collectors.toList());

 

 

병렬 스트림 주의사항

 

상태 공유 피하기

// 잘못된 예 - 상태 공유로 인한 문제
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = new ArrayList<>();

numbers.parallelStream()
       .map(n -> n * 2)
       .forEach(result::add);  // 동시성 문제 발생

// 올바른 예
List<Integer> safeResult = numbers.parallelStream()
                                .map(n -> n * 2)
                                .collect(Collectors.toList());

 

 

순서 의존성 주의

// 순서에 의존적인 작업 - 병렬 처리 부적합
String result = strings.parallelStream()
                      .reduce("", (a, b) -> a + b);  // 순서 보장 안됨

// 올바른 방법
String betterResult = String.join("", strings);  // 더 효율적

 

 

주의점

  1. 데이터 크기가 작은 경우 순차 처리가 더 효율적
  2. 공유 상태 수정은 피하기
  3. 올바른 데이터 구조 선택
  4. 순서 의존성 고려
  5. 성능 측정 후 판단

 

마치며

Stream API의 효율적인 사용과 병렬 스트림의 적절한 활용은 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 하지만 무조건적인 병렬 처리가 아닌, 상황에 맞는 적절한 선택이 중요합니다.

개요

Flutter에서 위젯의 크기를 제어하는 방법은 다양합니다. 그 중 가장 많이 사용되는 ConstrainedBox와 SizedBox의 차이점과 각각의 사용 사례에 대해 알아보겠습니다.

SizedBox의 이해

 

기본 사용법

// 고정 크기 지정
SizedBox(
  width: 100,
  height: 50,
  child: Container(
    color: Colors.blue,
  ),
)

// 간격 생성
SizedBox(height: 10)  // 세로 간격
SizedBox(width: 10)   // 가로 간격

// 최대 크기로 확장
SizedBox.expand(
  child: Container(
    color: Colors.blue,
  ),
)

 

SizedBox의 특징

  • 정확한 크기를 지정할 때 사용
  • 간단한 간격을 만들 때 유용
  • child가 없을 경우 빈 공간으로 사용
  • 성능상 가장 가벼운 위젯 중 하나

 

 

ConstrainedBox의 이해

 

기본 사용법

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100,
    maxWidth: 200,
    minHeight: 50,
    maxHeight: 100,
  ),
  child: Container(
    color: Colors.blue,
  ),
)

// 유용한 생성자들
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 100, height: 100),
  child: Container(color: Colors.blue),
)

ConstrainedBox(
  constraints: BoxConstraints.loose(Size(200, 100)),
  child: Container(color: Colors.blue),
)

 

ConstrainedBox의 특징

  • 최소/최대 크기 제약 설정 가능
  • 자식 위젯의 크기를 유연하게 제어
  • 복잡한 레이아웃 제약 조건 설정 가능

 

 

실제 사용 예시

 

SizedBox 활용

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('First Item'),
        SizedBox(height: 16),  // 간격 추가
        Container(
          color: Colors.blue,
          child: SizedBox(
            width: 100,
            height: 100,
            child: Center(
              child: Text('Fixed Size'),
            ),
          ),
        ),
        SizedBox(height: 16),  // 간격 추가
        Text('Last Item'),
      ],
    );
  }
}

 

 

ConstrainedBox 활용

class FlexibleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ConstrainedBox(
          constraints: BoxConstraints(
            minHeight: 50,
            maxHeight: 200,
          ),
          child: Container(
            color: Colors.green,
            child: Text('This box can grow between 50 and 200'),
          ),
        ),
        ConstrainedBox(
          constraints: BoxConstraints.tightFor(
            width: double.infinity,
            height: 100,
          ),
          child: Card(
            child: Center(
              child: Text('Full width, fixed height'),
            ),
          ),
        ),
      ],
    );
  }
}

 

주요 차이점과 선택 기준

 

SizedBox 사용 시나리오

정확한 크기가 필요할 때

// 고정 크기 버튼
SizedBox(
  width: 200,
  height: 50,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('Fixed Size Button'),
  ),
)

 

간단한 간격이 필요할 때

Column(
  children: [
    Text('Item 1'),
    SizedBox(height: 8),  // 작은 간격
    Text('Item 2'),
    SizedBox(height: 16), // 중간 간격
    Text('Item 3'),
    SizedBox(height: 24), // 큰 간격
    Text('Item 4'),
  ],
)

 

 

ConstrainedBox 사용 시나리오

유동적인 크기 제약이 필요할 때

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100,
    maxWidth: MediaQuery.of(context).size.width * 0.8,
    minHeight: 50,
  ),
  child: Container(
    padding: EdgeInsets.all(16),
    child: Text('This box adapts to content'),
  ),
)

 

반응형 레이아웃 구현 시

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 600,  // 태블릿/데스크톱에서 최대 너비 제한
  ),
  child: ListView(
    children: [
      // 리스트 아이템들
    ],
  ),
)

 

 

성능 고려사항

 

SizedBox

// 권장: 간단하고 효율적
SizedBox(width: 100, height: 100)

// 비권장: 불필요한 중첩
Container(
  width: 100,
  height: 100,
)

 

ConstrainedBox

// 권장: 필요한 경우만 사용
ConstrainedBox(
  constraints: BoxConstraints(maxWidth: 200),
  child: Text('Limited width text'),
)

// 비권장: 불필요한 제약 사용
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 100, height: 100),
  child: Container(),  // SizedBox를 사용하는 것이 더 효율적
)

 

 

마치며

  • SizedBox는 정확한 크기나 간격이 필요할 때 사용
  • ConstrainedBox는 유연한 크기 제약이 필요할 때 사용
  • 성능을 고려할 때는 가능한 한 간단한 위젯을 선택
  • 레이아웃의 목적과 요구사항에 따라 적절한 위젯 선택이 중요

개요

기본적으로 부모 위젯에서 자식 위젯의 메소드를 호출할 수 있는 방법은 따로 없습니다. 하지만 불가능한것은 아니죠. 오늘은 그것에 대해서 알아보도록 하겠습니다.
 
 
 
 

Controller

부모 - 자식 위젯간에 Controller를 하나 두는 방법입니다. 부모 위젯에서 Controller를 생성하고 자식 위젯에게 전달해 메소드를 주입받는 방식이죠.
 

class TimerController {

  late Function() start;
  late Function() stop;

}

 
먼저 이렇게 Controller를 만듭니다.
 

class MainPage extends StatefulWidget {
  const MainPage({super.key});
  
  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {

  final TimerController _timerController = TimerController();

  @override
  Widget build(BuildContext context) {
    return CustomTimerWidget(
      controller: _timerController,
    );
  }
}

 
부모위젯에서 자식위젯에 TImerController를 주입해줍니다.
 

class CustomTimerWidget extends StatefulWidget {

  TimerController? controller;
  CustomTimerWidget({super.key, this.controller});

  @override
  State<CustomTimerWidget> createState() => _CustomTimerWidgetState();
}

class _CustomTimerWidgetState extends State<CustomTimerWidget> {
  
  void start() {
    print('timer start');
  }
  void stop() {
    print('timer stop');
  }
  _timerInit() {
    widget.controller?.start = start;
    widget.controller?.stop = stop;
  }
  
  @override
  void initState() {
    _timerInit();
    super.initState();
  }
  
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

 
자식 위젯에서 initState 시점에 부모로 부터 받은 controller에 Function을 넣어줍니다.
이제 부모위젯에서 자식 위젯의 메소드를 사용할 수 있게되었습니다.
 
 
이 방법은 flutter내에서 정말 많이 사용하고 있는 패턴같습니다. PageView, ListView 등등 다양한곳에서 Controller를 받는걸 볼 수 있습니다. 이 방법말고 GlobalKey를 이용해 자식위젯의 메소드를 사용할 수 는 있지만 같은 파일(.dart) 내에 존재하지 않는다면 사용 할 수 없다는 큰 단점이 있고, GlobalKey를 많이 사용할 수록 복잡해지는 단점이있어서 위와 같은 방식을 추천드립니다.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:mergeReleaseResources'.
> Multiple task action failures occurred:
   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: AAPT: error: file failed to compile.


* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 9s
Running Gradle task 'assembleRelease'...                           10.2s
Gradle task assembleRelease failed with exit code 1

 

 

앱 아이콘을 변경하고 이런 오류가 발생했습니다.

이미지 파일에도 문제가 없는데 말이죠...

 

앱 아이콘은 반드시 png 파일로 되어있어야 합니다.

 

그런데 이름만 png이지 실제 확장자는 JPEG로 되어있더군요

 

 

jpeg -> png 로 변환하고 다시 build하니 잘 되었습니다.

플러터 앱 아이콘 변경하는 법

 

flutter_launcher_icons 패키지를 통해 쉽게 바꿀 수 있지만 패키지를 사용하기 전에 직접 바꾸는 법을 알아보도록 하겠습니다.

 

flutter_launcher_icons | Dart package

A package which simplifies the task of updating your Flutter app's launcher icon.

pub.dev

 

 

 

 

아이콘 만들기

 

 

App Icon Generator

 

www.appicon.co

 

 

이미지를 업로드하고 원하는 플랫폼을 선택한 후에 Generate를 누르면

 

 

이렇게 파일이 생성됩니다.

 

 

 

 

안드로이드

android/app/src/main/res 에 들어가면

이런 데이터들이 있는데 이곳에다가 아까 다운받은 아이콘안에 android 폴더 내부데이터를 모두 이곳으로 옮겨줍니다.

 

 

 

 

iOS

ios/Runner/Assets.xcassets 폴더에 Assets.appiconset 폴더가 있는데 이것도 마찬가지로 다운받은 폴더에 Assets.appiconset 를 덮어쓰기 해주시면됩니다.

 

iOS

 

끝!

플러서 앱 이름 바꾸는 법

 

 

안드로이드

app/src/main/AndroidManifest.xml 에 들어가서

<application
    android:label="앱 이름"

label에 이름을 변경하면 됩니다.

 

 

iOS

ios/Runner/Info.plist 에 들어가서

<key>CFBundleDisplayName</key>
<string>앱 이름</string>

 

CFBundleDisplayName 을 찾고 아래 string 에 이름을 변경하면 됩니다.

 

iOS

 

StatefulWidget의 생명주기 메서드

 

createState()

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();  // 가장 먼저 호출
}

 

initState()

class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription _subscription;
  
  @override
  void initState() {
    super.initState();  // 반드시 super 호출
    
    // 초기화 작업 수행
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen((data) => print('Stream data: $data'));
  }
}

 

didChangeDependencies()

class _MyWidgetState extends State<MyWidget> {
  ThemeData? _theme;
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    
    // 상위 위젯의 데이터 의존성 변경 시 호출
    _theme = Theme.of(context);  // InheritedWidget 데이터 접근
  }
}

 

 

build()

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('Widget Content'),
    );
  }
}

 

 

didUpdateWidget()

class _MyWidgetState extends State<MyWidget> {
  @override
  void didUpdateWidget(MyWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    // 위젯이 업데이트될 때 호출
    if (widget.someProperty != oldWidget.someProperty) {
      // 속성 변경에 따른 처리
    }
  }
}

 

 

dispose()

class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription _subscription;
  
  @override
  void dispose() {
    // 리소스 해제
    _subscription.cancel();
    super.dispose();  // 반드시 마지막에 super 호출
  }
}

 

 

생명주기 단계별 주의사항

 

initState

  • super.initState() 반드시 먼저 호출
  • context 사용 불가 (didChangeDependencies에서 사용)
  • 비동기 작업 시작 가능
  • 한 번만 호출됨

didChangeDependencies

  • InheritedWidget 데이터 접근
  • 여러 번 호출될 수 있음
  • 상위 위젯의 데이터 변경 시 호출

build

  • 순수 함수여야 함
  • 상태 변경 금지 (setState 호출 금지)
  • 가능한 가벼운 연산만 수행

dispose

  • 리소스 해제 필수
  • super.dispose() 마지막에 호출
  • mounted 체크 후 setState 호출

 

일반적인 실수와 해결방법

 

메모리 누수 방지

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription? _subscription;
  
  @override
  void initState() {
    super.initState();
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen((data) {
          if (!mounted) return;  // mounted 체크
          setState(() {
            // 상태 업데이트
          });
        });
  }
  
  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
}

 

비동기 작업 처리

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    _loadData();
  }
  
  Future<void> _loadData() async {
    try {
      await Future.delayed(Duration(seconds: 2));
      if (!mounted) return;
      setState(() {
        // 상태 업데이트
      });
    } catch (e) {
      print('Error: $e');
    }
  }
}

 

 

Context 사용

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // Error: context 사용 불가
    // Theme.of(context);
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // OK: context 사용 가능
    final theme = Theme.of(context);
  }
}

 

 

마치며

위젯의 생명주기를 잘 이해하고 적절히 활용하면 메모리 누수를 방지하고 효율적인 상태 관리가 가능합니다. 특히 dispose() 메서드에서의 리소스 해제와 비동기 작업 시의 mounted 체크는 매우 중요합니다.

'Language > Flutter' 카테고리의 다른 글

[Flutter] 앱 아이콘 변경  (0) 2024.11.22
[Flutter] 앱 이름 변경  (0) 2024.11.21
[Dart] fold() 메소드  (0) 2024.11.19
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Spread 연산자(...)  (0) 2024.11.17

개요

오늘은 List의 fold() 메소드에 대해서 빠르게 알아봅시다.

 

  T fold<T>(T initialValue, T combine(T previousValue, E element)) {
    var value = initialValue;
    for (E element in this) value = combine(value, element);
    return value;
  }

 

생신건 이렇게 생겼습니다. JAVA만 파던 저에게는 뭐하는 메소드인지 감이 잘 안잡혔습니다. 함께 알아보도록 하죠.

 

 

fold()

fold는 initialValue을 시작으로 combine 값을 연산하는 방식입니다.

 

간단하게 수익률을 계산하는 메소드를 만들어보겠습니다.

void main() {

  double money = 1000000;

  List<double> rate = [5, -3.4, 0, 1.7, -4.5];

  double result = rate.fold(money,
      (previousValue, element) => previousValue + (previousValue * (element / 100)),);

}

 

fold 첫번째 인자에는 최초 시작값을 정해줍니다. 초기자금인 1,000,000원을 최초 시작값으로 설정합니다.

두번째 인자에는 T combine(T previousValue, E element) 를 넣어줍니다.

previousValue는 이전 계산된 값이고, element는 List에서 현재 인덱스가 가리키고있는 값을 의미합니다.

즉 fold가 첫번째 iterate 를 예를 들면 1000000 + (1000000 * (5 / 100)) 가 되는 것입니다.

 

 

오늘은 간단하게 fold 메소드에 대해서 알아보았습니다.

'Language > Flutter' 카테고리의 다른 글

[Flutter] 앱 이름 변경  (0) 2024.11.21
[Flutter] 위젯 생명주기(Lifecycle)  (0) 2024.11.20
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Spread 연산자(...)  (0) 2024.11.17
[Dart] Null Safety  (0) 2024.11.16

개요

Flutter에서 setState는 가장 기본적인 상태 관리 방법입니다. StatefulWidget의 상태를 변경하고 UI를 다시 그리도록 하는 메커니즘을 제공합니다. 이번 포스트에서는 setState의 개념과 사용법에 대해 자세히 알아보겠습니다.

 

 

setState의 기본 개념

 

StatefulWidget과 State

// StatefulWidget 예시
class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;  // 상태 변수

  @override
  Widget build(BuildContext context) {
    return Text('Count: $_count');
  }
}

 

 

setState 기본 사용법

class _CounterState extends State<Counter> {
  int _count = 0;

  void _incrementCounter() {
    setState(() {
      _count++;  // 상태 변경
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

 

 

setState의 동작 원리

 

State 생명주기

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // 초기화 코드
  }

  @override
  void dispose() {
    // 정리 코드
    super.dispose();
  }

  @override
  void setState(VoidCallback fn) {
    super.setState(fn);
    // setState가 호출되면 build 메서드가 다시 실행됨
  }

  @override
  Widget build(BuildContext context) {
    // UI 구성
    return Container();
  }
}

 

 

setState 호출 시 일어나는 일

  1. setState 함수 호출
  2. 상태 업데이트
  3. build 메서드 재실행
  4. 위젯 트리 재구성
  5. UI 업데이트

 

 

실전 활용 예시

 

기본적인 카운터 앱

class CounterApp extends StatefulWidget {
  const CounterApp({Key? key}) : super(key: key);

  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      if (_counter > 0) {
        _counter--;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter App')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Count:',
              style: TextStyle(fontSize: 20),
            ),
            Text(
              '$_counter',
              style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrementCounter,
                  child: Icon(Icons.remove),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _incrementCounter,
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

 

폼 데이터 관리

class UserForm extends StatefulWidget {
  @override
  _UserFormState createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
  String _name = '';
  String _email = '';
  bool _subscribed = false;

  void _updateName(String value) {
    setState(() {
      _name = value;
    });
  }

  void _updateEmail(String value) {
    setState(() {
      _email = value;
    });
  }

  void _toggleSubscription(bool? value) {
    setState(() {
      _subscribed = value ?? false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: [
          TextField(
            onChanged: _updateName,
            decoration: InputDecoration(
              labelText: 'Name',
            ),
          ),
          TextField(
            onChanged: _updateEmail,
            decoration: InputDecoration(
              labelText: 'Email',
            ),
          ),
          CheckboxListTile(
            title: Text('Subscribe to newsletter'),
            value: _subscribed,
            onChanged: _toggleSubscription,
          ),
          ElevatedButton(
            onPressed: () {
              // 폼 제출 로직
              print('Name: $_name');
              print('Email: $_email');
              print('Subscribed: $_subscribed');
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

 

 

리스트 관리

class TodoList extends StatefulWidget {
  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final List<String> _todos = [];
  final TextEditingController _controller = TextEditingController();

  void _addTodo() {
    if (_controller.text.isNotEmpty) {
      setState(() {
        _todos.add(_controller.text);
        _controller.clear();
      });
    }
  }

  void _removeTodo(int index) {
    setState(() {
      _todos.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _controller,
                  decoration: InputDecoration(
                    hintText: 'Enter new todo',
                  ),
                ),
              ),
              IconButton(
                icon: Icon(Icons.add),
                onPressed: _addTodo,
              ),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _todos.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(_todos[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () => _removeTodo(index),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

 

 

setState 사용 시 주의사항

 

비동기 작업에서의 setState

class _MyWidgetState extends State<MyWidget> {
  String _data = '';

  // 잘못된 사용
  void _fetchData() async {
    final response = await API.getData();
    if (!mounted) return;  // 위젯이 여전히 트리에 있는지 확인
    setState(() {
      _data = response;
    });
  }
}

 

성능 고려사항

// 비효율적인 방법
setState(() {
  for (var i = 0; i < 1000; i++) {
    // 상태 변경
  }
});

// 효율적인 방법
void _updateMultipleStates() {
  final updates = List.generate(1000, (index) => index);
  setState(() {
    // 한 번에 모든 상태 업데이트
    _items.addAll(updates);
  });
}

 

setState의 장단점

장점

  • 사용이 간단하고 직관적
  • 작은 규모의 앱에서 효과적
  • Flutter의 기본 제공 기능으로 추가 패키지 불필요

단점

  • 복잡한 상태 관리에는 적합하지 않음
  • 위젯 트리가 깊어지면 상태 전달이 어려움
  • 전역 상태 관리에는 부적합

 

 

마치며

setState는 Flutter에서 가장 기본적인 상태 관리 방식입니다. 작은 규모의 앱이나 로컬 상태 관리에는 매우 효과적이지만, 앱이 커지고 복잡해질수록 다른 상태 관리 솔루션을 고려해야 할 수 있습니다. 다음 포스트에서는 Provider나 Riverpod와 같은 더 강력한 상태 관리 솔루션에 대해 알아보도록 하겠습니다.

'Language > Flutter' 카테고리의 다른 글

[Flutter] 위젯 생명주기(Lifecycle)  (0) 2024.11.20
[Dart] fold() 메소드  (0) 2024.11.19
[Dart] Spread 연산자(...)  (0) 2024.11.17
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15

+ Recent posts