개요

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

개요

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

개요

Dart 2.12 버전부터 도입된 Null Safety는 널 참조로 인한 런타임 에러를 방지하기 위한 기능입니다. 이를 통해 개발자는 컴파일 타임에 잠재적인 널 참조 오류를 발견하고 수정할 수 있습니다. 바로 들어가봅시다.
 
 

Non-nullable 타입

기본적으로 모든 타입은 null을 허용하지 않습니다.

String name = 'John';     // OK
int age = 25;            // OK
String name = null;      // Error
int age = null;         // Error

 

Nullable 타입

타입 뒤에 ?를 붙여 null을 허용할 수 있습니다.

String? nullableName = 'John';  // OK
nullableName = null;           // OK

int? nullableAge = 25;         // OK
nullableAge = null;           // OK

 
 

Null 검사 연산자

null 인식 연산자 (?)

객체가 null일 수 있는 경우 안전하게 접근하기 위해 사용합니다.

String? name = 'John';
print(name?.length);    // name이 null이 아니면 길이 출력, null이면 null 출력

String? nullName = null;
print(nullName?.length);  // null 출력

// 체이닝
class User {
  String? email;
}

User? user = User();
print(user?.email?.length);  // email이 null이면 null 출력

 
 

null 주장 연산자 (!)

nullable 타입을 non-nullable 타입으로 강제 변환합니다. null일 경우 런타임 에러가 발생하므로 주의해서 사용해야 합니다.

String? nullableName = 'John';
String nonNullableName = nullableName!;  // OK

String? nullName = null;
String nonNullName = nullName!;  // Runtime Error

 
 

null 병합 연산자 (??)

왼쪽 피연산자가 null인 경우 오른쪽 피연산자를 반환합니다.

String? name = null;
String displayName = name ?? 'Anonymous';  // 'Anonymous' 출력

String? firstName = null;
String? middleName = null;
String? lastName = 'Doe';
String displayName = firstName ?? middleName ?? lastName ?? 'Anonymous';  // 'Doe' 출력

 
 

late 키워드와 Null Safety

late 변수 선언

초기화를 나중에 하는 non-nullable 변수를 선언할 수 있습니다.

class User {
  late String email;  // 나중에 초기화할 non-nullable 변수
  
  void init(String userEmail) {
    email = userEmail;  // 초기화
  }
}

 
 

late final 변수

한 번만 초기화할 수 있는 변수를 선언합니다.

class User {
  late final String email;
  
  void init(String userEmail) {
    email = userEmail;  // 첫 번째 초기화 OK
    email = 'new@email.com';  // Error: final 변수는 재할당 불가
  }
}

 
 

주의사항

null 주장 연산자(!) 사용 시 주의사항

// 위험한 코드
void processUser(User? user) {
  print(user!.name);  // user가 null이면 런타임 에러
}

// 안전한 코드
void processUser(User? user) {
  if (user == null) return;
  print(user.name);  // null 체크 후 안전하게 접근
}

 
조건부 속성 접근

class User {
  final String? email;
  
  User({this.email});
  
  bool get hasEmail => email?.isNotEmpty == true;
  
  String get displayEmail => email ?? 'No email provided';
}

 
컬렉션에서의 Null Safety

// 리스트 요소가 null일 수 있는 경우
List<String?> nullableStrings = ['Hello', null, 'World'];

// 리스트 자체가 null일 수 있는 경우
List<String>? nullableList = null;

// 둘 다 null일 수 있는 경우
List<String?>? bothNullable = null;

// null이 아닌 요소만 처리
void processStrings(List<String?> strings) {
  for (final string in strings) {
    if (string != null) {
      print(string.toUpperCase());
    }
  }
}

 
 

결론

Null Safety는 코드의 안정성을 크게 향상시키는 중요한 기능입니다. 적절히 사용하면 널 참조 오류를 미리 방지하고, 코드의 의도를 더 명확하게 표현할 수 있습니다. 특히 Flutter 애플리케이션 개발에서는 필수적인 기능이므로, 잘 이해하고 활용하시기 바랍니다.

개요

Dart에서는 변수를 선언할 때 다양한 키워드를 사용할 수 있습니다. 각 키워드는 서로 다른 특성과 용도를 가지고 있어, 상황에 맞게 적절한 키워드를 선택하는 것이 중요합니다. 이번 글에서는 각 키워드의 특징과 사용법에 대해 자세히 알아보겠습니다.
 
 
 

var

var는 타입 추론을 사용하는 변수 선언 방식입니다. 변수를 초기화할 때 할당되는 값의 타입에 따라 자동으로 변수의 타입이 결정됩니다.
 

void main() {
  var name = 'John';     // String으로 추론
  var age = 25;          // int로 추론
  var height = 175.5;    // double로 추론
  var isStudent = true;  // bool로 추론
  var numbers = [1,2,3]; // List<int>로 추론
}

 
 

특징

한번 추론된 타입은 변경할 수 없습니다.

void main() {
    var name = 'Jone';
    name = 'Jane';
    
    name = 10; // ERROR !!
}

 
 
초기값을 지정하지않으면 dynamic 타입이 됩니다.

void main() {
    var name;
    name = 'Jane';
    
    name = 10; // OK
}

 
 
 

dynamic

dynamic은 모든 타입의 값을 저장할 수 있는 특별한 타입입니다. 런타임에 타입 체크가 이루어집니다.
 

void main() {
  dynamic value = 'Hello';
  print(value.runtimeType);  // String
  
  value = 42;
  print(value.runtimeType);  // int
  
  value = true;
  print(value.runtimeType);  // bool
}

 
 

특징

타입에 제약을 받지않고 할당할 수 있습니다.

dynamic value = 'Hello';
value = 42;        // OK
value = true;      // OK
value = [1,2,3];   // OK

 
다만, 런타임 에러의 위험이 있습니다.

dynamic value = 42;
print(value.length);  // Runtime Error

 
주로 JSON 파싱과 같이 타입이 동적으로 결정되는 경우 사용됩니다.
 
 
 

final

final은 한 번만 값을 할당할 수 있는 불변 변수를 선언할 때 사용합니다.

void main() {
  final String name = 'John';
  final age = 25;  // 타입 추론 가능
  
  name = 'Jane';  // Error
}

 
 

특징

런타임에 값이 결정될 수 있습니다.

final time = DateTime.now();  // OK
final random = Random().nextInt(100);  // OK

 
클래스의 인스턴스 변수로 사용 가능합니다.

class Person {
  final String name;
  final int age;
  
  Person(this.name, this.age);
}

 
 
 

late

late는 변수를 나중에 초기화할 것임을 명시하는 키워드입니다.

void main() {
  late String name;
  
  // 나중에 초기화
  name = 'John';
  print(name);  // OK
}

 
 

특징

null safety와 함께 사용됩니다.

class Person {
  late String name;  // null이 아닌 값으로 반드시 초기화되어야 함
  
  void init(String userName) {
    name = userName;
  }
}

 
초기화하기전에 에러가 발생합니다.

late String name;
print(name);  // Error: LateInitializationError

 
lazy initialization 이 가능합니다.

late String expensiveOperation = _loadData();
// expensiveOperation이 실제로 사용될 때만 _loadData() 호출

 
 
 

const

const는 컴파일 타임 상수를 선언할 때 사용합니다.

void main() {
  const PI = 3.14159;
  const int maxAttempts = 3;
  const String greeting = 'Hello';
}

 
 

특징

컴파일 타임에 값이 결정되어야합니다.

const time = DateTime.now();  // Error: 컴파일 타임에 값을 알 수 없음
const random = Random().nextInt(100);  // Error

 
깊은 불변성을 가집니다.

const list = [1, 2, 3];  // 리스트의 내용도 변경 불가
const map = {'name': 'John', 'age': 25};  // 맵의 내용도 변경 불가

 
메모리 효율성이 있습니다.

const v1 = [1, 2, 3];
const v2 = [1, 2, 3];
print(identical(v1, v2));  // true: 같은 메모리 공간 공유

 
 
 
 

성능과 메모리 고려사항

const vs final

  • const는 컴파일 타임 상수로, 메모리를 더 효율적으로 사용
  • final은 런타임에 값이 결정되어 약간의 오버헤드 발생

var vs dynamic

  • var는 타입이 고정되어 있어 성능상 이점
  • dynamic은 런타임 타입 체크로 인한 오버헤드 발생

late 키워드의 영향

  • 초기화 체크로 인한 약간의 런타임 오버헤드
  • 메모리는 실제 초기화 시점에만 할당

 

결론

각 키워드는 그만의 특징과 용도가 있습니다. 상황에 맞는 적절한 키워드를 선택하는 것이 중요합니다:

  • var: 지역 변수, 타입이 명확한 경우
  • dynamic: 타입이 동적으로 변하는 경우
  • final: 한 번 초기화 후 변경되지 않는 값
  • late: 지연 초기화가 필요한 경우
  • const: 컴파일 타임 상수

코드의 가독성, 유지보수성, 그리고 성능을 고려하여 적절한 키워드를 선택하시기 바랍니다.

개요

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 관계를 해결할 수 있습니다.

 

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

 

 

android 빌드를 할때 

flutter Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.8.0, expected version is 1.6.0.

라는 메세지가 떴습니다.

 

 

android - settings.gradle

 

com.android.application version 을 7.3.0 -> 7.4.2 로 변경하면 됩니다.

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

[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Spread 연산자(...)  (0) 2024.11.17
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15
[Flutter] Widget의 크기제약  (0) 2024.11.07

+ Recent posts