- [Flutter] "Do it! 플러터 앱 프로그래밍" - 데이터베이스에 데이터 저장하기 | 데이터베이스 준비, 데이터 처리, 질의문 작성2022년 02월 18일 01시 55분 55초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 10장을 실습하였다. 데이터베이스를 앱과 연동하는 방법이다. DBMS로는 SQLite를 사용하였다. sqflite 패키지를 이용해 Map 형태로 데이터를 CRUD 하는 방식과 query를 이용해 검색, 삭제, 수정하는 방식을 다루었다.
Do it! 플러터 앱 프로그래밍
플러터 기본 & 고급 위젯은 물론오픈 API와 파이어베이스를 이용한 앱 개발부터 배포까지!플러터 SDK 2.x 버전을 반영한 개정판!이 책은 플러터의 기초부터 고급 활용법까지 다루어 다양한 영역에
book.naver.com
데이터베이스 준비
sqflite 패키지 설치
pub.dev에서 SQLite 사용을 위한 sqflite 패키지를 찾아 설치한다(https://pub.dev/packages/sqflite/install).
flutter pub add sqflitepath 패키지 설치
pub.dev에서 내부 저장소 사용을 위한 path 패키지를 찾아 설치한다(https://pub.dev/packages/path/install).
flutter pub add path데이터베이스 만들기
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) => db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ), version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container(), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); }, child: const Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } }// todo.dart class Todo { String? title; String? content; int? active; int? id; Todo({this.title, this.content, this.active, this.id}); Map<String, dynamic> toMap() { // toMap 함수는 데이터를 Map 형태로 변환한다. sqflite 패키지는 데이터를 Map 형태로 다룬다. return { 'id': id, 'title': title, 'content': content, 'active': active, }; } }// addTodo.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqlite_api.dart'; import 'todo.dart'; class AddTodoApp extends StatefulWidget { final Future<Database> db; AddTodoApp(this.db); @override State<StatefulWidget> createState() => _AddTodoApp(); } class _AddTodoApp extends State<AddTodoApp> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Todo 추가'), ), body: Container( child: Center( child: Column( children: <Widget>[], ), ), ), ); } }
데이터베이스에서 데이터 처리하기
데이터 입력하기
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) => db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ), version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container(), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 } }// addTodo.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqlite_api.dart'; import 'todo.dart'; class AddTodoApp extends StatefulWidget { final Future<Database> db; AddTodoApp(this.db); @override State<StatefulWidget> createState() => _AddTodoApp(); } class _AddTodoApp extends State<AddTodoApp> { TextEditingController? titleController; // 제목 텍스트 필드 TextEditingController? contentController; // 내용 텍스트 필드 @override void initState() { super.initState(); titleController = new TextEditingController(); contentController = new TextEditingController(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Todo 추가'), ), body: Container( child: Center( child: Column( children: <Widget>[ Padding( padding: EdgeInsets.all(10), child: TextField( controller: titleController, decoration: InputDecoration(labelText: '제목'), ), ), Padding( padding: EdgeInsets.all(10), child: TextField( controller: contentController, decoration: InputDecoration(labelText: '할일'), ), ), // 버튼을 눌렀을 때 pop 함수를 호출하면서 데이터를 메인에 전달한다. ElevatedButton( onPressed: () { Todo todo = Todo( title: titleController!.value.text, content: contentController!.value.text, active: 0); Navigator.of(context).pop(todo); }, child: Text('저장하기'), ), ], ), ), ), ); } }
[그림 1] 데이터 입력하기
데이터 검색하기
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return Card( child: Column( children: <Widget>[ Text(todo.title!), Text(todo.content!), Text('${todo.active == 1 ? 'true' : 'false'}'), ], ), ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }
[그림 2] 데이터 검색하기
데이터 수정하기
알림창을 통해 [그림 3]과 같이 체크를 true / false로 변경하는 기능을 구현한다.
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { // Card 위젯이 칸막이를 만들고 그 안에서 위젯을 자유롭게 꾸밀 수 있었다면, ListTile은 title, subtitle, leading, trailing 옵션으로 위젯의 위치를 지정할 수 있다. return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Text( '체크 : ${todo.active == 1 ? 'true' : 'false'}'), Container( height: 1, color: Colors.blue, ) ], ), ), onTap: () async { Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), content: Text('Todo를 체크하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { setState(() { todo.active == 1 ? todo.active = 0 : todo.active = 1; }); Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('아니요')), ], ); }); _updateTodo(result); }, ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터를 수정하기 함수 void _updateTodo(Todo todo) async { final Database database = await widget.db; await database.update( 'todos', todo.toMap(), // id값으로 수정할 데이터 찾기 where: 'id = ?', whereArgs: [todo.id], ); setState(() { todoList = getTodos(); }); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }
[그림 3] 체크 true / false 기능 알림 창에 텍스트 필드를 추가하여 [그림 4]와 같이 할 일 내용 수정 기능을 구현한다.
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { // Card 위젯이 칸막이를 만들고 그 안에서 위젯을 자유롭게 꾸밀 수 있었다면, ListTile은 title, subtitle, leading, trailing 옵션으로 위젯의 위치를 지정할 수 있다. return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Text( '체크 : ${todo.active == 1 ? 'true' : 'false'}'), Container( height: 1, color: Colors.blue, ) ], ), ), onTap: () async { TextEditingController controller = new TextEditingController( text: todo.content); // 알림창 안에서 할일 내용 수정하기 위함 Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), // 알림창 안에서 할일 내용 수정하기 위한 텍스트 필드 content: TextField( controller: controller, keyboardType: TextInputType.text, ), actions: <Widget>[ TextButton( onPressed: () { todo.active == 1 ? todo.active = 0 : todo.active = 1; todo.content = controller.value.text; Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('아니요')), ], ); }); _updateTodo(result); }, ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터를 수정하기 함수 void _updateTodo(Todo todo) async { final Database database = await widget.db; await database.update( 'todos', todo.toMap(), // id값으로 수정할 데이터 찾기 where: 'id = ?', whereArgs: [todo.id], ); setState(() { todoList = getTodos(); }); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }
[그림 4] 데이터 수정하기
데이터 삭제하기
import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database) }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { // Card 위젯이 칸막이를 만들고 그 안에서 위젯을 자유롭게 꾸밀 수 있었다면, ListTile은 title, subtitle, leading, trailing 옵션으로 위젯의 위치를 지정할 수 있다. return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Text( '체크 : ${todo.active == 1 ? 'true' : 'false'}'), Container( height: 1, color: Colors.blue, ) ], ), ), onTap: () async { TextEditingController controller = new TextEditingController( text: todo.content); // 알림창 안에서 할일 내용 수정하기 위함 Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), // 알림창 안에서 할일 내용 수정하기 위한 텍스트 필드 content: TextField( controller: controller, keyboardType: TextInputType.text, ), actions: <Widget>[ TextButton( onPressed: () { todo.active == 1 ? todo.active = 0 : todo.active = 1; todo.content = controller.value.text; Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('아니요')), ], ); }); _updateTodo(result); }, // 할 일 아이템을 길게 눌렀을 때 삭제 알림창 onLongPress: () async { Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), content: Text('${todo.content}를(을) 삭제하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('아니오')), ], ); }); _deleteTodo(result); }, ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터 삭제하기 함수 void _deleteTodo(Todo todo) async { final Database database = await widget.db; await database.delete('todos', where: 'id=?', whereArgs: [todo.id]); setState(() { todoList = getTodos(); }); } // 데이터 수정하기 함수 void _updateTodo(Todo todo) async { final Database database = await widget.db; await database.update( 'todos', todo.toMap(), // id값으로 수정할 데이터 찾기 where: 'id = ?', whereArgs: [todo.id], ); setState(() { todoList = getTodos(); }); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }
[그림 5] 데이터 삭제하기
질의문으로 추가 기능 만들기
완료한 일만 모아서 보기 - 검색 질의
Map 형태가 아니라 query를 작성해서 데이터를 처리하는 방법이다. 실습 중 [그림 6]과 같은 무한 로딩 현상이 발생해 애를 먹었다. 원인은 CircularProgressIndicator 함수를 빠져나오는 부분이 없었기 때문이다.
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; import 'clearList.dart'; // 완료한 일만 보여주는 클래스 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database), '/clear': (context) => ClearListApp(database) // 완료한 일만 보여주는 클래스 경로 }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), actions: <Widget>[ // 버튼을 눌렀을 때 ClearListApp으로 이동하는 코드 TextButton( onPressed: () async { await Navigator.of(context).pushNamed('/clear'); setState(() { todoList = getTodos(); }); }, child: Text( '완료한 일', style: TextStyle(color: Colors.white), )) ], ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { // Card 위젯이 칸막이를 만들고 그 안에서 위젯을 자유롭게 꾸밀 수 있었다면, ListTile은 title, subtitle, leading, trailing 옵션으로 위젯의 위치를 지정할 수 있다. return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Text( '체크 : ${todo.active == 1 ? 'true' : 'false'}'), Container( height: 1, color: Colors.blue, ) ], ), ), onTap: () async { TextEditingController controller = new TextEditingController( text: todo.content); // 알림창 안에서 할일 내용 수정하기 위함 Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), // 알림창 안에서 할일 내용 수정하기 위한 텍스트 필드 content: TextField( controller: controller, keyboardType: TextInputType.text, ), actions: <Widget>[ TextButton( onPressed: () { todo.active == 1 ? todo.active = 0 : todo.active = 1; todo.content = controller.value.text; Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('아니요')), ], ); }); _updateTodo(result); }, // 할 일 아이템을 길게 눌렀을 때 삭제 알림창 onLongPress: () async { Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), content: Text('${todo.content}를(을) 삭제하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('아니오')), ], ); }); _deleteTodo(result); }, ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } // 데이터 삭제하기 함수 void _deleteTodo(Todo todo) async { final Database database = await widget.db; await database.delete('todos', where: 'id=?', whereArgs: [todo.id]); setState(() { todoList = getTodos(); }); } // 데이터 수정하기 함수 void _updateTodo(Todo todo) async { final Database database = await widget.db; await database.update( 'todos', todo.toMap(), // id값으로 수정할 데이터 찾기 where: 'id = ?', whereArgs: [todo.id], ); setState(() { todoList = getTodos(); }); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }// clearList.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqlite_api.dart'; import 'todo.dart'; class ClearListApp extends StatefulWidget { Future<Database> database; ClearListApp(this.database); @override State<StatefulWidget> createState() => _ClearListApp(); } class _ClearListApp extends State<ClearListApp> { Future<List<Todo>>? clearList; @override void initState() { super.initState(); clearList = getClearList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('완료한 일'), ), body: Container( child: Center( child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: // return CircularProgressIndicator(); // [그림 6] 무한 로딩의 원인이었음 if (snapshot.hasData) { return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Container( height: 1, color: Colors.blue, ) ], ), ), ); }, itemCount: (snapshot.data as List<Todo>).length, ); } } return Text('No data'); }, future: clearList, ), ), ), ); } Future<List<Todo>> getClearList() async { final Database database = await widget.database; List<Map<String, dynamic>> maps = await database.rawQuery( 'select title, content, id from todos where active=1'); // 완료한 일(active가 1인 경우)만 가져오는 query return List.generate(maps.length, (i) { return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), id: maps[i]['id']); }); } }
[그림 6] 무한 로딩 현상 
[그림 7] 완료한 일 완료한 일 모두 지우기 - 삭제 질의
// clearList.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqlite_api.dart'; import 'todo.dart'; class ClearListApp extends StatefulWidget { Future<Database> database; ClearListApp(this.database); @override State<StatefulWidget> createState() => _ClearListApp(); } class _ClearListApp extends State<ClearListApp> { Future<List<Todo>>? clearList; @override void initState() { super.initState(); clearList = getClearList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('완료한 일'), ), body: Container( child: Center( child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: // return CircularProgressIndicator(); // 무한 로딩의 원인이었음 if (snapshot.hasData) { return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Container( height: 1, color: Colors.blue, ) ], ), ), ); }, itemCount: (snapshot.data as List<Todo>).length, ); } } return Text('No data'); }, future: clearList, ), ), ), // 완료한 일 삭제 버튼 floatingActionButton: FloatingActionButton( onPressed: () async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('완료한 일 삭제'), content: Text('완료한 일을 모두 삭제할까요?'), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text('아니오')), ], ); }); if (result == true) { _removeAllTodos(); } }, child: Icon(Icons.remove), ), ); } void _removeAllTodos() async { final Database database = await widget.database; database.rawDelete( 'delete from todos where active=1'); // 완료한 일(active가 1인 경우)만 삭제하는 query setState(() { clearList = getClearList(); }); } Future<List<Todo>> getClearList() async { final Database database = await widget.database; List<Map<String, dynamic>> maps = await database.rawQuery( 'select title, content, id from todos where active=1'); // 완료한 일(active가 1인 경우)만 가져오는 query return List.generate(maps.length, (i) { return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), id: maps[i]['id']); }); } }
[그림 8] 완료한 일 삭제 모두 완료로 수정하기 - 수정 질의
// main.dart import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'addTodo.dart'; import 'todo.dart'; import 'clearList.dart'; // 완료한 일만 보여주는 클래스 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 데이터베이스를 생성하는 함수 호출 Future<Database> database = initDatabase(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/', routes: { '/': (context) => DatabaseApp(database), '/add': (context) => AddTodoApp(database), '/clear': (context) => ClearListApp(database) // 완료한 일만 보여주는 클래스 경로 }, ); } // home: DatabaseApp(), // ); // } // 데이터베이스를 생성하는 함수 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'todo_database.db'), // todo_database.db 파일에 테이블이 없으면 테이블을 만든다. onCreate: (db, version) { return db.execute( "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, " "title TEXT, content TEXT, active INTEGER)", ); }, version: 1, ); } } class DatabaseApp extends StatefulWidget { final Future<Database> db; DatabaseApp(this.db); @override State<StatefulWidget> createState() => _DatabaseApp(); } class _DatabaseApp extends State<DatabaseApp> { Future<List<Todo>>? todoList; // Future로 선언할 할 일 목록은 계속 값이 변하므로 따로 변수를 선언해 initState 함수에서 호출하도록 한다. @override void initState() { super.initState(); todoList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Database Example'), actions: <Widget>[ // 버튼을 눌렀을 때 ClearListApp으로 이동하는 코드 TextButton( onPressed: () async { await Navigator.of(context).pushNamed('/clear'); setState(() { todoList = getTodos(); }); }, child: Text( '완료한 일', style: TextStyle(color: Colors.white), )) ], ), body: Container( child: Center( // FutureBuilder 위젯은 서버에서 데이터를 받거나 파일에 데이터를 가져올 때 사용한다. // 데이터를 가져오는 동안에 시간이 걸리기 때문에 그 사이에 표시할 위젯을 만들기 위해 switch문으로 snapshot.connectionState를 이용해 지금 상태를 확인한다. // 상태가 done이 되면 가져온 데이터를 바탕으로 ListView.builder를 이용해 화면에 표시한다. child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { // Card 위젯이 칸막이를 만들고 그 안에서 위젯을 자유롭게 꾸밀 수 있었다면, ListTile은 title, subtitle, leading, trailing 옵션으로 위젯의 위치를 지정할 수 있다. return ListView.builder( itemBuilder: (context, index) { Todo todo = (snapshot.data as List<Todo>)[index]; return ListTile( title: Text( todo.title!, style: TextStyle(fontSize: 20), ), subtitle: Container( child: Column( children: <Widget>[ Text(todo.content!), Text( '체크 : ${todo.active == 1 ? 'true' : 'false'}'), Container( height: 1, color: Colors.blue, ) ], ), ), onTap: () async { TextEditingController controller = new TextEditingController( text: todo .content); // 알림창 안에서 할일 내용 수정하기 위함 Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), // 알림창 안에서 할일 내용 수정하기 위한 텍스트 필드 content: TextField( controller: controller, keyboardType: TextInputType.text, ), actions: <Widget>[ TextButton( onPressed: () { todo.active == 1 ? todo.active = 0 : todo.active = 1; todo.content = controller.value.text; Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('아니요')), ], ); }); _updateTodo(result); }, // 할 일 아이템을 길게 눌렀을 때 삭제 알림창 onLongPress: () async { Todo result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('${todo.id} : ${todo.title}'), content: Text('${todo.content}를(을) 삭제하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pop(todo); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('아니오')), ], ); }); _deleteTodo(result); }, ); }, itemCount: (snapshot.data as List<Todo>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: todoList, ), ), ), floatingActionButton: Column( children: <Widget>[ FloatingActionButton( onPressed: () async { final todo = await Navigator.of(context).pushNamed('/add'); if (todo != null) { _insertTodo(todo as Todo); // 데이터 입력하기 함수 호출 } }, heroTag: null, child: Icon(Icons.add), ), SizedBox( height: 10, ), FloatingActionButton( onPressed: () async { _allUpdate(); }, heroTag: null, child: Icon(Icons.update), ), ], mainAxisAlignment: MainAxisAlignment.end, )); } void _allUpdate() async { final Database database = await widget.db; await database.rawUpdate('update todos set active=1 where active = 0'); // 모든 할 일 완료로 수정하는 query setState(() { todoList = getTodos(); }); } // 데이터 삭제하기 함수 void _deleteTodo(Todo todo) async { final Database database = await widget.db; await database.delete('todos', where: 'id=?', whereArgs: [todo.id]); setState(() { todoList = getTodos(); }); } // 데이터 수정하기 함수 void _updateTodo(Todo todo) async { final Database database = await widget.db; await database.update( 'todos', todo.toMap(), // id값으로 수정할 데이터 찾기 where: 'id = ?', whereArgs: [todo.id], ); setState(() { todoList = getTodos(); }); } // 데이터 입력하기 함수 void _insertTodo(Todo todo) async { final Database database = await widget.db; await database.insert('todos', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); // 충돌이 발생하면 새 데이터로 교체하고자 replace로 선언함 // todoList를 갱신하는 코드 setState(() { todoList = getTodos(); }); } // 데이터베이스에서 데이터를 가져오는 함수 // await widget.db에 오류가 있었다. // path 패키지를 설치하고, 함수 위치를 _insertTodo 함수 아래로 내렸더니 해결되었다. Future<List<Todo>> getTodos() async { final Database database = await widget.db; final List<Map<String, dynamic>> maps = await database.query('todos'); return List.generate(maps.length, (i) { int active = maps[i]['active'] == 1 ? 1 : 0; return Todo( title: maps[i]['title'].toString(), content: maps[i]['content'].toString(), active: active, id: maps[i]['id']); }); } }
[그림 9] 모든 체크를 true로 수정 728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글