들어가기 앞서...
스크롤이 가능한 위젯에 대해 공부하겠다.
- SingleChildScrollView
- ListView
- GridView
- ReorderableListView
- CustomScrollView - SliverList
- CustomScrollView - SliverGrid
- CustomScrollView - SliverAppBar
- CustomScrollView - SliverPersistentHeader
- Scrollbar
- RefreshIndicator
본문으로...
1. SingChildScrollView 공부!
import 'package:flutter/material.dart';
import 'package:single_child_scroll_view_study/constant/colors.dart';
import 'package:single_child_scroll_view_study/layout/main_layout.dart';
class SingleChildScrollViewScreen extends StatelessWidget {
final List<int> numbers = List.generate(100, (index) => index);
SingleChildScrollViewScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'SingleChildScrollView',
body: renderPerformance(),
);
}
//1. 기본 렌더링 법
Widget renderSimple() {
// 만약에 child 가 화면을 넘어가면 스크롤 가능하게 되고, 화면 안넘어가면 스크롤 안됌.
return SingleChildScrollView(
child: Column(
children: rainbowColors.map((e) => renderContainer(color: e)).toList(),
),
);
}
//2. 화면이 넘어가지 않아도 스크롤이 가능하게 하기
Widget renderAlwaysScroll() {
return SingleChildScrollView(
// default : 스크롤이 안되게!
// physics: NeverScrollableScrollPhysics(),
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [renderContainer(color: Colors.black)],
),
);
}
//3. 화면 위젯이 잘리지 않게 하기
Widget renderClip() {
return SingleChildScrollView(
clipBehavior: Clip.none,
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [renderContainer(color: Colors.black)],
),
);
}
//4. 바운스 스타일
Widget renderPhysics() {
return SingleChildScrollView(
// Android 스타일
physics: ClampingScrollPhysics(),
// IOS 스타일
// physics: BouncingScrollPhysics(),
child: Column(
children: rainbowColors.map((e) => renderContainer(color: e)).toList(),
),
);
}
//5. SingleChildScrollView 퍼포먼스
// SingleChildScrollView 는 랜더를 전부다 해버린다. 아직 보지도 않은 리스트들의 마지막까지 출력을해버리면, 리소스 낭비가 심해질것이다.
Widget renderPerformance() {
return SingleChildScrollView(
child: Column(
children: numbers
.map((e) => renderContainer(
// 빨강 부터 보라색까지 가져오려는식 나머지를 구함.
color: rainbowColors[e % rainbowColors.length], index: e))
.toList()),
);
}
Widget renderContainer({
required Color color,
int? index,
}) {
if(index != null) {
print(index);
}
return Container(
height: 300,
color: color,
);
}
}
결과)
2. ListView 공부!
1) 버튼이 중복되니 버튼마저 mapping 가능하게 바꾸자!
builder 와 Text 만 다룰 것이다. 이 두개만 넣어주면 ElevatedButton을 매핑 할 수 있게끔 할 것이다.
class ScreenModel {
final WidgetBuilder builder;
final String name;
ScreenModel({
required this.builder,
required this.name,
});
}
class HomeScreen extends StatelessWidget {
final screens = [
ScreenModel(
builder: (_) => SingleChildScrollViewScreen(),
name: 'SingleChildScrollViewScreen',
),
ScreenModel(
builder: (_) => ListViewScreen(),
name: 'ListViewScreen',
),
];
...
}
결과)
1) ListView 와 ListView.builder() 의 차이점
결과는 같다.
Listview() 는 0-99 까지 다렌더링 시켜버린다.(한번에 다 그리기때문에 메모리를 차지한다. 퍼포먼스에 문제가 생긴다. 100 말고 몇천개, 몇만개일때, 무거운 위젯일때는 문제가 생긴다. )
ListView.Builder() 는 0-3 그리고 스크롤할 때마다 렌더링 시킨다. (보고있는것만 메모리에 담고 나머지는 메모리에 삭제시킨다. 그러니 매우 효율적이다!)
Widget renderDefault() {
return ListView(
children: numbers
.map((e) => renderContainer(
color: rainbowColors[e % rainbowColors.length],
index: e,
))
.toList(),
);
}
}
Widget renderBuilder() {
return ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length],
index: index,
);
},
);
}
2) separated 사용
itemBuilder 중간 중간에 separatorBuilder 가 출력된다.
그렇기때문에 광고 넣기에 아주 좋다.
여기서 분기처리를 해서 몇번째 itemBuilder 후에 separatorBuilder 를 실행할수 있다.(예 5번째 마다 출력되게!)
separatorBuilder: (context, index) {
// 0 번째는 안출력되게 하기 위함
index += 1;
// 5 개의 item마다 배너 보여주기
if(index % 5 == 0) {
return renderContainer(
color: Colors.black,
index: index,
height: 100,
);
}
return Container();
}
결과)
3. GridView 공부!
좌우 위아래 타일처럼 배치가능하다.
1) GridView.count()
100개를 전부 렌더링 해버린다. GridView.count 도 퍼포먼스로는 아쉽다.
class GridViewScreen extends StatelessWidget {
List<int> numbers = List.generate(100, (index) => index);
GridViewScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'GridViewScreen',
body: renderDefulat();
}
//1. 기본, 랜더링 다해버림
Widget renderDefulat() {
return GridView.count(
// 주체: 가로
// 가로로 몇개 넣을래?
crossAxisCount: 2,
// 가로 간경
crossAxisSpacing: 12.0,
// 세로 간격
mainAxisSpacing: 12.0,
children: numbers
.map((e) => renderContainer(
color: rainbowColors[e % rainbowColors.length], index: e))
.toList(),
),
)
}
Widget renderContainer({
required Color color,
required int index,
double? height,
}) {
print(index);
return Container(
height: height ?? 300,
color: color,
child: Center(
child: Text(
index.toString(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 30.0,
),
),
),
);
}
}
2) GridView.builder()
builder 라고 적혀있는 constructor 는 효율성이 좋은 거다.
그렇기 때문에 보이는 것만 랜더링 시키고 스크롤을 이동하면 그 다음것을 랜더링 시킨다.
SliverGridDelegateWithFixedCrossAxisCount
가로에 최대 배치 개수를 정해서 grid 를 그린다.
Widget renderBuilderCrossAxisCount() {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 가로에 최대 몇개 배치
crossAxisCount: 2,
// 가로 여백 사이즈
crossAxisSpacing: 12.0,
// 세로 여백 사이즈
mainAxisSpacing: 12.0,
),
itemBuilder: (context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length], index: index);
},
);
}
SliverGridDelegateWithMaxCrossAxisExtent
위젯의 최대길이를 정해서 균등하게 위젯의 사이즈를 배분해서 grid 를 그린다.
Widget renderBuilderCrossAxis() {
return GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
// 위젯들의 최대 길이
maxCrossAxisExtent: 200,
),
itemBuilder: (context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length], index: index);
},
// item 총 개수 정할 수 있다.(없으면 무한대!)
itemCount: 100,
);
}
4. ReorderableListView!
순서를 이동 시킬 수 있다. 화면에서는 순서를 바꿔주지만, 실제데이터에는 영향이 안끼친다.
1) ReorderableListView 기본
builder constructor 를 사용 안하고 기본 constructor 에 children 에 넣어놨기 때문에 전부 다 랜더링하기 때문에 퍼포먼스적으론 좋지 않다.
1-1) oldIndex 와 newIndex 산정하는 법
// 화면에서 순서를 바꿔주지만 실제 데이터에는 영향은 없다.
// onReorder : 순서를 바꾸면 실행이된다.
// oldIndex 순서를 바꾸기전 index, newIndex 순서를 바꾼 후 index
// oldIndex 와 newIndex 는 절대적으로 옮기기전 index 를 따른다
설명)
list = [red, orange, yellow];
인덱스 는 [0, 1, 2] 이다.
1. 만약에 red 를 yellow 뒤로 옮기고 싶다.
red : 0 oldIndex -> 3 newIndex 이다.
2. 만약에 yellow 를 red 앞으로 옮기고 싶다.
yellow : 2 oldIndex -> 0 newIndex 이다.
--------------------------------------------------------------------
*
oldIndex 와 newIndex 는 절대적으로 옮기기전의 index 로 산정한다.
옮기고 난 후가 아니다! 실제 인덱스 구하는 것은 우리가 분기처리 해줘서 구해야 한다.
*
// 실제 인덱스 구하기 식
if(oldIndex < newIndex ){
newIndex -= 1;
}
1-2) ReorderableListView 는 고유의 key 가 필요하다.
왜냐하면 프로그래밍 적으로는 다같은 Container 로 인식한다. 우리가
100개의 컨테이너를 반복문으로 돌리고 있는데 시스템이 봤을때는 다똑같다고 인식한다. 근데 우리가 다르게 인식시켜주려면, key 값을 고유의 값으로 넣어주면 다르게 인식을한다.
결과)
2) ReorderableListView.builder()
당연히 랜더링은 화면에서 보여지는 만큼만 되고, 스크롤 하면서 그 다음것을 불러온다.
스크롤도 가능하고 이동도 가능하지만, 인덱스도 유지못하고, 색상도 유지를 못한다.
Widget renderBuilder() {
return ReorderableListView.builder(
itemBuilder: (context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length], index: index);
},
itemCount: 100,
onReorder: (int oldIndex, int newIndex) {
// onReorder : 순서를 바꾸면 onReorder 이 호출 된다.
// oldIndex 순서를 바꾸기 전 index, newIndex 순서를 바꾼 후 index
// oldIndex 와 newIndex 는 절대적으로 옮기기전 index 를 따른다.
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = numbers.removeAt(oldIndex);
numbers.insert(newIndex, item);
});
},
);
}
2-1) numbers를 참조해서 숫자와 색상을 유지하고 싶다면?
Widget renderBuilder() {
return ReorderableListView.builder(
itemBuilder: (context, index) {
return renderContainer(
color: rainbowColors[numbers[index] % rainbowColors.length],
index: numbers[index],
);
},
itemCount: numbers.length,
onReorder: (int oldIndex, int newIndex) {
// onReorder : 순서를 바꾸면 onReorder 이 호출 된다.
// oldIndex 순서를 바꾸기 전 index, newIndex 순서를 바꾼 후 index
// oldIndex 와 newIndex 는 절대적으로 옮기기전 index 를 따른다.
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = numbers.removeAt(oldIndex);
numbers.insert(newIndex, item);
});
},
);
}
결과)
5. CustomScrollView - SliverList 공부!
여러개 스크롤 가능 한 위젯을 하나의 위젯에 집어 넣어 사용한다.
Sliver에는 무조건 Sliver~ 로 되어있는 파라미터로 넣어줘야한다. 아니면 에러난다.
1) Column 안에다가 ListView 를 20 개 정도를 만들고 추가적으로 GridView 넣는다면?
지금까지 배운데로 진행을 하자면,
Widget renderPractice() {
return Column(
children: [
Expanded(
child: ListView(
children: rainbowColors
.map(
(e) => renderContainer(
color: e,
index: 1,
),
)
.toList(),
),
),
Expanded(
child: GridView.count(
crossAxisCount: 2,
children: rainbowColors
.map(
(e) => renderContainer(
color: e,
index: 1,
),
)
.toList(),
),
),
],
);
}
결과)
Expanded 를 꼭 사용해야한다! 안쓰면 바로 터진다.
Columm() 안에 모든 List 또는 스크롤이 가능한 위젯을 사용할 때는 Expanded 를 사용해줘야한다.
ListView 는 리스트라는 것을 무한정 하게 넣을 수 있는것이다. 이론상으로 무한한 높이를 차지할 수 있다.
그래서 Column 위젯의 최대 크기 만큼만 차지해라라는 의미로 Expanded 를 써주는 거다.
2) slivers 에서 AppBar 도 만들 수 있다.
return Scaffold(
body: CustomScrollView(
// list 형태의 위젯들을 한번에 쓸수 있게끔 해준다! 하지만 특정 되어있다.
slivers: [
// ios : appBar 가 스크롤이 된다.
// android : 아무 작동 없다.
SliverAppBar(
title: Text('CustomScrollViewScreen'),
),
],
),
);
3) listView(children) 에 넣는 형태처럼 만들 수 있다.
데이터 전부다 랜더링 하고 퍼포먼스적으로 떨어짐
// ListView 기본 생성자와 유사함
SliverList renderChildSliverList() {
return SliverList(
// delegate : 어떤 형태로 SliverList 를 만들어낼지 정할 수 있다.(전부다 랜더링 or .builder() 로 조금씩 랜더링 하냐)
// appbar 같이 스크롤 되어서 사라진다.
// 모든 리스트가 한번에 랜더링 된다.
delegate: SliverChildListDelegate(
numbers
.map(
(e) => renderContainer(
color: rainbowColors[e % rainbowColors.length], index: e),
)
.toList(),
),
);
}
4) listView.builder() 같은 형태처럼 만들 수 있다.
화면에 보이는 것만 랜더링 하고 퍼포먼스적으로 우수함
// ListView.builder 생성자와 유사함
SliverList renderBuilderSliverList() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length],
index: index,
);
},
// 숫자 제한: 100 개까지만 랜더링 한다.
childCount: 100,
),
);
}
ListView.builder 와 SliverChildBuilderDelegate 와 다른 점은 itemBuilder 라는 함수를 itemBuilder 라는 namedParameter 에 넣어줬지만, SliverList 에서는 첫번째 파라미터에 넣는다.
6. CustomScrollView - SliverGrid 공부!
기본선언
slivers: [
SliverGrid(
delegate: delegate,
gridDelegate: gridDelegate,
// gridDelegate 에는 두개만 기억하면 된다.
// SliverGridDelegateWithMaxCrossAxisExtent() : 최대 넓이 정해서, 넓이 안에서 균등하게 위젯 사이즈 분배한다.
// SliverGridDelegateWithCrossAxisCount() : 가로로 최대 몇개를 분배할지
),
]
1) GridView.count() 와 같은 형태로 만들 수 있다.
데이터 전부다 랜더링 하고 퍼포먼스적으로 떨어짐
// GridView.count 유사함
SliverGrid renderChildSliverGrid() {
return SliverGrid(
delegate: SliverChildListDelegate(
numbers
.map(
(e) => renderContainer(
color: rainbowColors[e % rainbowColors.length],
index: e,
),
)
.toList(),
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
);
}
결과)
2) GridView.builder() 같은 형태처럼 만들 수 있다.
화면에 보이는 것만 랜더링 하고 퍼포먼스적으로 우수함
// GridView.builder 와 비슷함
SliverGrid renderSliverGridBuilder() {
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) {
return renderContainer(
color: rainbowColors[index % rainbowColors.length],
index: index,
);
},
// 숫자 제한
childCount: 100,
),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
// 가로 최대 넓이
maxCrossAxisExtent: 150,
));
}
결과)
7. CustomScrollView - SliverAppBar 공부!
Ios 같은 경우 스크롤의 일부가 되어버렸다.
형태를 마음대로 바꿀수 있다.
// 스크롤 하다가 위로 당기면 앱바가 나온다. default 는 false 이다.
floating: true,
// AppBar
SliverAppBar renderSliverAppBar() {
return SliverAppBar(
// 리스트의 중간에서 위로 당기면 AppBar 가 나오게 설정
floating: true,
// 완전 고정, 기본 AppBar 설정이다.
pinned: false,
// true: 중간에 멈출 수 있다. false: 중간이 없이 올라가거나 내려간다. 단, floating: true 일때만 사용가능
snap: false,
// true: 맨위에서 스크롤 했을때 남는 공간을 차지한다. 단, physics : BouncingPhysics 를 썼을때만 사용가능
stretch: true,
// 최대 사이즈 설정
expandedHeight: 200,
// 최소 사이즈 설정
collapsedHeight: 150,
// AppBar 공간을 차지하는 부분 설정, 앱바의 전체 부분을 차지한다.
flexibleSpace: FlexibleSpaceBar(
background: Image.asset(
'asset/img/image_1.jpeg',
fit: BoxFit.cover,
),
title: Text('FlexibleSpaceBar'),
),
title: Text('CustomScrollViewScreen'),
);
}
8. CustomScrollView - SliverPersistemHeader 공부!
appBar 같은 경우는 맨 위에만 넣을 수 있는데, label , 제목 같은 걸 넣고 싶을때 사용한다.
직접 클래스를 제작해줘야한다!
// SliverPersistentHeaderDelegate 를 상속 해야한다.
// @override 를 4개를 꼭 해줘야한다.
class _SliverFixedHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
// TODO: implement build
throw UnimplementedError();
}
@override
// TODO: implement maxExtent
double get maxExtent => throw UnimplementedError();
@override
// TODO: implement minExtent
double get minExtent => throw UnimplementedError();
@override
//covariant : 상속된 클래스도 사용가능, 이거를(SliverPersistentHeaderDelegate) 상속한 클래스 일수 도있다는 것을 명시해주는것이다.
// 편의상 바꿔준다. 현재 상황에서는 이미 _SliverFixedHeaderDelegate에서 상속을 했으니 다른걸 상속했을 경우는 없다.
// oldDelegate 는 오래된 것을 의미한거다. build 가 될 때 이전인 기존에 존재하던 delegate
// thisDelegate 는 새로운 delegate 이다.
// shouldRebuild - 새로 build 를 해야할지 말지 결정해주는 함수
// false : build 안함, true : build 다시함
// 그러니 특정 조건을 넣어주면 됌. child 값이 바뀌거나 minHeight, maxHeight 가 바뀔때 해주면 됌
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// TODO: implement shouldRebuild
throw UnimplementedError();
}
}
// SliverPersistentHeaderDelegate 를 상속 해야한다.
// @override 를 4개를 꼭 해줘야한다.
class _SliverFixedHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double maxHeight;
final double minHeight;
_SliverFixedHeaderDelegate({
required this.child,
required this.maxHeight,
required this.minHeight,
});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(
child: child,
);
}
@override
// 최대 높이
double get maxExtent => maxHeight;
@override
// 최소 높이
double get minExtent => minHeight;
@override
//covariant : 상속된 클래스도 사용가능, 이거를(SliverPersistentHeaderDelegate) 상속한 클래스 일수 도있다는 것을 명시해주는것이다.
// 편의상 바꿔준다. 현재 상황에서는 이미 _SliverFixedHeaderDelegate에서 상속을 했으니 다른걸 상속했을 경우는 없다.
// oldDelegate 는 오래된 것을 의미한거다. build 가 될 때 이전인 기존에 존재하던 delegate
// thisDelegate 는 새로운 delegate 이다.
// shouldRebuild - 새로 build 를 해야할지 말지 결정해주는 함수
// false : build 안함, true : build 다시함
// 그러니 특정 조건을 넣어주면 됌. child 값이 바뀌거나 minHeight, maxHeight 가 바뀔때 해주면 됌
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.minExtent != minHeight ||
oldDelegate.maxExtent != maxHeight;
}
}
class CustomScrollViewScreen extends StatelessWidget {
final List<int> numbers = List.generate(100, (index) => index);
CustomScrollViewScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
// list 형태의 위젯들을 한번에 쓸수 있게끔 해준다! 하지만 특정 되어있다.
slivers: [
// ios : appBar 가 스크롤이 된다.
renderSliverAppBar(),
renderHeader(),
renderChildSliverGrid(),
renderHeader(),
renderSliverGridBuilder(),
renderHeader(),
renderBuilderSliverList(),
],
),
);
}
// Header
SliverPersistentHeader renderHeader() {
return SliverPersistentHeader(
// 헤더에 쌓인다.
pinned: true,
delegate: _SliverFixedHeaderDelegate(
child: Container(
color: Colors.black,
child: Center(
child: Text(
'내용',
style: TextStyle(color: Colors.white),
),
),
),
minHeight: 75,
maxHeight: 150,
),
);
}
...
}
9. Scrollbar 공부!
웹에서는 스크롤바가 기본적으로 보이는 데, 앱 같은경우에는 스크롤바가 없는 경우가 많다. 하지만 보여줘야한다고 하면 추가해주면된다.
android, ios 둘다 기본적으로 스크롤 바가 안 보인다.
class ScrollbarScreen extends StatelessWidget {
final List<int> numbers = List.generate(100, (index) => index);
ScrollbarScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'ScrollbarScreen',
body: Scrollbar(
child: SingleChildScrollView(
child: Column(
children: numbers
.map(
(e) => renderContainer(
color: rainbowColors[e % rainbowColors.length], index: e),
)
.toList(),
),
),
),
);
}
}
결과)
10. RefreshIndicator 공부!
list에서 로딩상태를 보여줄수 있는 위젯이다.
안드로이드 디폴트
ios 디폴트 바운스!
class RefreshIndicatorScreen extends StatelessWidget {
final List<int> numbers = List.generate(100, (index) => index);
RefreshIndicatorScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'RefreshIndicatorScreen',
body: RefreshIndicator(
onRefresh: () async {
// 서버요청! 대신 딜레이 걸어줌
await Future.delayed(Duration(seconds: 3));
},
child: ListView(
children: numbers
.map(
(e) => renderContainer(
color: rainbowColors[e % rainbowColors.length],
index: e,
),
)
.toList(),
),
),
);
}
}

'⭐️ 개발 > 플러터' 카테고리의 다른 글
데이터 직렬화, 역직렬화 (0) | 2023.07.05 |
---|---|
[프로젝트] 미세먼지 앱 (0) | 2022.12.29 |
[이론] WidgetsFlutterBinding.ensureInitialized(); 란? (0) | 2022.12.22 |
[프로젝트] 일정 스케줄러 (0) | 2022.12.22 |
사느냐 vs 만드냐 (0) | 2022.12.22 |