HyunJun 기술 블로그

Scrollable Widgets) SingleChildScrollView, List View, Grid View 본문

Flutter

Scrollable Widgets) SingleChildScrollView, List View, Grid View

공부 좋아 2023. 7. 25. 14:25
728x90
반응형

Scrollable Widgets

기본적으로 플러터에서는 스크롤 가능한 위젯으로 위젯을 구현하지 않으면 모바일 기기의 세로 화면을 넘어서게 되면 에러가 발생하게 된다. 해서 스크롤을 구현하기 위해서는 Scrollable Widgets를 사용해서 구현해야 한다.

 

1. SingleChildScrollView

하나의 자식 위젯 내에 있는 위젯들을 수직으로 스크롤 가능하게 구현하기 위해 사용하는 위젯이다.

 

 

여러 개의 컨테이너의 색상을 담을 colors.dart 파일을 작성한다.

import 'package:flutter/material.dart';

const rainbowColors = [
  Colors.red,
  Colors.orange,
  Colors.yellow,
  Colors.green,
  Colors.blue,
  Colors.indigo,
  Colors.purple,
];

 

기본적으로 사용되는 MainLayout을 main_layout.dart에 구현한다.

import 'package:flutter/material.dart';

class MainLayout extends StatelessWidget {
  final String title;
  final Widget body;

  const MainLayout({
    super.key,
    required this.title,
    required this.body,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: body,
    );
  }
}

 

SingleChildScrollView에 빨, 주, 노, 초, 파, 남, 보 색상을 map으로 돌려 renderContainer를 통해 컨테이너를 생성하는 코드를 구현한다.

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/const/colors.dart';
import 'package:scrollable_widgets/layout/main_layout.dart';

class SingleChildScrollViewScreen extends StatelessWidget {
  const SingleChildScrollViewScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // MainLayout을 통해 Scaffold 형식의 위젯을 구현하고 body로 renderSingleChildScrollView()를 넘긴다.
    return MainLayout(
        title: "SingleChildScrollView",
        body: renderSingleChildScrollView()
    );
  }

  // SingleChildScrollView 위젯 렌더링 메서드를 구현한다.
  Widget renderSingleChildScrollView(){
    return SingleChildScrollView(
      child: Column(
        children: rainbowColors.map(
              (e) => renderContainer(
            color: e,
          ),
        ).toList(),
      ),
    );
  }

  // 색상을 받아 컨테이너를 리턴하는 렌더 컨테이너 메서드를 구현한다.
  Widget renderContainer({
    required Color color,
  }) {
    return Container(
      height: 300,
      color: color,
    );
  }
}

1) clipBehavior

자식 위젯 즉, Scrollable Widgets 안에 배치한 위젯들이 부모 위젯의 경계를 넘어갔을 때 발생하는 오버플로우(overflow) 상황에서 어떻게 처리할지 결정하는 속성이다.

 

  • Clip.none: 자식 위젯이 부모 위젯의 경계를 넘어가더라도 클리핑 하지 않고 그대로 나타낸다. 따라서 오버플로우가 발생할 수 있다.
  • Clip.antiAlias: 자식 위젯이 부모 위젯의 경계를 넘어가면 경계를 기준으로 안티앨리어싱(antialiasing)을 적용하여 부드럽게 잘라낸다.
  • Clip.antiAliasWithSaveLayer: 자식 위젯이 부모 위젯의 경계를 넘어가면 안티앨리어싱과 함께 레이어를 저장하여 부드럽게 잘라낸다.
  • Clip.hardEdge: 자식 위젯이 부모 위젯의 경계를 넘어가면 경계를 기준으로 각진 모서리를 가지고 잘라낸다.

 

 

컨테이너 1개를 렌더링 하는 메서드를 구현한다.

  Widget renderClip(){
    return SingleChildScrollView(
      physics: AlwaysScrollableScrollPhysics(),
      child: Column(
        children: [
          renderContainer(color: Colors.black)
        ],
      ),
    );
  }

 

 

기존의 renderSingleChildScrollView를 renderClip()로 변경한다.

@override
  Widget build(BuildContext context) {
    return MainLayout(
      title: "SingleChildScrollView",
      body: renderClip()
    );
  }

 

별다른 설정이 없다면 해당 Scrollable Widget 안에서 스크롤 시 자식 위젯이 잘리면서 스크롤이 된다.

 

이때, Clip.none을 설정해 주게 되면,

  Widget renderClip(){
    return SingleChildScrollView(
      clipBehavior: Clip.none,
      physics: AlwaysScrollableScrollPhysics(),
      child: Column(
        children: [
          renderContainer(color: Colors.black)
        ],
      ),
    );
  }

아래처럼 잘리지 않고 해당 자식 위젯의 사이즈가 유지되게 된다.

2) physics

스크롤 가능한 위젯들의 스크롤 동작을 어떻게 처리할지를 지정하는 속성이다.

 

  • BouncingScrollPhysics: iOS 스타일의 스크롤 동작을 제공하며, 스크롤이 경계에 닿을 때 바운스 효과를 가진다.
  • ClampingScrollPhysics: 스크롤이 경계에 닿을 때 최대값 또는 최소값으로 고정시킨다.
  • AlwaysScrollableScrollPhysics: 언제나 스크롤이 가능하도록 한다.
  • NeverScrollableScrollPhysics: 스크롤이 절대로 불가능하도록 한다.
  • PageScrollPhysics: 페이지별 스크롤 동작을 제공한다.

 

physics를 설정하지 않을 시 기본값으로 AlwaysScrollableScrollPhysics로 설정되지만, ios는 기본적으로 BouncingScrollPhysics 스타일로 기본으로 적용되기 때문에 physics를 설정하지 않으면 ios는 BouncingScrollPhysics, 안드로이드는 ClampingScrollPhysics 방식으로 동작한다. 명시적으로 설정을 해주면 운영체제 상관없이 해당 방식으로 고정 가능하다.

 

(왼쪽) ClampingScrollPhysics, (오른쪽) BouncingScrollPhysics

3) 성능

SingleChildScrollView는 자식 위젯을 한꺼번에 렌더링 하려고 하기 때문에, 자식 위젯 안에 위젯이 많아지거나 위젯이 복잡해지면 렌더링 속도라던지, 메모리 사용량 등, 성능에 영향을 줄 수 있다. 예를 들어 자식 위젯으로 50000개의 위젯을 가지고 있다면 해당 50000개의 위젯이 생성되고 렌더링 되기 전까지 해당 화면이 뜨지 않는다. 예시로 30000개 컨테이너 위젯을 포함한 SingleChildScrollView를 실행시켜 보았다.

 

2. List View

1) 기본적인 ListView

ListView는 SingleChildScrollView와 다르게 기본적으로 child가 아닌 children을 매개변수로 받는다. 하지만 아래처럼 ListView를 간단하게 구현 시 모든 위젯을 한 번에 렌더링 하는 단점은 여전히 가지고 있다.

class ListViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  ListViewScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return MainLayout(
        title: "ListViewScreen",
        body: renderDefault()
    );
  }

  Widget renderDefault() {
    return ListView(
        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) ListView.builder

이때, ListView.builder와 itemCount, itemBuilder를 통해 구현을 하게 되면

  Widget renderBuilder() {
    return ListView.builder(
        itemCount: 100,
        itemBuilder: (context, index) {
          return renderContainer(
              color: rainbowColors[index % rainbowColors.length], index: index);
        });
  }
@override
  Widget build(BuildContext context) {
    return MainLayout(
      title: "ListViewScreen",
      body: renderBuilder(),
    );
  }

 

아래처럼 매우 효율적으로, 보이는 부분만 렌더링을 하게 되며, 화면에서 안 보이게 되면 위젯을 지우고 보이는 순간에 위젯을 만들어서 렌더링을 하게 된다. 이때 자연스럽게 스크롤이 되기 위해 보이는 부분에서 조금 더 불러오게 된다.

 

 

3) ListView.separated

기본적으로 ListView.builder와 똑같이 동작하지만, separatorBuilder가 추가되고, 각각의 List Item 사이에 위젯을 추가할 수 있다.

@override
  Widget build(BuildContext context) {
    return MainLayout(
      title: "ListViewScreen",
      body: renderSeparated(),
    );
  }
Widget renderSeparated() {
    return ListView.separated(
      itemCount: 100,
      itemBuilder: (context, index) {
        return renderContainer(
            color: rainbowColors[index % rainbowColors.length], index: index);
      },
      separatorBuilder: (context, index) {
        return renderContainer(
          color: Colors.white,
          index: index,
          height: 50,
        );
      },
    );
  }

 

n 개의 Item마다 광고를 띄운다던지, 간격을 준다던지 할 때 유용하게 사용할 수 있다.

  Widget renderSeparated() {
    return ListView.separated(
      itemCount: 100,
      itemBuilder: (context, index) {
        return renderContainer(
            color: rainbowColors[index % rainbowColors.length], index: index);
      },
      separatorBuilder: (context, index) {
        if (index != 0 && index % 5 == 0) {
          return renderContainer(
            color: Colors.black,
            index: index,
            height: 500,
          );
        }

        return renderContainer(
          color: Colors.white,
          index: index,
          height: 10.0,
        );
      },
    );
  }

 

3. GridView

ListView가 1차원적으로 한 방향으로 정렬을 했다면 Grid View는 2차원적인 정렬이 가능한 Scrollable Widget이다.

1) GridView.count()

일반적인 ListView 혹은 SingleChildScrollView를 사용했던 것처럼 모든 위젯을 한꺼번에 생성하고 렌더링 한다.

 

  • crossAxisCount: 메인 축(현재는 세로)의 반대인 크로스 축(현재는 가로)에 몇 개까지 배치할 것인지 지정
  • crossAxisSpacing: 크로스 축 아이템(가로 아이템 사이)들 간의 간격을 지정
  • mainAxisSpacing: 메인 축 아이템(세로 아이템 사이)들 간의 간격을 지정
import 'package:flutter/material.dart';
import 'package:scrollable_widgets/const/colors.dart';
import 'package:scrollable_widgets/layout/main_layout.dart';

class GridViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  GridViewScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return MainLayout(title: "GridViewScreen", body: renderCount());
  }

  // 위젯을 한번에 다 그린다.
  Widget renderCount() {
    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()

itemCount 매개변수를 전달할지 않을 시, 무한히 위젯이 그려진다.

  @override
  Widget build(BuildContext context) {
    return MainLayout(title: "GridViewScreen", body: renderBuilderCrossAxisCount());
  }
  // 보여지는 위젯들만을 생성 및 렌더링한다.
  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,
        );
      },
      itemCount: 100,
    );
  }

gridDelegate에 SliverGridDelegateWithFixedCrossAxisCount를 전달하고, itemBuilder를 사용하며, itemCount로 총 생성할 위젯 수를 지정할 수 있다.

 

 

3) SliverGridDelegateWithMaxCrossAxisExtent()

crossAxisCount를 통해 크로스 축의 아이템 수를 정하는 것이 아닌, GridView Item 들이 가로(cross 축)로 최대 maxCorssAxisExtent 만큼 차지하도록 하여 자동으로 레이아웃을 지정해 준다.

  @override
  Widget build(BuildContext context) {
    return MainLayout(title: "GridViewScreen", body: renderMaxExtent());
  }
  Widget renderMaxExtent() {
    return GridView.builder(
      gridDelegate:
          SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100),
      itemBuilder: (context, index) {
        return renderContainer(
          color: rainbowColors[index % rainbowColors.length],
          index: index,
        );
      },
      // itemCount: 100,
    );
  }

728x90
반응형
Comments