2023. 4. 8. 15:35ㆍ모바일어플개발/Flutter
안녕하세요~ totally 개발자입니다.
SQFLITE (SQLite)
오늘은 SQFLITE(로컬 데이터베이스)를 사용하여 맞춤 영어 단어장을 만들어보도록 하겠습니다. 이것을 사용하면 예전에 알아보았던 SharedPreference 대신에 많은 데이터를 주고 받으며 저장할 수 있는 장점이 있습니다. 오늘 실습을 위해서는 기본적인 데이터베이스의 개념과 SQL의 CRUD 개념을 이해할 필요가 있습니다. 이 CRUD는 Create, Read, Update, Delete의 약자로 SQL에서는 INSERT, SELECT, UPDATE, DELETE 키워드로 쿼리(query) 명령문을 작성하게 됩니다. 바로 실습을 통해 영어 단어장을 만들어보도록 하겠습니다. 모든 소스 코드는 맨 아래에 첨부하였으니 참고하시기 바랍니다.
Step 1: pubspec.yaml에 sqflite와 path를 추가해줍니다. flutter pub get 해주시거나 Ctrl(Command) + S로 반영해주시면 됩니다.

Step 2: 모델을 위해 word.dart를 생성하여 다음처럼 입력합니다. 1 book 책 이렇게 있을 때 1은 id, book은 name, 책은 meaning이 됩니다.

Step 3: 싱글톤을 기반으로 databaseConfig.dart 파일을 생성하여 DatabaseService 클래스를 작성해줍니다. _internal을 통해 private 생성자를 구현하며 databaseConfig 메소드를 통해 데이터베이스 변수를 초기화해줍니다. 먼저 데이터베이스를 열어준 뒤 21번째 줄처럼 테이블을 생성해주고 id를 Primary Key로 지정합니다. 이 Primary Key는 고유식별값으로 중복을 허용하지 않습니다.

Step 4: 그 아래 부분에 데이터 삽입을 위한 insert 메소드를 작성합니다.

insert 메소드의 parameter로 테이블 이름, 그 다음으로는 테이블에 삽입할 데이터를 map 형태로 변환하여 넣어줍니다.
Step 5: 그 다음 모든 단어들을 가져오기 위해 selectWords 메소드를 작성합니다. selectWord는 1개의 단어를 가져오고 selectWords는 모든 단어들을 가져오는 메소드이기 때문에 반드시 분리합니다.

Step 6: selectWord 메소드를 입력하여 단어 데이터를 1개만 가져올 수 있도록 만들어줍니다.

63번째 줄에 보시면 where과 whereArgs가 있는데 이 where를 추가하여 어떤 데이터를 가져올 것인지에 대한 조건을 추가해줄 수 있으며 ? 부분은 whereArgs로 지정해주면 됩니다.
Step 7: 단어 수정을 위해 updateWord 부분 메소드를 작성합니다.

Step 8: 단어 삭제를 위해 deleteWord 부분 메소드를 작성합니다.

여기까지 완료하셨다면 databaseConfig.dart 부분은 마무리됩니다.
Step 9: main.dart에 다음처럼 변수들을 선언합니다. currentCount는 단어 id를 부여하기 위한 목적입니다.

Step 10: 기본 Scaffold 위젯을 구성해주며 여기에서 45~56번째의 floatingActionButton 통해 단어 추가 버튼을 눌렀을 때 단어를 추가할 수 있는 입력창이 나오게 선언합니다. addWordDialog는 아래에서 선언할 것이기 때문에 아래를 참고하시기 바랍니다.

Step 11: body 부분을 다음처럼 구성해줍니다. 기존에 FutureBuilder 원리와 동일합니다.

Step 12: 단어를 보여주는 wordBox 위젯을 작성합니다.

Step 13: updateButton를 만들어줍니다. 이 update를 할 때 기존에 어떤 데이터를 입력했는지 보통 보여주기 때문에 128번째 줄처럼 기존 단어를 가져온 뒤 그 word를 updateWordDialog(word)로 전달해줍니다.

Step 14: deleteButton를 만들어줍니다.

Step 15: 단어 추가를 위한 입력창인 addWordDialog를 다음처럼 만들어줍니다. 184~202번째 줄에 필요한 로직을 참고하시기 바랍니다. 여기 setState가 있는 이유는 _wordList를 다시 업데이트해주어 상태를 다시 반영하기 위함입니다.

Step 16: updateWordDialog도 입력합니다. 여기에서는 FutureBuilder로 감싸주어야 합니다.

Step 17: 마지막으로 deleteWordDialog까지 작성하고 마무리해주면 됩니다.

Step 18: 결과는 아래와 같습니다.

[전체 소스 코드]
[word.dart]
class Word { | |
final int id; | |
final String name; | |
final String meaning; | |
Word({required this.id, required this.name, required this.meaning}); | |
Map<String, dynamic> toMap() { | |
return {'id': id, 'name': name, 'meaning': meaning}; | |
} | |
} |
[databaseConfig.dart]
import 'package:path/path.dart'; | |
import 'package:project/word.dart'; | |
import 'package:sqflite/sqflite.dart'; | |
class DatabaseService { | |
static final DatabaseService _database = DatabaseService._internal(); | |
late Future<Database> database; | |
factory DatabaseService() => _database; | |
DatabaseService._internal() { | |
databaseConfig(); | |
} | |
Future<bool> databaseConfig() async { | |
try { | |
database = openDatabase( | |
join(await getDatabasesPath(), 'word_database.db'), | |
onCreate: (db, version) { | |
return db.execute( | |
'CREATE TABLE words(id INTEGER PRIMARY KEY, name TEXT, meaning TEXT)', | |
); | |
}, | |
version: 1, | |
); | |
return true; | |
} catch (err) { | |
print(err.toString()); | |
return false; | |
} | |
} | |
Future<bool> insertWord(Word word) async { | |
final Database db = await database; | |
try { | |
db.insert( | |
'words', | |
word.toMap(), | |
conflictAlgorithm: ConflictAlgorithm.replace, | |
); | |
return true; | |
} catch (err) { | |
return false; | |
} | |
} | |
Future<List<Word>> selectWords() async { | |
final Database db = await database; | |
final List<Map<String, dynamic>> data = await db.query('words'); | |
return List.generate(data.length, (i) { | |
return Word( | |
id: data[i]['id'], | |
name: data[i]['name'], | |
meaning: data[i]['meaning'], | |
); | |
}); | |
} | |
Future<Word> selectWord(int id) async { | |
final Database db = await database; | |
final List<Map<String, dynamic>> data = | |
await db.query('words', where: "id = ?", whereArgs: [id]); | |
return Word( | |
id: data[0]['id'], name: data[0]['name'], meaning: data[0]['meaning']); | |
} | |
Future<bool> updateWord(Word word) async { | |
final Database db = await database; | |
try { | |
db.update( | |
'words', | |
word.toMap(), | |
where: "id = ?", | |
whereArgs: [word.id], | |
); | |
return true; | |
} catch (err) { | |
return false; | |
} | |
} | |
Future<bool> deleteWord(int id) async { | |
final Database db = await database; | |
try { | |
db.delete( | |
'words', | |
where: "id = ?", | |
whereArgs: [id], | |
); | |
return true; | |
} catch (err) { | |
return false; | |
} | |
} | |
} |
[main.dart]
import 'package:project/databaseConfig.dart'; | |
import 'package:flutter/material.dart'; | |
import 'word.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp(title: 'Flutter App', home: HomePage()); | |
} | |
} | |
class HomePage extends StatefulWidget { | |
const HomePage({Key? key}) : super(key: key); | |
@override | |
State<HomePage> createState() => _HomePageState(); | |
} | |
class _HomePageState extends State<HomePage> { | |
final TextEditingController _nameController = TextEditingController(); | |
final TextEditingController _meaningController = TextEditingController(); | |
final DatabaseService _databaseService = DatabaseService(); | |
Future<List<Word>> _wordList = DatabaseService() | |
.databaseConfig() | |
.then((_) => DatabaseService().selectWords()); | |
int currentCount = 0; | |
@override | |
void initState() { | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('SQLITE'), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: () { | |
showDialog( | |
context: context, | |
barrierDismissible: false, | |
builder: (BuildContext context) => addWordDialog(), | |
); | |
}, | |
child: const Icon( | |
Icons.add, | |
), | |
), | |
body: Container( | |
padding: const EdgeInsets.all(10), | |
child: FutureBuilder( | |
future: _wordList, | |
builder: (context, snapshot) { | |
if (snapshot.hasData) { | |
currentCount = snapshot.data!.length; | |
if (currentCount == 0) { | |
return const Center( | |
child: Text("No data exists."), | |
); | |
} else { | |
return ListView.builder( | |
itemCount: snapshot.data!.length, | |
itemBuilder: (context, index) { | |
return wordBox( | |
snapshot.data![index].id, | |
snapshot.data![index].name, | |
snapshot.data![index].meaning); | |
}, | |
); | |
} | |
} else if (snapshot.hasError) { | |
return const Center( | |
child: Text("Error."), | |
); | |
} else { | |
return const Center( | |
child: CircularProgressIndicator( | |
strokeWidth: 2, | |
), | |
); | |
} | |
}, | |
), | |
), | |
); | |
} | |
Widget wordBox(int id, String name, String meaning) { | |
return Row( | |
children: [ | |
Container( | |
padding: const EdgeInsets.all(15), | |
child: Text("$id"), | |
), | |
Container( | |
padding: const EdgeInsets.all(15), | |
child: Text(name), | |
), | |
Container( | |
padding: const EdgeInsets.all(15), | |
child: Text(meaning), | |
), | |
Expanded( | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: [ | |
updateButton(id), | |
const SizedBox(width: 10), | |
deleteButton(id), | |
], | |
), | |
), | |
], | |
); | |
} | |
Widget updateButton(int id) { | |
return ElevatedButton( | |
onPressed: () { | |
Future<Word> word = _databaseService.selectWord(id); | |
showDialog( | |
context: context, | |
barrierDismissible: false, | |
builder: (BuildContext context) => updateWordDialog(word), | |
); | |
}, | |
style: ButtonStyle( | |
backgroundColor: MaterialStateProperty.all(Colors.green), | |
), | |
child: const Icon(Icons.edit)); | |
} | |
Widget deleteButton(int id) { | |
return ElevatedButton( | |
onPressed: () => showDialog( | |
context: context, | |
barrierDismissible: false, | |
builder: (BuildContext context) => deleteWordDialog(id), | |
), | |
style: ButtonStyle( | |
backgroundColor: MaterialStateProperty.all(Colors.red), | |
), | |
child: const Icon(Icons.delete)); | |
} | |
Widget addWordDialog() { | |
return AlertDialog( | |
title: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
const Text("단어 추가"), | |
IconButton( | |
onPressed: () => Navigator.of(context).pop(), | |
icon: const Icon( | |
Icons.close, | |
), | |
), | |
], | |
), | |
content: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
TextField( | |
controller: _nameController, | |
decoration: const InputDecoration(hintText: "단어를 입력하세요.,"), | |
), | |
const SizedBox(height: 15), | |
TextField( | |
controller: _meaningController, | |
decoration: const InputDecoration( | |
hintText: "뜻을 입력하세요.", | |
), | |
), | |
const SizedBox(height: 15), | |
ElevatedButton( | |
onPressed: () { | |
_databaseService | |
.insertWord(Word( | |
id: currentCount + 1, | |
name: _nameController.text, | |
meaning: _meaningController.text)) | |
.then( | |
(result) { | |
if (result) { | |
Navigator.of(context).pop(); | |
setState(() { | |
_wordList = _databaseService.selectWords(); | |
}); | |
} else { | |
print("insert error"); | |
} | |
}, | |
); | |
}, | |
child: const Text("생성"), | |
), | |
], | |
), | |
); | |
} | |
Widget updateWordDialog(Future<Word> word) { | |
return AlertDialog( | |
title: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
const Text("단어 수정"), | |
IconButton( | |
onPressed: () => Navigator.of(context).pop(), | |
icon: const Icon( | |
Icons.close, | |
), | |
), | |
], | |
), | |
content: FutureBuilder( | |
future: word, | |
builder: (context, snapshot) { | |
if (snapshot.hasData) { | |
_nameController.text = snapshot.data!.name; | |
_meaningController.text = snapshot.data!.meaning; | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
TextField( | |
controller: _nameController, | |
decoration: const InputDecoration(hintText: "단어를 입력하세요."), | |
), | |
const SizedBox(height: 15), | |
TextField( | |
controller: _meaningController, | |
decoration: const InputDecoration( | |
hintText: "뜻을 입력하세요.", | |
), | |
), | |
const SizedBox(height: 15), | |
ElevatedButton( | |
onPressed: () { | |
_databaseService | |
.updateWord(Word( | |
id: snapshot.data!.id, | |
name: _nameController.text, | |
meaning: _meaningController.text)) | |
.then( | |
(result) { | |
if (result) { | |
Navigator.of(context).pop(); | |
setState(() { | |
_wordList = _databaseService.selectWords(); | |
}); | |
} else { | |
print("update error"); | |
} | |
}, | |
); | |
}, | |
child: const Text("수정"), | |
), | |
], | |
); | |
} else if (snapshot.hasError) { | |
return const Center( | |
child: Text("Error occurred!"), | |
); | |
} else { | |
return const Center( | |
child: CircularProgressIndicator( | |
strokeWidth: 2, | |
), | |
); | |
} | |
}), | |
); | |
} | |
Widget deleteWordDialog(int id) { | |
return AlertDialog( | |
title: const Text("이 단어를 삭제하시겠습니까?"), | |
content: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
ElevatedButton( | |
onPressed: () { | |
_databaseService.deleteWord(id).then( | |
(result) { | |
if (result) { | |
Navigator.of(context).pop(); | |
setState(() { | |
_wordList = _databaseService.selectWords(); | |
}); | |
} else { | |
print("delete error"); | |
} | |
}, | |
); | |
}, | |
child: const Text("예"), | |
), | |
ElevatedButton( | |
onPressed: () => Navigator.of(context).pop(), | |
child: const Text("아니오"), | |
), | |
], | |
), | |
], | |
), | |
); | |
} | |
} |
[유튜브 강좌 영상]
'모바일어플개발 > Flutter' 카테고리의 다른 글
[053] 플러터 (Flutter) 배우기 - bottom_picker 사용하여 날짜 선택하기 (0) | 2023.04.14 |
---|---|
[052] 플러터 (Flutter) 배우기 - showDatePicker 사용하여 날짜 선택하기 (0) | 2023.04.13 |
[050] 플러터 (Flutter) 배우기 - Rest API 사용(GET, POST) (0) | 2023.04.05 |
[049] 플러터 (Flutter) 배우기 - Singleton (싱글톤) 개념 이해하기 (0) | 2023.04.04 |
[048] 플러터 (Flutter) 배우기 - Factory Pattern (팩토리 패턴) 이해하기 (0) | 2023.04.03 |