Flutter로 멋진 UI 빌드

Flutter는 단일 코드베이스에서 멋진 네이티브 컴파일 모바일 애플리케이션, 웹 애플리케이션, 데스크톱 애플리케이션을 빌드할 수 있는 Google의 UI 도구 키트입니다. 이 Codelab에서는 Android, iOS, 웹(선택사항) 기반의 간단한 채팅 애플리케이션을 만듭니다.

이 Codelab에서는 첫 번째 Flutter 앱 작성 1부2부보다 더 심층적으로 Flutter에 관해 살펴봅니다. Flutter에 관해 좀 더 기본적인 사항을 알아보려는 경우 첫 번째 Flutter 앱 작성부터 시작하세요.

학습할 내용

  • Android와 iOS 모두에서 자연스럽게 보이는 Flutter 앱을 작성하는 방법
  • Android 스튜디오 및 IntelliJ용 Flutter 플러그인에서 지원하는 여러 단축키를 활용하여 Android 스튜디오 IDE를 사용하는 방법
  • Flutter 앱을 디버그하는 방법
  • 에뮬레이터, 시뮬레이터, 기기에서 Flutter 앱을 실행하는 방법

이 Codelab에서 학습하고 싶은 내용은 무엇인가요?

이 주제를 처음 접하므로 개요를 파악하고 싶습니다. 이 주제에 관해 약간 알고 있지만 한 번 더 확인하고 싶습니다. 프로젝트에 사용할 코드 예를 찾고 있습니다. 특정 항목에 관한 설명을 찾고 있습니다.

이 Codelab을 완료하려면 Flutter SDK(다운로드) 및 편집기(구성)라는 두 가지 소프트웨어가 필요합니다. 이 Codelab에서는 Android 스튜디오를 사용한다고 가정합니다. 그러나 원하는 편집기를 사용할 수 있습니다.

다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.

  • 컴퓨터에 연결되어 있으며 개발자 모드로 설정된 실제 기기(Android 또는 iOS)
  • Android Emulator
  • iOS 시뮬레이터
  • Chrome 브라우저
  • Windows, macOS 또는 Linux 데스크톱(Flutter의 데스크톱 지원을 사용 설정한 경우)

Android에서 실행하는 경우 Android 스튜디오에서 몇 가지를 설정해야 합니다. iOS에서 실행하는 경우 Mac에 Xcode가 설치되어 있어야 합니다. 자세한 내용은 편집기 설정을 참고하세요.

간단한 템플릿 Flutter 앱을 만듭니다. 이 시작 앱을 수정하여 완성된 앱을 만듭니다.

b2f84ff91b0e1396.pngAndroid 스튜디오를 실행합니다.

  1. 열려 있는 프로젝트가 없다면 시작 페이지에서 Start a new Flutter app을 선택합니다. 이미 프로젝트가 열려 있다면 File > New > New Flutter Project를 선택합니다.
  2. 프로젝트 유형으로 Flutter Application을 선택하고 Next를 클릭합니다.
  3. Flutter SDK 경로가 SDK의 위치를 지정하는지 확인합니다. (텍스트 필드가 비어 있다면 Install SDK를 선택합니다.)
  4. 프로젝트 이름으로 friendly_chat을 입력하고 Next를 클릭합니다.
  5. Android 스튜디오에서 추천하는 기본 패키지 이름을 사용하고 Next를 클릭합니다.
  6. Finish를 클릭합니다.
  7. Android 스튜디오가 SDK를 설치하고 프로젝트를 만들 때까지 기다립니다.

b2f84ff91b0e1396.png또는 명령줄에서 Flutter 앱을 만듭니다.

$ flutter create friendly_chat
$ cd friendly_chat
$ dart migrate --apply-changes
$ flutter run

문제가 있나요?

간단한 템플릿 앱을 만드는 방법에 관한 자세한 내용은 테스트 드라이브 페이지를 참고하세요. 또는 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

이 섹션에서는 기본 샘플 앱을 수정하여 채팅 앱으로 만듭니다. 목표는 Flutter를 사용하여 다음과 같은 기능을 갖춘 간단하고 확장 가능한 채팅 앱인 FriendlyChat을 빌드하는 것입니다.

  • 앱이 실시간으로 문자 메시지를 표시합니다.
  • 사용자는 텍스트 문자열 메시지를 입력하고 Return 키 또는 Send 버튼을 눌러 메시지를 보낼 수 있습니다.
  • UI는 Android 및 iOS 기기는 물론 웹에서도 실행됩니다.

DartPad에서 완성된 앱을 사용해 보세요.

기본 앱 Scaffold 만들기

추가하는 첫 번째 요소는 앱의 정적 제목을 표시하는 간단한 앱 바입니다. 이 Codelab의 다음 섹션을 진행하면서 점진적으로 더 많은 반응형 및 스테이트풀(Stateful) UI 요소를 앱에 추가합니다.

main.dart 파일은 Flutter 프로젝트의 lib 디렉터리에 있으며 앱 실행을 시작하는 main() 함수를 포함하고 있습니다.

b2f84ff91b0e1396.pngmain.dart의 모든 코드를 다음 코드로 대체합니다.

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'FriendlyChat',
      home: Scaffold(
        appBar: AppBar(
          title: Text('FriendlyChat'),
        ),
      ),
    ),
  );
}

cf1e10b838bf60ee.png 유용한 정보

  • 명령줄 앱이든 AngularDart 앱이든 Flutter 앱이든 상관없이 Dart 프로그램은 모두 main() 함수로 시작합니다.
  • main()runApp() 함수 정의는 자동으로 생성된 앱과 동일합니다.
  • runApp() 함수는 Flutter 프레임워크가 런타임 시 확장하여 화면에 표시하는 Widget을 인수로 사용합니다.
  • 이 채팅 앱은 UI에 머티리얼 디자인 요소를 사용하므로 MaterialApp 객체를 만들어 runApp() 함수에 전달합니다. MaterialApp 위젯은 앱 위젯 트리의 루트가 됩니다.
  • home 인수는 앱에서 사용자에게 표시되는 기본 화면을 지정합니다. 이 경우 기본 화면은 단순한 AppBar를 하위 위젯으로 갖는 Scaffold 위젯으로 구성됩니다. 이는 머티리얼 앱에서 일반적입니다.

b2f84ff91b0e1396.png편집기에서 Run 아이콘 6869d41b089cc745.png을 클릭하여 앱을 실행합니다. 앱을 처음 실행하면 어느 정도 시간이 걸릴 수 있습니다. 이후 단계에서는 앱이 더 빠르게 실행됩니다.

febbb7a3b70462b7.png

앱을 실행하면 다음과 같이 표시됩니다.

Pixel 3XL

iPhone 11

채팅 화면 빌드

대화형 구성요소의 기반을 마련하기 위해 이 간단한 앱을 서로 다른 두 개의 위젯 서브클래스로 나눕니다. 즉, 절대 변경되지 않는 루트 수준 FriendlyChatApp 위젯 그리고 메시지를 전송할 때와 내부 상태가 변경될 때 다시 빌드되는 하위 ChatScreen 위젯으로 나눕니다. 지금은 이 두 클래스 모두 StatelessWidget을 확장할 수 있습니다. 나중에 ChatScreen스테이트풀(Stateful) 위젯으로 수정합니다. 이렇게 하면 필요에 따라 상태를 변경할 수 있습니다.

b2f84ff91b0e1396.png다음과 같이 FriendlyChatApp 위젯을 만듭니다.

  1. main() 내에서 MaterialAppM 앞쪽에 커서를 놓습니다.
  2. 마우스 오른쪽 버튼으로 클릭하고 Refactor > Extract > Extract Flutter Widget을 선택합니다.

a133a9648f86738.png

  1. ExtractWidget 대화상자에 FriendlyChatApp을 입력하고 Refactor 버튼을 클릭합니다. MaterialApp 코드가 FriendlyChatApp이라는 새로운 스테이트리스(Stateless) 위젯에 배치되며, main() 함수가 runApp() 함수 호출 시 이 클래스를 호출하도록 업데이트됩니다.
  2. home: 뒤에 있는 텍스트 블록을 선택합니다. 이 텍스트 블록은 Scaffold(로 시작해서 Scaffold를 닫는 괄호인 )로 끝납니다. 끝 쉼표는 포함하지 않습니다.
  3. ChatScreen,을 입력하다 보면 표시되는 팝업에서 ChatScreen()을 선택합니다. (노란색 원 안에 등호로 표시된 ChatScreen 항목을 선택합니다. 그러면 상수가 아닌 빈 괄호가 있는 클래스가 제공됩니다.)

b2f84ff91b0e1396.png다음과 같이 스테이트리스(Stateless) 위젯인 ChatScreen을 만듭니다.

  1. FriendlyChatApp 클래스의 27번 줄 근처에서 stless를 입력하기 시작합니다. 편집기에 Stateless 위젯을 만들 것인지 묻는 메시지가 표시됩니다. Return 키를 눌러 수락합니다. 그러면 상용구 코드가 표시되고 스테이트리스(Stateless) 위젯의 이름을 입력할 수 있도록 커서가 배치됩니다.
  2. ChatScreen을 입력합니다.

b2f84ff91b0e1396.png다음과 같이 ChatScreen 위젯을 업데이트합니다.

  1. ChatScreen 위젯 내에서 Container를 선택하고 Scaffold를 입력하기 시작합니다. 표시되는 팝업에서 Scaffold를 선택합니다.
  2. 괄호 안에 커서를 놓습니다. Return 키를 눌러 새 줄을 시작합니다.
  3. appBar,를 입력하다가 표시되는 팝업에서 appBar:를 선택합니다.
  4. appBar: 뒤에 AppBar,를 입력하다가 표시되는 팝업에서 AppBar 클래스를 선택합니다.
  5. 괄호 안에서 title,을 입력하다가 표시되는 팝업에서 title:을 선택합니다.
  6. title: 뒤에 Text,를 입력하다가 Text 클래스를 선택합니다.
  7. Text의 상용구 코드에는 data라는 단어가 포함되어 있습니다. data 뒤에 있는 첫 번째 쉼표를 삭제합니다. data,를 선택하여 'FriendlyChat'으로 바꿉니다. (Dart는 작은따옴표 또는 큰따옴표를 지원하지만 텍스트에 이미 작은따옴표가 포함되어 있는 경우가 아니라면 작은따옴표를 선호합니다.)

코드 창의 오른쪽 위 모서리를 살펴봅니다. 녹색 체크표시가 있다면 코드가 분석을 통과한 것입니다. 축하합니다.

cf1e10b838bf60ee.png 유용한 정보

이 단계에서는 다음과 같은 Flutter 프레임워크의 주요 개념을 몇 가지 소개합니다.

  • build() 메서드에서 위젯으로 표시되는 사용자 인터페이스의 일부를 설명합니다. 프레임워크는 이러한 위젯을 위젯 계층 구조에 삽입할 때와 종속 항목이 변경될 때 FriendlyChatAppChatScreenbuild() 메서드를 호출합니다.
  • @override는 태그가 지정된 메서드가 슈퍼클래스의 메서드를 재정의한다는 것을 나타내는 Dart 주석입니다.
  • ScaffoldAppBar와 같은 일부 위젯은 머티리얼 디자인 앱에만 적용됩니다. Text와 같은 다른 위젯은 일반적이며 어떤 앱에서든 사용할 수 있습니다. Flutter 프레임워크에 있는 다양한 라이브러리의 위젯은 호환 가능하며 단일 앱에서 함께 작동할 수 있습니다.
  • main() 메서드를 간소화하면 핫 리로드가 사용 설정됩니다. 핫 리로드는 main()을 다시 실행하지 않기 때문입니다.

b2f84ff91b0e1396.png핫 리로드 48583acd5d1a5e12.png 버튼을 클릭하면 변경사항을 거의 즉시 확인할 수 있습니다. UI를 별도의 클래스로 나누고 루트 위젯을 수정해도 UI에 눈에 띄는 변화가 보이지 않습니다.

문제가 있나요?

앱이 올바르게 실행되지 않는다면 오타가 있는지 확인합니다. 필요한 경우 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

이 섹션에서는 사용자가 채팅 메시지를 입력하고 보낼 수 있는 사용자 컨트롤을 빌드하는 방법을 알아봅니다.

64fd9c97437a7461.png

기기에서 텍스트 필드를 클릭하면 소프트 키보드가 표시됩니다. 사용자는 소프트 키보드에서 비어 있지 않은 문자열을 입력하고 Return 키를 눌러 채팅 메시지를 보낼 수 있습니다. 또는 입력란 옆에 있는 그래픽 Send 버튼을 눌러 입력한 메시지를 보낼 수 있습니다.

지금은 메시지 작성을 위한 UI가 채팅 화면 상단에 있습니다. 그러나 다음 단계에서 메시지를 표시하기 위한 UI를 추가한 후 메시지 작성 UI를 채팅 화면 하단으로 이동합니다.

대화형 텍스트 입력란 추가

Flutter 프레임워크는 TextField라는 머티리얼 디자인 위젯을 제공합니다. 이 위젯은 입력란의 동작을 맞춤설정하기 위한 속성이 있는 StatefulWidget(변경 가능한 상태가 있는 위젯)입니다. State는 위젯이 빌드될 때 동기식으로 읽을 수 있으며 위젯의 전체 기간 동안 변경될 수 있는 정보입니다. FriendlyChat 앱에 첫 번째 스테이트풀(Stateful) 위젯을 추가하려면 몇 가지를 수정해야 합니다.

b2f84ff91b0e1396.png다음과 같이 ChatScreen 클래스를 스테이트풀(Stateful)로 변경합니다.

  1. class ChatScreen extends StatelessWidget 줄에서 ChatScreen을 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시합니다.
  3. 메뉴에서 Convert to StatefulWidget을 선택합니다. 클래스가 상태 관리를 위한 새로운 _ChatScreenState 클래스를 포함하는 스테이트풀(Stateful) 위젯의 상용구 코드로 자동으로 업데이트됩니다.

텍스트 필드와의 상호작용을 관리하려면 입력란의 내용을 읽고 채팅 메시지 전송 후 필드를 지우는 데 TextEditingController 객체를 사용합니다.

b2f84ff91b0e1396.pngTextEditingController_ChatScreenState.에 추가합니다.

_ChatScreenState 클래스의 첫 번째 줄로 다음 코드를 추가합니다.

final _textController = TextEditingController();

앱에 상태를 관리할 수 있는 기능이 있으므로 이제 입력란 및 Send 버튼이 있는 _ChatScreenState 클래스를 빌드할 수 있습니다.

b2f84ff91b0e1396.png다음과 같이 _buildTextComposer 함수를 _ChatScreenState에 추가합니다.

  Widget _buildTextComposer() {
    return  Container(
        margin: EdgeInsets.symmetric(horizontal: 8.0),
      child: TextField(
        controller: _textController,
        onSubmitted: _handleSubmitted,
        decoration: InputDecoration.collapsed(
            hintText: 'Send a message'),
      ),
    );
  }

cf1e10b838bf60ee.png 유용한 정보

  • Flutter에서 위젯의 스테이트풀(Stateful) 데이터는 State 객체에 캡슐화됩니다. 그런 다음, State 객체는 StatefulWidget 클래스를 확장하는 위젯과 연결됩니다.
  • 위의 코드는 TextField 위젯이 구성된 Container 위젯을 반환하는 비공개 메서드인 _buildTextComposer()를 정의합니다.
  • Container 위젯은 화면의 가장자리와 입력란의 각 측면 사이에 가로 여백을 추가합니다.
  • EdgeInsets.symmetric에 전달된 단위는 기기의 픽셀 비율에 따라 특정 수의 실제 픽셀로 변환되는 논리 픽셀입니다. 이와 상응하는 Android 용어(밀도 독립형 픽셀) 또는 iOS 용어(포인트)가 더 익숙할 수 있습니다.
  • onSubmitted 속성은 비공개 콜백 메서드인 _handleSubmitted()를 제공합니다. 처음에 이 메서드는 필드를 지우기만 합니다. 그러나 나중에는 이 메서드를 확장하여 채팅 메시지를 보냅니다.
  • TextEditingController가 있는 TextField를 사용하면 텍스트 필드를 제어할 수 있습니다. 이 컨트롤러는 필드를 지우고 값을 읽습니다.

b2f84ff91b0e1396.png다음과 같이 텍스트 컨트롤러를 지우는 _handleSubmitted 함수를 _ChatScreenState에 추가합니다.

  void _handleSubmitted(String text) {
    _textController.clear();
  }

텍스트 작성기 위젯 추가

b2f84ff91b0e1396.png_ChatScreenState.build() 메서드를 업데이트합니다.

다음과 같이 appBar: AppBar(...) 줄 뒤에 body: 속성을 추가합니다.

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

cf1e10b838bf60ee.png 유용한 정보

  • _buildTextComposer 메서드는 텍스트 입력란을 캡슐화하는 위젯을 반환합니다.
  • _buildTextComposerbody 속성에 추가하면 앱에 텍스트 입력 사용자 컨트롤이 표시됩니다.

b2f84ff91b0e1396.png앱을 핫 리로드합니다. 다음과 비슷한 화면이 표시됩니다.

Pixel 3XL

iPhone 11

반응형 Send 버튼 추가

다음으로, 텍스트 필드 오른쪽에 Send 버튼을 추가합니다. 이때 레이아웃에 좀 더 많은 구조가 추가됩니다.

b2f84ff91b0e1396.png다음과 같이 _buildTextComposer 함수에서 Row: 안에 TextField를 래핑합니다.

  1. _buildTextComposer에서 TextField를 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시하여 Wrap with widget을 선택합니다. 그러면 TextField를 래핑하는 새 위젯이 추가됩니다. 자리표시자 이름이 선택되며, IDE가 새 자리표시자 이름이 입력될 때까지 기다립니다.
  3. Row,를 입력하다가 표시되는 목록에서 Row를 선택합니다. Row의 생성자에 관한 정의가 포함된 팝업이 표시됩니다. child 속성에는 빨간색 테두리가 있으며 Analyzer가 필수 children 속성이 누락되었음을 알려줍니다.
  4. child 위로 마우스를 가져가면 팝업이 표시됩니다. 팝업에 속성을 children으로 변경할지 묻는 메시지가 표시됩니다. 그에 맞는 옵션을 선택합니다.
  5. children 속성은 단일 위젯이 아닌 목록을 사용합니다. (지금은 목록에 항목이 하나만 있지만 곧 다른 항목을 추가할 것입니다.) children: 텍스트 뒤에 왼쪽 대괄호([)를 입력하여 위젯을 항목 하나로 구성된 목록으로 변환합니다. 편집기는 오른쪽 닫기 괄호도 제공합니다. 닫기 대괄호를 삭제합니다. 몇 줄 아래에 그리고 Row를 닫는 오른쪽 괄호 바로 앞에 오른쪽 대괄호와 쉼표를 차례로(],) 입력합니다. 이제 Analyzer가 녹색 체크표시로 코드 분석을 통과했음을 알려줍니다.
  6. 이제 코드는 올바르지만 형식이 제대로 지정되지 않았습니다. 코드 창에서 마우스 오른쪽 버튼으로 클릭하고 Reformat Code with dartfmt를 선택합니다.

b2f84ff91b0e1396.png다음과 같이 Flexible 안에 TextField를 래핑합니다.

  1. Row를 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시하여 Wrap with widget을 선택합니다. 그러면 TextField를 래핑하는 새 위젯이 추가됩니다. 자리표시자 이름이 선택되며, IDE가 새 자리표시자 이름이 입력될 때까지 기다립니다.
  3. Flexible,을 입력하다가 표시되는 목록에서 Flexible을 선택합니다. Row의 생성자에 관한 정의가 포함된 팝업이 표시됩니다.
Widget _buildTextComposer() {
  return  Container(
    margin: EdgeInsets.symmetric(horizontal: 8.0),
    child:  Row(                             // NEW
      children: [                            // NEW
         Flexible(                           // NEW
          child:  TextField(
            controller: _textController,
            onSubmitted: _handleSubmitted,
            decoration:  InputDecoration.collapsed(
                hintText: 'Send a message'),
          ),
        ),                                    // NEW
      ],                                      // NEW
    ),                                        // NEW
  );
}

cf1e10b838bf60ee.png 유용한 정보

  • Row를 사용하면 입력란 옆에 Send 버튼을 배치할 수 있습니다.
  • Flexible 위젯에서 TextField를 래핑하면 Row가 텍스트 필드의 크기를 자동으로 조정하여 버튼에서 사용하지 않는 남은 공간을 사용합니다.
  • 오른쪽 대괄호 뒤에 쉼표를 추가하면 형식 지정 도구가 코드의 형식을 지정하는 방법을 알 수 있습니다.

다음으로, Send 버튼을 추가합니다. 머티리얼 앱이므로 상응하는 머티리얼 아이콘 2de111ba4b057a1e.png을 사용합니다.

b2f84ff91b0e1396.pngSend 버튼을 Row에 추가합니다.

Send 버튼은 Row 목록의 두 번째 항목이 됩니다.

  1. Flexible 위젯의 오른쪽 닫기 대괄호와 쉼표 끝에 커서를 놓고 Return 키를 눌러 새 줄을 시작합니다.
  2. Container,를 입력하다가 표시되는 팝업에서 Container를 선택합니다. 컨테이너의 괄호 안에 커서를 놓습니다. Return 키를 눌러 새 줄을 시작합니다.
  3. 다음 코드 줄을 컨테이너에 추가합니다.
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
    icon: const Icon(Icons.send),
    onPressed: () => _handleSubmitted(_textController.text)),

cf1e10b838bf60ee.png 유용한 정보

  • IconButtonSend 버튼을 표시합니다.
  • icon 속성은 머티리얼 라이브러리의 Icons.send 상수를 지정하여 새 Icon 인스턴스를 만듭니다.
  • Container 위젯 안에 IconButton을 배치하면 버튼이 입력란 옆에서 시각적으로 공간에 더 잘 맞도록 버튼의 여백 간격을 맞춤설정할 수 있습니다.
  • onPressed 속성은 익명 함수를 사용하여 _handleSubmitted() 메서드를 호출하며 _textController를 사용하여 메시지의 내용을 전달합니다.
  • Dart에서는 때로 함수 선언에 화살표 구문(=> expression)이 사용되기도 합니다. 이는 { return expression; }의 약식 표현이며 한 줄 함수에만 사용됩니다. 익명 및 중첩 함수를 포함한 Dart 함수 지원에 관한 개요는 Dart 언어 둘러보기를 참고하세요.

b2f84ff91b0e1396.png앱을 핫 리로드하면 다음과 같이 Send 버튼이 표시됩니다.

Pixel 3XL

iPhone 11

버튼의 색상은 기본 머티리얼 디자인 테마에서 제공되는 검은색입니다. 앱의 아이콘에 강조 색상을 지정하려면 색상 인수를 IconButton에 전달하거나 다른 테마를 적용합니다.

b2f84ff91b0e1396.png다음과 같이 _buildTextComposer()에서 IconTheme: 안에 Container를 래핑합니다.

  1. _buildTextComposer() 함수 맨 위에 있는 Container를 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시하여 Wrap with widget을 선택합니다. 그러면 Container를 래핑하는 새 위젯이 추가됩니다. 자리표시자 이름이 선택되며, IDE가 새 자리표시자 이름이 입력될 때까지 기다립니다.
  3. IconTheme,을 입력하다 보면 표시되는 목록에서 IconTheme을 선택합니다. child 속성은 빨간색 상자로 둘러싸여 있으며, Analyzer가 data 속성이 필요함을 알려줍니다.
  4. data 속성을 추가합니다.
return IconTheme(
  data: IconThemeData(color: Theme.of(context).accentColor), // NEW
  child: Container(

cf1e10b838bf60ee.png 유용한 정보

  • 아이콘은 IconTheme 위젯에서 색상, 불투명도, 크기를 상속하며 이 위젯은 IconThemeData 객체를 사용하여 이러한 특성을 정의합니다.
  • IconThemedata 속성은 현재 테마의 ThemeData 객체를 지정합니다. 그러면 버튼(및 위젯 트리의 이 부분에 있는 여타의 아이콘)에 현재 테마의 강조 색상이 지정됩니다.
  • BuildContext 객체는 앱의 위젯 트리에서 위젯의 위치를 처리하는 핸들입니다. 각 위젯에는 StatelessWidget.build 또는 State.build 함수에서 반환하는 위젯의 상위 요소가 되는 자체 BuildContext가 있습니다. 즉, _buildTextComposer()는 캡슐화하는 State 객체에서 BuildContext 객체에 액세스할 수 있습니다. 컨텍스트를 메서드에 명시적으로 전달할 필요가 없습니다.

b2f84ff91b0e1396.png앱을 핫 리로드합니다. 이제 다음과 같이 Send 버튼이 파란색으로 표시됩니다.

Pixel 3XL

iPhone 11

문제가 있나요?

앱이 올바르게 실행되지 않는다면 오타가 있는지 확인합니다. 필요한 경우 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

e57d18c5bb8f2ac7.png특별한 것을 찾을 수 있어요!

앱을 디버그하는 방법에는 몇 가지가 있습니다. IDE를 사용하여 직접 중단점을 설정하거나 Dart DevTools(Chrome DevTools가 아님)를 사용할 수 있습니다. 이 Codelab에서는 Android 스튜디오 및 IntelliJ를 사용하여 중단점을 설정하는 방법을 보여줍니다. VS Code와 같은 다른 편집기를 사용하는 경우 디버깅에 DevTools를 사용합니다. Dart DevTools에 관해 기본적인 사항을 알아보려는 경우 웹에서 첫 번째 Flutter 앱 작성의 2.5단계를 참고하세요.

Android 스튜디오 및 IntelliJ IDE를 사용하면 에뮬레이터, 시뮬레이터 또는 기기에서 실행되는 Flutter 앱을 디버그할 수 있습니다. 이러한 편집기를 사용하여 다음을 할 수 있습니다.

  • 앱을 디버그할 기기 또는 시뮬레이터를 선택할 수 있습니다.
  • 콘솔 메시지를 확인할 수 있습니다.
  • 코드에 중단점을 설정할 수 있습니다.
  • 런타임 시 변수를 검사하고 표현식을 평가할 수 있습니다.

Android 스튜디오 및 IntelliJ 편집기는 앱이 실행되는 동안 시스템 로그를 표시하며, 중단점 관련 작업을 하고 실행 흐름을 제어할 수 있는 Debugger UI를 제공합니다.

6ea611ca007eb43c.png

중단점 관련 작업

b2f84ff91b0e1396.png다음과 같이 중단점을 사용하여 Flutter 앱을 디버그합니다.

  1. 중단점을 설정하려는 소스 파일을 엽니다.
  2. 중단점을 설정하려는 코드 줄을 찾아 클릭한 후 Run > Toggle Line Breakpoint를 선택합니다. 또는 줄 번호 오른쪽에 있는 거터를 클릭하여 중단점을 전환할 수 있습니다.
  3. 디버그 모드로 실행하지 않았다면 앱을 중지합니다.
  4. Run > Debug를 사용하거나 UI에서 Run debug 버튼을 클릭하여 앱을 다시 시작합니다.

편집기가 Debugger UI를 시작하고 중단점에 도달하면 앱 실행을 일시중지합니다. 그러면 Debugger UI의 컨트롤을 사용하여 오류의 원인을 파악할 수 있습니다.

FriendlyChat 앱의 build() 메서드에 중단점을 설정하여 디버거 사용을 연습한 후 앱을 실행하고 디버그합니다. 스택 프레임을 검사하여 앱의 메서드 호출 기록을 확인할 수 있습니다.

기본적인 앱 스캐폴딩 및 화면이 준비되었으므로 이제 채팅 메시지가 표시되는 영역을 정의할 준비가 되었습니다.

de23b9bb7bf84592.png

채팅 메시지 목록 구현

이 섹션에서는 컴포지션(여러 작은 위젯의 생성 및 결합)을 사용하여 채팅 메시지를 표시하는 위젯을 만듭니다. 하나의 채팅 메시지를 나타내는 위젯으로 시작합니다. 그런 다음, 상위의 스크롤 가능한 목록에 이 위젯을 중첩합니다. 마지막으로, 기본 앱 Scaffold에 스크롤 가능한 목록을 중첩합니다.

b2f84ff91b0e1396.png다음과 같이 ChatMessage 스테이트리스(Stateless) 위젯을 추가합니다.

  1. FriendlyChatApp 클래스 뒤에 커서를 놓고 stless를 입력합니다. (클래스의 순서는 중요하지 않지만 이 순서를 사용하면 코드를 솔루션과 더 쉽게 비교할 수 있습니다.)
  2. 클래스 이름으로 ChatMessage를 입력합니다.

b2f84ff91b0e1396.png다음과 같이 RowChatMessagebuild() 메서드에 추가합니다.

  1. return Container()의 괄호 안에 커서를 놓고 Return 키를 눌러 새 줄을 시작합니다.
  2. 다음과 같이 margin 속성을 추가합니다.
margin: EdgeInsets.symmetric(vertical: 10.0),
  1. Container'의 하위 요소는 Row가 됩니다. Row의 목록에는 2개의 위젯, 즉 아바타 및 텍스트 열이 포함되어 있습니다.
return Container(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        margin: const EdgeInsets.only(right: 16.0),
        child: CircleAvatar(child: Text(_name[0])),
      ),
      Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(_name, style: Theme.of(context).textTheme.headline4),
          Container(
            margin: EdgeInsets.only(top: 5.0),
            child: Text(text),
          ),
        ],
      ),
    ],
  ),
);
  1. 다음과 같이 ChatMessage 맨 위에 text 변수 및 생성자를 추가합니다.
class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text}); // NEW
  final String text;                 // NEW

이 시점에서 Analyzer가 _name이 정의되지 않은 것에 관해 문제를 제기합니다. 다음 단계에서 이 문제를 해결합니다.

b2f84ff91b0e1396.png_name 변수를 정의합니다.

아래와 같이 _name 변수를 정의합니다. 이때 Your Name을 고유한 자체 이름으로 바꿉니다. 이 변수를 사용하여 보내는 사람 이름으로 각 채팅 메시지에 라벨을 지정합니다. 이 Codelab에서는 편의를 위해 값을 하드 코딩하지만, 대부분의 앱은 인증을 통해 보내는 사람의 이름을 검색합니다. main() 함수 뒤에 다음 코드 줄을 추가합니다.

String _name = 'Your Name';

cf1e10b838bf60ee.png 유용한 정보

  • ChatMessagebuild() 메서드는 채팅 메시지를 보낸 사용자를 나타내는 간단한 그래픽 아바타, 보내는 사람 이름이 포함된 Column 위젯, 메시지 텍스트를 표시하는 Row를 반환합니다.
  • CircleAvatar_name 변숫값의 첫 번째 문자를 하위 Text 위젯에 전달하여 사용자의 첫 번째 이니셜로 라벨을 지정함으로써 맞춤설정됩니다.
  • crossAxisAlignment 매개변수는 Row 생성자에서 CrossAxisAlignment.start를 지정하여 상위 위젯을 기준으로 아바타 및 메시지를 배치합니다. 아바타의 경우 기본 축이 가로인 Row 위젯이 상위 요소이므로 CrossAxisAlignment.start는 세로축을 따라 가장 높은 위치에 아바타를 배치합니다. 메시지의 경우 기본 축이 세로인 Column 위젯이 상위 요소이므로 CrossAxisAlignment.start는 가로축을 따라 가장 왼쪽 위치에 텍스트를 정렬합니다.
  • 아바타 옆에 2개의 Text 위젯이 세로로 정렬되어 맨 위에 보내는 사람 이름을 표시하고 아래에 메시지 텍스트를 표시합니다.
  • Theme.of(context)는 앱의 기본 Flutter ThemeData 객체를 제공합니다. 이후 단계에서 이 기본 테마를 재정의하여 Android 및 iOS에서 앱의 스타일을 서로 다르게 지정합니다.
  • ThemeDatatextTheme 속성을 사용하면 headline4와 같은 텍스트의 머티리얼 디자인 논리 스타일에 액세스할 수 있으므로 글꼴 크기 및 기타 텍스트 속성의 하드 코딩을 방지할 수 있습니다. 이 예에서는 보내는 사람 이름이 메시지 텍스트보다 더 크게 표시되도록 스타일이 지정됩니다.

b2f84ff91b0e1396.png앱을 핫 리로드합니다.

텍스트 필드에 메시지를 입력합니다. Send 버튼을 눌러 메시지를 지웁니다. 텍스트 필드에 장문의 메시지를 입력하여 텍스트 필드가 오버플로될 때 어떤 일이 발생하는지 확인합니다. 나중에 9단계에서 Expanded 위젯 안에 열을 래핑하여 Text 위젯을 래핑합니다.

UI에 채팅 메시지 목록 구현

다음으로 할 일은 채팅 메시지 목록을 가져와서 UI에 표시하는 것입니다. 사용자가 메시지 기록을 확인할 수 있도록 이 목록을 스크롤 가능하게 설정해야 합니다. 또한 목록에는 메시지가 시간순으로 표시되고 가장 최근의 메시지가 표시되는 목록의 맨 아래 행에 배치되어야 합니다.

b2f84ff91b0e1396.png_messages 목록을 _ChatScreenState에 추가합니다.

다음과 같이 _ChatScreenState 정의에서 각 채팅 메시지를 나타내는 _messages라는 List 멤버를 추가합니다.

class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];      // NEW
  final _textController = TextEditingController();

b2f84ff91b0e1396.png_ChatScreenState.에서 _handleSubmitted() 메서드를 수정합니다.

사용자가 텍스트 필드의 채팅 메시지를 보내면 앱이 메시지 목록에 새 메시지를 추가해야 합니다. 다음과 같이 _handleSubmitted() 메서드를 수정하여 이 동작을 구현합니다.

void _handleSubmitted(String text) {
  _textController.clear();
  ChatMessage message = ChatMessage(    //NEW
    text: text,                         //NEW
  );                                    //NEW
  setState(() {                         //NEW
    _messages.insert(0, message);       //NEW
  });                                   //NEW
 }

b2f84ff91b0e1396.png메시지 내용 제출 후 다시 텍스트 필드에 포커스를 지정합니다.

  1. 다음과 같이 FocusNode_ChatScreenState에 추가합니다.
class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();    // NEW
  1. 다음과 같이 focusNode 속성을 _buildTextComposer()TextField에 추가합니다.
child: TextField(
  controller: _textController,
  onSubmitted: _handleSubmitted,
  decoration: InputDecoration.collapsed(hintText: 'Send a message'),
  focusNode: _focusNode,  // NEW
),
  1. 다음과 같이 _handleSubmitted()에서 setState() 호출 후 TextField에 포커스를 지정하도록 요청합니다.
    setState(() {
      _messages.insert(0, message);
    });
    _focusNode.requestFocus();  // NEW

cf1e10b838bf60ee.png 유용한 정보

  • 목록의 각 항목은 ChatMessage 인스턴스입니다.
  • 목록은 비어 있도록 초기화합니다.
  • setState()를 호출하여 _messages를 수정하면 프레임워크가 위젯 트리의 이 부분이 변경되었으며 UI를 다시 빌드해야 함을 알게 됩니다. setState()에서 동기 작업만 실행해야 합니다. 그러지 않으면 작업이 완료되기 전에 프레임워크가 위젯을 다시 빌드할 수 있기 때문입니다.
  • 일반적으로 일부 비공개 데이터가 setState() 호출 외부에서 변경된 후 빈 클로저를 사용하여 이 메서드를 호출할 수 있습니다. 그러나 setState의 클로저 내부에서 데이터를 업데이트하는 것이 기본 설정이므로 나중에 잊지 말고 이를 호출해야 합니다.

b2f84ff91b0e1396.png앱을 핫 리로드합니다.

텍스트 필드에 텍스트를 입력하고 Return 키를 누릅니다. 텍스트 필드에 또다시 포커스가 지정됩니다.

메시지 목록 배치

이제 채팅 메시지 목록을 표시할 준비가 되었습니다. _messages 목록에서 ChatMessage 위젯을 가져와서 스크롤 가능한 목록의 ListView 위젯에 배치합니다.

b2f84ff91b0e1396.png다음과 같이 _ChatScreenStatebuild() 메서드에서 Column 안에 ListView를 추가합니다.

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text ('FriendlyChat')),
    body: Column(                                            // MODIFIED
      children: [                                            // NEW
        Flexible(                                            // NEW
          child: ListView.builder(                           // NEW
            padding: EdgeInsets.all(8.0),                    // NEW
            reverse: true,                                   // NEW
            itemBuilder: (_, int index) => _messages[index], // NEW
            itemCount: _messages.length,                     // NEW
          ),                                                 // NEW
        ),                                                   // NEW
        Divider(height: 1.0),                                // NEW
        Container(                                           // NEW
          decoration: BoxDecoration(
            color: Theme.of(context).cardColor),             // NEW
          child: _buildTextComposer(),                       // MODIFIED
        ),                                                   // NEW
      ],                                                     // NEW
    ),                                                       // NEW
  );
}

cf1e10b838bf60ee.png 유용한 정보

  • ListView.builder 팩토리 메서드는 목록의 항목마다 한 번 호출되는 함수를 제공하여 요청 시 목록을 빌드합니다. 이 함수는 각 호출 시 새 위젯을 반환합니다. 또한 빌더는 children 매개변수의 변화를 자동으로 감지하고 재빌드를 시작합니다.
  • ListView.builder 생성자에 전달되는 매개변수는 목록의 내용 및 모양을 맞춤설정합니다.
  • padding은 메시지 텍스트 주위에 공백을 만듭니다.
  • itemCount는 목록의 메시지 수를 지정합니다.
  • itemBuilder[index]의 각 위젯을 빌드하는 함수를 제공합니다. 현재 빌드 컨텍스트가 필요하지 않으므로 IndexedWidgetBuilder의 첫 번째 인수를 무시해도 됩니다. 인수의 이름을 밑줄(_)만 사용하여 지정하는 것은 인수가 사용되지 않음을 나타내는 규칙입니다.
  • 이제 Scaffold 위젯의 body 속성에는 입력란 및 Send 버튼뿐 아니라 받은 메시지 목록이 포함됩니다. 레이아웃은 다음과 같은 위젯을 사용합니다.
  • Column: 직속 하위 요소를 세로로 배치합니다. Column 위젯은 하위 위젯 목록(Row와 동일)을 사용하며 이 목록은 입력란의 행 및 스크롤 가능한 목록이 됩니다.
  • ListView의 상위 요소로서 Flexible: 수신된 메시지 목록을 확장하여 Column 높이를 채우고 TextField를 고정된 크기로 유지하도록 프레임워크에 알립니다.
  • Divider: 메시지 표시를 위한 UI와 메시지 작성을 위한 텍스트 입력란 사이에 가로선을 그립니다.
  • 텍스트 작성기의 상위 요소로서 Container: 배경 이미지, 패딩, 여백, 기타 일반적인 레이아웃 세부정보를 정의합니다.
  • decoration: 배경 색상을 정의하는 새 BoxDecoration 객체를 만듭니다. 이 경우에는 기본 테마의 ThemeData 객체에서 정의한 cardColor를 사용합니다. 이렇게 하면 메시지 작성을 위한 UI가 메시지 목록과 다른 배경을 갖게 됩니다.

b2f84ff91b0e1396.png앱을 핫 리로드합니다. 다음과 같은 화면이 표시됩니다.

Pixel 3XL

iPhone 11

b2f84ff91b0e1396.png방금 빌드한 메시지 작성 및 표시 UI를 사용하여 채팅 메시지를 몇 개 보내봅니다.

Pixel 3XL

iPhone 11

문제가 있나요?

앱이 올바르게 실행되지 않는다면 오타가 있는지 확인합니다. 필요한 경우 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

위젯에 애니메이션을 추가하여 앱의 사용자 환경을 더욱 유려하고 직관적으로 만들 수 있습니다. 이 섹션에서는 채팅 메시지 목록에 기본 애니메이션 효과를 추가하는 방법을 알아봅니다.

사용자가 새 채팅 메시지를 보내는 경우 수신한 메시지가 메시지 목록에 단순히 표시되는 대신, 메시지에 애니메이션을 적용하여 화면 하단에서 위로 수직으로 천천히 올라오게 합니다.

Flutter에서 애니메이션은 유형이 있는 값 및 상태(예: 정방향, 역방향, 완료됨, 닫음)가 포함된 Animation 객체로 캡슐화됩니다. 위젯에 애니메이션 객체를 연결하거나 애니메이션 객체의 변경사항을 수신 대기할 수 있습니다. 애니메이션 객체의 속성 변경사항에 따라 프레임워크는 위젯이 표시되는 방식을 수정하고 위젯 트리를 다시 빌드할 수 있습니다.

애니메이션 컨트롤러 지정

AnimationController 클래스를 사용하여 애니메이션 실행 방식을 지정합니다. AnimationController를 사용하면 애니메이션 시간 및 재생 방향(정방향 또는 역방향)과 같은 애니메이션의 중요한 특성을 정의할 수 있습니다.

b2f84ff91b0e1396.png다음과 같이 TickerProviderStateMixin을 포함하도록 _ChatScreenState 클래스 정의를 업데이트합니다.

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {   // MODIFIED
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  ...

b2f84ff91b0e1396.png다음과 같이 ChatMessage 클래스 정의에서 애니메이션 컨트롤러를 저장할 변수를 추가합니다.

class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text, required this.animationController}); // MODIFIED
  final String text;
  final AnimationController animationController;      // NEW
  ...

b2f84ff91b0e1396.png다음과 같이 _handleSubmitted() 메서드에 애니메이션 컨트롤러를 추가합니다.

void _handleSubmitted(String text) {
  _textController.clear();
  var message = ChatMessage(
    text: text,
    animationController: AnimationController(      // NEW
      duration: const Duration(milliseconds: 700), // NEW
      vsync: this,                                 // NEW
    ),                                             // NEW
  );                                               // NEW
  setState(() {
    _messages.insert(0, message);
  });
  _focusNode.requestFocus();
  message.animationController.forward();           // NEW
}

cf1e10b838bf60ee.png 유용한 정보

  • AnimationController는 애니메이션의 런타임 시간을 700밀리초로 지정합니다. (이 시간이 길어질수록 애니메이션 효과 재생이 느려지므로 전환이 서서히 진행되는 것을 확인할 수 있습니다. 실제로 앱을 실행할 때는 이 시간을 더 짧게 설정할 수 있습니다.)
  • 애니메이션 컨트롤러는 새 ChatMessage 인스턴스에 연결되며 메시지가 채팅 목록에 추가될 때마다 애니메이션이 정방향으로 재생되도록 지정합니다.
  • AnimationController를 만들 때 vsync 인수를 전달해야 합니다. vsync는 애니메이션을 정방향으로 추진하는 하트비트(Ticker)의 소스입니다. 이 예에서는 _ChatScreenStatevsync로 사용하므로 TickerProviderStateMixin 믹스인을 _ChatScreenState 클래스 정의에 추가합니다.
  • Dart에서 믹스인을 사용하면 클래스 본문을 여러 클래스 계층 구조에서 재사용할 수 있습니다. 자세한 내용은 Dart 언어 둘러보기클래스에 기능 추가: 믹스인 섹션을 참고하세요.

SizeTransition 위젯 추가

애니메이션에 SizeTransition 위젯을 추가하면 텍스트가 미끄러지듯 표시되면서 점점 더 많이 노출되는 애니메이션을 ClipRect에 적용하는 효과가 있습니다.

b2f84ff91b0e1396.png다음과 같이 SizeTransition 위젯을 ChatMessagebuild() 메서드에 추가합니다.

  1. ChatMessagebuild() 메서드에서 첫 번째 Container 인스턴스를 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시하여 Wrap with widget을 선택합니다.
  3. SizeTransition을 입력합니다. child: 속성 주위에 빨간색 상자가 표시됩니다. 이는 위젯 클래스에서 필수 속성이 누락되었음을 나타냅니다. SizeTransition, 위로 마우스를 가져가면 도움말 팝업이 표시되어 sizeFactor가 필요함을 알려주고 이를 만들도록 제안합니다. 이 옵션을 선택하면 속성이 null 값과 함께 표시됩니다.
  4. nullCurvedAnimation 인스턴스로 바꿉니다. 그러면 두 속성, 즉 parent(필수) 및 curve의 상용구 코드가 추가됩니다.
  5. parent 속성에서 nullanimationController로 바꿉니다.
  6. curve 속성에서 nullCurves 클래스의 상수 중 하나인 Curves.easeOut으로 바꿉니다.
  7. sizeFactor 뒤에(하지만 동일한 수준에) 코드 줄을 추가하고 0.0 값과 함께 axisAlignment 속성을 SizeTransition에 입력합니다.
@override
Widget build(BuildContext context) {
  return SizeTransition(             // NEW
    sizeFactor:                      // NEW
        CurvedAnimation(parent: animationController, curve: Curves.easeOut),  // NEW
    axisAlignment: 0.0,              // NEW
    child: Container(                // MODIFIED
    ...

cf1e10b838bf60ee.png 유용한 정보

  • CurvedAnimation 객체는 SizeTransition 클래스와 함께 이즈 아웃(ease out) 애니메이션 효과를 생성합니다. 이즈 아웃 효과를 사용하면 애니메이션 시작 시 메시지 표시가 빠르게 진행되다가 중지될 때까지 점점 더 느려집니다.
  • SizeTransition 위젯은 텍스트가 미끄러지듯 표시되면서 점점 더 많이 노출되는 애니메이션을 ClipRect에 적용하는 역할을 합니다.

애니메이션 폐기

애니메이션이 더 이상 필요하지 않을 경우 애니메이션 컨트롤러를 폐기하여 리소스를 확보하는 것이 좋습니다.

b2f84ff91b0e1396.pngdispose() 메서드를 _ChatScreenState.에 추가합니다.

_ChatScreenState 하단에 다음 메서드를 추가합니다.

@override
void dispose() {
  for (var message in _messages){
    message.animationController.dispose();
  }
  super.dispose();
}

b2f84ff91b0e1396.png이제 코드는 올바르지만 형식이 제대로 지정되지 않았습니다. 코드 창에서 마우스 오른쪽 버튼으로 클릭하고 Reformat Code with dartfmt를 선택합니다.

b2f84ff91b0e1396.png앱을 핫 리로드(또는 실행 중인 앱에 채팅 메시지가 포함된 경우 핫 리스타트)하고 메시지를 몇 개 입력하여 애니메이션 효과를 관찰합니다.

애니메이션을 더 실험해 보려면 다음과 같은 아이디어를 몇 가지 시도해 볼 수 있습니다.

  • _handleSubmitted() 메서드에 지정된 duration 값을 수정하여 애니메이션 속도를 높이거나 낮춥니다.
  • Curves 클래스에 정의된 상수를 사용하여 다양한 애니메이션 커브를 지정합니다.
  • SizeTransition 대신 FadeTransition 위젯에서 Container를 래핑하여 페이드 인 애니메이션 효과를 만듭니다.

문제가 있나요?

앱이 올바르게 실행되지 않는다면 오타가 있는지 확인합니다. 필요한 경우 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

이 선택적 단계에서는 보낼 텍스트가 있을 때만 Send 버튼을 사용할 수 있도록 설정하고 장문의 메시지를 래핑하며 Android 및 iOS 기기에 맞는 네이티브 스타일 맞춤설정을 추가하는 등 앱에 정교한 세부사항을 몇 가지 지정합니다.

Send 버튼이 컨텍스트를 인식하도록 설정

현재는 입력란에 텍스트가 없을 때도 Send 버튼이 사용 설정된 것으로 표시됩니다. 입력란에 보낼 텍스트가 있는지 여부에 따라 버튼의 모양이 변경되도록 할 수 있습니다.

b2f84ff91b0e1396.png다음과 같이 사용자가 입력란에 입력할 때마다 true가 되는 비공개 변수인 _isComposing을 정의합니다.

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  bool _isComposing = false;            // NEW

b2f84ff91b0e1396.pngonChanged() 콜백 메서드를 _ChatScreenState.에 추가합니다.

_buildTextComposer() 메서드에서 다음과 같이 onChanged 속성을 TextField에 추가하고 onSubmitted 속성을 업데이트합니다.

Flexible(
  child: TextField(
    controller: _textController,
    onChanged: (String text) {            // NEW
      setState(() {                       // NEW
        _isComposing = text.isNotEmpty;   // NEW
      });                                 // NEW
    },                                    // NEW
    onSubmitted: _isComposing ? _handleSubmitted : null, // MODIFIED
    decoration:
        InputDecoration.collapsed(hintText: 'Send a message'),
    focusNode: _focusNode,
  ),
),

b2f84ff91b0e1396.png_ChatScreenState.에서 onPressed() 콜백 메서드를 업데이트합니다.

계속해서 _buildTextComposer() 메서드에서 다음과 같이 IconButtononPressed 속성을 업데이트합니다.

Container(
  margin: EdgeInsets.symmetric(horizontal: 4.0),
  child: IconButton(
      icon: const Icon(Icons.send),
      onPressed: _isComposing                            // MODIFIED
          ? () => _handleSubmitted(_textController.text) // MODIFIED
          : null,                                        // MODIFIED
      )
      ...
)

b2f84ff91b0e1396.png다음과 같이 텍스트 필드가 지워지면 _isComposing을 false로 설정하도록 _handleSubmitted를 수정합니다.

void _handleSubmitted(String text) {
  _textController.clear();
  setState(() {                             // NEW
    _isComposing = false;                   // NEW
  });                                       // NEW

  ChatMessage message = ChatMessage(
  ...

cf1e10b838bf60ee.png 유용한 정보

  • onChanged 콜백은 사용자가 텍스트를 수정했음을 TextField에 알립니다. TextField는 값이 필드의 현재 값에서 변경될 때마다 이 메서드를 호출합니다.
  • onChanged 콜백은 setState()를 호출하여 필드에 텍스트가 포함되어 있으면 _isComposing의 값을 true로 변경합니다.
  • _isComposing이 false이면 onPressed 속성이 null로 설정됩니다.
  • 또한 빈 문자열을 메시지 목록에 추가하지 않도록 onSubmitted 속성을 수정하였습니다.
  • 이제 _isComposing 변수가 Send 버튼의 동작 및 시각적 모양을 제어합니다.
  • 사용자가 텍스트 필드에 문자열을 입력하면 _isComposingtrue,가 되고 버튼 색상이 Theme.of(context).accentColor로 설정됩니다. 사용자가 Send 버튼을 누르면 프레임워크가 _handleSubmitted()를 호출합니다.
  • 사용자가 텍스트 필드에 아무것도 입력하지 않으면 _isComposingfalse,가 되고 위젯의 onPressed 속성이 null로 설정되어 Send 버튼이 사용 중지됩니다. 프레임워크는 버튼 색상을 Theme.of(context).disabledColor로 자동으로 변경합니다.

b2f84ff91b0e1396.png앱을 핫 리로드하여 사용해 보세요.

긴 줄 래핑

사용자가 메시지 표시를 위한 UI의 너비를 초과하는 채팅 메시지를 보내는 경우 메시지 전체가 표시되도록 줄이 래핑되어야 합니다. 현재는 오버플로되는 줄이 잘리며 시각적 오버플로 오류가 표시됩니다. 텍스트가 올바르게 래핑되도록 하는 간단한 방법은 Expanded 위젯 내부에 텍스트를 배치하는 것입니다.

b2f84ff91b0e1396.png다음과 같이 Expanded 위젯으로 Column 위젯을 래핑합니다.

  1. ChatMessagebuild() 메서드에서 ContainerRow 내부에 있는 Column 위젯을 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시합니다.
  3. Expanded,를 입력하다가 표시되는 가능한 객체 목록에서 Expanded를 선택합니다.

다음 코드 샘플은 이렇게 변경한 후 ChatMessage 클래스의 모습을 보여줍니다.

...
Container(
  margin: const EdgeInsets.only(right: 16.0),
  child: CircleAvatar(child: Text(_name[0])),
),
Expanded(            // NEW
  child: Column(     // MODIFIED
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(_name, style: Theme.of(context).textTheme.headline4),
      Container(
        margin: EdgeInsets.only(top: 5.0),
        child: Text(text),
      ),
    ],
  ),
),                    // NEW
...

cf1e10b838bf60ee.png 유용한 정보

Expanded 위젯을 사용하면 하위 위젯(예: Column)이 하위 위젯에 레이아웃 제약 조건(이 경우에는 Column의 너비)을 적용할 수 있습니다. 여기에서는 Text 위젯의 너비를 제한하며 이는 일반적으로 텍스트 내용에 의해 결정됩니다.

Android 및 iOS 맞춤설정

앱의 UI에 자연스러운 스타일과 느낌을 구현하려면 FriendlyChatApp 클래스의 build() 메서드에 테마 및 몇 가지 간단한 로직을 추가하면 됩니다. 이 단계에서는 다양한 기본 색상과 강조 색상 세트를 적용하는 플랫폼 테마를 정의합니다. 또한 Android에서는 머티리얼 디자인 IconButton을, iOS에서는 CupertinoButton을 사용하도록 Send 버튼을 맞춤설정합니다.

b2f84ff91b0e1396.pngmain.dart에서 main() 메서드 뒤에 다음 코드를 추가합니다.

final ThemeData kIOSTheme = ThemeData(
  primarySwatch: Colors.orange,
  primaryColor: Colors.grey[100],
  primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = ThemeData(
  primarySwatch: Colors.purple,
  accentColor: Colors.orangeAccent[400],
);

cf1e10b838bf60ee.png 유용한 정보

  • kDefaultTheme ThemeData 객체는 Android의 색상(주황색이 강조된 자주색)을 지정합니다.
  • kIOSTheme ThemeData 객체는 iOS의 색상(주황색이 강조된 연회색)을 지정합니다.

b2f84ff91b0e1396.png다음과 같이 FriendlyChatApp 클래스를 수정하여 앱의 MaterialApp 위젯 theme 속성을 사용해 테마를 변경합니다.

  1. 다음과 같이 파일 맨 위에 foundation 패키지를 가져옵니다.
import 'package:flutter/foundation.dart';  // NEW
import 'package:flutter/material.dart';
  1. 다음과 같이 적절한 테마를 선택하도록 FriendlyChatApp 클래스를 수정합니다.
class FriendlyChatApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FriendlyChat',
      theme: defaultTargetPlatform == TargetPlatform.iOS // NEW
        ? kIOSTheme                                      // NEW
        : kDefaultTheme,                                 // NEW
      home: ChatScreen(),
    );
  }
}

b2f84ff91b0e1396.pngAppBar 위젯(앱 UI 상단의 배너)의 테마를 수정합니다.

  1. _ChatScreenStatebuild() 메서드에서 다음 코드 줄을 찾습니다.
      appBar: AppBar(title: Text('FriendlyChat')),
  1. 두 개의 오른쪽 괄호())) 사이에 커서를 놓고 쉼표를 입력한 후 Return 키를 눌러 새 줄을 시작합니다.
  2. 다음 두 줄을 추가합니다.
elevation:
   Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
  1. 코드 창에서 마우스 오른쪽 버튼으로 클릭하고 Reformat Code with dartfmt를 선택합니다.

cf1e10b838bf60ee.png 유용한 정보

  • 테마를 선택하는 데 최상위 defaultTargetPlatform 속성 및 조건부 연산자가 사용됩니다.
  • elevation 속성은 AppBar의 z 좌표를 정의합니다. z 좌표의 4.0 값에는 정의된 섀도우가 있으며(Android), 0.0 값에는 섀도우가 없습니다(iOS).

b2f84ff91b0e1396.pngAndroid 및 iOS의 Send 아이콘을 맞춤설정합니다.

  1. main.dart의 맨 위에 다음 가져오기를 추가합니다.
import 'package:flutter/cupertino.dart';   // NEW
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
  1. _ChatScreenState_buildTextComposer() 메서드에서 IconButtonContainer의 하위 요소로 할당하는 코드 줄을 수정합니다. 할당을 플랫폼에 따른 조건부로 변경합니다. 다음과 같이 iOS인 경우 CupertinoButton을 사용하고 iOS가 아니면 IconButton을 계속 유지합니다.
Container(
   margin: EdgeInsets.symmetric(horizontal: 4.0),
   child: Theme.of(context).platform == TargetPlatform.iOS ? // MODIFIED
   CupertinoButton(                                          // NEW
     child: Text('Send'),                                    // NEW
     onPressed: _isComposing                                 // NEW
         ? () =>  _handleSubmitted(_textController.text)     // NEW
         : null,) :                                          // NEW
   IconButton(                                               // MODIFIED
       icon: const Icon(Icons.send),
       onPressed: _isComposing ?
           () =>  _handleSubmitted(_textController.text) : null,
       )
   ),

b2f84ff91b0e1396.pngContainer 위젯에서 최상위 Column을 래핑하고 위쪽 가장자리에 연회색 테두리를 지정합니다.

이 테두리는 iOS에서 앱 바를 앱 본문과 시각적으로 구별하는 데 도움이 됩니다. Android에서 테두리를 숨기려면 다음과 같이 이전 코드 샘플에서 앱 바에 사용한 것과 동일한 로직을 적용합니다.

  1. _ChatScreenStatebuild() 메서드에서 body: 뒤에 나오는 Column을 선택합니다.
  2. Option+Return(macOS) 또는 Alt+Enter(Linux 및 Windows) 키를 눌러 메뉴를 표시하여 Wrap with Container를 선택합니다.
  3. Column의 끝부분 뒤에 그러나 Container의 종결 괄호 앞에, 플랫폼에 따라 적절한 버튼을 조건부로 추가하는 코드를 추가합니다(아래 참고).
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('FriendlyChat'),
      elevation:
          Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
    ),
    body: Container(
        child: Column(
          children: [
            Flexible(
              child: ListView.builder(
                padding: EdgeInsets.all(8.0),
                reverse: true,
                itemBuilder: (_, int index) => _messages[index],
                itemCount: _messages.length,
              ),
            ),
            Divider(height: 1.0),
            Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ],
        ),
        decoration: Theme.of(context).platform == TargetPlatform.iOS // NEW
            ? BoxDecoration(                                 // NEW
                border: Border(                              // NEW
                  top: BorderSide(color: Colors.grey[200]!), // NEW
                ),                                           // NEW
              )                                              // NEW
            : null),                                         // MODIFIED
  );
}

b2f84ff91b0e1396.png앱을 핫 리로드합니다. Android 및 iOS에 따라 다양한 색상, 섀도우, 아이콘 버튼이 표시됩니다.

Pixel 3XL

iPhone 11

문제가 있나요?

앱이 올바르게 실행되지 않는다면 오타가 있는지 확인합니다. 필요한 경우 다음 링크의 코드를 사용하면 정상으로 돌려놓을 수 있습니다.

축하합니다.

이제 Flutter 프레임워크를 사용하여 크로스 플랫폼 모바일 앱을 빌드하는 방법의 기본사항을 알게 되었습니다.

학습한 내용

  • Flutter 앱을 처음부터 끝까지 빌드하는 방법
  • Android 스튜디오 및 IntelliJ에서 제공하는 일부 단축키를 사용하는 방법
  • 에뮬레이터, 시뮬레이터, 기기에서 Flutter 앱을 실행하고 핫 리로드하며 디버그하는 방법
  • 위젯 및 애니메이션으로 사용자 인터페이스를 맞춤설정하는 방법
  • Android 및 iOS의 사용자 인터페이스를 맞춤설정하는 방법

다음 단계

다른 Flutter Codelab 중 하나를 진행해 보세요.

다음을 통해 Flutter에 관한 학습을 계속해 나가세요.

단축키에 관한 자세한 내용은 다음을 참고하세요.

샘플 코드를 다운로드하여 참조로 샘플을 확인하거나 특정 섹션에서 Codelab을 시작할 수 있습니다. Codelab의 샘플 코드 복사본을 가져오려면 터미널에서 다음 명령어를 실행합니다.

 git clone https://github.com/flutter/codelabs

이 Codelab의 샘플 코드는 friendly_chat 폴더에 있습니다. 번호가 지정된 각 단계의 폴더는 이 Codelab에서 번호가 지정된 단계와 일치합니다. 또한 이러한 각 단계에 있는 lib/main.dart 파일의 코드를 DartPad 인스턴스로 드롭하여 DartPad에서 실행할 수도 있습니다.