์คํฌ๋กค๊ณผ ๋ฆฌ์คํธ - ListView, GridView
์ง๋ ๊ธ์์ Column์ผ๋ก ์์ ฏ์ ์ธ๋ก๋ก ๋์ด๋๋ ๋ฒ์ ๋ดค๋ค. ๊ทธ๋ผ ํญ๋ชฉ์ด ํ๋ฉด๋ณด๋ค ๋ง์์ง๋ฉด? ํ ์ผ์ด 30๊ฐ์ธ ๋ชฉ๋ก์ Column์ ๊ทธ๋๋ก ๋ฃ์ด๋ณด์.
Column(
children: List.generate(30, (i) => ListTile(title: Text('ํ ์ผ $i'))),
)ํ๋ฉด ์๋์ชฝ์ ๋ ธ๋-๊ฒ์ ์ค๋ฌด๋ฌ๊ฐ ์๊ธฐ๊ณ ์ฝ์์ ์๋ฌ๊ฐ ์ฐํ๋ค.
A RenderFlex overflowed by 1160 pixels on the bottom.Column์ ์์์ ์ ๋ถ ๋์ด๋์ ๋ฟ์ด๋ผ, ํ๋ฉด์ ๋์ด๊ฐ๋ ์คํฌ๋กค์ด ๋์ง ์๋๋ค. ์คํฌ๋กค์ด ํ์ํ๋ฉด ์คํฌ๋กค๋๋ ์์ ฏ์ ์จ์ผ ํ๋ค.
ํด๊ฒฐ 1: ์ ์ ์์ด๋ฉด SingleChildScrollView
SingleChildScrollView(
child: Column(
children: List.generate(30, (i) => ListTile(title: Text('ํ ์ผ $i'))),
),
)SingleChildScrollView๋ ์์ ํ๋๋ฅผ ์คํฌ๋กค๋๊ฒ ๊ฐ์ธ๋ ์์ ฏ์ด๋ค. ์ค์ ํ๋ฉด์ฒ๋ผ ํญ๋ชฉ์ด ์ ๊ณ ๊ธธ์ด๊ฐ ์ ํด์ง ํ๋ฉด์ ์๋ง๋ค. ๋ค๋ง ์์์ ์ ๋ถ ํ๊บผ๋ฒ์ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ํญ๋ชฉ์ด ์๋ฐฑ ๊ฐ๊ฐ ๋๋ฉด ๋๋ ค์ง๋ค.
ํด๊ฒฐ 2: ๋ง์ ์์ด๋ฉด ListView
ListView(
children: List.generate(30, (i) => ListTile(title: Text('ํ ์ผ $i'))),
)ListView๋ ์คํฌ๋กค์ด ์ฒ์๋ถํฐ ๋ค์ด ์๋ ์ธ๋ก ๋ชฉ๋ก์ด๋ค. ์ด๊ฒ๋ children์ ์ ๋ถ ๋ฃ์ผ๋ฉด ์ ๋ถ ๋ง๋ค์ด์ง๋, ํญ๋ชฉ์ด ๋ง์ ๋๋ ์๋ ๋ฐฉ์์ ์ด๋ค.
ListView.builder: ํ๋ฉด์ ๋ณด์ด๋ ๊ฒ๋ง ๋ง๋ ๋ค
final todos = List.generate(1000, (i) => 'ํ ์ผ $i');
ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(title: Text(todos[index]));
},
)itemBuilder๋ **"index๋ฒ์งธ ํญ๋ชฉ์ ์ด๋ ๊ฒ ๊ทธ๋ ค๋ผ"**๋ ์ฝ๋ฐฑ์ด๋ค. Flutter๊ฐ ํ๋ฉด์ ๋ณด์ด๋ ํญ๋ชฉ๋ง ์ด ์ฝ๋ฐฑ์ผ๋ก ๋ง๋ค๊ณ , ์คํฌ๋กคํด์ ์ง๋๊ฐ ํญ๋ชฉ์ ๋ฒ๋ฆฐ๋ค. 1000๊ฐ๋ฅผ ๋ฃ์ด๋ ํ๋ฉด์ ๋ณด์ด๋ 10๊ฐ ์ํ๋ง ์ค์ ๋ก ๋ง๋ค์ด์ง๋ค. 3๋ฒ ๊ธ์์ ๋ฐฐ์ด "ํจ์๋ฅผ ๋๊ธฐ๋" ํจํด์ด๊ณ , context์ index๋ฅผ ์ง์ ๋ฐ๋ ๋งค๊ฐ๋ณ์ ์ด๋ฆ๊น์ง ์ ์ด์ผ ํ๋ ๊ฒ๋ ๊ทธ๋ ๋ณธ ๊ท์น์ด๋ค.
๊ตฌ๋ถ์ ์ด ํ์ํ๋ฉด ListView.separated๊ฐ ์๋ค.
ListView.separated(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(title: Text(todos[index])),
separatorBuilder: (context, index) => const Divider(),
)Column ์์ ListView๋ฅผ ๋ฃ์ผ๋ฉด ์๋ฌ๊ฐ ๋๋ค
ํ๋ฉด ์์ชฝ์ ์ ๋ชฉ์ ๋๊ณ ๊ทธ ์๋์ ๋ชฉ๋ก์ ๋ฃ๊ณ ์ถ์ด์ Column ์์ ListView๋ฅผ ๋ฃ์ด๋ณด์.
Column(
children: [
const Text('๋ด ํ ์ผ'),
ListView.builder( // ์ฌ๊ธฐ์ ์๋ฌ๋จ
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(title: Text(todos[index])),
),
],
)Vertical viewport was given unbounded height.์ง๋ ๊ธ์ ๊ท์น์ด ๊ทธ๋๋ก ์ ์ฉ๋๋ค. "์ ์ฝ์ ์๋๋ก ๋ด๋ ค๊ฐ๋ค." Column์ ์์์๊ฒ ์ธ๋ก ํฌ๊ธฐ ์ ํ์ ์ฃผ์ง ์๊ณ , ListView๋ "์ธ๋ก๋ก ์ผ๋ง๋ ์ง ๋์ด๋ ์ ์๋ค"๋ ์ํ์์ ๋ด ๋์ด๋ฅผ ์ ํ ์ ์๋ค. ์คํฌ๋กค ์์ญ์ ์๊ธฐ๊ฐ ๋ณด์ด๋ ๋์ด๊ฐ ์ ํด์ ธ ์์ด์ผ ํ๋๋ฐ, ๊ทธ ๊ธฐ์ค์ด ์๋ ๊ฒ์ด๋ค.
ํด๊ฒฐ: Expanded๋ก ๋จ์ ๋์ด๋ฅผ ์ค๋ค
Column(
children: [
const Text('๋ด ํ ์ผ'),
Expanded( // ๋จ์ ์ธ๋ก ๊ณต๊ฐ์ ListView์ ์ค๋ค
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(title: Text(todos[index])),
),
),
],
)์ง๋ ๊ธ์์ ๋ฐฐ์ด Expanded๊ฐ ์ฌ๊ธฐ์ ๋ค์ ๋์จ๋ค. ์ ๋ชฉ์ด ์ฐ๊ณ ๋จ์ ๋์ด๋ฅผ ListView์ ์ฃผ๋ฉด, ListView๋ ๊ทธ ๋์ด ์์์ ์คํฌ๋กค๋๋ค.
shrinkWrap: true๋ก "๋ด์ฉ๋งํผ๋ง ๋์ด๋ฅผ ์ก์๋ผ"๊ณ ํด๋ ์๋ฌ๋ ์ฌ๋ผ์ง๋ค.
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), // ๋ฐ๊นฅ ์คํฌ๋กค์ ๋งก๊ธธ ๋
children: [...],
)๋ค๋ง shrinkWrap์ ํญ๋ชฉ์ ์ ๋ถ ๊ณ์ฐํด์ผ ํด์ builder์ ์ฅ์ ์ด ์ฌ๋ผ์ง๋ค. ํญ๋ชฉ์ด ์ ์ ๋๋ง ์ฐ๊ณ , ๊ธฐ๋ณธ์ Expanded๋ก ํ์ด์ผ ํ๋ค.
GridView: ๊ฒฉ์๋ก ๋ฐฐ์นํ๊ธฐ
GridView.count(
crossAxisCount: 2, // ๊ฐ๋ก๋ก 2์นธ
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: List.generate(6, (i) => Container(
color: Colors.blue[100 * (i + 1)],
child: Center(child: Text('$i')),
)),
)crossAxisCount: 2๋ ๊ฐ๋ก๋ก 2์นธ์ด๋ผ๋ ๋ป์ด๋ค. ์นธ์ด ๋ช ๊ฐ์ผ์ง ์ ํด์ ธ ์์ ๋ ์ด๋ค. ํญ๋ชฉ์ด ๋ง์ผ๋ฉด ListView์ฒ๋ผ builder ๋ฐฉ์์ด ์๋ค.
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 100,
itemBuilder: (context, index) => Container(
color: Colors.blue[100],
child: Center(child: Text('$index')),
),
)gridDelegate๋ "๊ฒฉ์๋ฅผ ์ด๋ป๊ฒ ๋๋์ง"๋ฅผ ์ ํ๋ ๊ฐ์ฒด๋ค. ์นธ ์ ๋์ ์นธ ํ๋์ ์ต๋ ๋๋น๋ฅผ ์ ํ๊ณ ์ถ์ผ๋ฉด SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 150)๋ฅผ ์ด๋ค. ์ด๋ฌ๋ฉด ํ๋ฉด ํญ์ ๋ฐ๋ผ ์นธ ์๊ฐ ์์์ ๋ฐ๋์ด์ ํฐ๊ณผ ํ๋ธ๋ฆฟ ๋์์ ์ข๋ค.
ํญ๋ชฉ์ ์ํ๊ฐ ์์ ๋: key
์ฒดํฌ๋ฐ์ค์ฒ๋ผ ํญ๋ชฉ์ด ์๊ธฐ ์ํ๋ฅผ ๊ฐ์ง๋ ๋ชฉ๋ก์์ ์ค๊ฐ ํญ๋ชฉ์ ์ง์ฐ๋ฉด, ์ํ๊ฐ ์๋ฑํ ํญ๋ชฉ์ ๋ถ๋ ์ผ์ด ์๊ธด๋ค. ํญ๋ชฉ๋ง๋ค key๋ฅผ ๋ฌ์์ "์ด ํญ๋ชฉ์ด ๋๊ตฌ์ธ์ง"๋ฅผ ์๋ ค์ค์ผ ํ๋ค.
ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoTile(
key: ValueKey(todo.id), // ์์น๊ฐ ์๋๋ผ id๋ก ๊ตฌ๋ถ
todo: todo,
);
},
)Compose LazyColumn์ items(list, key = { it.id })์ ๊ฐ์ ๋ฌธ์ ์ด๊ณ ๊ฐ์ ํด๋ฒ์ด๋ค. ํญ๋ชฉ์ ์ํ๊ฐ ์๋ ๋จ์ํ ๋ชฉ๋ก์ ์ ๊ฒฝ ์ฐ์ง ์์๋ ๋๋ค.
๊ทธ ๋ฐ์ ์คํฌ๋กค ์์ ฏ
CustomScrollView๋ ์ฑ๋ฐ๊ฐ ๊ฐ์ด ์ ํ๋ ํ๋ฉด์ฒ๋ผ ์ฌ๋ฌ ์ข
๋ฅ์ ์คํฌ๋กค ์์ญ์ ํ๋๋ก ๋ฌถ์ ๋ ์ด๋ค. ์์ SliverList, SliverGrid, SliverAppBar ๊ฐ์ "์ฌ๋ฆฌ๋ฒ" ์์ ฏ์ ๋ฃ๋๋ค. ์ง๊ธ์ "๋ณต์กํ ์คํฌ๋กค์ด ํ์ํ๋ฉด ์ด๋ฐ ๊ฒ ์๋ค" ์ ๋๋ง ์์๋๊ณ , ํ์ํ ๋ ๋ฐ๋ก ๋ค๋ฃฌ๋ค.
Compose, SwiftUI์ ๋น๊ตํ๋ฉด
// Compose
LazyColumn {
items(todos, key = { it.id }) { todo ->
Text(todo.title)
}
}// SwiftUI
List(todos) { todo in
Text(todo.title)
}ํ๊ณ ์ถ์ ๊ฒ | Flutter | Compose | SwiftUI |
๊ธด ๋ชฉ๋ก (ํ์ํ ๊ฒ๋ง ์์ฑ) |
|
|
|
์งง์ ๋ด์ฉ ์คํฌ๋กค |
|
|
|
๊ฒฉ์ |
|
|
|
๊ตฌ๋ถ์ |
|
|
|
ํญ๋ชฉ ์๋ณ |
|
|
|
Compose์ Flutter๋ ๊ตฌ์กฐ๊ฐ ๋น์ทํ๋ค. ๋ ๋ค ์คํฌ๋กค ๋ชฉ๋ก์ด "๋์ด๊ฐ ์ ํด์ง ๊ณณ"์ ๋ค์ด๊ฐ์ผ ํด์, Compose์์ Column(Modifier.verticalScroll()) ์์ LazyColumn์ ๋ฃ์ผ๋ฉด ์๋ฌ๊ฐ ๋๋ ๊ฒ๊ณผ Flutter์ Column ์ ListView ์๋ฌ๋ ๊ฐ์ ์ด์ ๋ค.
์ ๋ฆฌ
Column์ ํ๋ฉด์ ๋์ด๊ฐ๋ ์คํฌ๋กค๋์ง ์์ผ๋ฏ๋ก, ์คํฌ๋กค์ด ํ์ํ๋ฉดSingleChildScrollView๋ListView๋ฅผ ์ด๋ค- ํญ๋ชฉ์ด ์ ์ผ๋ฉด
SingleChildScrollView, ๋ง์ผ๋ฉดListView.builder๋ก ํ๋ฉด์ ๋ณด์ด๋ ํญ๋ชฉ๋ง ๋ง๋ ๋ค Column์์ListView๋ฅผ ๋ฃ์ผ๋ฉดunbounded height์๋ฌ๊ฐ ๋๊ณ ,Expanded๋ก ๊ฐ์ธ์ ๋์ด๋ฅผ ์ ํด์ค๋ค- ๊ฒฉ์๋
GridView์ด๊ณ , ์นธ ์๋crossAxisCount, ์นธ ๋๋น ๊ธฐ์ค์maxCrossAxisExtent๋ก ์ ํ๋ค - ์ํ๊ฐ ์๋ ํญ๋ชฉ์ ์ถ๊ฐยท์ญ์ ํ ๋๋
ValueKey(id)๋ก ํญ๋ชฉ์ ๊ตฌ๋ถํ๋ค
๋ค์ ๊ธ์์๋ TextField, Form, GestureDetector๋ก ์
๋ ฅ๊ณผ ํฐ์น๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
