[051] 플러터 (Flutter) 배우기 - sqflite 사용하여 단어장 만들기(로컬 데이터베이스)

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};
}
}
view raw 051_word.dart hosted with ❤ by GitHub

 

 

[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("아니오"),
),
],
),
],
),
);
}
}
view raw 051_main.dart hosted with ❤ by GitHub

 

[유튜브 강좌 영상]

 

https://youtu.be/kJuAvOON7_A

 

반응형