Flutter

Flutter 튜토리얼 시작하기 - 3.2

일태우 2019. 9. 28. 16:03

저번 Flutter 튜토리얼 시작하기 - 3.1에 이어서 3단계를 진행해보도록 하겠다.

3단계 : Stateful widget 추가하기

Stateless widget은 변경할 수 없다 불변이다, 해당 widget의 속성들은 변경이 불가능하다.

 

Stateful widget은 자신의 생명주기 동안 변경되는 상태를 그대로 유지한다. stateful widget을 구현하는 것은 최소 2개의 클래스가 필요하다. 1) StatefulWidget 클래스와 , 2) State 클래스가 필요하다. StatefulWidget 클래스는 State 클래스의 인스턴스를 구현하고 자기 자신은 변경할 수 없다 하지만 State 클래스는 widget의 생명주기 동안 지속된다.

 

이번 단계에서는, State 클래스인 RandomWordsState를 생성하는 stateful widget인 RandomWords를 추가할 것인데 MyApp stateless widget(main.dart의 My App클래스) 자식으로 RandomWords를 추가해보도록 하자.

 

  1. main.dart의 아래에 state 클래스의 껍데기만 작성해보자
    class RandomWordsState extends State<RandomWords> {
      
    }
    State<RandomWords> 구문은 제네릭 타입으로 RandomWords를 사용하는데, 이는 State 클래스에서 데이터 타입을 RandomWords로 특화한다는 의미이다. 이 클래스는 생성된 단어와 (스크롤을 할 때 무한히 생성) 즐겨 찾는 단어(심장 아이콘을 토글하여 추가 또는 삭제)를 저장한다, RandomWordsState 클래스는 RandomWords 클래스에 의존한다.
  2. main.dart의 아래에 stateful widget인 RandomWords를 작성해보자, RandomWords는 State 클래스를 생성하는 것 외에는 다른 작업은 거의 하지 않는다.
    class RandomWords extends StatefulWidget {
      @override
      RandomWordsState createState() => RandomWordsState();
    }
    1에서 state class를 추가한 직후 Android Studio가 build 메서드의 구현이 빠졌다고 알려주는 걸 볼 수있는데 이제 RandomWordsState 클래스로 다시 돌아가서 단어 쌍을 생성하는 build 메서드를 작성 해 보자
  3. build() 메서드를 RandomWordsState 클래스에 추가하자
    class RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        final wordPair = WordPair.random();
        return Text(wordPair.asPascalCase);
      }
    }
  4. MyApp 클래스로 돌아가서 단어 생성 코드들을 지우자 그리고 RandomWords widget으로 대체 해보자
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        //final wordPair = WordPair.random();
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Welcome to Flutter'),
            ),
            body: Center(
              //child: Text(wordPair.asPascalCase),
              child: RandomWords(),
            ),
          ),
        );
      }
    }
  5. app을 재시작 해보면 별 다른 변경없이 전과 동일하게 작동할 것이다.(hot reload나 프로젝트를 저장했을 때 바뀌는 것도 동일하다)
혹시나 Hot reload 할때 다음의 경고가 표시되면 app를 재시작 해보자

Reloading....
Some program elements were changed during reload but did not run when the view was reassembled; you might need to restart the app (by pressing “R”) for the changes to have an effect.
(몇몇의 프로그램의 요소들은 리로드 중에 바뀌었지만 뷰를 다시 어셈블 할때 실행 되지 않았습니다. 변경 사항을 적용 하기 위해 app을 다시 시작 해야 합니다.)


이런 경고는 잘못된 경고 일 수도 있지만 app을 재시작 하는 것은 변경 사항의 반영을 보장한다.

문제가 생겼다면?

app이 작동이 안 된다면 오타가 있는지 확인을 해보고 다음의 파일이 알맞게 작성됐는지 확인해 보자

  • lib/main.dart
    // Copyright 2018 The Flutter team. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        //final wordPair = WordPair.random();
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Welcome to Flutter'),
            ),
            body: Center(
              //child: Text(wordPair.asPascalCase),
              child: RandomWords(),
            ),
          ),
        );
      }
    }
    
    class RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        final wordPair = WordPair.random();
        return Text(wordPair.asPascalCase);
      }
    }
    
    class RandomWords extends StatefulWidget {
      @override
      RandomWordsState createState() => RandomWordsState();
    }

4단계 : 무한 스크롤 ListView 제작하기

이번 단계에서는 RandomWordsState에 단어들을 생성하고 보여주는 목록을 추가 해보자, 사용자가 스크롤하면 ListView widget에 보여지는 목록이 무한대로 커진다.

ListViewbuilder 팩토리 생성자(Constructor)는 list를 느리게 보여주게 할 수 있다.

  1. 제안된 단어를 저장해 두기 위해 RandomWordsState 클래스에 _suggestions 리스트를 추가하자. 그리고 _biggerFont 변수를 폰트 사이즈를 크게 하기 위해 추가하자
    class RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18.0);
      //...
    }
    식별자의 앞에 _(밑줄 혹은 언더바)는 Dart 언어의 중요한 문법 중 하나인데, _ 가 붙으면 라이브러리 전용(private)이라는 의미이다 자세한 것은 Dart Important concepts를 참고하자.

    다음으로 _buildSuggestions() 메서드를 RandomWordsState 클래스에 추가 해 볼 건데, 이 메서드는 ListView를 빌드한다.

    ListView 클래스는 builder 속성으로 itemBuilder를 제공하는데 해당 속성은 builder의 행위(Factory)callback함수로 구성된 익명 메서드로 작성한다. 2개의 파라미터(BuildContext와 row iterator(행 반복자)인 i)는 메서드에 전달된다. 이 iterator는 0부터 시작하고 메서드가 호출될 때마다 증가하는데, 제안된 모든 단어에 대해 ListTile(단어가 들어가는 item)에 한번, Divider(ListTile을 구분선)에 한번씩 총 두번 증가한다.
  2. _buildSuggestions() 메서드를 RandomWordsState 클래스에 추가하자
     Widget _buildSuggestions() {
        return ListView.builder(
          padding: const EdgeInsets.all(16.0),
          itemBuilder: /*1*/ (context, i) {
            if (i.isOdd) return Divider(); /*2*/
    
            final index = i ~/ 2; /*3*/
            if(index >= _suggestions.length) {
              _suggestions.addAll(generateWordPairs().take(10)); /*4*/
            }
            return _buildRow(_suggestions[index]);
          }
        );
      }
    • /*1*/ itemBuilder 메서드는 제안된 단어마다 한번씩 호출되며 각 단어는 ListTile에 배치된다. i가 짝수일 땐, 메서드는 ListTile를 추가한다. i가 홀수일 땐, 시각적으로 항목(여기서는 ListTile들)을 구분하기 위해 Divider widget을 추가한다. 참고로 이 구분선은 device가 작으면 보기 힘들 수도 있다.
    • /*2*/ i가 짝수일때 1 픽셀 높이를 가지는 Divider widget를 반환하는데 ListView의 각 행 앞에 배치된다.
    • /*3*/ i ~/ 2 구문은 i를 2로 나누고 integer(정수형)으로 결과를 반환한다. 예를들어 1, 2, 3, 4, 5는 0, 1, 1, 2, 2가 반환 된다. indexListView의 현재 단어 개수를 저장하는데 i ~/ 2에 의해 divider widget의 개수는 뺀 결과가 저장된다.
    • /*4*/ index가 _suggestions 리스트의 크기보다 크거나 같을때가 참인데 이 말은 즉 제안된 단어가 10개 일 때 ListView에 10개가 전부 추가 되었을 상황이다. 이런 경우 _suggestions 리스트에 10개의 단어를 더 생성하여 추가한다.
    • The _buildSuggestions() 메서드는 단어 마다 _buildRow() 메서드를 호출하는데 이 메서드는 ListTile widget에 단어를 넣어 반환한다.
  3. _buildRow() 메서드를 RandomWordsState 클래스에 추가하자
      Widget _buildRow(WordPair pair) {
        return ListTile(
          title: Text(
              pair.asPascalCase,
              style: _biggerFont,
          ),
        );
      }
  4. RandomWordsState 클래스로 돌아가서, build()의 내용을 _buildSuggestions()를 사용하도록 바꿔보자 (Scaffold는 기본 Material 디자인 레이아웃을 구현한다)
    class RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18.0);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Startup Name Generator'),
          ),
          body: _buildSuggestions(),
        );
      }
    //....
  5. MyApp 클래스로 돌아가서, build() 메서드의 title를 수정하고, home을 RandomWords widget를 사용하도록 수정 해보자
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        //final wordPair = WordPair.random();
        return MaterialApp(
          title: 'Startup Name Generator',
          home: RandomWords(),
        );
      }
    }
  6. app을 재시작하면, 우리는 얼마나 스크롤하던 문제 없는 단어 리스트를 볼 수 있다!
    완성된 startup namer generator!

문제가 생겼다면?

app이 작동이 안 된다면 오타가 있는지 확인을 해보고 다음의 파일이 알맞게 작성됐는지 확인해 보자

  • lib/main.dart
    // Copyright 2018 The Flutter team. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        //final wordPair = WordPair.random();
        return MaterialApp(
          title: 'Startup Name Generator',
          home: RandomWords(),
        );
      }
    }
    
    class RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18.0);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Startup Name Generator'),
          ),
          body: _buildSuggestions(),
        );
      }
    
      Widget _buildSuggestions() {
        return ListView.builder(
          padding: const EdgeInsets.all(16.0),
          itemBuilder: /*1*/ (context, i) {
            if (i.isOdd) return Divider(); /*2*/
    
            final index = i ~/ 2; /*3*/
            if(index >= _suggestions.length) {
              _suggestions.addAll(generateWordPairs().take(10)); /*4*/
            }
            return _buildRow(_suggestions[index]);
          }
        );
      }
    
      Widget _buildRow(WordPair pair) {
        return ListTile(
          title: Text(
              pair.asPascalCase,
              style: _biggerFont,
          ),
        );
      }
    }
    
    class RandomWords extends StatefulWidget {
      @override
      RandomWordsState createState() => RandomWordsState();
    }

축하합니다!

여기까지 진행하셨으면 우리는 iOS와 Android 둘 환경 모두 작동하는 Flutter app을 제작한겁니다. 여기까지의 진행 과정에서 우리는 다음 항목들을 익힐 수 있었습니다.

  • 아무것도 없는 상태에서 Flutter app 만들기
  • Dart 코드 써보기
  • 외부, third party 라이브러리 활용하기
  • 빠른 개발 사이클을 위해 hot reload를 사용하기
  • stateful widget 구현하기
  • 무한히 스크롤되고 천천히 불러오는 리스트 만들기

여기까지 진행하여 Flutter 튜토리얼 따라하기 글은 마치지만, Google Developers Codelabs에는 이 app을 더욱 확장 시킬 수 있는 파트가 있습니다. 관심있으신 분은 들어가보세요!

 

Google Codelabs

Google Developers Codelabs provide a guided, tutorial, hands-on coding experience. Most codelabs will step you through the process of building a small application, or adding a new feature to an existing application. They cover a wide range of topics such a

codelabs.developers.google.com