16장에 이어서 앱을 재시작해도 데이터를 유지할 수 있도록 데이터베이스를 적용한다.
드리프트 플러그인을 사용하면 직접 SQL을 작성하지 않고도 SQLite를 사용할 수 있다.
CREATE TABLE
INSERT INTO
SELECT
UPDATE
DELETE
위젯을 밀어서 삭제하는 기능을 제공한다.
Dismissible(
key: ObjectKey(schedule.id),
direction: DismissDirection.endToStart,
onDismissed: (DismissDirection direction) {},
child: Container(),
);
// lib/model/schedule.dart
import 'package:drift/drift.dart';
class Schedules extends Table {
IntColumn get id => integer().autoIncrement()(); // PRIMARY KEY, 정수 열
TextColumn get content => text()(); // 내용, 글자 열
DateTimeColumn get date => dateTime()(); // 일정 날짜, 날짜 열
IntColumn get startTime => integer()(); // 시작시간
IntColumn get endTime => integer()(); // 종료시간
}
// lib/database/drift_database.dart
import "package:calendar_scheduler/model/schedule.dart";
import "package:drift/drift.dart";
import "package:drift/native.dart";
import "package:path_provider/path_provider.dart";
import "package:path/path.dart" as p;
import "dart:io";
// private값까지 불러올 수 있음
part "drift_database.g.dart"; // part 파일 지정
// 사용할 테이블 등록
(tables: [
Schedules,
])
class LocalDatabase extends _$LocalDatabase {
LocalDatabase() : super(_openConnection());
// 데이터를 조회하고 변화 감지
Stream<List<Schedule>> watchSchedules(DateTime date) =>
(select(schedules)..where((tbl) => tbl.date.equals(date))).watch();
Future<int> createSchedule(SchedulesCompanion data) =>
into(schedules).insert(data);
Future<int> removeSchedule(int id) =>
(delete(schedules)..where((tbl) => tbl.id.equals(id))).go();
int get schemaVersion => 1;
}
// 데이터베이스 파일 생성 및 연동
LazyDatabase _openConnection() {
return LazyDatabase(() async {
// 데이터베이스 파일 저장할 폴더
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, "db.sqlite"));
return NativeDatabase(file);
});
}
// 코드생성으로 생성할 클래스 상속
커맨드에서 flutter pub run build_runner build
을 입력해 drift_database.g.dart파일을 생성한다.
이때 정상적으로 생성이 되지 않는다면 다음 종속성을 추가해준다.
dependencies:
drift_dev: ^2.14.1
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import "package:get_it/get_it.dart";
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// intl 패키지 초기화(다국어화)
await initializeDateFormatting("ko_kr", null);
// DB 생성
final database = LocalDatabase();
// GetIt에 데이터베이스 변수 주입
GetIt.I.registerSingleton<LocalDatabase>(database);
runApp(MaterialApp(home: HomeScreen()));
}
import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:calendar_scheduler/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler/component/schedule_card.dart';
import 'package:calendar_scheduler/component/today_banner.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime selectedDate = DateTime.utc(
DateTime.now().year, DateTime.now().month, DateTime.now().day);
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(30.0))),
backgroundColor: PRIMARY_COLOR,
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true, // 배경 탭 했을때 화면닫기
builder: (_) => ScheduleBottomSheet(
selectedDate: selectedDate,
),
// BottomSheet의 높이를 화면의 최대 높이로 정의하고 스크롤 가능하게 변경
isScrollControlled: true);
},
child: const Icon(Icons.add),
),
body: SafeArea(
child: Column(
children: [
MainCalendar(
selectedDate: selectedDate,
onDaySelected: onDaySelected,
),
const SizedBox(
height: 8.0,
),
StreamBuilder<List<Schedule>>(
stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
builder: (context, snapshot) {
return TodayBanner(
selectedDate: selectedDate,
count: snapshot.data?.length ?? 0);
},
),
const SizedBox(
height: 8.0,
),
Expanded(
// 남은 공간 모두 차지
child: StreamBuilder<List<Schedule>>(
// 일정 정보가 Stream으로 제공 되기 때문
stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
builder: (context, snapshot) {
// 데이터가 없을 대
if (!snapshot.hasData) {
return Container();
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
// 현재 index에 해당되는 일정
final schedule = snapshot.data![index];
return Dismissible(
key: ObjectKey(schedule.id),
direction: DismissDirection.startToEnd,
onDismissed: (DismissDirection direction) {
// 밀기 했을때 실핼 함수
GetIt.I<LocalDatabase>().removeSchedule(schedule.id);
},
child: Padding(
padding: const EdgeInsets.only(
bottom: 8.0, left: 8.0, right: 8.0),
child: ScheduleCard(
startTime: schedule.startTime,
endTime: schedule.endTime,
content: schedule.content,
),
));
},
);
},
))
],
)),
);
}
void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
setState(() {
this.selectedDate = selectedDate;
});
}
}
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextField extends StatelessWidget {
final String label;
final bool isTime;
final FormFieldSetter<String> onSaved;
final FormFieldValidator<String> validator;
const CustomTextField(
{required this.label,
required this.isTime,
required this.onSaved,
required this.validator,
super.key});
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: PRIMARY_COLOR,
fontWeight: FontWeight.w600,
),
),
Expanded(
flex: isTime ? 0 : 1,
child: TextFormField(
// 폼 저장 시 실행 함수
onSaved: onSaved,
// 폼 검증 시 실행 함수
validator: validator,
cursorColor: Colors.grey,
// 시간 관련 텍스트 필드가 아니면 한 줄 이상 작성 가능
maxLines: isTime ? 1 : null,
expands: !isTime,
// 시간 관련 텍스트 필드는 기본 숫자 키보드 아니면 일반 글자 키보드 보여주기
keyboardType:
isTime ? TextInputType.number : TextInputType.multiline,
// 시간 관련 텍스트 필드는 숫자만 입력하도록 제한
inputFormatters: isTime
? [
FilteringTextInputFormatter.digitsOnly,
]
: [],
decoration: InputDecoration(
// 테두리 삭제
border: InputBorder.none,
// 배경색 지정
filled: true,
// 배경색
fillColor: Colors.grey[300],
// 접미사 추가
suffixText: isTime ? "시" : null),
),
),
],
);
}
}
import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
class ScheduleBottomSheet extends StatefulWidget {
// 선택된 날짜 상위 위젯에서 입력받기
final DateTime selectedDate;
const ScheduleBottomSheet({required this.selectedDate, super.key});
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
final GlobalKey<FormState> formKey = GlobalKey();
int? startTime; // 시작 시간 저장 변수
int? endTime; // 종료 시간 저장 변수
String? content; // 일정 내용 저장 변수
Widget build(BuildContext context) {
// 키보드 높이 가져오기
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Form(
key: formKey,
child: SafeArea(
child: Container(
// 화면의 반을 차지
height: MediaQuery.of(context).size.height / 2 + bottomInset,
color: Colors.white,
child: Padding(
padding:
// 패딩에 키보드 높이를 추가해서 위젯 전반적으로 위로 올려주기
EdgeInsets.only(left: 8, right: 8, top: 8, bottom: bottomInset),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomTextField(
label: "시작 시간",
isTime: true,
onSaved: (String? val) {
// 저장이 실행되면 startTime 변수에 텍스트 필드 값 저장
startTime = int.parse(val!);
},
validator: timeValidator,
)),
SizedBox(
width: 16.0,
),
Expanded(
child: CustomTextField(
label: "종료 시간",
isTime: true,
onSaved: (String? val) {
// 저장이 실행되면 endTime 변수에 텍스트 필드 값 저장
endTime = int.parse(val!);
},
validator: timeValidator,
)),
],
),
const SizedBox(
height: 8.0,
),
Expanded(
child: CustomTextField(
label: "내용",
isTime: false,
onSaved: (String? val) {
// 저장이 실행되면 content 변수에 텍스트 필드 값 저장
content = val;
},
validator: contentValidator,
)),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onSavePressed,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(1)),
backgroundColor: PRIMARY_COLOR,
),
child: const Text(
"저장",
style: TextStyle(color: Colors.white),
),
),
)
],
),
),
),
),
);
}
void onSavePressed() async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
await GetIt.I<LocalDatabase>().createSchedule(SchedulesCompanion(
startTime: Value(startTime!),
endTime: Value(endTime!),
content: Value(content!),
date: Value(widget.selectedDate),
));
// 일정 생성 후 화면 뒤로가기
Navigator.of(context).pop();
}
}
String? timeValidator(String? val) {
if (val == null) {
return "값을 입력해주세요.";
}
int? number;
try {
number = int.parse(val);
} catch (e) {
return "숫자를 입력해주세요.";
}
if (number < 0 || number > 24) {
return "0시부터 24시 사이를 입력해주세요.";
}
return null;
}
String? contentValidator(String? val) {
if (val == null || val.length == 0) {
return "값을 입력해주세요.";
}
return null;
}
}