본글은 flutter 공부를 위해 아래 영상을 gemini로 요약한것입니다.
https://www.youtube.com/watch?v=3iZAxxa7vdc&list=PL82uaKJraAIJgB8SMOknKQHOnMomaM7Fc&index=1

 


Flutter 위젯, Minecraft로 이해하기

안녕하세요! 이번 포스팅에서는 Flutter 개발의 핵심인 위젯(Widget) 개념을 쉽고 재미있게 설명해 드리려고 합니다. Flutter를 처음 접하는 분들이나 위젯 개념이 아직 어렵게 느껴지는 분들을 위해 Minecraft 게임에 비유하여 자세히 알아보겠습니다.

위젯이란 무엇일까?

Flutter에서 위젯은 화면에 보이는 모든 것을 나타냅니다. 텍스트, 이미지, 버튼 같은 기본적인 요소부터 화면 레이아웃을 구성하는 복잡한 요소까지, 모든 것이 위젯으로 이루어져 있어요. 마치 Minecraft에서 블록 하나하나가 모여 멋진 건축물을 만드는 것과 같습니다.

위젯의 구성 요소: 중첩과 인수

Minecraft에서 집을 짓는 과정을 떠올려볼까요? 집은 기초, 벽, 지붕 등 여러 부분으로 나눌 수 있습니다. Flutter의 위젯도 마찬가지입니다.

  • 중첩(Nesting): 큰 위젯 안에 작은 위젯들이 포함될 수 있습니다. 예를 들어, 집이라는 위젯 안에 기초 위젯, 벽 위젯, 지붕 위젯이 들어갈 수 있죠. 그리고 벽 위젯 안에는 다시 창문 위젯이나 문 위젯이 들어갈 수 있습니다. 마치 Minecraft에서 여러 종류의 블록을 쌓아 올리는 것처럼요.
  • 인수(Arguments): 각 위젯은 인수를 통해 다른 위젯을 내부에 배치할 수 있습니다. 예를 들어, 기초 위젯에는 foundation이라는 인수가 있어서 그 안에 기초 위젯을 넣어줄 수 있습니다. 벽 위젯에는 wall 인수를 통해 벽 위젯을 넣고, 지붕 위젯에는 roof 인수를 통해 지붕 위젯을 넣는 식이죠. 이렇게 인수를 통해 위젯들을 조립하며 복잡한 UI를 구성해 나갑니다.

위젯, 마치 큐브와 모양처럼!

위젯과 인수를 비유하자면, 위젯은 큐브이고 인수는 그 큐브의 특정 모양이라고 생각할 수 있습니다. 우리는 올바른 위젯을 올바른 인수에 할당해야 합니다. 예를 들어, 네모난 구멍에 세모난 블록을 넣을 수 없는 것과 같아요. 정확한 위젯을 올바른 위치에 배치해야 원하는 UI를 만들 수 있습니다.

마치며

Flutter의 위젯은 처음에는 복잡하게 느껴질 수 있지만, Minecraft의 블록처럼 생각하면 훨씬 쉽게 이해할 수 있습니다. 위젯들이 중첩되고 인수를 통해 서로 연결되면서 강력하고 유연한 UI를 만들어낼 수 있다는 점을 기억해주세요.

플루터를 공부하려고 여러사이트 들을 들어가니니 Widget이나 Element,RenderObject 있었습니다. 그렇다면 그특징은 무었이고 또 차이는 무었일까요?

 

Widget

 

  • Widget은 Flutter에서 UI를 구축하기 위한 기본적인 구성 요소입니다.
  • Widget은 화면에 무엇을 그릴지에 대한 정보를 가지고 있으며, 재사용 가능하고 조합 가능한 조각들로 구성됩니다.
  • Widget은 불변(immutable)하며, 한 번 정의된 후에는 변경되지 않습니다.
  • Widget은 특정한 구조, 레이아웃, 스타일, 상태 및 상호작용을 정의할 수 있습니다.
  • Widget은 화면에 렌더링될 때 Element로 변환됩니다.

=>  프레임워크의 공개 API 또는 청사진입니다. 개발자는 일반적으로 위젯 구성만 처리합니다.

 

Element

 

  • Element는 Widget의 인스턴스를 나타내는 실제 요소입니다.
  • Element는 Widget의 구체적인 인스턴스에 대한 정보를 가지고 있으며, 화면에 표시되는 데 필요한 작업을 수행합니다.
  • Element는 상태를 가질 수 있으며, 상태가 변경될 때마다 화면을 업데이트할 수 있습니다.
  • Element는 Widget 트리를 구성하고 관리하며, 필요에 따라 Widget의 생성, 수정, 삭제 등을 처리합니다.
  • Element는 화면에 표시되는 데 필요한 최소한의 작업만 수행하며, 화면을 업데이트할 때 효율적인 방식으로 처리됩니다.

=> 위젯과 위젯의 렌더 객체를 관리합니다. 트리의 모든 위젯 인스턴스에는 해당 요소가 있습니다.

 

RenderObject

 

  • RenderObject는 Flutter의 렌더링 엔진에서 UI 요소를 표현하고 그리기 위한 저수준의 추상화입니다.
  • RenderObject는 UI 요소의 크기, 위치, 그리기, 레이아웃 등을 정의합니다.
  • RenderObject는 화면에 표시되는 실제 UI 요소를 나타냅니다.
  • RenderObject는 Widget과 Element 사이에서 동작하며, Widget은 RenderObject를 생성하고 업데이트하기 위한 구성 요소를 제공하고, Element는 RenderObject를 관리하고 렌더링 작업을 수행합니다.

=> 특정 위젯 인스턴스를 배치하는 일을 담당합니다. 적중 테스트 및 제스처와 같은 사용자 상호 작용도 처리합니다.

 

https://www.kodeco.com/books/flutter-apprentice/v1.0/chapters/4-understanding-widgets

결과적으로 Widget은 UI 구성을 추상화한 개념이고, Element는 Widget의 구체적인 인스턴스로서 화면에 대한 정보와 상태를 가지며 렌더링 작업을 수행합니다. RenderObject는 렌더링 엔진에서 실제 UI 요소를 표현하고 그리기 위한 저수준의 추상화로서 크기, 위치, 그리기, 레이아웃 등을 정의합니다. Widget은 Element로 변환되고, Element은 RenderObject와 함께 작동하여 화면에 UI를 그리는 작업을 수행합니다. 이

 

참고 :

https://medium.flutterdevs.com/how-flutter-renders-widgets-34d4f272b3c3

 

How Flutter Renders Widgets?

In this Article, We are going to learn about How Exactly Flutter Renders Widgets. This may might not be a Key-Facets in the Daily…

medium.flutterdevs.com

https://www.kodeco.com/books/flutter-apprentice/v1.0/chapters/4-understanding-widgets

 

Flutter Apprentice, Chapter 4: Understanding Widgets

Dive into the theory behind widgets. Get a better understanding of how widgets are rendered, how their lifecycle works and which tools to use to debug them.

www.kodeco.com

 

'Flutter > 프레임워크' 카테고리의 다른 글

[Flutter] 동작원리 및 특징  (0) 2023.06.15

- 시작하며 -

 

Flutter는 구글에서 개발한 오픈 소스 프레임워크로, 모바일 애플리케이션 개발을 위한 툴킷입니다. 플러터는 크로스 플랫폼 개발을 지원합니다. 다른 크로스 플랫폼 개발을 지원하는 React Native가 있지만 성능면에서는 자바 스크립트를 사용하는 React Native에 비해 성능 면에서 뛰어납니다. 

출처 - Flutter vs Native vs React-Native: Examining performance

그렇다면 왜 Flutter가 성능면에서 native앱과 비교했을 때 차이가 거의 없는지에 대해 그리고 Flutter의 여러 특징들에 대해서 알아볼까 합니다.

 

- 본문 - 

https://www.kodeco.com/books/flutter-apprentice/v1.0/chapters/1-getting-started

 

컴파일된 언어

 

 플러터는 다트(Dart) 언어를 사용하여 개발됩니다. 다트는 JIT(Just-In-Time) 컴파일러와 AOT(Ahead-Of-Time) 컴파일러를 모두 지원합니다. JIT 컴파일러는 개발 시에 핫 리로딩과 같은 기능을 제공하고, AOT 컴파일러는 애플리케이션을 미리 컴파일하여 실행 속도를 향상 시킵니다.

 

핫 리로딩(Hot Reload)

 

 플러터는 핫 리로딩 기능을 제공하여 애플리케이션 코드의 변경 사항을 실시간으로 적용할 수 있습니다. 개발자는 앱을 다시 컴파일하거나 재시작할 필요 없이 코드를 수정하고 결과를 즉시 확인할 수 있습니다. 이는 애플리케이션 개발 시간을 단축하고 실험 및 디버깅을 쉽게 할 수 있게 합니다.

 

네이티브 코드와의 상호 작용

 

 플러터는 네이티브 코드와 통신하기 위해 Flutter 엔진을 사용합니다. Flutter 엔진은 C/C++로 작성되어 있으며, 네이티브 플랫폼과 직접적으로 상호 작용할 수 있는 인터페이스를 제공합니다. 이를 통해 플러터 앱은 네이티브 앱과 동일한 성능을 제공할 수 있습니다.

 

하드웨어 가속

 

 플러터는 GPU 가속을 지원합니다. 이는 애니메이션과 그래픽 처리 등에 대한 성능을 향상 시키는 데 도움을 줍니다. GPU 가속을 사용하면 앱의 퍼포먼스가 향상되어 네이티브 앱과 유사한 경험을 제공할 수 있습니다.

 

최적화된 위젯 엔진

 플러터는 위젯 기반 구조를 가지고 있습니다. 위젯은 애플리케이션의 UI 구성 요소를 표현하는 데 사용됩니다. 플러터는 위젯의 변경 사항을 효율적으로 업데이트하고, 필요한 경우에만 다시 렌더링 합니다. 이를 통해 플러터 앱은 필요한 최소한의 작업만 수행하여 성능을 최적화할 수 있습니다.

 

최적화된 렌더링 엔진

 

 플러터는 Skia 그래픽 라이브러리를 사용하여 UI를 그리고 렌더링 합니다. Skia는 고성능 그래픽 엔진으로 알려져 있으며, 플러터는 Skia를 이용하여 UI를 빠르고 부드럽게 렌더링합니다.

 

 

- 마치며 - 

 이러한 이유로 플러터는 네이티브 앱과 거의 동일한 성능을 제공한다는 것을 알 수 있었습니다. 그렇다면 다음 포스팅에는 Flutter의 구조와 위젯들을 알아봅시다. 

 

참고:

https://catsbi.oopy.io/dbe067c8-0ff1-484a-ad34-8b9051c0b6d5

 

CH.1 플러터

목차

catsbi.oopy.io

 

https://dev-repository.tistory.com/108

 

Flutter 개념, 구조 및 장점

Flutter를 본격적으로 사용해보기 전에 Flutter가 어떤 것인지, 어떤 식으로 동작하는지와 장점에 대해 정리해본다. Flutter란? Flutter는 구글에서 2017년 5월에 출시한 모바일/웹/데스크톱 크로스 플랫

dev-repository.tistory.com

https://www.kodeco.com/books/flutter-apprentice/v1.0/chapters/1-getting-started

 

Flutter Apprentice, Chapter 1: Getting Started

Welcome to Flutter! This chapter explains what Flutter is, why you should use it and how to get your development environment set up.

www.kodeco.com

 

- 시작하며 -

 

오랜 시간이 걸리는 작업이나 외부 리소스와의 상호작용을 효율적으로 처리하는 데에는 비동기 작업이 필수적입니다. 이번 포스팅에서는 다트에서 비동기 작업을 어떻게 처리하는지, 그리고 어떤 기능들을 사용하는지 알아보도록 하겠습니다.

 

- 본문 -

 

다트에서 비동기 작업을 처리하기 위해 asyncawait 키워드를 사용합니다. async 키워드는 함수 선언 앞에 붙여 비동기 함수임을 나타내고, await 키워드는 비동기 작업의 완료를 기다릴 때 사용됩니다. 이를 통해 코드를 보다 직관적이고 동기적으로 작성할 수 있습니다.

 

다트에서 비동기 작업은 FutureStream을 사용하여 처리됩니다. Future는 비동기 작업의 결과를 나타내는 객체이며, await 키워드와 함께 사용하여 비동기 작업의 완료를 기다릴 수 있습니다. 또한, Stream은 비동기적인 데이터의 흐름을 나타내는 객체로 여러 개의 값을 비동기적으로 제공하고 받을 수 있습니다.

 

Future의 팩토리 메서드로는

Future(FutureOr<T> computation())
  • Timer.run을 사용하여 비동기적으로 computation 함수를 호출한 결과를 가지고 있는 Future 객체를 생성합니다.
  • computation 함수는 FutureOr<T>를 반환해야 합니다.
  • 특정 코드 블록이 이벤트 루프에 의해 더 이상 블로킹되지 않고 비동기적으로 실행되어야 할 때 사용됩니다
Future.delayed(Duration duration, [FutureOr<T> computation()?])
  • 지정된 시간(duration) 후에 computation 함수를 실행하여 그 결과를 가지고 있는 Future 객체를 생성합니다.
  • computation 함수는 선택적 매개변수이며, 기본값은 null입니다. FutureOr<T>를 반환해야 합니다.
  • 특정 시간이 경과한 후에 작업을 비동기적으로 수행해야 할 때 사용됩니다.
Future.error(Object error, [StackTrace? stackTrace])
  • 에러를 가지고 있는 Future 객체를 생성합니다.
  • error 매개변수에는 발생한 에러를 전달하고, stackTrace 매개변수는 선택적으로 사용될 수 있는 에러의 스택 트레이스 정보를 전달합니다.
  • 에러 처리가 필요한 상황에서 사용됩니다.
Future.microtask(FutureOr<T> computation())
  • scheduleMicrotask를 사용하여 비동기적으로 computation 함수를 호출한 결과를 가지고 있는 Future 객체를 생성합니다.
  • computation 함수는 FutureOr<T>를 반환해야 합니다.
  • 마이크로태스크 큐에 넣어야 하는 작업을 비동기적으로 실행해야 할 때 사용됩니다. 마이크로태스크는 다른 이벤트 루프 이벤트들보다 우선순위가 높습니다.
Future.sync(FutureOr<T> computation())
  • 즉시 호출된 computation 함수의 결과를 가지고 있는 Future 객체를 생성합니다.
  • computation 함수는 FutureOr<T>를 반환해야 합니다.
  • 동기적인 작업의 결과를 비동기적인 작업의 형태로 처리하고자 할 때 사용됩니다.
Future.value([FutureOr<T>? value]):
  • 주어진 값(value)으로 완료된 Future 객체를 생성합니다.
  • value는 선택적 매개변수이며, FutureOr<T> 타입의 값을 전달할 수 있습니다.
  • 이미 완료된 값을 가지고 있는 Future를 생성하고자 할 때 사용됩니다.

여기서 FutureOr<T>들은 다트(Dart)에서 사용되는 제네릭 타입으로, T 타입 또는 Future<T> 타입을 나타냅니다. 즉, FutureOr<T>는 T 타입의 값 또는 Future<T> 객체를 가질 수 있는 유연한 타입을 의미합니다.

 

FutureOr<T>의 사용은 비동기 작업을 처리할 때 유용합니다. 보통 비동기 작업의 결과는 Future<T> 객체로 반환되지만, 때로는 동기적인 작업의 결과를 처리하기 위해 T 타입의 값을 직접 사용하고 싶을 수 있습니다. FutureOr<T>는 이러한 상황에서 T 타입의 값을 처리하거나 Future<T> 객체를 대신 사용할 수 있도록 해줍니다.

 

이 정도의 기본 지식만 가지고 Dart 공식문서의 코드들을 풀이해 봅시다


예: 비동기 함수를 잘못 사용하는 경우

// This example shows how *not* to write asynchronous Dart code.

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(
      const Duration(seconds: 2),
      () => 'Large Latte',
    );

void main() {
  print(createOrderMessage());
}

//출력
//Your order is: Instance of '_Future<String>'

해당코드는 이럼 흐름으로 흘러갑니다

1. main함수에서 createOrderMessage()라는 문자열을 함수를 호출합니다.

2. 호출된 createOrderMessage()에서는 order의 값을 가져오는 Future<String>을 반환하는 fetchUserOrder()라는 함수를 호출합니다.

3. fetchUserOrder()는 딜레이 때문에 2초 뒤 'Large Latte'라는 값을 반환한다고 하지만 해당 함수는 비동기함수이기 때문에 await 가 없는 이상 createOrderMessage()에서는 해당값을 기다리지 않고 Your order is: Instance of '_Future<String>'를 반환해 버립니다

4. print 함수는 createOrderMessage()가 일반적인 동기함수이기 때문에 Your order is: Instance of '_Future<String>'를 출력합니다.


이 코드에서 알 수 있는 점은 아래와 같습니다.

1. Futture라는 제네릭 형태의 클래스가 있으며 해당함수는 비동기 함수를 구현할 때 사용한다

2. 따로 처리를 하지 않는 이상  비동기 함수를 기다리지 않는다.

3. Future<String>의 문자열 표현법은 Instance of 'Future<String>'

 

그렇다면 이제 이 코드를 조금 수정하여 Your order is: Large Latte가 출력되게 바꿔봅시다.

 

Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    Future.delayed(
      const Duration(seconds: 2),
      () => 'Large Latte',
    );

void main() async {
  print(await createOrderMessage());
}

//출력
//Your order is: Large Latte

createOrderMessage 함수에 async 키워드를 추가하고, fetchUserOrder 함수를 await 키워드와 함께 호출하여 비동기 작업이 완료될 때까지 기다리도록 수정했습니다. 이를 통해 비동기 작업의 결과를 올바르게 처리할 수 있게 되었습니다.


예: 비동기 함수 내에서 실행

Future<void> printOrderMessage() async {
  print('Awaiting user order...');
  var order = await fetchUserOrder();
  print('finish await');
  print('Your order is: $order');
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex and slow.
  return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}

void main() async {
  countSeconds(4);
  await printOrderMessage();
  print('finish');
}

// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
  for (var i = 1; i <= s; i++) {
    Future.delayed(Duration(seconds: i), () => print(i));
  }
}

//출력
//Awaiting user order...
//1
//2
//3
//4
//finish await
//Your order is: Large Latte
//finish

1. main함수에서 countSeconds(4)를 호출합니다. 

2. countSeconds(4)는 1초 후 1을, 이후 2초 후 2를 출력하며 이런 방식으로 4까지 출력할 것을 for문으로 Future.delayed() 함수를 호출하여 예약합니다. 여기서 main 함수로 넘어와서 main 함수에서는 printOrderMessage()를 await 하게 호출합니다.

3. await 하게 호출한 printOrderMessage()에서는 'Awaiting user order...'를 호출하고 fetchUserOrder()의 값을 await 하게 호출합니다.

4. await 하게 호출한 fetchUserOrder() 4초 후 'Large Latte'를 반환할 것을 예약합니다. 그렇지만 await 하게 불러왔으므로 4초를 기다리고 order에 'Large Latte'라는 값을 넣어줍니다. 그사이 countSeconds로 예약한 출력들은 4초가 지났기에 모두 출력됩니다.

5. 이후 printOrderMessage()에서는 finish await을 출력 이후 'Your order is: $order'이라는 값을 출력합니다. 여기서 order의 값은 await으로 Large Latte라는 값을 넘겨받았기 때문에 Your order is: Instance of '_Future <String>'가 출력되는 것이 아니라 Your order is: Large Latte를 출력합니다.

6. main함수에서는 printOrderMessage()의 작업이 모두 끝났기 때문에 finish를 추력하고 종료됩니다.

 


이예시는 await이 어떻게 동작하는지 보여줍니다. await이 해당 값을 사용할 때만 기다리는 것이 아닌 await이 호출된 즉시 기다린다는 것을 보여줍니다. 또한 await을 한상태에서도 이미 예약한 작업들을 무시하고 기다리는 것이 아닌 따로 돌아간다는 것을 보여줍니다.


예: try-catch를 사용한 비동기 및 대기

Future<void> printOrderMessage() async {
  try {
    print('Awaiting user order...');
    var order = await fetchUserOrder();
    print('test');
    print(order);
  } catch (err) {
    print('Caught error: $err');
  }
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex.
  var str = Future.delayed(
      const Duration(seconds: 4),
      () => throw 'Cannot locate user order');
  return str;
}

void main() async {
  await printOrderMessage();
}

//출력
//Awaiting user order...
//Caught error: Cannot locate user order

1. main함수에서 printOrderMessage()를 await 하게 호출합니다.

2. printOrderMessage()에서는 try catch 문을 이용하여 에러가 생기경우 Caught error: $err형태로 출력합니다.

3. try catch 문안에서는 Awaiting user order... 를 출력 후 fetchUserOrder()를 await 하게 호출합니다.

4. await하게 호출한 fetchUserOrder()에서는 4초 후 'Cannot locate user order'라는 에러를 throw 해줍니다. 이후 printOrderMessage() 에서는 이 throw를 catch 하여 Caught error: Cannot locate user order를 출력하고  try catch 문은 닫히고 printOrderMessage() 함수는 종료됩니다.


이 코드에서 비동기작업을 try-catch 하여 비동기에서도 해당 작업의 에러들을 catch 할 수 있다는 것을 보여줍니다.

 

- 마치며 -

 

비동기 작업 기능은 개발자에게 편의성과 성능을 제공하여 생산성을 높이는데 큰 도움을 줄 수 있는 기능입니다. 이번에배운 Future클래스를 잘 사용한다면 비동기처리구현을 더욱 쉽게 구현할 수 있을 것입니다. 포스팅은 여기까지 마치며 다음에는 다트의 Stream으로 찾아오겠습니다.


참고 :

https://api.dart.dev/stable/3.0.4/dart-async/Future-class.html

 

Future class - dart:async library - Dart API

The result of an asynchronous computation. An asynchronous computation cannot provide a result immediately when it is started, unlike a synchronous computation which does compute a result immediately by either returning a value or by throwing. An asynchron

api.dart.dev

https://api.dart.dev/stable/3.0.4/dart-async/FutureOr-class.html

 

FutureOr class - dart:async library - Dart API

A type representing values that are either Future or T. This class declaration is a public stand-in for an internal future-or-value generic type, which is not a class type. References to this class are resolved to the internal type. It is a compile-time er

api.dart.dev

https://dart.dev/codelabs/async-await

 

Asynchronous programming: futures, async, await

Learn about and practice writing asynchronous code in DartPad!

dart.dev

https://brunch.co.kr/brunchbook/dartforflutter

 

[브런치북] 플러터를 위한 다트 언어

플러터는 구글이 만든 크로스 플랫폼 앱 개발 프레임워크입니다. 즉 하나의 코드로 다양한 환경에서 동작하는 앱을 개발할 수 있습니다. 현재는 모바일 양대산맥인 안드로이드와 iOS를 동시에

brunch.co.kr

 

- 시작하며 -

 

다트(Dart) 프로그래밍에서는 코드를 간결하고 효율적으로 작성하기 위해 다양한 기능과 문법을 활용할 수 있습니다. 이 중에서도 캐스케이드 연산자와 스프레드 연산자는 많은 개발자들에게 유용한 도구로 인정받고 있습니다. 이번 포스팅에서는 캐스케이드 연산자와 스프레드 연산자에 대해 자세히 알아보고, 실제 코드를 통해 그 활용 방법을 살펴보겠습니다.

 

- 본문 - 

 

캐스케이드 연산자 (Cascade Operator) 

 

캐스케이드( .., ?.. )를 사용하면 동일한 개체에 대해 일련의 작업을 수행할 수 있습니다. 인스턴스 멤버에 액세스 하는 것 외에도 동일한 개체에서 인스턴스 메서드를 호출할 수도 있습니다. 이렇게 하면 종종 임시 변수를 만드는 단계가 줄어들고 더 유동적인 코드를 작성할 수 있습니다.

다트 공식문서 중 ...

class Person {
  String name;
  int age;
  void introduce() {
    print("My name is $name, and I'm $age years old.");
  }
}

void main() {
  var person1 = Person();
  person1.name = "Alice";
  person1.age = 25;
  person1.introduce(); // 출력: My name is Alice, and I'm 25 years old.
  
  // Cascade Operator 적용
  var person2 = Person()
    ..name = "Alice"
    ..age = 25
    ..introduce(); // 출력: My name is Alice, and I'm 25 years old.
}

위코드처럼 객체에 대한 연속적인 작업을 간단하게 수행할 수 있는 모습을 보면 알 수 있듯이 여러 코드들을 단축시켜 가독성을 높일 수 있을 것입니다.

 


 

스프레드 연산자

 

 

Dart는 목록, 맵 및 세트 리터럴에서 스프레드 연산자 ( ... ) 및 null 인식 스프레드 연산자 ( ...? )를 지원합니다....? 스프레드 연산자는 컬렉션에 여러 값을 삽입하는 간결한 방법을 제공합니다.

다트 공식문서 중...

void main() {
  var list1 = [1, 2, 3];
  var list2 = [4, 5, 6];
  var combinedList = [...list1, ...list2];
  print(combinedList); // 출력: [1, 2, 3, 4, 5, 6]
  
  var set1 = {1, 2, 3};
  var set2 = {3, 4, 5};
  var combinedSet = {...set1, ...set2};
  print(combinedSet); // 출력: {1, 2, 3, 4, 5}
  
  var map1 = {"name": "Alice", "age": 25};
  var map2 = {"city": "New York"};
  var combinedMap = {...map1, ...map2};
  print(combinedMap); // 출력: {name: Alice, age: 25, city: New York}
}

 

위코드처럼 List, Set, Map의 스프레드 연산자를 사용하여 컬렉션을 결합하고, 펼쳐진 결과를 새로운 변수에 할당하였습니다. 이를 통해 컬렉션을 확장하거나 결합하는 작업을 보다 간결하게 수행할 수 있습니다. 또한 더 나아간다면 저번에 배운 take나 where 같은 함수들을 사용한다면 더욱 유용하게 쓸 수 있을 것입니다.

 

- 마치며 -

 

이러한 함수들은 함수형 프로그래밍에서는 "chaining" 또는 "method chaining"이라고도 불립니다. 캐스케이드 연산자는 객체의 연속적인 작업을 처리할 때, 스프레드 연산자는 컬렉션을 결합하거나 확장할 때 자주 사용됩니다. 이러한 함수들은 코드의 가독성을 향상 시키고 작업을 간결하게 표현할 수 있어 유지보수성을 높여줄 아주 좋은 코드를 짤 수 있을 것으로 보입니다.


참고 :

https://dart.dev/language/operators#cascade-notation

 

Operators

Learn about the operators Dart supports.

dart.dev

https://dart.dev/language/collections#spread-operators

 

Collections

Summary of the different types of collections in Dart.

dart.dev

 

-시작하며-

 

필자는 코틀린을 주로 사용했기 때문인지 이렇게 언어들을 배울 때 기본적인  변수생성이나 클래스 같은 여러 규칙들을 보고 나면 코틀린의 map이나 groupBy, forEach 같은 Collection과 Iterable을 다루는 데 도움을 주는 여러 함수들을 찾아보곤 합니다. 그러므로 오늘은 Dart의 고차 함수들에 대해서 알아봅시다.

 

 Collection과 Iterable을 사용하는 함수들은 여러 연속적이거나 set이나 map 같은 연속적이지 않더라도 여러 데이터 묶음들이 담긴 Collection들을 다루는 함수들입니다. 대부분의 앱이나 프로그램의 특성상 많은 종류의 데이터들을 다루게 됩니다. 이런 데이터들은 Collection에 넣어 관리를 하는데 이렇게 Collection들을 관리를 하는데 언어자체에서 도와주는 이런 함수들이 많기 때문에 이런 함수 들은 알아두면 쉽게 개발을 할 수 있을 것입니다.


-본문-

 

필터링 함수

 

where()

 

주어진 조건에 따라 컬렉션의 요소를 필터링하여 새로운 컬렉션을 생성합니다.

var numbers = [1, 2, 3, 4, 5];
var evenNumbers = numbers.where((number) => number % 2 == 0);
print(evenNumbers); // 출력: (2, 4)

 

변환 함수

 

map()

 

주어진 변환 함수를 각 요소에 적용하여 새로운 컬렉션을 생성합니다.

var numbers = [1, 2, 3, 4, 5];
var squaredNumbers = numbers.map((number) => number * number);
print(squaredNumbers); // 출력: (1, 4, 9, 16, 25)

 

축소 함수

 

fold()

 

초기값과 조합 함수를 사용하여 컬렉션의 요소를 반복적으로 결합하여 단일 값으로 축소합니다.

var numbers = [1, 2, 3, 4, 5];
var sum = numbers.fold(0, (previous, current) => previous + current);
print(sum); // 출력: 15

 

reduce()

 

조합 함수를 사용하여 컬렉션의 요소를 순차적으로 결합하여 단일 값으로 축소합니다.

var numbers = [1, 2, 3, 4, 5];
var product = numbers.reduce((previous, current) => previous * current);
print(product); // 출력: 120

 

순회 함수

 

forEach()

 

컬렉션의 각 요소에 대해 주어진 동작을 수행합니다.

var numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) => print(number));
// 출력:
// 1
// 2
// 3
// 4
// 5

 

조건 확인 함수

 

any()

 

주어진 조건을 만족하는 요소가 컬렉션에 있는지 확인합니다.

var numbers = [1, 2, 3, 4, 5];
var hasEvenNumber = numbers.any((number) => number % 2 == 0);
print(hasEvenNumber); // 출력: true

 

every()

 

컬렉션의 모든 요소가 주어진 조건을 만족하는지 확인합니다.

var numbers = [1, 2, 3, 4, 5];
var allPositive = numbers.every((number) => number > 0);
print(allPositive); // 출력: true

 

정렬 함수

 

sort()

 

컬렉션의 요소를 정렬합니다.

var numbers = [5, 2, 4, 1, 3];

// 순서대로 정렬
numbers.sort();
print(numbers); // 출력: [1, 2, 3, 4, 5]

// 역순으로 정렬
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 출력: [5, 4, 3, 2, 1]

// 절대값에 기반한 정렬
numbers.sort((a, b) => a.abs().compareTo(b.abs()));
print(numbers); // 출력: [1, 2, 3, 4, 5]

 

기타 함수

 

skip()

 

주어진 개수만큼 컬렉션의 요소를 건너뜁니다.

var numbers = [1, 2, 3, 4, 5];
var skippedNumbers = numbers.skip(2);
print(skippedNumbers); // 출력: (3, 4, 5)

 

take()

 

주어진 개수만큼 컬렉션의 요소를 선택합니다.

var numbers = [1, 2, 3, 4, 5];
var selectedNumbers = numbers.take(3);
print(selectedNumbers); // 출력: (1, 2, 3)

 

toList()

 

Iterable을 List로 변환합니다.

var numbers = [1, 2, 3, 4, 5];
var numberList = numbers.toList();
print(numberList); // 출력: [1, 2, 3, 4, 5]

 

toSet()

 

Iterable을 Set으로 변환합니다.

var numbers = [1, 2, 2, 3, 4, 5, 5];
var numberSet = numbers.toSet();
print(numberSet); // 출력: {1, 2, 3, 4, 5}

-마치며- 

 

저도 Kotlin으로 코딩을 해보았지만 이번에 Dart로 Collection 관련함수들을 공부하면서 기존에 코딩할 때 잊고 있던 여러함들을 기억할 수 있어서 좋은 기회가 되었던 것 같습니다.

 

참고 :

https://dart.dev/codelabs/iterables

 

Iterable collections

An interactive guide to using Iterable objects such as lists and sets.

dart.dev

 

'Flutter > Dart' 카테고리의 다른 글

[Dart] 비동기 작업 클래스, Future  (0) 2023.06.12
[Dart] Cascade Operator 와 Spread Operator  (0) 2023.06.10
[Dart] Dart를 알아봅시다  (0) 2023.06.08

- 시작하며 -

 Flutter를 사용하기에 앞서 Flutter가 사용하는 언어인 Dart에 대해서 알아봅시다.

다트(Dart)는 구글이 디자인한 멀티 플랫폼 프로그래밍 언어이다. 이는 2011년 10월 10일~12일까지 열렸던 "GOTO 콘퍼런스"에서 공개되었다. 다트는 자바스크립트를 대체가능하며 크로스 플랫폼 프로그래밍 언어를 목표로 설계되었다.

출처 : 위키피디아

 이처럼 Flutter의 언어인 Dart는 구글의 안드로이드 OS와 애플사의 IOS에서의 하이브리드 앱을 재작 하기 위해서 구글에서 고안한 언어입니다.  또한 Dart는 기본적으로 C언어의 문법과 거의 같으며 Java, C#, Javascript와 같은 기능적 스트럭처를 추가한 언어로, 언급된 언어보다 간결하고 강력한 기능을 지원합니다

- 본문 -

https://dart.dev/

 

Dart programming language

Dart is a client-optimized language for fast apps on any platform

dart.dev

다트는 위링크에서 손쉽게 돌려볼 수 있습니다.

 


 

기본규칙

main 함수
main() {

}

다른 언어들과 비슷하게 Dart는 main 함수에서 돌아갑니다

 

 주석
//한줄 주석
  
/*
 * 여러줄 주석
 */
  
///문서 주석

주석 또한 다른 언어들과 비슷한 점을 가지고는 있지만 /// 이주석 같은 경우 문서 주석으로  dartdoc와 같은 문서 생성 도구를 통해 문서를 자동으로 생성해 줍니다.

 

출력, 세미콜론(;)
print("Hello Dart");

출력함수의 경우 다른 언어들처럼 print함수를 통해 Console에 값을 출력할 수 있습니다.

다트는 C나 Java처럼 문장의 마지막 세미콜론을 찍어 컴파일러에게 여기가 문장의 마지막이라는 것을 알려줍니다.

 

메서드의 리턴 타입
add(){
	return 3+2;
}

main(){
	int sum = add();
	print(add());
}

메서든 리턴타입을 추론할 수 있기 때문에 생략이 가능합니다.

 

1급 객체

 

Dart의 경우 변수, 함수, 객체를 포함한 모든 것이 1급 객체입니다. 그렇기 때문에 new라는 키워드가 필요 없으며 메모리에 자동으로 올라가기 때문에 바로 사용할 수 있습니다.


변수와 상수
변수와 상수

 

타입추론이 가능한 변수

var이나 final, const 같은 경우 해당값이 주어진다면 그 값에 맞는 타입을 추론하여 변수의 타입을 지정할 수 있습니다.

변수 var 값을 변경할수있는 변수입니다.
상수 final 실행중에 값이 결정됩니다. 값을 변경할수 없는 상수입니다.
const 컴파일시에 값이 결정됩니다. 값을 변경할수 없는 수입니다.
//var 
var age = 25;
var name = 'John';
var isStudent = true;

//final
final int age = 25;
final String name = 'John';
final bool isStudent = true;

//const
const int age = 25;
const String name = 'John';
const bool isStudent = true;

이렇게 타입을 추론하는 변수 말고도 자료형을 사용하여 변수를 선언할 수도 있습니다.

자료형 설명
String 단따옴표와 쌍따옴표로 이루어진 문자열 , 참고로 Dart 에서는 문자는  String으로 표현
int 정수(다른언어처럼 short 난 long이없고 int 로 정수 전채를 표형함)
double 실수(위 int와 비슷한이유로 float가없다)
bool 참(True) 또는 거짓(False) 값을 가지는 자료형입니다.
List<T> 순서가 있는 항목의 목록을 나타내는 자료형입니다. 대괄호([])로 둘러싸인 요소의 목록을 사용하여 리스트를 생성할 수 있습니다.
Map<K, V> 키와 값의 쌍으로 이루어진 데이터의 집합을 나타내는 자료형입니다. 중괄호({})로 둘러싸인 키-값 쌍의 목록을 사용하여 맵을 생성할 수 있습니다.
dynamic 동적 타입을 나타내는 특수한 자료형입니다. 변수가 동적 타입을 가지면 컴파일러는 해당 변수의 타입을 추론하지 않고 런타임에 타입을 결정합니다.
//String
String name = "John";
String message = 'Hello, Dart!';
String concatenated = name + ' says ' + message;

//int
int age = 25;
int quantity = 10;
int result = age * quantity;

//double
double pi = 3.14;
double radius = 2.5;
double area = pi * radius * radius;

//bool
bool isRaining = true;
bool isSunny = false;
bool isCloudy = !isRaining;

//List<T>
List<int> numbers = [1, 2, 3, 4, 5];
List<String> names = ['Alice', 'Bob', 'Charlie'];
List<dynamic> mixed = [1, 'two', true];

//Map<K, V>
Map<String, int> ages = {'Alice': 25, 'Bob': 30, 'Charlie': 35};
Map<String, dynamic> person = {'name': 'John', 'age': 40, 'isStudent': false};

//dynamic
dynamic value1 = 10;
dynamic value2 = 'Hello';
dynamic value3 = true;
Nullable

Dart에서도 Kotlin처럼 nullable 처리를 할 수 있습니다. Nullable 타입을 사용하려면 변수의 타입 뒤에?를 붙입니다. 이를 통해 해당 변수가 null을 가질 수 있음을 표시합니다.

int? nullableValue = null;
String? nullableString = 'Hello';

널 처리의 경우 Kotlin과 매우 유사합니다

  Dart Kotlin 설명
널 체크 연산자 ! !! Nullable 타입의 변수를 사용할 때 변수가 null이 아님을 명시적으로 표시할때 사용합니다.
조건부 접근 연산자 ?. ?. Nullable 타입의 변수를 사용할 때 변수가 null이 아닌 경우에만 접근하려는 경우에 사용합니다.
널 병합 연산자  ?? ?: Nullable 타입의 변수가 null일 때 대체 값을 지정하여야하는 경우 사용합니다.
int? nullableValue = null;
String? nullableString = 'Hello';
//------------------------------------------------
int? nullableValue = 5;
int nonNullableResult = nullableValue!; // nullableValue는 null이 아님을 보장
//------------------------------------------------
String? nullableString = 'Hello';
int? stringLength = nullableString?.length; // nullableString이 null이면 stringLength도 null
//------------------------------------------------
String? nullableString = null;
String nonNullableString = nullableString ?? 'Default Value'; // nullableString이 null이면 'Default Value' 할당

 

연산자
산술 연산자 + 덧셈 연산을 수행합니다.
- 뺄셈 연산을 수행합니다.
* 곱셈 연산을 수행합니다.
/ 나눗셈 연산을 수행하고 결과를 부동 소수점으로 반환합니다.
% 나머지 연산을 수행합니다.
할당 연산자 = 우변의 값을 좌변에 할당합니다.
+=, -=, *=, /=, %= 우변의 값을 좌변의 값과 함께 연산하여 결과를 좌변에 할당합니다.
증감 연산자 ++ 피연산자의 값을 1 증가시킵니다.
-- 피연산자의 값을 1 감소시킵니다.
비트 연산자 비트 왼쪽 시프트 연산자
<<
왼쪽 피연산자의 비트를 오른쪽 피연산자만큼 왼쪽으로 이동시킵니다. 이동된 비트는 왼쪽에서부터 0으로 채워집니다.
비트 오른쪽 시프트 연산자
>>
왼쪽 피연산자의 비트를 오른쪽 피연산자만큼 오른쪽으로 이동시킵니다. 이동된 비트는 부호 비트(최상위 비트)와 동일한 값으로 채워집니다.
비교 연산자 == 두 값이 같은지 비교합니다.
!= 두 값이 다른지 비교합니다.
>, <, >=, <= 크기 비교를 수행합니다.
논리 연산자 && 논리 AND 연산을 수행합니다.
|| 논리 OR 연산을 수행합니다.
! 논리 NOT 연산을 수행합니다.
조건부 연산자 ? 조건식의 참/거짓에 따라 값을 선택합니다.
타입 확인 및 캐스트 연산자 is 객체의 타입을 확인합니다.
as 객체를 지정된 타입으로 변환합니다.
멤버십 연산자 in 특정 요소가 컬렉션(리스트, 맵 등)에 속하는지를 확인하는 데 사용됩니다
// 산술 연산자
int sum = 10 + 5;  // 15
int difference = 10 - 5;  // 5
int product = 10 * 5;  // 50
double quotient = 10 / 5;  // 2.0
int remainder = 10 % 3;  // 1

// 할당 연산자
int num1 = 10;
num1 += 5;  // num1 = num1 + 5; // 15
num1 -= 3;  // num1 = num1 - 3; // 12
num1 *= 2;  // num1 = num1 * 2; // 24
num1 /= 4;  // num1 = num1 / 4; // 6.0
num1 %= 4;  // num1 = num1 % 4; // 2

// 증감 연산자
int num2 = 5;
num2++;  // num2 = num2 + 1; // 6
num2--;  // num2 = num2 - 1; // 5

// 비트 연산자:
int a = 5;  // 이진수: 0000 0101
int b = 3;  // 이진수: 0000 0011

int bitwiseAnd = a & b;  // 비트 AND 연산: 0000 0001 (1)
int bitwiseOr = a | b;  // 비트 OR 연산: 0000 0111 (7)
int bitwiseXor = a ^ b;  // 비트 XOR 연산: 0000 0110 (6)
int bitwiseNotA = ~a;  // 비트 NOT 연산: 1111 1010 (-6)
int leftShift = a << 2;  // 비트 왼쪽 시프트 연산: 0001 0100 (20)
int rightShift = a >> 1;  // 비트 오른쪽 시프트 연산: 0000 0010 (2)

// 비교 연산자
int num3 = 10;
int num4 = 5;

bool isEqual = (num3 == num4);  // false
bool isNotEqual = (num3 != num4);  // true
bool isGreater = (num3 > num4);  // true
bool isLess = (num3 < num4);  // false
bool isGreaterOrEqual = (num3 >= num4);  // true
bool isLessOrEqual = (num3 <= num4);  // false

// 논리 연산자
bool condition3 = true;
bool condition4 = false;

bool andResult = (condition3 && condition4);  // false
bool orResult = (condition3 || condition4);  // true
bool notResult = !condition3;  // false

// 멤버십 연산자
List<int> numbers = [1, 2, 3, 4, 5];

bool isPresent = 3 in numbers;  // true
bool isMissing = 6 in numbers;  // false

 


흐름제어문
조건문

 if, else if, else 문, switch문, 3항 연산자  => 자바의 조건문과 같음

if (condition1) {
  // condition1이 참일 때 실행될 코드
} else if (condition2) {
  // condition1이 거짓이고 condition2가 참일 때 실행될 코드
} else {
  // 모든 조건이 거짓일 때 실행될 코드
}
//-------------------------------------------------------------------
switch (expression) {
  case value1:
    // value1에 대한 코드
    break;
  case value2:
    // value2에 대한 코드
    break;
  default:
    // 어떤 경우에도 일치하지 않을 때 실행될 코드
}
//-------------------------------------------------------------------
(condition) ? expression1 : expression2;

 

반복문

for, while, do-while은 자바, for-in문은 코틀린을 닮음

for (var i = 0; i < 5; i++) {
  // 반복 실행되는 코드
}
//-------------------------------------------------------------------
while (condition) {
  // 조건이 참인 동안 반복 실행되는 코드
}
//-------------------------------------------------------------------
do {
  // 조건이 참인 동안 반복 실행되는 코드
} while (condition);
//-------------------------------------------------------------------
var numbers = [1, 2, 3, 4, 5];
for (var number in numbers) {
  // numbers의 각 요소에 대해 반복 실행되는 코드
}

함수
선언부
반환타입 함수이름(매개변수) {
  // 함수 몸체
}

위에서 말했듯이 리턴이 명시적이라 타입 추론이 가능하다면 반환타입을 생략가능

 

선택적 매개변수

Dart에서는 선택적 매개변수를 사용하여 함수 호출 시 일부 매개변수를 생략할 수 있습니다. 선택적 매개변수는 []로 감싼 것으로 표시되며, 기본값을 지정할 수도 있습니다.

void printUserInfo(String name, [int age = 0, String city = 'Unknown']) {
  print('Name: $name');
  print('Age: $age');
  print('City: $city');
}

void main() {
  printUserInfo('Alice');
}

 

이름 지정 매개변수

이름 지정 매개변수는 함수 호출 시 매개변수의 이름을 명시적으로 지정하여 인수를 전달하는 방식입니다. 함수 선언 시 중괄호 {}로 감싼 매개변수들을 선언하고, 함수 호출 시에는 매개변수의 이름과 함께 인수를 전달합니다. 이름 지정 매개변수를 사용하면 인수의 순서에 구애받지 않고 명시적으로 값을 전달할 수 있습니다.

void printUserInfo(String name,{int age = 3 , String? city}) {
  print('Name: $name');
  print('Age: $age');
  print('City: $city');
}

void main() {
  printUserInfo('John', age: 30, city: 'New York');
}

이름지정 매개변수 또한 선택적 매개변수처럼 생략이 가능하기 때문에 생략할 경우를 대비해 그 값이 Nullable 하거나 default 값을 가지고 있어야 합니다. 그렇지만 함수 자체에서 그값이 필수적으로 필요할때는 매개변수 선언부 자료형 앞에 required를 추가하여 값을 필수적으로 넣어줄 것을 강제할 수 있습니다.

 

익명함수

익명 함수는 이름이 없는 함수로, 다른 함수에 전달하거나 변수에 할당하여 사용할 수 있습니다. 익명 함수는 () { } 형태로 작성됩니다.

void executeFunction(Function function) {
  function();
}

void main() {
  executeFunction(() {
    print('This is an anonymous function.');
  });
}

 

화살표 함수

화살표 함수는 단일 표현식을 가진 함수를 간략하게 작성하는 방법입니다. => 기호를 사용하여 함수의 매개변수와 표현식을 연결합니다. 반환값은 자동으로 표현식의 결과로 처리됩니다.

int multiply(int a, int b) => a * b;

void main() {
  print(multiply(5, 3));  // 15
}

클래스

Dart에서 클래스는 객체 지향 프로그래밍의 핵심 요소입니다. 클래스는 객체의 구조와 행위를 정의하는 데 사용되며, 객체를 생성하기 위한 템플릿 역할을 합니다. 클래스를 사용하여 객체를 생성하고 관리할 수 있습니다.

 

선언부
class ClassName extends ParentClass { //extends ParentClass 부분은 상속할경우 추가
  // 멤버 변수 (속성)
  dataType variableName;

  // 생성자
  ClassName(constructorArguments) {
    // 생성자 로직
  }

  // 멤버 메서드 (행위)
  // @override -> 상속함수 재정의시 anotation 사용
  returnType methodName(methodArguments) {
    // 메서드 로직
  }

  // 기타 멤버 (게터, 세터 등)
  //부모의 함수는 super로 호출
}

 

인터페이스
//선언부
class InterfaceName {
  // 인터페이스의 메서드 및 속성 정의
}

class ClassName implements InterfaceName {
  // 인터페이스에서 정의한 모든 메서드 및 속성 구현
}

//예시
class Printable {
  void printInfo();
}

class Person implements Printable {
  String name;

  Person(this.name);

  @override
  void printInfo() {
    print('Person: $name');
  }
}

void main() {
  Person person = Person('Alice');
  person.printInfo();  // 출력: Person: Alice
}

Dart에서 인터페이스는 암시적으로 정의되며, 클래스가 특정 인터페이스를 구현하기 위해서는 해당 인터페이스의 모든 멤버를 구현해야 합니다. 인터페이스는 클래스와 분리된 개체로 존재하지 않으며, 단지 클래스가 구현해야 하는 메서드 및 속성의 시그니처를 제공합니다.

 


- 마치며 -

이번에 Dart를 공부하면서 언어 자체에 큰 어려움은 없었다고 느꼈습니다. 1급 객체와 익명 함수의 지원만으로도 많은 작업을 수행할 수 있어서 놀라웠습니다. 또한, Dart에서는 long, short, float, char와 같은 자료형이 없다는 점이 흥미로웠습니다. 이전에는 작은 테스트 케이스에는 short나 byte를 자주 사용했는데, int가 모든 정수를 포함한다는 사실도 신기했지만, 메모리 관리에는 우려가 있었습니다. 이에 대해 조사해보았지만, 현대의 컴퓨팅 환경에서는 정수의 크기에 따른 메모리 사용이 큰 문제가 되지 않는다는 것을 알게 되었습니다. 이것이 최근 언어들의 특징이라고 생각하니 흥미로웠습니다.

 

참고 :

https://ko.wikipedia.org/wiki/%EB%8B%A4%ED%8A%B8_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%EC%96%B8%EC%96%B4) 

 

다트 (프로그래밍 언어) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

https://dart.dev/language

 

Introduction to Dart

A brief introduction to Dart programs and important concepts.

dart.dev

 

 

+ Recent posts