Flutter version 2.2.3
Dart version 2.13.4
0. 패키지 설치
- Flutter 프로젝트를 진행할 때, 여러모로 편리하다.
- Flutter 프로젝트의 pubspec.yaml에서 command + shift + P에서 "add"를 입력한 다음, 패키지 명을 입력하면 패키지의 최신버전을 불러와준다.
이 프로젝트에서 사용한 패키지들 null-safety가 적용된 버전이다.
get: ^4.1.4 // https://pub.dev/packages/get
intl: ^0.17.0 // https://pub.dev/packages/intl
path_provider: ^2.0.2 // https://pub.dev/packages/path_provider
sqflite: ^2.0.0+3 // https://pub.dev/packages/sqflite
1. database_helper.dart
final String todoTable = 'todo';
final String todoMemoTable = 'todoMemo';
class DBHelper {
DBHelper() : super();
// 싱글톤 클래스
DBHelper._();
static final DBHelper db = DBHelper._();
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database as Database;
_database = await initDB();
return _database as Database;
}
initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, 'myDb.db');
return await openDatabase(
path, // .db 파일 경로
version: 1,
onCreate: (Database db, int version) async {
// INTEGER -> int,
// REAL -> num, int + double
// TEXT -> String,
// BLOB -> Uint8List,
// sqflite는 boolean을 지원 안함 -> INTEGER, {false: 0, true: 1}로 저장
await db.execute(
'CREATE TABLE $todoTable (pk INTEGER PRIMARY KEY AUTOINCREMENT, todo TEXT, type TEXT, complete INTEGER)'); // [1]
await db.execute(
'CREATE TABLE $todoMemoTable (pk INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, todoPk INTEGER, createdAt INTEGER, FOREIGN KEY(todoPk) REFERENCES $todoTable(pk) ON DELETE CASCADE ON UPDATE NO ACTION)'); // [2]
// 여러 테이블을 생성하고싶으면 이처럼 여러번 선언을 하던지 for 문을 사용해도 된다.
},
);
}
deleteDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, 'myDb.db');
return await deleteDatabase(path); // .db 삭제
}
// Todo
// Create Todo
createTodo(Todo todo) async {
final db = await database;
var res = await db.insert(todoTable, todo.toJson());
return res;
}
// Read Todo
getTodo(int pk) async {
final db = await database;
var res = await db.query(todoTable, where: 'pk = ?', whereArgs: [pk]); // [3]
return res.isNotEmpty ? Todo.fromJson(res.first) : Null;
}
// Read All Todos
getAllTodos() async {
final db = await database;
var res = await db.query(todoTable);
List<Todo> list =
res.isNotEmpty ? res.map((c) => Todo.fromJson(c)).toList() : [];
return list;
}
// Update Todo
updateTodo(Todo todo) async {
final db = await database;
var res = await db.update(todoTable, todo.toJson(),
where: 'pk = ?', whereArgs: [todo.pk]);
return res;
}
// Delete Todo
deleteTodo(int pk) async {
final db = await database;
db.delete(todoTable, where: 'pk = ?', whereArgs: [pk]);
}
// Delete All Todos
deleteAllTodos() async {
final db = await database;
db.rawDelete('Delete from $todoTable');
}
// TodoMemo
// Create TodoMemo
createTodoMemo(TodoMemo todoMemo) async {
final db = await database;
var res = await db.insert(todoMemoTable, todoMemo.toJson());
return res;
}
// Read TodoMemo
getTodoMemo(int pk) async {
final db = await database;
var res = await db.query(todoMemoTable, where: 'pk = ?', whereArgs: [pk]);
return res.isNotEmpty ? TodoMemo.fromJson(res.first) : Null;
}
// Read All TodoMemos
getAllTodoMemosByTodoPk(int todoPk) async {
final db = await database;
var res =
await db.query(todoMemoTable, where: 'todoPk = ?', whereArgs: [todoPk]);
List<TodoMemo> list =
res.isNotEmpty ? res.map((c) => TodoMemo.fromJson(c)).toList() : [];
return list;
}
// Update TodoMemo
updateTodoMemo(TodoMemo todo) async {
final db = await database;
var res = await db.update(todoMemoTable, todo.toJson(),
where: 'pk = ?', whereArgs: [todo.pk]);
return res;
}
// Delete TodoMemo
deleteTodoMemo(int pk) async {
final db = await database;
db.delete(todoMemoTable, where: 'pk = ?', whereArgs: [pk]);
}
// Delete All TodoMemos
deleteAllTodoMemos() async {
final db = await database;
db.rawDelete('Delete from $todoMemoTable');
}
}
- 'complete INTEGER'
- SQFlite는 boolean이 없다. true면 1, false면 0 Integer 활용
- 'createdAt INTEGER'
- DateTime 형식도 없기 때문에 Integer나 String으로 변환
- 2.2 todo_memo_model.dart 의 createdAt 부분 참조
- 'where: 'pk = ?', whereArgs = [pk]'
- ?에 순차적으로 whereArgs의 value가 들어감
- Example
- 'where: "col1 LIKE ? and col2 = ? and col3 = ?", whereArgs: ['$exLikeVal%', exStrVal, exIntVal],'
2.1 todo_model.dart
class Todo {
int? pk;
String? todo;
String type;
int? complete;
Todo({
this.pk,
this.todo,
this.type = "기타",
this.complete = 0,
});
bool get getCompleteAsBool { // [1]
return (complete == 1);
}
void toggleComplete() {
if (getComplete) {
complete = 0;
} else {
complete = 1;
}
}
factory Todo.fromJson(Map<String, dynamic> json) => new Todo(
pk: json["pk"],
todo: json["todo"],
type: json["type"],
complete: json["complete"],
);
Map<String, dynamic> toJson() => {
"pk": pk,
"todo": todo,
"type": type,
"complete": complete,
};
}
- bool get getCompleteAsBool
- Integer로 저장되는 complete를 boolean으로 변환하는 getter
2.2 todo_memo_model.dart
class TodoMemo {
int? pk;
String? content;
int? todoPk;
int createdAt;
TodoMemo({
this.pk,
this.content,
this.todoPk,
createdAt,
}) : createdAt = DateTime.now().millisecondsSinceEpoch; // [1]
DateTime getCreatedAtDateTime() {
return DateTime.fromMillisecondsSinceEpoch(createdAt);
}
factory TodoMemo.fromJson(Map<String, dynamic> json) => new TodoMemo(
pk: json["pk"],
content: json["content"],
todoPk: json["todoPk"],
createdAt: json["createdAt"],
);
Map<String, dynamic> toJson() => {
"pk": pk,
"content": content,
"todoPk": todoPk,
"createdAt": createdAt,
};
}
- ': createdAt = DateTime.now().millisecondsSinceEpoch'
- 현재시간을 밀리초로 환산하여 Integer로 반환
3.1 todo_bloc.dart
class TodoBloc {
TodoBloc() {
getTodos();
}
final _todosController = StreamController<List<Todo>>.broadcast(); // [1]
get todos => _todosController.stream;
dispose() {
_todosController.close();
}
getTodos() async {
_todosController.sink.add(await DBHelper().getAllTodos());
}
addTodos(Todo todo) async {
await DBHelper().createTodo(todo);
getTodos();
}
deleteTodo(int pk) async {
await DBHelper().deleteTodo(pk);
getTodos();
}
deleteAll() async {
await DBHelper().deleteAllTodos();
getTodos();
}
updateTodo(Todo todo) async {
await DBHelper().updateTodo(todo);
getTodos();
}
}
- 'StreamController.broadcast()'
- Single Subscription인 스트림은 한군데에서 리슨할 수 있지만, 이를 여러 군데에서 리슨할 수 있도록 변경
- 같은 데이터를 다른 뷰에서 처리할 수 있게된다.
3.2 todo_memo_bloc.dart
class TodoMemoBloc {
int todoPk;
TodoMemoBloc(this.todoPk) {
getTodoMemos(todoPk);
}
final _todoMemosController = StreamController<List<TodoMemo>>.broadcast();
get todoMemos => _todoMemosController.stream;
dispose() {
_todoMemosController.close();
}
getTodoMemos(int todoPk) async {
_todoMemosController.sink
.add(await DBHelper().getAllTodoMemosByTodoPk(todoPk));
}
addTodos(TodoMemo todoMemo) async {
await DBHelper().createTodoMemo(todoMemo);
getTodoMemos(todoPk);
}
deleteTodo(int pk) async {
await DBHelper().deleteTodoMemo(pk);
getTodoMemos(todoPk);
}
deleteAll() async {
await DBHelper().deleteAllTodoMemos();
getTodoMemos(todoPk);
}
updateTodo(TodoMemo todoMemo) async {
await DBHelper().updateTodoMemo(todoMemo);
getTodoMemos(todoPk);
}
}
4. main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp( // [1]
title: 'Todo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
accentColor: Color(0xFF6F35A5),
canvasColor: const Color(0xFFfafafa),
scaffoldBackgroundColor: Colors.white,
primaryColor: Color(0xFFF1E6FF),
textTheme:
Theme.of(context).textTheme.apply(bodyColor: Color(0xFF3C4046)),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
getPages: [
GetPage(
name: '/', page: () => TodoList(), transition: Transition.fadeIn),
GetPage(
name: '/memos/:todoPk',
page: () => TodoMemoList(),
transition: Transition.fade),
],
);
}
}
- 'GetMaterialApp'
- GetX의 routes, snackbars, i18n, bottomSheets, dialogs, 고수준 api들을 컨텍스트 없이 사용하기 위해 필요하다.
5. todo_list.dart
class TodoList extends StatefulWidget {
TodoList({Key? key}) : super(key: key);
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
List<Todo> todosDatas = [
Todo(todo: '메모 내용이양', type: '했어!', complete: 1),
Todo(todo: '메모 내용이에용', type: '해야되!', complete: 0),
Todo(todo: '메모 내용!', type: '해야될까?', complete: 0),
];
final TodoBloc todoBloc = TodoBloc();
@override
void dispose() {
super.dispose();
todoBloc.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo'),
actions: [
IconButton(
onPressed: () {
DBHelper().deleteDB();
},
icon: Icon(Icons.delete))
],
),
body: StreamBuilder(
stream: todoBloc.todos,
builder: (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data?.length,
itemBuilder: (BuildContext context, int index) {
Todo item = snapshot.data![index];
return Dismissible(
key: UniqueKey(),
onDismissed: (direction) {
todoBloc.deleteTodo(item.pk as int);
},
child: ListTile(
onTap: () {
Get.toNamed("/memos/${item.pk}");
},
leading: Text(
item.pk.toString(),
),
title: Text(item.todo as String),
subtitle: Text(item.type),
trailing: Checkbox(
onChanged: (bool? value) {
item.toggleComplete();
todoBloc.updateTodo(item);
},
value: item.complete == 1 ? true : false,
),
),
);
},
)
: Center(
child: Center(
child: Text('아무것도 엄써 ㅇㅅㅇ!'),
),
);
},
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
heroTag: null,
child: Icon(Icons.remove),
onPressed: () {
todoBloc.deleteAll();
},
),
SizedBox(
height: 16.0,
),
FloatingActionButton(
heroTag: null,
child: Icon(Icons.add),
onPressed: () {
Todo newTodo = todosDatas[Random().nextInt(todosDatas.length)];
todoBloc.addTodos(newTodo);
},
),
],
),
);
}
}
6. todo_memo_list.dart
class TodoMemoList extends StatefulWidget {
const TodoMemoList({Key? key}) : super(key: key);
@override
_TodoMemoListState createState() => _TodoMemoListState();
}
class _TodoMemoListState extends State<TodoMemoList> {
final TodoMemoBloc todoMemoBloc =
TodoMemoBloc(int.parse(Get.parameters['todoPk']!));
int? todoPk;
@override
void initState() {
super.initState();
todoPk = int.parse(Get.parameters['todoPk']!);
}
@override
void dispose() {
super.dispose();
todoMemoBloc.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo Memos $todoPk'),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
TodoMemo todoMemo = TodoMemo(
content: '메모 내용입니다.',
todoPk: todoPk,
);
todoMemoBloc.addTodos(todoMemo);
},
)
],
),
body: StreamBuilder(
stream: todoMemoBloc.todoMemos,
builder:
(BuildContext context, AsyncSnapshot<List<TodoMemo>> snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data?.length,
itemBuilder: (BuildContext context, int index) {
TodoMemo item = snapshot.data![index];
return Dismissible(
key: UniqueKey(),
onDismissed: (direction) {
todoMemoBloc.deleteTodo(item.pk as int);
},
child: ListTile(
leading: Text(
item.pk.toString(),
),
title: Text(item.content as String),
subtitle: Text(
DateFormat('yyyy년 MM월 dd일 – kk시 mm분')
.format(item.getCreatedAtDateTime()),
),
),
);
},
)
: Center(
child: Center(
child: Text('아무것도 엄써 ㅇㅅㅇ~'),
),
);
},
),
);
}
}
https://alexband.tistory.com/55
Flutter Local Storage (SQLite) 사용하기 with BLoC Pattern
Flutter 에서 sqflite 를 사용하여 로컬 에서 데이터를 관리 해 봅시다. 참고로 sqflite 는 flutter 에서 sqlite 의 사용을 도와주는 패키지 이름 입니다. 기본적인 개발환경은 구성이 되어있다는 전제하에
alexband.tistory.com
https://devmemory.tistory.com/13
Flutter - When you store data in SQFLlite
기본적으로 SQLite에서는 NULL, INTEGER, REAL, TEXT, BLOB형태로 저장이 됩니다 NULL은 데이터 값이 비어있을때 INTEGER는 정수형 (int value, DateTime, bool 등) DateTime의 경우 INSERT할 때..
devmemory.tistory.com
https://software-creator.tistory.com/9
Flutter - 스트림. 다트에서 비동기 프로그래밍
Flutter - 스트림. 다트에서 비동기 프로그래밍 목차 스트림이란? 스트림 간단한 예제 스트림 다양하게 처리하기 스트림 내부 구조 서브스크립션 브로드 캐스트 스트림 컨트롤러 1. Stream이란? 스트
software-creator.tistory.com
'아카이브 > Flutter' 카테고리의 다른 글
[FCM] Firebase Cloud Messaging 설정 (0) | 2021.07.16 |
---|---|
[Migration] FCM Null Safety 적용 (0) | 2021.07.15 |
[Migration] Flutter 1.22.6에서 2.2.3으로 마이그레이션 하기 (0) | 2021.07.15 |
[GetX] Obx로 Chip 활용하기 (0) | 2021.06.02 |
[위젯] Chip, shape (0) | 2021.05.28 |