Flutter TodoList 예제

|

todo_item.dart

class TodoItem {
  String name = '';
  bool isChecked = false;

  TodoItem({this.name});
}


main.dart

import 'package:fileio/todo_item.dart';
import 'package:flutter/material.dart';

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

class SnowApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Todo list',
        theme: ThemeData(
          primaryColor: Colors.deepPurple,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: Text('Todo list'),
          ),
          body: TodoListWidget(),
        ));
  }
}

class TodoListWidget extends StatefulWidget {
  @override
  State createState() => TodoListWidgetState();
}

class TodoListWidgetState extends State<TodoListWidget> {
  final controller = TextEditingController();
  final list = List();

  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  void addTodo(TodoItem item) {
    setState(() {
      list.add(item);
    });
  }

  void removeTodo(TodoItem item) {
    setState(() {
      list.remove(item);
    });
  }

  void setChecked(TodoItem item, bool isChecked) {
    setState(() {
      item.isChecked = isChecked;
    });
  }

  Widget buildListTime(BuildContext context, TodoItem item) {
    return ListTile(
      onTap: () {
        setChecked(item, !item.isChecked);
      },
      leading: item.isChecked == true
          ? Icon(Icons.check_box)
          : Icon(Icons.check_box_outline_blank),
      title: Text(
        item.name,
        style: item.isChecked
            ? TextStyle(
                decoration: TextDecoration.lineThrough,
                fontStyle: FontStyle.italic,
              )
            : null,
      ),
      trailing: IconButton(
        icon: Icon(Icons.delete_forever),
        onPressed: () {
          removeTodo(item);
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            Expanded(
              child: TextField(
                controller: controller,
              ),
            ),
            RaisedButton(
              child: Text(
                'Add',
              ),
              onPressed: () {
                addTodo(TodoItem(name: controller.text));
                controller.text = '';
              },
            ),
          ],
        ),
        Expanded(
          child: ListView(
            children: list.map((item) => buildListTime(context, item)).toList(),
          ),
        )
      ],
    );
  }
}

Flutter TextFile 쓰기/읽기

|

TextFile 쓰기/읽기

import 'dart:io';

import 'package:flutter/material.dart';

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

class SnowApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'File IO Example',
      theme: ThemeData(
        primaryColor: Colors.indigoAccent,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('File IO Example'),
        ),
        body: FileIoTest(),
      ),
    );
  }
}

class FileIoTest extends StatelessWidget {
  void saveToFile(String filepath, String text) {
    final file = File(filepath);
    file.createSync();

    file.writeAsStringSync(text, mode: FileMode.append);
  }

  void loadFromFile(String filepath) {
    final file = File(filepath);
    print('Filepath: ${file.absolute.path}');

    final lines = file.readAsLinesSync();
    for (String line in lines) {
      print(line);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          RaisedButton(
            child: Text('Save to file'),
            onPressed: () {
              final filepath = 'snowdeer.txt';
              final text = "Hello, SnowDeer\n";

              saveToFile(filepath, text);
            },
          ),
          RaisedButton(
            child: Text('Load from file'),
            onPressed: () {
              final filepath = 'snowdeer.txt';
              loadFromFile(filepath);
            },
          ),
        ],
      ),
    );
  }
}

Flutter openFile, saveFile Dialog 사용

|

openFile, saveFile Dialog 사용 예제

먼저 pubspec.yaml 파일에 다음 항목을 추가해줍니다.


pubspec.yaml

dependencies:
  file_chooser: ^0.1.2
  path_provider: ^1.6.10
  path_provider_macos: ^0.0.4+3

여기서 file_chooser는 구글에서 만든 파일을 선택할 수 있는 인터페이스를 제공해주는 라이브러리이며, path_providerDocuments와 같은 특정 디렉토리를 쉽게 찾을 수 있도록 함수를 제공해주는 라이브러리입니다.

여기서 테스트하는 App은 MacOS 버전이기 때문에 macos/Runner/DebugProfile.entitlements 파일에 다음 Permission도 추가해줍니다.


macos/Runner/DebugProfile.entitlements

<dict>
  ...
	<key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>


main.dart

import 'dart:io';

import 'package:file_chooser/file_chooser.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('File Dialog Example'),
        ),
        body: SnowDeerExampleWidget(),
      ),
    );
  }
}

class SnowDeerExampleWidget extends StatefulWidget {
  @override
  State createState() => SnowDeerExampleWidgetState();
}

class SnowDeerExampleWidgetState extends State<SnowDeerExampleWidget> {
  void showOpenDialog() async {
    String initDirectory;
    if (Platform.isMacOS || Platform.isWindows) {
      initDirectory = (await getApplicationDocumentsDirectory()).path;
    }

    showOpenPanel(
      allowsMultipleSelection: true,
      initialDirectory: initDirectory,
    ).then((value) {
      final paths = value.paths;

      for (int i = 0; i < paths.length; i++) {
        final path = paths[i];
        print('- path: $path');
      }
    });
  }

  void showSaveDialog() {
    showSavePanel().then((value) {
      final paths = value.paths;

      for (int i = 0; i < paths.length; i++) {
        final path = paths[i];
        print('- path: $path');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          children: [
            RaisedButton(
              child: Text('Show open dialog'),
              onPressed: () {
                showOpenDialog();
              },
            ),
            RaisedButton(
              child: Text('Show save dialog'),
              onPressed: () {
                showSaveDialog();
              },
            ),
          ],
        ),
      ),
    );
  }
}

Flutter 기존 프로젝트에 MacOS 실행 환경 추가

|

Flutter 기존 프로젝트에 MacOS 실행 환경 추가

기존에 만들어진 Flutter 프로젝트에 MacOS 실행 환경을 추가하는 방법입니다. 리눅스 PC에서 생성한 Flutter 프로젝트를 MacOS에서 실행하면 처음에 다음과 같은 메시지가 발생합니다.

flutter run -d macos
Launching lib/main.dart on macOS in debug mode...
Exception: No macOS desktop project configured. See
https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-project to learn about adding
macOS support to a project.


해결법

flutter create . 명령어를 실행하면 됩니다.

$ flutter create .

Recreating project ....
  snowdeer_flutter_sample.iml (created)
  macos/Runner.xcworkspace/contents.xcworkspacedata (created)
  macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (created)
  macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (created)
  macos/Runner/DebugProfile.entitlements (created)
  macos/Runner/Base.lproj/MainMenu.xib (created)
  macos/Runner/MainFlutterWindow.swift (created)
  macos/Runner/Configs/Debug.xcconfig (created)
  macos/Runner/Configs/Release.xcconfig (created)
  macos/Runner/Configs/Warnings.xcconfig (created)
  macos/Runner/Configs/AppInfo.xcconfig (created)
  macos/Runner/AppDelegate.swift (created)
  macos/Runner/Info.plist (created)
  macos/Runner/Release.entitlements (created)
  macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (created)
  macos/Runner.xcodeproj/project.pbxproj (created)
  macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (created)
  macos/Flutter/Flutter-Debug.xcconfig (created)
  macos/Flutter/Flutter-Release.xcconfig (created)
  macos/.gitignore (created)
  android/snowdeer_flutter_sample_android.iml (created)
  .idea/runConfigurations/main_dart.xml (created)
  .idea/libraries/KotlinJavaRuntime.xml (created)
Wrote 33 files.

All done!

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;