방명록
- [Flutter] "Do it! 플러터 앱 프로그래밍" - 데이터베이스에 데이터 저장하기 | 데이터베이스 준비, 데이터 처리, 질의문 작성2022년 02월 18일 01시 55분 55초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 10장을 실습하였다. 데이터베이스를 앱과 연동하는 방법이다. DBMS로는 SQLite를 사용하였다. sqflite 패키지를 이용해 Map 형태로 데이터를 CRUD 하는 방식과 query를 이용해 검색, 삭제, 수정하는 방식을 다루었다.
데이터베이스 준비
sqflite 패키지 설치
pub.dev에서 SQLite 사용을 위한 sqflite 패키지를 찾아 설치한다(https://pub.dev/packages/sqflite/install).
flutter pub add sqflite
path 패키지 설치
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('저장하기'), ), ], ), ), ), ); } }
데이터 검색하기
// 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']); }); } }
데이터 수정하기
알림창을 통해 [그림 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']); }); } }
알림 창에 텍스트 필드를 추가하여 [그림 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']); }); } }
데이터 삭제하기
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']); }); } }
질의문으로 추가 기능 만들기
완료한 일만 모아서 보기 - 검색 질의
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']); }); } }
완료한 일 모두 지우기 - 삭제 질의
// 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']); }); } }
모두 완료로 수정하기 - 수정 질의
// 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']); }); } }
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글