이번 글에서는 Tab & Page View 사용시 불필요한 랜더링을 없앨 수 있는 방법에 대해서 알아보도록 하자.
탭뷰로 UI를 생성해보면, 탭 전환시에 화면의 스크롤 포지션들이 유지되지 못하고 초기화되는 것을 확인할 수 있다.
스크롤 포지션이 유지되지 못하고 초기화되는 이유는 화면이 뷰에서 사라질 때에 Dispose 되었다가, 다시 뷰가 보여질 때에 화면을 랜더링하기 때문이다.
이런 부분을 해결하기 위해 위젯의 key에 PageStorageKey를 생성해서 넣어주게 되면 스크롤 포지션이 유지되지만, 문제는 여전히 랜더링을 다시 한다는 이슈는 남게된다.
랜더링이 다시 되는지 테스트를 해보도록 하자.
테스트를 하기 위해 100개의 이미지를 불러와서 보여주는 코드를 먼저 작성하도록 하자.
Stateful 위젯으로 생성하여 initState에서 이미지 100개를 불러오도록 하자.
List<String> images = [];
void initState() {
super.initState();
_fetchPost();
}
Future<void> _fetchPost() async {
http.Response _response = await http
.get(Uri.parse("https://picsum.photos/v2/list?page=0&limit=100"));
if (_response.statusCode == 200) {
List<dynamic> _data = json.decode(_response.body);
List<String> _images =
_data.map((e) => e["download_url"].toString()).toList();
setState(() {
images = _images;
});
}
}
TabController 위젯으로 탭뷰를 생성해 주었다.
build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: appBar(title: "Tabbar Performance"),
body: TabBarView(
children: [
// 이미지 탭
Container(),
],
),
),
);
}
Widget
이미지 탭의 코드이다. 단순히 네트워크 이미지를 보여주는 UI이다.
ListView.builder(
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
width: MediaQueryData.fromWindow(window).size.width / 2,
height: MediaQueryData.fromWindow(window).size.width / 2,
child: Image.network(_url(widget.images[index]),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: CircularProgressIndicator(
color: Colors.deepOrange,
),
);
},
),
);
})
위젯이 랜더링 될때에 로그를 출력해 보자.
String _url(String url) {
print(url);
return url;
}
처음에 5번까지 이미지가 보여지다가 화면을 전환하고 다시 돌아오면 랜더링이 다시 되는 것을 확인할 수 있다.
이번에는 ListView에 PageStorageKey를 추가하여 스크롤이 유지될 수 있게 변경해보자.
key: const PageStorageKey("velog/sample/performance/list_builder"),
ListView를 마지막까지 다 보고 화면을 전환시켜 보자. 스크롤이 유지되면서 100개의 이미지를 다시 랜더링하는 것을 확인할 수 있다.
불필요한 랜더링을 없애기 위해 StatefulWidget으로 다시 생성해주자.
class PerformanceImageTabWidget extends StatefulWidget {
final List<String> images;
const PerformanceImageTabWidget({
super.key,
required this.images,
});
State<PerformanceImageTabWidget> createState() =>
_PerformanceImageTabWidgetState();
}
class _PerformanceImageTabWidgetState extends State<PerformanceImageTabWidget> {
Widget build(BuildContext context) {
return ListView.builder(
key: const PageStorageKey("velog/sample/performance/list_builder"),
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
width: MediaQueryData.fromWindow(window).size.width / 2,
height: MediaQueryData.fromWindow(window).size.width / 2,
child: Image.network(
_url(widget.images[index]),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: CircularProgressIndicator(
color: Colors.deepOrange,
),
);
},
),
);
});
}
String _url(String url) {
print(url);
return url;
}
}
AutomaticKeepAliveClientMixin을 mixin해주게 되면 리스트 뷰의 페이지 상태 값을 저장하여 랜더링을 하지 않고 유지하게 된다.
wantKeepAlive를 재정의 해야하는데, true면 페이지 상태 값이 유지가 된다.
class _PerformanceImageTabWidgetState extends State<PerformanceImageTabWidget>
with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true;
간단하게 불필요한 랜더링을 줄이는 방법에 대해서 살펴 봤는데, ListView를 생성할 때에 무조건 적으로 AutomaticKeepAliveClientMixin을 사용하여야 하는건 아니다. 탭뷰나 페이지 뷰와 같은 위젯의 랜더링을 최소화하기 위할 때에만 사용하는 것이 좋다.