Flutter에서 로컬 데이터베이스를 쉽게 사용할 수 있게 해주는 강력한 패키지입니다.
watchSchedules()처럼 Stream을 반환하는 메서드를 제공하므로, 데이터가 변경될 때마다 별도의 작업 없이 UI를 자동으로 업데이트할 수 있습니다.DB 인스턴스를 단 한 번만 생성해 전역 보관함에 등록해두고, 어느 파일에서든 편리하게 꺼내 쓸 수 있도록 돕는 의존성 주입(DI) 패키지입니다.
registerSingleton을 사용해 등록하면, 앱 어디서나 GetIt.I<LocalDatabase>()를 통해 동일한 DB 인스턴스에 접근할 수 있습니다.Form 위젯을 제어하기 위해 사용하는 고유한 키입니다.
formKey.currentState!.validate(): 내부 모든 TextFormField의 validator를 한꺼번에 실행합니다.formKey.currentState!.save(): 모든 필드의 onSaved 콜백을 일괄 호출하여 데이터를 수집합니다.지속적으로 들어오는 데이터 흐름(Stream)을 구독(Subscribe)하고, 데이터가 바뀔 때마다 위젯을 자동으로 다시 빌드(Rebuild)해주는 위젯입니다.
watch~ 메서드와 조합하면 DB 데이터가 추가/삭제되는 순간 즉각 화면이 갱신됩니다.DB 작업(저장, 조회, 삭제)은 기기의 저장소에 직접 접근하기 때문에 시간이 다소 소요되는 작업입니다.
await를 붙여 해당 작업이 완료될 때까지 기다린 후 다음 코드를 실행하도록 제어합니다.Flutter 엔진 초기화를 보장한 뒤, LocalDatabase 인스턴스를 하나만 만들어 GetIt 보관함에 싱글톤으로 등록합니다.
// main.dart
void main() async {
// Flutter 프레임워크가 초기화되었는지 확인 (비동기 플러그인 사용 시 필수)
WidgetsFlutterBinding.ensureInitialized();
final database = LocalDatabase();
// GetIt 전역 보관함에 Singleton으로 등록
GetIt.I.registerSingleton<LocalDatabase>(database);
runApp(const MyApp());
}
// custom_text_field.dart
final FormFieldSetter<String> onSaved;
final FormFieldValidator<String> validator;
const CustomTextField({
required this.label,
required this.isTime,
required this.onSaved,
required this.validator,
Key? key,
}) : super(key: key);
// ... 중략 ...
TextFormField(
onSaved: onSaved,
validator: validator,
// ... 기타 스타일링 속성
)
schedule_bottom_sheet.dart
GlobalKey를 이용해 입력값을 일괄 검증 및 저장하고, Drift DB에 데이터를 저장(insert)합니다.
// schedule_bottom_sheet.dart
void onSavePressed() async {
// 1. 모든 TextFormField의 유효성 검증 수행
if (formKey.currentState!.validate()) {
// 2. 검증 통과 시 모든 필드의 onSaved() 실행
formKey.currentState!.save();
// 3. GetIt에서 DB 인스턴스를 꺼내와 데이터 비동기 저장
await GetIt.I<LocalDatabase>().createSchedule(
SchedulesCompanion(
startTime: Value(startTime!),
endTime: Value(endTime!),
content: Value(content!),
date: Value(widget.selectedDate),
),
);
// 4. 저장 완료 후 바텀시트 닫기
Navigator.of(context).pop();
}
}
// schedule_bottom_sheet.dart
void onSavePressed() async {
// 1. 모든 TextFormField의 유효성 검증 수행
if (formKey.currentState!.validate()) {
// 2. 검증 통과 시 모든 필드의 onSaved() 실행
formKey.currentState!.save();
// 3. GetIt에서 DB 인스턴스를 꺼내와 데이터 비동기 저장
await GetIt.I<LocalDatabase>().createSchedule(
SchedulesCompanion(
startTime: Value(startTime!),
endTime: Value(endTime!),
content: Value(content!),
date: Value(widget.selectedDate),
),
);
// 4. 저장 완료 후 바텀시트 닫기
Navigator.of(context).pop();
}
}
home_screen.dart

- 오늘 알게 된 점
GetIt의 싱글톤 보장: registerSingleton을 쓰면 앱 전체에서 단 하나의 객체 인스턴스만 공유되므로, DB 연결 객체처럼 리소스를 무겁게 먹고 통일성이 중요한 객체를 관리할 때 매우 유용합니다.
**Form의 편리함: formKey.currentState 제어를 통해 수많은 입력 필드의 컨트롤러를 일일이 만들지 않고도 일괄적으로 validation과 data 컬렉션을 수행할 수 있어 코드가 획기적으로 줄어듭니다.
Reactive UI 구현: Drift의 Stream 기반 메서드(watch~)와 StreamBuilder를 조합하면, 개발자가 수동으로 setState()를 호출하지 않아도 데이터 변경이 감지되어 UI가 실시간 반응형으로 바뀝니다.
비동기 흐름 제어: DB 작업은 반드시 async/await 처리를 확실히 해주어야 합니다. 만약 저장이 끝나기도 전에 Navigator.pop()이 먼저 실행되면 데이터 누락이나 예기치 못한 사이드 이펙트가 발생할 수 있습니다.**
- 아직 헷갈리는 점 & 해결 완료!
Q. SchedulesCompanion에서 값들을 굳이 Value()로 감싸서 넣는 이유가 무엇인가요?
A. Drift에서 일반 raw 데이터 타입 대신 Value()로 감싸는 이유는 '값이 null인 것'과 '값 자체가 전달되지 않은 것(Absent)'을 명확히 구분하기 위해서입니다.
기본값이나 AutoIncrement(자동 증가 ID) 필드가 있을 때, 값을 안 주면(absent) DB가 알아서 처리해야 하는데, 그냥 null을 넣어버리면 DB 컬럼에 진짜 NULL을 덮어씌우려는 것으로 오해할 수 있습니다. 그래서 명확하게 값을 전달할 때는 Value(값) 형태로 감싸서 넘겨야 합니다!
Q. snapshot.hasData가 false가 되는 시점은 언제인가요? 데이터가 0개일 때인가요, 아니면 로딩 중일 때인가요?
A. 'DB 응답을 기다리는 로딩 중(연결 중)'일 때 false가 됩니다. > 데이터가 조회되었는데 결과가 0개(빈 리스트 [])인 경우에는 데이터 자체는 존재하는 것이기 때문에 snapshot.hasData는 true가 됩니다! (이때 snapshot.data!.isEmpty가 true가 되는 구조입니다.) 따라서 데이터가 아예 안 들어온 최초 로딩 시점을 걸러내기 위해 !snapshot.hasData 체크가 필수적입니다.