본문 바로가기
카테고리 없음

[Flutter]. 10. Flutter tutorial on codelab(6)

by 바른생활머시마 2023. 1. 28.
728x90
반응형

지난 번에 Like 버튼을 추가하였습니다. 물론, Like 표시 된 단어들을 보는 기능은 아직 만들지 않았지만, 의도 된 디자인에 맞게 코드를 수정하는 경험을 통해, 주어진 디자인을 바탕으로 어떻게 코드를 작성/수정하면 좋을지 한번 시도 해 볼 수 있는 좋은 경험이 되는 것 같습니다.

https://learn-and-give.tistory.com/48

 

[Flutter]. 09. Flutter tutorial on codelab(5)

앞에서 화면을 꾸미는 내용을 좀 다뤄봤습니다. https://learn-and-give.tistory.com/47 [Flutter]. 07. Flutter tutorial on codelab(4) 앞에서 UI를 재정의 하는 코드를 삽입하는 방법과 Refactoring 명령으로 Wrapping 하는

learn-and-give.tistory.com

 

목표

 

이번에 해 볼 내용은, 아래 그림처럼, 왼쪽에 Navigation 구역을 만들고, Like를 누르면 Like로 선택 된 단어 목록을 보여주는 기능을 추가 하는 것입니다.

 앞에서처럼, 일단 이 목표 기획을 보고, 어떤 부분을 변경하게 될지 한번 생각 해 봅시다.

  • MyHomePage가 있던 구역을 두 개의 구역으로 나누고 한쪽에는 Navigation을 배치합니다.
  • Favorites에 해당하는 새로운 Page의 추가도 필요하겠네요.
  • Navigation의 버튼 선택에 따라 오른쪽 화면이 교체되는 처리를 하는 방법도 배우게 되겠네요.

 

Contents 구역 수정

자세한 설명 없이 HyHomePage의 내용을 아래 코드로 교체 하라고 되어 있습니다. 교체를 한 후, 설명을 이어서 하는 형식이니 교체를 해보죠.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}


class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

 

코드를 추가하고 나니, 아래와 같은 빨간 표시가 나오는데, 좀 찜찜하죠.

눌러서 무슨 내용인가 살펴보니, 아마도 git과 관련해서 코드가 수정 된 부분을 표시 해 주는 것 같은데, 빨간색이라 무슨 문제가 있는 것 아닌가 좀 신경이 쓰이네요.

일단, 실행 한번 시켜보면 이상 여부를 알 수 있겠다 싶어서 실행시켜보니 아래와 같이 실행은 잘 되네요.

코드를 좀 살펴보면....

기존의 MyHomePage의 내용은 GeneratorPage라는 새로운 Class로 변경되었고, MyHomePage는 Scaffold 위젯으로써, Row 위젯 내부에  SafeArea와 Expanded를 가집니다. SafeArea는 Navigation 역할을 하기 위해 NavigationRail이라는 위젯이 있고, NavigationRailDestination이라는 위젯으로 두 개의 버튼을 가지고 있으며, 이때 선택 된 기본 index와 버튼이 선택 되었을 때 호출되는 콜백 함수가 함께 정의 되어 있습니다. 포함 관계를 정리 해 보면 아래와 같습니다.

이렇게 정리해 놓고 보니 대충 어떻게 만들어졌는지 이해가 되네요.

현재는 콜백함수에서 선택 된 index만 출력하게 되어 있는데 이 코드를 페이지가 전환 되도록 해줘야겠군요. 새로운 페이지를 아직 만들지 않았으니 그 전에는 정상 동작을 하지 않을 것이고..

 

 새로 사용한 위젯에 대한 간단한 설명이 있습니다.

  • SafeArea - 하위 위젯들이 하드웨어 노치나 상태바 등에 가려지지 않고 안전하게(Safe) 보여지도록 해준다고 합니다. 그래서, 그 내부의 버튼들이 있는 구역이 가려지지 않게 됩니다.extended 옵션을 조정하여 버튼 옆에 텍스트 표시 여부를 설정 할 수 있습니다. extended : true로 설정하면 아래와 같이 보이게 됩니다.

extended : true

  • Expanded 위젯을 쓰면, 다른 위젯들은 꼭 필요한 최소한의 영역을 가지도록 설정해주는 위젯입니다. 즉, 크기가 지정 된 구역을 제외하고 나머지를 '탐욕스럽게(greedy)' 확장(expanded)하여 영역을 정합니다. 앱 크기 전체를 가로로 길게 늘이면 좌측의 NavigationRail 구역은 필요한 영역을 그대로 유지하고 남은 구역을 Expanded 위젯으로 감싼 부분이 다 차지가헤 되었습니다.

 

 

 

Stateless versus stateful widgets

추상적인 '상태'라는 말로 진행을 해 오고 있지만, 아직 상태가 정확히 어떤 것을 말하는지, Stateless의 less가 어떤 의미인지 아직 정확히 이해 못하겠는데, 때마침 관련 된 설명이 나오니 한번 확인 해 보겠습니다.

 

설명 된 내용은, 지금까지 MyAppState가 모든 상태를 처리했고, 이것은 지금까지 사용한 모든 위젯들이 stateless이기 때문이라고 합니다. 

 stateless로 작성해서 그런 것 같은데.... 위젯에 상태값이 있느냐 없느냐에 따라 stateless인지 아닌지를 판단한다는 것은 알겠는데, 어떤 위젯을 만들 때 stateless로 할 것인지 여부의 결정은 어떻게 하는 것인지 아직 잘 모르겠네요.위젯을 재정의 하여 거기 state 변수를 추가하면 같은 위젯을 state가 있는 것으로 쓸 수 있지 않나???

 이런 의문점이 들면, 이해가 되었을 때 더 확실히 기억되겠죠.ㅋㅋ

 

 이에 대한 설명을 위해 NavigationRail의 Selected Index 값을 어떻게 유지 할 것인가에 대해서 설명합니다. 

지금까지 해 온 방식을 따르자면, MyAppState에 selectedIndex 변수를 추가하고 거기서 관리하면 될 것 같은데... 어떤 상태는 그냥 위젯 내부에만 있어도 되는 경우가 있다는 소개를 하면서, StatefulWidget이라는 위젯 종류를 소개합니다. Stateless와 상반되는 역할을 하는데, 어떤 경우에 Stateless/Stateful을 쓰는지 이해하면 관련 된 내용들도 이해 될 것 같네요. 아마도, 우리가 모르는 매우 중요한 사항이 있기 때문에 중요하게 다루고 있겠죠??

 

 추측을 해 보자면,... 최적화 때문에 그런 것 아닐까 일단 한번 찍어봅시다. 

 

 수정 방법은 단순히 상속 받는 Class를 변경해주면 되는 것이 아니고, IDE의 기능(Refactoring)으로 Stateful을 상속 받도록 처리해주는 방법을 소개하고 있습니다. 전혀 예상치 못했는데, Stateful을 상속받게되면  State를 다루는 별도의 클래스가 또 추가되네요. 즉, 하나의 Stateless 클래스를 Stateful로 변경하면 StatefulWidge을 상속받은 위젯 클래스 하나와 해당 클래스를  템플릿으로 사용하는 State 클래스를 상속받은 State용 클래스가 추가 됩니다. Build는 State용 클래스에 들어가 있네요. 참고로, 클래스 앞의 밑줄(underscore _ )는 해당 클래스를 private로 설정해주는 dart 문법이라고 합니다.

 

 

 이제 우리가 사용하려던 state인 selected index를 반영하는 방법을 알아보겠습니다.

새로 만들어진 state 클래스의 멤버로 추가(state로 추가한다는 의미겠죠?)하고, 이 state를 설정 할 수 있는 인터페이스를 만들어 주게 됩니다.

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

 음....dart  문법을 정확히 모르는 상태라서 코드만 보고는 조금 아리송한 부분도 있었는데,

 아래 코드에서, 어느 쪽이 state명인지 정확히 알 수가 없었습니다. 

selectedIndex: selectedIndex,

아래 코드 설명에서, 기존에는 숫자 0으로 하드 코딩 되어 있던 것을, 추가 된 속성으로 초기화를 했다고 하니, 추가 된 속성 state는 오른쪽, 즉 초기화에 사용되는 값이고, 왼쪽의 selectedIndex는 NavigationRail이라는 위젯의 고유한 속성값일 것 같습니다. 

setState라는 함수는 콜백함수로 전달 받은 값, 즉 선택 된 index를 이 속성에 넣어주도록 합니다.

 

즉,

이 NavigationRail에 selectedIndex라는 state를 추가하고,

위젯 초기화 할 때 이 state 값에 따라서 화면 표시를 초기화 하고,

Navigation버튼이 눌려지면 그 index를 전달받아 state 속성에 할당

요렇게 정리 되겠네요.

 

설명을 보니, setState에서 state 변수에 값을 할당해주는 이 호출이 앞에서 사용했던 notifyListeners 호출과 동일하게 UI를 업데이트 해주는 효과가 있다고 합니다.

 

 대충 좀 이해되는 느낌?? ㅋㅋ

 

다음에는 이 값을 이용하여 화면이 업데이트 되는 것과 NavigatinoRail의 Text가 앱 화면 크기에 따라 보여졌다 감춰졌다 하는 처리를 어떻게 하는지 살펴보겠습니다.

728x90
반응형

댓글