[Flutter] SingleChildScrollView의 바닥에 버튼 배치하기

다용도리모콘·2022년 3월 1일
0

Wiki - Flutter

목록 보기
7/7

전체가 스크롤되는 페이지의 맨 아래에 버튼을 놓고 싶다.

이런 화면에서 버튼을 맨 아래로 보내고 싶을 때 나는 보통 Spacer()를 사용한다.

SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    const SizedBox(height: 40,),
                    TextFormField(initialValue: '아이디',),
                    TextFormField(initialValue: '패스워드',),
                    const Spacer(),	//여기에 추가.
                    ElevatedButton(
                      onPressed: () {},
                      child: const Text('로그인'),
                    ),
                  ],
                ),
              ),
            );

원했던 레이아웃은 아래와 같은 것인데

현실은...

════════ Exception caught by rendering library ═════════════════════════════════ The following assertion was thrown during performLayout(): RenderFlex children have non-zero flex but incoming height constraints are unbounded. 로 시작하는 긴 에러가 발생한다.

Spacer는 Column 내부에서 허락되는 최대의 크기만큼 늘어나는데 SingleChildScrollView로 인해 무제한으로 늘어나게 되기 때문에 에러가 발생하는 듯하다. SingleChildScrollView를 없애면 일시적으로 문제를 해결한 것처럼 느껴지지만 예제처럼 TextField가 있을 경우 소프트키보드가 올라왔을 때 pixel overflow가 발생할 수 있다.

난감한 pixel overflow;;;

본질적으로 문제를 해결하기 위해서는 Spacer가 적절한 정도까지만 늘어날 수 있게 Column의 크기를 제한하면 될 것 같긴한데 어떤 방법들이 있는지 찾아보았다.

방법 1. Column의 크기를 제한하기.

LayoutBuilder의 contraint를 통해 현재 화면(지금의 경우 스마트폰 화면 전체로 보면 된다.)의 maxHeight를 구해 ContraintBox를 그리고 IntrinsicHeight로 Colum의 높이가 부모에 맞춰지도록 제한하는 방법이다.

stackoverflow에 돌아다니는 MediaQuery로 화면 크기 구해서 Container 크기를 지속적으로 계산하는 방식의 해결방법과 해결 방식은 비슷하다.

다만, IntrinsicHeight는 부모와 자신의 최대, 최소 높이를 적절히 판단해 그려지기 때문에 사용비용이 큰 위젯이다. (참고: https://api.flutter.dev/flutter/widgets/IntrinsicHeight-class.html)

LayoutBuilder(
      builder: (context, constraint) {
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(minHeight: constraint.maxHeight),
            child: IntrinsicHeight(
              child: Column(
                children: const <Widget>[
               		const SizedBox(height: 240,),
               		TextFormField(initialValue: '아이디',),
               		TextFormField(initialValue: '패스워드',),
               		const Spacer(),
               		ElevatedButton(
                 		onPressed: () {},
                 		child: const Text('로그인'),
               		),
                ],
              ),
            ),
          ),
        );
      },
    );

방법 2. CustomScrollView 사용하기.

CustomScrollView의 내부에서 SliverFillRemaining를 사용하는 것이 포인트다. 이 위젯은 viewport(사용자에게 보여지는 화면)에서 남겨진 공간을 채워준다. SliverFillRemaining 내부에 있는 Colum(본질적으로는 Spacer)은 부모 위젯이 viewport 높이만큼만 늘어나기 때문에 거기에 맞춰 적절한 정도로만 늘어나게 된다.

CustomScrollView(
      slivers: [
        SliverFillRemaining(
          hasScrollBody: false,
          child: Column(
            children: const <Widget>[
               const SizedBox(height: 240,),
               TextFormField(initialValue: '아이디',),
               TextFormField(initialValue: '패스워드',),
               const Spacer(),
               ElevatedButton(
                 onPressed: () {},
                 child: const Text('로그인'),
               ),
            ],
          ),
        ),
      ],
    );

정리하며

매번 예제와 같은 레이아웃을 그릴 때마다 이전에 해결했던 방법을 잊고 구글링 하는 것 같아 기록을 위해 포스트를 쓰게 되었다. 기존에는 방법1을 사용해서 문제를 해결했는데 포스트를 쓰기 위해 stackoverflow에서 여러 해결 방법들을 검색해 보다 더 괜찮은 것 같은 방법2를 알게 되어 기록 이상의 수확이 있었다. 혹시 더 나은 방법을 알고 있다면 꼭 댓글로 알려주시길. 포스트에 대한 어떤 의견도 언제나 환영입니다.

후일담(2022.06.08)

SliverFillRemaining 를 까보니 내부에 첫번째 방법에서 사용된 IntrinsicHeight가 존재했다. 결국 둘다 같은 방식으로 작동하고 있었던 것... 둘 중 직관적인 방법을 사용하면 될 것 같다.

0개의 댓글