iOS Build를 하거나 앱스토어 등록을 위해 Achive 할때 Xcode 에서 generatedpluginregistrant.m 파일에 module not found 에러가 발생했습니다. module 중 가장 상위의 module을 찾을 수 없다 하여 모듈문제인줄 알고 오랫동안 찾아다녔네요.. 해결방법 공유합니다.

 

 

아래 순서대로 하나씩 따라해보면서 해결해봅시다.

 

 

1. Xcode로 Open한 Project가 Runner.xcworkspace 인지 확인

Runner.xcodeproj 로 열었다면 당장 끄고 같은 폴더 내에 Runner.xcworkspace로 열어서 다시 시도해봅시다

 

 

 

2. 프로젝트/iOS/Podfile 주석과 버전 확인

platform :ios, '12.0' 부분이 주석처리되었다면 활성화해주시고 Xcode - Runner - Build Settings - Deployment - iOS Deployment Target 과 버전을 일치시켜줍시다.

프로젝트/iOS/Podfile

 

Xcode - Runner - Build Settings - Deployment - iOS Deployment Targe

 

Runner - General - Minimum Deployments에 iOS 버전도 함께 확인하시기바랍니다. 최소버전이 더 높으면 말이 안되니까요

 

3. 다시 빌드

flutter clean
flutter pub get
cd ios
pod install

 

한번 다시 시도해봅시다.

 

 

4.  다른방법들

 

Podfile 새로 받아보자

# Uncomment this line to define a global platform for your project
platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

 

제꺼 Podfile 복사 붙혀넣기 하고 버전수정해서 다시 시도해봅시다.

 

 

Edit Scheme 에서 Build Configuration을 변경해보자

Xcode 중앙 상단에 Runner를 클릭해서 Edit Scheme... 를 클릭하고 Info - Build Configuration 설정을 확인해보자

Debug로 되어있다면 얼른 Release로 변경하고 다시 시도해봅시다.

 

 

 

다른 방법으로 해결했다면 댓글로 방법 알려주세요.

 

오류 메세지

Warning: Pub installs executables into $HOME/.pub-cache/bin, which is not on your path.
You can fix that by adding this to your shell's config file (.bashrc, .bash_profile, etc.):

  export PATH="$PATH":"$HOME/.pub-cache/bin"

 

 

dart pub global activate rename

bundle id를 변경하던 와중 오류가 발생했습니다. 이 오류를 해결해봅시다.

 

 

 

 

해결방법

open ~/.zshrc

터미널에서 위와같이 입력해줍니다.

 

 

export PATH="$PATH":"$HOME/.pub-cache/bin"

맨 밑줄에 이렇게 입력하고 저장해줍니다.

 

 

source ~/.zshrc

이렇게 입력해서 변경된 내용을 적용시켜줍니다.

 

 

 

끝!

개요

오늘은 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을 사용하는게 조금 쉽지 않았는데 만들고보니 뿌듯하네요

개요

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는 유연한 크기 제약이 필요할 때 사용
  • 성능을 고려할 때는 가능한 한 간단한 위젯을 선택
  • 레이아웃의 목적과 요구사항에 따라 적절한 위젯 선택이 중요

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

 

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

개요

Spread 연산자(...)는 Dart 2.3 버전에서 도입된 기능으로, 컬렉션의 요소들을 다른 컬렉션에 편리하게 삽입할 수 있게 해주는 연산자입니다. 이번 포스트에서는 Spread 연산자의 다양한 사용법과 활용 사례에 대해 알아보겠습니다.

 

 

 

Spread 연산자

 

List에서의 사용

void main() {
  var list1 = [1, 2, 3];
  var list2 = [4, 5, 6];
  
  // Spread 연산자를 사용하여 리스트 병합
  var combined = [...list1, ...list2];
  print(combined);  // [1, 2, 3, 4, 5, 6]
  
  // 개별 요소와 함께 사용
  var withMore = [0, ...list1, 4];
  print(withMore);  // [0, 1, 2, 3, 4]
}

 

Set에서의 사용

void main() {
  var set1 = {1, 2, 3};
  var set2 = {3, 4, 5};
  
  // Set 병합 (중복제거)
  var combinedSet = {...set1, ...set2};
  print(combinedSet);  // {1, 2, 3, 4, 5}
}

 

 

Map에서의 사용

void main() {
  var map1 = {'a': 1, 'b': 2};
  var map2 = {'c': 3, 'd': 4};
  
  // Map 병합
  var combinedMap = {...map1, ...map2};
  print(combinedMap);  // {a: 1, b: 2, c: 3, d: 4}
  
  // 충돌이 있는 경우 나중에 오는 값이 우선
  var map3 = {'a': 5, 'e': 6};
  var overrideMap = {...map1, ...map3};
  print(overrideMap);  // {a: 5, b: 2, e: 6}
}

 

 

Null-aware Spread 연산자 (...?)

void main() {
  List<int>? nullableList;
  var list = [1, 2, ...?nullableList];
  print(list);  // [1, 2]
  
  nullableList = [3, 4];
  list = [1, 2, ...?nullableList];
  print(list);  // [1, 2, 3, 4]
}

 

조건부 사용

void main() {
  var items = [1, 2];
  List<int>? extraItems;
  
  // null이 아닐 때만 spread
  var combined = [
    ...items,
    if (extraItems != null) ...extraItems,
  ];
  print(combined);  // [1, 2]
}

 

 

 

Spread 활용

 

Flutter 위젯 트리에서의 활용

class MyWidget extends StatelessWidget {
  final List<Widget>? optionalWidgets;
  
  const MyWidget({Key? key, this.optionalWidgets}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Fixed Widget'),
        ...?optionalWidgets,
        Text('Another Fixed Widget'),
      ],
    );
  }
}

 

데이터 모델 복사 및 업데이트

class User {
  final String name;
  final Map<String, dynamic> preferences;
  
  User(this.name, this.preferences);
  
  User copyWith({
    String? name,
    Map<String, dynamic>? extraPreferences,
  }) {
    return User(
      name ?? this.name,
      {
        ...preferences,
        if (extraPreferences != null) ...extraPreferences,
      },
    );
  }
}

 

API 응답 병합

Future<Map<String, dynamic>> fetchUserData() async {
  final basicInfo = await fetchBasicInfo();
  final preferences = await fetchPreferences();
  final settings = await fetchSettings();
  
  return {
    ...basicInfo,
    'preferences': {...preferences},
    'settings': {...settings},
  };
}

 

 

더 자세히

중첩된 컬렉션

void main() {
  var nested = [
    [1, 2],
    [3, 4],
    [5, 6],
  ];
  
  // 중첩 리스트 펼치기
  var flattened = [
    for (var list in nested) ...list,
  ];
  print(flattened);  // [1, 2, 3, 4, 5, 6]
}

 

메모리 사용

// 새로운 리스트 생성 - 메모리 사용
var newList = [...oldList];  // 전체 복사

// 참조만 생성 - 메모리 효율적
var reference = oldList;     // 참조만 복사

 

 

대규모 컬렉션 처리

void processLargeCollections() {
  var largeList1 = List.generate(10000, (i) => i);
  var largeList2 = List.generate(10000, (i) => i + 10000);
  
  // 메모리 사용량이 큼
  var combined = [...largeList1, ...largeList2];
  
  // 대안: Iterator 사용
  var iterator = Chain([largeList1.iterator, largeList2.iterator]);
}

 

 

주의사항

 

타입 안전성

void main() {
  List<int> numbers = [1, 2, 3];
  List<String> strings = ['a', 'b', 'c'];
  
  // 컴파일 에러: 타입 불일치
  // List<int> combined = [...numbers, ...strings];
  
  // 올바른 방법: dynamic 또는 Object 사용
  List<dynamic> combined = [...numbers, ...strings];
}

 

 

Const 컨텍스트

// const 컨텍스트에서 사용 가능
const list1 = [1, 2, 3];
const list2 = [4, 5, 6];
const combined = [...list1, ...list2];  // OK

// 런타임 값에는 const 사용 불가
var runtime = [1, 2, 3];
const invalid = [...runtime];  // Error

 

 

 

결론

Spread 연산자는 컬렉션을 다룰 때 매우 유용한 도구입니다. 특히 Flutter 개발에서 위젯 트리를 구성할 때 자주 사용되며, 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 다만, 대규모 컬렉션을 다룰 때는 메모리 사용량을 고려해야 하며, 타입 안전성에도 주의를 기울여야 합니다.

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

[Dart] fold() 메소드  (0) 2024.11.19
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15
[Flutter] Widget의 크기제약  (0) 2024.11.07

개요

Flutter에는 부모위젯으로부터 제약을 받아 자신의 크기를 결정하는 위젯들이 존재합니다.

만약 부모의 크기가 정해지지 않다면 자신의 크기를 정하지 못해 오류가 발생합니다. 이에 대해서 알아봅시다.

 

 

Column - Column - (Expanded, Flexible)

Expanded와 Flexible은 부모의 사이즈를 채워주는 위젯입니다.

Expanded나 Flexible를 적용한 모습

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded( // Flexible 로 마찬가지
        child: Container(
          color: Colors.blue,
          child: ...,
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

Column 안에 Expanded를 넣으면 깔끔하게 채워줍니다. 이게 가능한 이유는

  1. Column / Row 는 부모로부터 제약조건을 받아야한다. 없다면 하위 위젯에 크기에 따라서 정해진다.
  2. Scaffold 의 body사이즈는 MediaQuery의 크기(화면 크기)에서 AppBar, BottomNavigationBar등이 차지하는 공간을 제외한 나머지 영역의 크기로 정해져있어 크기 제약조건이 존재한다.
  3. Column의 부모위젯에 제약조건이 존재한다.

따라서 Expanded 위젯을 사용하더라도 오류가 발생하지 않습니다.

 

 

그런데 만약에 Column - Column 안에 Expanded가 들어간다면

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Column(
        children: [
          Expanded(
            child: Container(
              color: Colors.blue,
              child: ...,
            ),
          ),
        ],
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

Column 안에 Column 에 Expanded를 하면 

RenderFlex children have non-zero flex but incoming height constraints are unbounded

오류가 발생합니다.

 

그 이유는

  1. 내부 Column의 크기 제약조건이 존재하지않는다.
  2. 내부 Column의 크기는 그 하위 위젯의 크기에 따라 결정된다.
  3. 내부 Column안에 Expanded 위젯은 부모의 크기제약에 따라 크기가 결정된다.

2번과 3번이 서로 크기 제약조건을 의존하고있는걸 확인할 수 있습니다. 따라서 오류가 발생하는것입니다.

 

이 같은 조건은 Row - Row에서도 동일하게 동작합니다.

 

 

ListView

ListView는 기본적으로 크기제약이 없고 가능한 모든 공간을 사용하려고 합니다. 제 기준 가장 사용하기 까다로운 위젯입니다. 왜냐하면 ListView를 단독으로 사용할일이 거의 없기 때문입니다.

 

보통 SingleChildScrollView 위젯내에서 사용하거나 Column 위젯에서 ListView를 사용하는데 오류가 가장 많이 나더라구요.

 

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) => Text('텍스트$index'),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이렇게 Column 안에 놓는다? Vertical viewport was given unbounded height. 바로 오류납니다.

 

위와 마찬가지로 ListView의 크기제약이 없어서 그렇습니다. 이 같은 상황에 해결법은 Expanded 위젯을 사용하는겁니다.

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded(
        child: ListView.builder(
          itemCount: 10,
          itemBuilder: (context, index) => Text('텍스트$index'),
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이제 오류가 안납니다. 그런데 보통 body에는 처음에 SingleChildScrollView 를 사용해서 전체 스크롤을 가능하게 해줍니다. 화면크기만으로는 컨텐츠를 몇개 못담으니까요.

 

 

body: SingleChildScrollView(
    child: Column(
      children: [
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
        Expanded(
          child: ListView.builder(
            itemCount: 10,
            itemBuilder: (context, index) => Text('텍스트$index'),
          ),
        ),
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
      ],
    ),
  )

 

RenderFlex children have non-zero flex but incoming height constraints are unbounded.

 

 

  1. SingleChildScrollView는 자식(Column)에게 높이 제약을 전달
  2. Column은 이 unbounded 제약을 받은 상태에서 Expanded를 만남, 즉 자식위젯들에게 높이 제약을 전달
  3. Expanded는 남은 공간을 차지하려고 하는데, unbounded 상태에서는 '남은 공간'이 얼마인지 계산할 수 없음
  4. 따라서 오류 발생

네 이럴때는 Expanded를 제거해야합니다. 하지만 여기에서 Expanded를 제거한다고 해결되지 않습니다.

 

Vertical viewport was given unbounded height.

오류가 발생합니다.

 

이도 ListView 첫번째 예제와 똑같이 생겼거든요. 그럼 어떻게 해결해야할까요?

 

shrinkWrap: true,

 

ListView에 속성중에 shrinkWrap 옵션이 있습니다. 해당 옵션은 false가 default인데, 이 옵션이 true가 되면 ListView가 자신의 자식들의 높이 합만큼만 크기를 가지도록 만듭니다. 그 말은 무한한 크기를 가지려는 성질을 제한하는 옵션이라는 겁니다. 이 옵션을 사용하면 SingleChildScrollView - Column - ListView 관계를 해결할 수 있습니다.

 

다만, 단점으로는 모든 자식의 높이를 계산해서 자신의 크기를 정해야 하기때문에 자식이 많을수록 성능저하가 발생할 수 있다는 겁니다. 따라서 사용할때는 주의를 갖고 사용해야합니다.

 

 

+ Recent posts