17장 데이터베이스 적용하기

송기영·2024년 1월 9일
0

플러터

목록 보기
19/25

16장에 이어서 앱을 재시작해도 데이터를 유지할 수 있도록 데이터베이스를 적용한다.

17.1. 사전지식

17.1.1 SQL, SQLite

드리프트 플러그인을 사용하면 직접 SQL을 작성하지 않고도 SQLite를 사용할 수 있다.

SQL의 구조

  • CREATE TABLE

  • INSERT INTO

  • SELECT

  • UPDATE

  • DELETE

17.1.2 Dismissible 위젯

위젯을 밀어서 삭제하는 기능을 제공한다.

Dismissible(
	key: ObjectKey(schedule.id),
	direction: DismissDirection.endToStart,
	onDismissed: (DismissDirection direction) {},
	child: Container(),
);

17.2 구현하기

17.2.1 모델 구현하기

// 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()(); // 종료시간
}

17.2.2 테이블 관련 코드 생성 및 쿼리 구현

// 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

17.2.3 main.dart

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()));
}

17.2.4 home_screen.dart

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;
    });
  }
}

17.2.5 custom_text_field.dart

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),
          ),
        ),
      ],
    );
  }
}

17.2.6 schedule_bottom_sheet.dart

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;
  }
}
profile
업무하면서 쌓인 노하우를 정리하는 블로그🚀 풀스택 개발자를 지향하고 있습니다👻

0개의 댓글