Introduction to widgets - (2)

|

제스처 핸들링

GestureDetector 위젯을 이용하면 다양한 사용자 제스처 인터랙션을 핸들링할 수 있습니다.

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

GestureDetector 위젯은 화면을 렌더링하는 요소는 갖고 있지 않지만, 사용자의 인터랙션을 감지할 수 있는 위젯입니다. childContainer를 터치(onTap)하면 미리 정의되어 있는 콜백(callback) 함수를 호출합니다.


사용자 입력에 반응하는 위젯 만들기(StatefulWidget)

이제 사용자 입력에 반응하는 위젯을 만들어보겠습니다. StatelessWidget의 경우는 모든 속성 값을 부모로부터 전달받으며 그 값은 final 멤버로 저장하게 됩니다. 즉, 더 이상 상태 변환이 없는 위젯이며 새로 갱신될 필요도 없습니다.

StatefulWidget는 상태 값을 가지며, 그 값이 바뀔 때 화면을 갱신할 필요가 있으면 새로 렌더링을 하는 위젯입니다. 관리해야 할 상태가 있고 내부적으로 State를 인스턴스를 가지게 되며, StatelessWidget에 비해 구조가 좀 더 복잡합니다.

class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this case nothing) provided by the parent and used
  // by the build  method of the State. Fields in a Widget
  // subclass are always marked "final".

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework that
      // something has changed in this State, which causes it to rerun
      // the build method below so that the display can reflect the
      // updated values. If you change _counter without calling
      // setState(), then the build method won't be called again,
      // and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called,
    // for instance, as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

위 예제에서 RaisedButton을 누르게 되면 onPressed() 메소드의 콜백으로 등록된 _increment() 메소드가 호출됩니다. _increment() 메소드 구현 내부에 있는 setState() 메소드를 통해 상태 변화를 위젯에 전달하게 되고, 해당 위젯은 화면을 새로 렌더링하게 됩니다.

StatefulWidgetState는 서로 분리되어 있으며 서로 다른 라이프사이클(Lifecycle)을 가집니다. Widget은 화면에 뿌리기 위한 일시적인 오브젝트이며, State는 그 상태 값을 유지하고 있는 영구적인 오브젝트라고 볼 수 있습니다.

위 예제는 사용자의 입력에 따라 해당 클래스내 build() 메소드로 결과를 바로 렌더링하도록 되어 있는 간단한 예제이지만, 좀 더 복잡한 프로그램에서는 좀 더 구조적인 구성이 필요하게 됩니다. Flutter에서는 콜백 형태로 전달받는 이벤트의 경우 위젯 트리의 위쪽 방향으로 전달되며, 현재 상태는 아래쪽의 StatelessWidget으로 전달되어 화면에 출력하는 형태로 되어 있습니다.

이러한 흐름은 다음 예제에서 볼 수 있습니다.

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}

위 예제에서 _CounterState 클래스는 build() 메소드를 이용해서 2개의 StatelessWidget를 출력하고 있습니다. 각 위젯들은 CounterIncrementorCounterDisplay 이며, CounterIncrementoronPressed() 메소드의 콜백 함수로 _increment() 메소드를 연결시켜 놓았고, CounterDisplay 위젯에 _counter 속성 값을 전달하고 있습니다.

Introduction to widgets - (1)

|

Fluttter 공식 홈페이지의 가이드 문서 내용입니다. 실제 문서 주소는 여기입니다.


Flutter Widgets

Flutter Widget들은 React에서 아이디어를 가져왔습니다. 상당히 많은 부분이 흡사합니다. 코드를 이용해서 UI를 구성하는 것 부터 State라는 개념을 이용해서 State가 변경되면 각 Widget들이 알아서 화면을 갱신하는 라이프 사이클까지 많은 부분이 비슷합니다. 각 컴포넌트는 트리 형태로 구성되어 있고, 각 컴포넌트의 화면을 새로 렌더링해야 할 지를 판단해서 자동으로 갱신을 해줍니다.

Image


Hello World

Flutter에서의 ‘Hello World’ 코드는 다음과 같습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

runApp() 함수는 Widget을 입력받아서 해당 위젯 트리의 root에 배치해줍니다. 위의 예제 코드에서는 Center 위젯이 root에 배치됩니다. Center 위젯은 Text라는 위젯을 자식(child)으로 갖고 있으며, 결국 화면에 ‘Hello, world!’라는 문구를 출력하게 되어 있습니다.

App을 개발하면서 일반적으로 위젯 성격에 따라 StatelessWidget 또는 StatefulWidget을 사용하게 될 것입니다. 위젯에서 제일 중요한 부분은 화면을 렌더링하는 것이며, 렌더링은 build() 함수를 구현하면서 이루어집니다.


기본 위젯들

  • Text : 텍스트를 출력해주는 위젯입니다.

  • Row, Column : 주의할 점이 Row는 Horizontal, Column은 Vertical 방향입니다. Row는 가로로 1줄을 차지하고 그 안에서 children 들끼리 공간을 나눠가진다고 생각하면 편합니다. Column은 그 반대입니다.

  • Stack : Stack은 위젯들끼리 서로 겹치게 배치할 수 있는 컨테이너의 일종입니다. Positionsed 위젯을 이용해서 안드로이드의 RelativeLayout 처럼 상대적인 위치에 자식 위젯들을 배치할 수 있습니다.

  • Container : child를 하나 가지는 기본적인 컨테이너입니다. BoxDecoration을 이용해서 배경(background)이나 외곽선(border), 그림자(shadow) 등을 꾸며줄 수 있고 margin이나 padding 등의 속성을 줄 수 있습니다.


기본 위젯 예제 코드

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: <Widget>[
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.headline6,
            ),
          ),
          Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: MyScaffold(),
  ));
}

위의 예제 코드에서 main() 함수를 보면 MaterialApp으로 프로그램을 실행하게 되어 있습니다. pubspec.yaml 내에 uses-material-design: true 옵션이 설정되어 있어야 가능한데, 대부분 기본적으로 이미 세팅되어 았기 때문에 크게 신경 쓸 필요가 없습니다.

flutter:
  uses-material-design: true

home 속성을 보면 MyScaffold() 위젯을 배치하도록 되어 있습니다. MyScaffold 위젯은 StatelessWidgetbuild() 메소드 내부를 보면, Column 위젯을 갖고 있습니다. Column 안에는 MyAppBar 위젯과 Expanded 위젯이 Vertical 방향으로 배치되어 있습니다.


Material Components를 활용한 간략화

위 예제 코드는 길이가 제법 긴데, Flutter는 다수의 Material Widget 들을 갖고 있기 때문에, 각 위젯들을 일일이 구현할 필요가 없습니다.

위 예제 코드의 MyAppBar 위젯과 MyScaffold 위젯은 material.dart 내에 정의되어 있는 AppBarScaffold 위젯으로 대체가 됩니다. (만약 iOS 스타일의 테마로 개발하시고 싶으면 Cupertino 컴포넌트 패키지를 활용해야 합니다.)

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: Text('Example title'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

C++ Task

|

Task

C++ 에서는 Thread 외에도 비동기적으로 작업을 수행할 수 있는 태스크(Task를 지원합니다. 태스크는 <future> 헤더를 필요하며 Promise, Future 두 개의 컴포넌트로 구성됩니다. 이 두 컴포넌트는 서로 데이터 채널을 통해 연결됩니다.


Thread와 Task 예제

#include <iostream>
#include <future>
#include <thread>

using namespace std;

int main() {
  cout << "Hello, World!" << endl;

  // Thread
  int count = 100;
  thread t([&] { count = count + 100; });
  t.join();
  cout << "count: " << count << endl;
  
  // Task
  auto f = async([&] { return count * 2; });
  cout << "count: " << f.get() << endl;

  return 0;
}

위의 예제로 볼 수 있는 Thread와 Task의 차이는 다음과 같습니다.

기준 Thread Task
컴포넌트 생성자와 자식 Thread Promise와 Future
통신 공유 변수 채널
동기화 join()을 이용한 대기 get()을 이용한 블록킹 호출
Exception 발생시 Thread 및 부모 프로그램 종료됨 Promise에서 Future로 Exception을 throw 함

Thread에서 데이터 통신을 위한 공유 변수는 Mutex 등으로 안전하게 보호해야 하지만, Task에서는 통신 채널이 이미 보호를 받고 있는 상태이기 때문에 Mutex 등을 사용할 필요가 없습니다.

Thread에서 예외가 발생하면, 해당 Thread는 종료가 되며 Thread 생성자 및 전체 프로그램도 종료가 됩니다. 하지만, Task에서는 Exception을 Future에게 발생시켜 예외 처리를 하도록 합니다.


Future 및 Promise 예제

#include <iostream>
#include <future>
#include <thread>

using namespace std;

void add(promise<int> &&resultPromise, int a, int b) {
  resultPromise.set_value(a + b);
}

int main() {
  cout << "Hello, World!" << endl;

  promise<int> sumPromise;
  auto sumFuture = sumPromise.get_future();

  int a = 100;
  int b = 200;
  thread sumThread(add, move(sumPromise), a, b);

  cout << "a + b = " << sumFuture.get() << endl;

  sumThread.join();

  return 0;
}

MacOS에서 Sandbox Permission 추가하기

|

MacOS 프로그램들은 기본적으로 Sandbox에서 동작하고 있습니다. 따라서 네트워크나 공유 자원 등에 접근할 때 Permission을 획득해야 사용할 수 있는 경우가 있습니다.

대표적인 예로 이미지를 네트워크를 통해 가져오는 Image.network 함수를 사용할 경우 다음 오류가 발생합니다.

SocketException: Connection failed (OS Error: Operation not permitted, errno = 1)

이 경우 macos/Runner/DebugProfile.entitlements 파일에 다음 권한을 추가해주면 됩니다.

    <key>com.apple.security.network.client</key>
    <true/>


MacOS의 Permission 리스트

MacOS에서 요구하는 Permission 리스트는 여기에서 확인하실 수 있습니다.

또한 런타임 중에 확인해야 하는 Hardened Runtime 리스트는 여기에서 확인 가능합니다.

Flutter BottomNavigationBar 예제

|

하단 탭을 이용한 네비게이션 예제 코드입니다.

main.dart

import 'package:flutter/material.dart';

void main() => runApp(SnowDeerApp());

class SnowDeerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SnowDeer App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: Colors.deepPurple,
      ),
      home: MainWidget(),
    );
  }
}

class MainWidget extends StatefulWidget {
  @override
  State createState() => _MainWidgetState();
}

class _MainWidgetState extends State<MainWidget> {
  var _index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hello Snowdeer'),
      ),
      body: Center(
        child: Text('Page Index: $_index'),
      ),
      bottomNavigationBar: BottomNavigationBar(
          currentIndex: _index,
          onTap: (index) {
            setState(() {
              _index = index;
            });
          },
          items: [
            BottomNavigationBarItem(
              title: Text('Home'),
              icon: Icon(Icons.home),
            ),
            BottomNavigationBarItem(
              title: Text('DoAction'),
              icon: Icon(Icons.email),
            ),
            BottomNavigationBarItem(
              title: Text('DoBehavior'),
              icon: Icon(Icons.favorite),
            ),
          ]),
    );
  }
}


실제 페이지 이동하는 예제

import 'package:flutter/material.dart';

void main() => runApp(SnowDeerApp());

class SnowDeerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SnowDeer App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: Colors.deepPurple,
      ),
      home: MainWidget(),
    );
  }
}

class MainWidget extends StatefulWidget {
  @override
  State createState() => _MainWidgetState();
}

class _MainWidgetState extends State<MainWidget> {
  var _index = 0;
  var _pages = [
    HomePageWidget(),
    DoActionPageWidget(),
    DoBehaviorPageWidget(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hello Snowdeer'),
        actions: [
          IconButton(
            icon: Icon(
              Icons.add,
              color: Colors.white,
            ),
            onPressed: () {},
          )
        ],
      ),
      body: _pages[_index],
      bottomNavigationBar: BottomNavigationBar(
          currentIndex: _index,
          onTap: (index) {
            setState(() {
              _index = index;
            });
          },
          items: [
            BottomNavigationBarItem(
              title: Text('Home'),
              icon: Icon(Icons.home),
            ),
            BottomNavigationBarItem(
              title: Text('DoAction'),
              icon: Icon(Icons.email),
            ),
            BottomNavigationBarItem(
              title: Text('DoBehavior'),
              icon: Icon(Icons.favorite),
            ),
          ]),
    );
  }
}

class HomePageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Home'),
    );
  }
}

class DoActionPageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('DoAction'),
    );
  }
}

class DoBehaviorPageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('DoBehavior'),
    );
  }
}