Dart Coding Style

|

Dart Coding Style 은 공식 홈페이지에서 확인할 수 있습니다.

Identifiers

클래스명, 변수명, 함수명 등을 표현하는 Identifiers에는 다음과 같이 가장 많이 사용되는 3가지 유형이 있습니다. Dart에서는 이 3가지 유형을 전부 활용하고 있습니다.

  • UpperCamelCase
  • lowerCamelCase
  • lowercase_with_underscores


클래스, enum 이나 typedef, extension 등에는 UpperCamelCase

class SliderMenu { ... }

class HttpRequest { ... }

typedef Predicate<T> = bool Function(T value);

extension MyFancyList<T> on List<T> { ... }

extension SmartIterable<T> on Iterable<T> { ... }


라이브러리, 패키지, 디렉토리, 소스 파일 이름 및 import prefix에는 lowercase_with_underscores

library peg_parser.source_scanner;

import 'file_system.dart';
import 'slider_menu.dart';

import 'dart:math' as math;
import 'package:angular_components/angular_components'
    as angular_components;
import 'package:js/js.dart' as js;


그 외의 이름에는 lowerCamelCase

클래스 멤버 변수, 최상위(Top-level) 선언, 변수, 파라메터 등은 전부 lowerCamelCase를 사용합니다.

var item;

HttpRequest httpRequest;

void align(bool clearItems) {
  // ...
}


상수값(Constant)에서도 lowerCamelCase

대부분의 언어에서는 상수 값을 나타내는 변수에는 모두 대문자(SCREAMING_CAPS)로 표현하는 경우가 많은데, Dart에서는 lowerCamelCase를 권장합니다. (과거에는 Dart에서도 SCREAMING_CAPS 스타일을 사용했으나 몇 가지 단점으로 인해 lowerCamelCase 스타일로 변경했습니다.)

const pi = 3.14;
const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');

class Dice {
  static final numberGenerator = Random();
}

물론 권장이기 때문에 다음과 같은 경우에는 예외적으로 SCREAMING_CAPS를 허용하기도 합니다.

  • SCREAMING_CAPS 형태 네이밍의 변수를 사용하고 있는 기존 코드나 라이브러리를 사용할 경우
  • Dart 코드를 Java 코드와 병행해서 개발할 경우


약어들의 스타일

약어들을 대문자로만 사용할 경우 가독성에 어려움이 발생할 수 있으며, 뜻이 모호해지기도 합니다. 예를 들어 HTTPSFTP와 같은 단어는 HTTPS FTP인지 HTTP SFTP인지 알아 볼 수 없습니다. 따라서 두 단어 이상의 약어들은 일반 단어 사용하듯이 대소문자를 사용하면 됩니다.

good 예시

HttpConnectionInfo
uiHandler
IOStream
HttpRequest
Id
DB

bad 예시

HTTPConnection
UiHandler
IoStream
HTTPRequest
ID
Db


언더스코어(_)를 prefix로 사용하지 말 것

언더스코어(_)는 private를 의미하기 때문에 사용하지 말아야 합니다.


변수 이름 앞에 prefix 사용할 필요 없음

Hungarian Notation과 같이 과거에는 변수가 어떤 용도로 사용되는 건지 코드 가독성을 위해 변수 타입에 대한 prefix를 붙이는 경우가 많았으나, Dart에서는 변수의 타입, 범위(Scope), Mutability 등 요소를 모두 알려주기 떄문에 별도의 prefix를 사용할 필요가 없습니다.


import 순서

dart, package, 그 외 코드 순으로 import 합니다. 또한 exportimport 뒤에 배치하며, 각 구문은 알파벳 순으로 정렬합니다.

import 'dart:async';
import 'dart:html';

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'util.dart';

import 'src/error.dart';
import 'src/foo_bar.dart';

export 'src/error.dart';


Formating

  • dartfmt을 이용해서 formatting 적용
  • formatter에 의존하기 전에 먼저 formatter-friendly한 형태로 코드를 정리
  • 한 라인에는 80글자까지(다만 URL이나 멀티라인의 경우는 예외)

if 문 중괄호

if (isWeekDay) {
  print('Bike to work!');
} else {
  print('Go dancing or read a book!');
}

if (overflowChars != other.overflowChars) {
  return overflowChars < other.overflowChars;
}

if 문에 else 구문이 없는 경우는 다음과 같이 한 줄로 표현도 가능합니다.

if (arg == null) return defaultValue;

모던 C++ 입문 책 요약 - (1)

|

모던 C++ 입문 책을 읽고 실수하기 쉽거나 유용한 팁, 더 나은 방법 등을 정리해보았습니다.

LVvalue

주소를 지정할 수 있는 항목을 Lvalue라고 합니다.

int i = 3;
i++;
const int j = 5;
j++;
(3 + 5)++;

위의 코드에서 오직 iLvalue입니다.

증감 연산자(++, –) 등은 가독성 등에서 좋지 않습니다. j++ 보다는 j+1이 가독성이 더 좋으며, 컴파일러 최적화도 더 쉽습니다.


할당 연산자

할당 연산자는 오른쪽에서 왼쪽 순서로 결합됩니다. 하지만 가독성에서 그렇게 좋은 편은 아닙니다. 할당 연산자를 쓰지 말고 전부 개별 라인으로 작성하기를 강제화하는 언어도 있습니다.

o3 = o2 = o1 = expr;


인라인(inline)

함수를 호출하면 내부적으로 레지스터(Register)에 저장하고 스택(Stack)에 인수를 복잡하는 작업을 수행합니다. 제법 무거운 비용이 발생하며 컴파일러는 최적화 과정을 통해 함수 호출을 inline하기도 합니다.

inline을 하면 해당 코드를 함수에 포함된 연산으로 대체합니다. inline 키워드를 이용해 직접 컴파일러에게 인라인 여부를 요청할 수 있습니다.

inline double square(double x) { return x * x; }

물론 컴파일러가 inline을 무조건 수행할 의무는 없으며, 최적화 과정에서 inline 선언되지 않은 함수를 inline 하기도 합니다.


main 함수

main() 함수는 주로 다음과 같은 형태로 작성됩니다.

int main() {
  // ...
}

int main(int argc, char* argv[]) {
  // ...
}

int main(int argc, char** argv[]) {
  for (int i = 0; i < argc i++) {
    cout << argv[i] << endl;
  }

  return 0;
}

main 함수릐 리턴 값은 표준을 준수할 경우 0을 리턴합니다. (<cstdlib>EXIT_SUCCESS 매크로에 정의되어 있습니다.) return 문을 생략하더라도 컴파일러가 자동으로 return 0;을 삽입하는 경우도 있습니다.


assert

assert는 C에서 상속받은 매크로이지만 여전히 유용하게 사용되고 있습니다. 괄호 부분이 false이면 프로그램을 종료합니다.

assert의 가장 큰 장점은 개발시 마음껏 사용하다가 릴리즈(release) 모드로 빌드하면 실행 파일에서 아무런 동작을 하지 않도록 할 수 있다는 점입니다.


Exception

throw를 이용해서 예외를 던질 수도 있습니다. 이 함수를 호출하는 부분에서 예외 처리를 하지 않으면 App Crash가 발생합니다.

matrix read_matrix_file(const char* fname, ...) {
  fstream f(fname);
  if (!f.is_open()) {
    throw "Cannot open file.";
  }
  // ...
}

C++에서 문자열, 숫자, 사용자 정의 타입 등 모든 타입을 예외로 처리할 수 있지만, 적절하게 처리하게 위해서는 예외 타입을 별도로 정의하거나 표준 라이브러리의 메소드를 사용하는게 좋습니다.

예외 처리는 try ~ catch 구문으로 수행할 수 있습니다. catch(...) 블록으로 모든 예외를 처리할 수도 있습니다.


endl\n

둘 다 다음 라인으로 넘어가는 개행 문자를 생성합니다. 하지만 두 방식은 동작에서 조금 차이가 있습니다.

효율적인 출력을 위해 버퍼링(Buffering)을 사용하는데, endl은 버퍼를 비우지만, \n은 버퍼를 비우지 않습니다. 버퍼를 비우는 작업은 디버거(Debugger) 없이 디버깅 할 때 프로그램의 Crash가 발생하는 출력을 찾는데 도움이 됩니다. 대신 I/O가 느려질 수 있는 단점이 있습니다.


stringstream

표준 라이브러리에서 제공하는 stringstream은 출력 가능한 모든 타입의 문자열을 만드는데 사용할 수 있습니다. str() 메소드를 이용해서 스트림의 내부 문자열을 반환할 수 있습니다.


C++의 I/O 오류 처리

C++의 I/O에는 안전 장치가 없습니다. 예를 들어 다음과 같은 코드에서

int main() {
  std::ifstream infile("some_missing_file.txt";
  int i = 0;
  infile >> i;

  infile.close();

  return 0;
}

파일이 만약 존재하지 않더라도 파일을 여는 작업은 실패하지 않습니다.

기본적으로 스트림은 예외를 발생하지 않습니다. 역사적으로 스트림이 예외 처리보다 먼저 등장했기에 그동안의 코드 호환성을 위해 어쩔 수 없이 이렇게 되었으며, 그래서 보통 별도 플래그 체크를 통해 오류를 확인합니다.

int main() {
  std::ifstream infile("some_missing_file.txt";
  if(infile.good()) {
    int i = 0;
    infile >> i;
  }

  infile.close();

  return 0;
}


배열

배열의 크기는 컴파일 타임(Compile-time)에 결정되어져야 합니다. 런타임(Runtime)에 동적으로 바꾸고 싶을 경우에는 vector 등을 이용해야 합니다.

Introduction to widgets - (3)

|

조금 복잡한 예제

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({this.product, this.inCart, this.onCartChanged})
      : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different parts
    // of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

ShoppingListItem 클래스는 일반적인 StatelessWidget 위젯의 패턴을 따르고 있습니다. final로 정의된 변수들(product, inCart, onCartChanged)은 생성자로부터 전달받아 할당됩니다. 그리고 build() 메소드내에서 렌더링에 활용됩니다.

사용자가 위젯을 터치(onTab())하면 ShoppingListItem 클래스 내에서 이벤트를 처리하는 것이 아니라 부모로부터 생성자를 통해 전달받은 onCartChanged() 콜백 함수를 호출합니다. 이러한 패턴은 상태(state)값을 해당 위젯이 아닌 상위의 부모에게 전달함으로써 state를 더 오래동안 지속되도록 할 수 있으며 ShoppingListItem 위젯은 상태 관리할 필요 없이 주어진 값만 렌더링하면 되기 때문에 훨씬 가벼운 위젯이 될 수 있습니다.

onCartChanged() 이벤트를 전달받은 상위 부모는 그 안에서 state를 바꾸며, 그 결과에 따라 ShoppingListItem 인스턴스를 새로 생성하며 렌더링도 다시 이루어지도록 합니다. 이러한 동작은 프레임워크가 변경된 부분만을 갱신하기 때문에 가볍고 빠릅니다.


부모 위젯 예제

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  // The framework calls createState the first time a widget
  // appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses the State object
  // instead of creating a new State object.

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

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need to change
      // _shoppingCart inside a setState call to trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping List'),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: <Product>[
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

ShoppingList 위젯은 StatefulWidget 위젯입니다. ShoppingList 위젯이 UI Tree에 추가되면, 프레임워크에서는 createState() 메소드를 호출하여 _ShoppingListState 안스턴스를 생성합니다.

만약 ShoppingList 위젯의 부모 위젯이 새로 갱신되면, 하위 자식 위젯들도 갱신이 됩니다. 이 때, ShoppingList 인스턴스는 새로 만들어지지만 _ShoppingListState 인스턴스는 기존에 만들어진 인스턴스를 재활용하게 됩니다. _ShoppingListStatebuild() 메소드를 통해 새로운 위젯을 그리게 되는데, 이 때 didUpdateWidget() 메소드를 오버라이딩(overriding)하게 되면 old Widget 과 current Widget을 비교 후 다시 그릴 필요가 있는지를 정할 수 있습니다.

setState()state가 변경되었음을 프레임워크에 알려주며 프레임워크에서는 자식 위젯들을 새로 build()합니다.

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,
      ),
    );
  }
}