외로운 Nova의 작업실

Flutter 프로그래밍 - 12(일정관리앱 로컬 DB적용) 본문

Programming/Flutter

Flutter 프로그래밍 - 12(일정관리앱 로컬 DB적용)

Nova_ 2024. 1. 22. 16:15

안녕하세요, 이번장에서는 일정 관리 앱에 로컬 DB를 적용하고 로컬 DB를 사용하여 일정을 저장하고 불러오는 기능을 사용해보겠습니다.

 

- 사전 지식

드리프트 플러그인을 사용하면 직접 SQL을 작성하지 않도록 SQLite를 사용할 수 있습니다. 드리프트와 같은 객체-관계 모델을 사용하면 테이블을 클래스로 표현하고 쿼리를 다트언어로 표현하면 드리프트가 자동으로 해당되는 테이블과 쿼리를 생성합니다. 아래는 관련한 자료입니다.

 

<TABLE 생성>

@DataClassName('User')
class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().customConstraint('UNIQUE')();
  TextColumn get email => text().customConstraint('UNIQUE')();
}
CREATE TABLE User (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE,
  email TEXT UNIQUE
);

 

<Insert>

final user = UsersCompanion.insert(
  name: 'John',
  email: 'john@example.com',
);

await into(users).insert(user);
CREATE TABLE User (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE,
  email TEXT UNIQUE
);

 

<select>

final query = select(users)..where((t) => t.name.equals('John'));

final usersList = await query.get();
SELECT * FROM User WHERE name = 'John';

 

 

<update>

final query = update(users)..where((t) => t.id.equals(1));

await query.write(UsersCompanion(
  name: Value('UpdatedName'),
  email: Value('updated@example.com'),
));
UPDATE User SET name = 'UpdatedName', email = 'updated@example.com' WHERE id = 1;

 

<delete>

final query = delete(users)..where((t) => t.id.equals(1));

await query.go();
UPDATE User SET name = 'UpdatedName', email = 'updated@example.com' WHERE id = 1;

 

Dissmissible 위젯은 위젯을 밀어서 삭제하는 기능을 제공합니다. 이를 통해 일정을 밀어서 삭제해보겠습니다. 관련한 기본적인 코드는 아래와 같습니다.

Dismissible(
      key: Key(itemText), // 각 Dismissible 위젯을 고유하게 식별하는 키
      onDismissed: (_) => onDismissed(), // Dismissible이 스와이프되었을 때 실행할 콜백 함수
      background: Container(
        color: Colors.red, // 스와이프할 때 보이는 배경 색상
        alignment: Alignment.centerRight,
        child: Icon(
          Icons.delete,
          color: Colors.white,
        ),
      ),

 

 

-구현하기

앱을 빌드하는 과정에서 드리프트 문법은 SQL문법으로 변경되어 빌드됩니다. 그래서 드리프트를 dev_dependencies에 놓는것입니다. 지금부터는 드리프트 문법을 이용해 데이터베이스를 생성하고 테이블을 만들고, 데이터를 삽입, 삭제, 업데이트 하는 SQL문을 만들도록 코드를 짜보겠습니다.

 

- 테이블 모델 구현

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

각 속성을 추가했다면 다시 한번 ()으로 함수를 한번더 실행하여 이어서 선언한 속성들이 SQL 문법으로 변경되도록 해줍니다.

 

- 테이블 관련 코드 생성

이제 드리프트에 Schedule 테이블을 등록해주면 드리프트가 자동으로 테이블과 관련된 기능을 코드로 생성합니다. 이제 /lib/database/drift_database.dart 파일을 만들어서 드리프트가 데이터베이스를 만들고 각 드리프트 문법을 SQL문으로 잘 만들도록 코드를 작성해보겠습니다.

  import 'package:calendar_shedular_local/model/schedule.dart';
  import 'package:drift/drift.dart';
  
  part 'drift_database.g.dart'; //part 파일 지정
  
  @DriftDatabase( //사용할 테이블 등록
    tables: [
      Schedules,
    ],
  )
  
  class LocalDatabase extends _$LocalDatabase {}

part 키워드를 이용해서 sql문을 출력할 파일을 지정해줍니다. 보통 현재 파일이름에 .g.dart를 추가하는 형식입니다. 현재 파일을 만들지 않았지만 드리프트를 실행하면 자동으로 생성됩니다. 한번 생성해보겠습니다. terminal에서 아래 명령어를 실행합니다.

flutter pub run build_runner build

.g.dart파일이 생긴것을 볼 수 있습니다. 잎파일에는 _$LocalDatabase가 있습니다.

abstract class _$LocalDatabase extends GeneratedDatabase {
  _$LocalDatabase(QueryExecutor e) : super(e);
  late final $SchedulesTable schedules = $SchedulesTable(this);
  @override
  Iterable<TableInfo<Table, dynamic>> get allTables =>
      allSchemaEntities.whereType<TableInfo<Table, Object?>>();
  @override
  List<DatabaseSchemaEntity> get allSchemaEntities => [schedules];
}

 

이제 테이블이 생성된 로컬 데이터베이스를 LocalDatabase클래스에 받아왔으니 INSERT, DELETE 기능을 구현해보겠습니다. 또한, 데이터베이스의 위치를 알려주기 위해 LazyDatabase 클래스(위치관련 클래스)를 반환해주는 _openConnection()함수를 만들고 이 함수를 이용해 객체를 상위로부터 입력받게하겠습니다.

import 'package:calendar_shedular_local/model/schedule.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'dart:io';

part 'drift_database.g.dart';

@DriftDatabase(
  tables: [
    Schedules,
  ],
)
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection()); //LazyDatabase 객체 입력받기

  @override
  int get schemaVersion => 1;

  // 데이터를 조회하고 변화 감지
  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();
}

//LazyDatabase 반환 함수
LazyDatabase _openConnection() {
  return LazyDatabase(() async {

    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase((file));
  });
}

 

이렇게 하면 각 네이티브 데이터베이스 위치에 db.sqlite라는 파일로 데이터베이스 파일이 만들어지게됩니다. 이제 설정을 마쳤으니 구현한 기능들을 프로젝트에서 사용하도록 main.dart에서 데이터베이스 변수를 주입해주겠습니다. 그래야 사용이 가능합니다.

 

- main.dart 데이터베이스 적용

 

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/screen/home_screen.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:calendar_shedular_local/database/drift_database.dart';
import 'package:get_it/get_it.dart';

void main() async{

  WidgetsFlutterBinding.ensureInitialized(); //플러터 프레임워크가 준비될때까지 대기

  await initializeDateFormatting(); //intl 초기화(다국어화)
  
  final database = LocalDatabase();
  
  GetIt.I.registerSingleton<LocalDatabase>(database); //GetIt에 데이터베이스 변수 주입하기

  runApp(
    MaterialApp(
      home: HomeScreen(),
    )
  );
}

GetIt 패키지는 의존성을 구현하는 플러그인으로 database 변수를 GetIt.I를 통해서 프로젝트 어디서든 사용할 수 있게 해줍니다.

 

- 일정 데이터를 폼과 연동

현재 custom_text_field와 schedule_bottom_sheet는 연동할 수 있는 인터페이스가 없습니다. 이를 만들어주기 위해 custom_text_field에 onSaved(텍스트 필드가 저장될때 실행되는 함수)와 validator(텍스트 필드가 저장될때 값을 검증하는 함수)를 만들어주겠습니다.

import 'package:calendar_shedular_local/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,

    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(  // ➋ 세로로 텍스트와 텍스트 필드를 위치
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: 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, // ➎ 시간 관련 텍스트 필드는 ‘시' 접미사 추가
            ),
          ),
        ),
      ],
    );
  }
}

 

이제 schedule_bottom_sheet.dart파일에 onSaved() 함수를 만들어주겠습니다. 이 함수로 text_field에서 값을 빼서 변수에 담아두겠습니다.

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/custom_text_field.dart';
import 'package:calendar_shedular_local/const/colors.dart';
import 'package:get_it/get_it.dart';
import '../database/drift_database.dart';

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({Key? key}) : super(key: key);

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  final GlobalKey<FormState> formKey = GlobalKey(); //폼키 생성

  int? startTime; //시작 시간 저장 변수
  int? endTime; // 종료 시간 저장 변수
  String? content; //일정 내용 저장 변수
  @override
  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 = int.parse(val!);
                        },
                        validator: timeValidator,
                      ),
                    ),
                    const SizedBox(width: 16.0),
                    Expanded(
                      child: CustomTextField(
                        // 종료시간 입력 필드
                        label: '종료 시간',
                        isTime: true,
                        onSaved: (String? val){
                          endTime = int.parse(val!);
                        },
                        validator: timeValidator,
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 8.0),
                Expanded(
                  child: CustomTextField(
                    // 내용 입력 필드
                    label: '내용',
                    isTime: false,
                    onSaved: (String? val){
                      content = val;
                    },
                    validator: contentValidator,
                  ),
                ),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    // [저장] 버튼
                    // ➌ [저장] 버튼
                    onPressed: onSavePressed,
                    style: ElevatedButton.styleFrom(
                      primary: PRIMARY_COLOR,
                    ),
                    child: Text('저장'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  void onSavePressed() {
    if (formKey.currentState!.validate()) {
      // ➊ 폼 검증하기
      formKey.currentState!.save(); // ➋ 폼 저장하기

      print(startTime); //시작 시간 출력
      print(endTime); //종료 시간 출력
      print(content); //내용 출력
    }
  }

  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;
  } // 내용값 검증
}

 

한번 실행해보겠습니다.

 

저장 버튼을 누르면 잘 프린트 되는 것을 확인할 수 있습니다.

 

- 일정 추가 연동

이제 Form에서 데이터를 빼올 수 있으니 이제 selectedDate 값만 HomeScreen으로부터 가져오고 이값을 이용해서 DB메소드를 이용해 INSERT 기능을 완성해보겠습니다. 아래는 HomeScreen 입니다.

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/main_calaner.dart';
import 'package:calendar_shedular_local/component/schedule_card.dart';
import 'package:calendar_shedular_local/component/today_banner.dart';
import 'package:calendar_shedular_local/component/schedule_bottom_sheet.dart';
import 'package:calendar_shedular_local/const/colors.dart';

class HomeScreen extends StatefulWidget{

  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>{

  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context){
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible: true, //배경을 탭했을때 BottomSheet 닫기
              builder: (_) => ScheduleBottomSheet(selectedDate: selectedDate,),
              isScrollControlled: true
          );
        },
        child: Icon(
          Icons.add,
        ),
      ),
      body: SafeArea(
        child: Column(
          children: [

            MainCalander(selectedDate: selectedDate, onDaySelected: onDaySelected,),

            SizedBox(height: 8.0,),

            TodayBanner(selectedDate: selectedDate, count: 1),

            SizedBox(height: 8.0,),

            ScheduleCard(startTime: 12, endTime: 14, content: '프로그래밍 공부')
          ],
        ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate){

    setState((){
      this.selectedDate = selectedDate;
  });
  }
}

 

아래는 schedule_bottom_sheet.dart 파일입니다.

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/custom_text_field.dart';
import 'package:calendar_shedular_local/const/colors.dart';
import 'package:get_it/get_it.dart';
import '../database/drift_database.dart';
import 'package:drift/drift.dart' hide Column;

class ScheduleBottomSheet extends StatefulWidget {

  final DateTime selectedDate;

  const ScheduleBottomSheet({
    required this.selectedDate,
      Key? key}) : super(key: key);

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  final GlobalKey<FormState> formKey = GlobalKey(); //폼키 생성

  int? startTime; //시작 시간 저장 변수
  int? endTime; // 종료 시간 저장 변수
  String? content; //일정 내용 저장 변수
  @override
  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 = int.parse(val!);
                        },
                        validator: timeValidator,
                      ),
                    ),
                    const SizedBox(width: 16.0),
                    Expanded(
                      child: CustomTextField(
                        // 종료시간 입력 필드
                        label: '종료 시간',
                        isTime: true,
                        onSaved: (String? val){
                          endTime = int.parse(val!);
                        },
                        validator: timeValidator,
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 8.0),
                Expanded(
                  child: CustomTextField(
                    // 내용 입력 필드
                    label: '내용',
                    isTime: false,
                    onSaved: (String? val){
                      content = val;
                    },
                    validator: contentValidator,
                  ),
                ),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    // [저장] 버튼
                    // ➌ [저장] 버튼
                    onPressed: onSavePressed,
                    style: ElevatedButton.styleFrom(
                      primary: PRIMARY_COLOR,
                    ),
                    child: Text('저장'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  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;
  } // 내용값 검증
}

 

- 일정 데이터 읽기

이제 LocalDatabase 클래스의 watchSchedules() 함수를 사용해서 달력에서 선택한 날짜에 해당하는 일정들을 불러와 Schedule 카드 리스트 뷰로 리스트를 만들겠습니다.

import 'dart:js_util';

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/main_calaner.dart';
import 'package:calendar_shedular_local/component/schedule_card.dart';
import 'package:calendar_shedular_local/component/today_banner.dart';
import 'package:calendar_shedular_local/component/schedule_bottom_sheet.dart';
import 'package:calendar_shedular_local/const/colors.dart';
import 'package:calendar_shedular_local/database/drift_database.dart';
import 'package:get_it/get_it.dart';

class HomeScreen extends StatefulWidget{

  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>{

  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context){
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible: true, //배경을 탭했을때 BottomSheet 닫기
              builder: (_) => ScheduleBottomSheet(selectedDate: selectedDate,),
              isScrollControlled: true
          );
        },
        child: Icon(
          Icons.add,
        ),
      ),
      body: SafeArea(
        child: Column(
          children: [

            MainCalander(selectedDate: selectedDate, onDaySelected: onDaySelected,),

            SizedBox(height: 8.0,),

            TodayBanner(selectedDate: selectedDate, count: 1),

            SizedBox(height: 8.0,),

            Expanded(child: StreamBuilder<List<Schedule>>(
              stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
              builder: (context, snapshot){ //snapshot이 데이터
                if(!snapshot.hasData){ //데이터가 없을때
                  return Container();
                }

                return ListView.builder(itemCount: snapshot.data!.length,
                itemBuilder: (context, index){ //index가 데이터
                  //현재 index에 해당하는 일정
                  final schedule = snapshot.data![index];
                  return 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;
  });
  }
}

 

- 일정 데이터 삭제하기

일정데이터는 Dismissible 위젯을 사용해서 왼쪽으로 밀어 삭제하는 기능을 탑재하겠습니다.

import 'dart:js_util';

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/main_calaner.dart';
import 'package:calendar_shedular_local/component/schedule_card.dart';
import 'package:calendar_shedular_local/component/today_banner.dart';
import 'package:calendar_shedular_local/component/schedule_bottom_sheet.dart';
import 'package:calendar_shedular_local/const/colors.dart';
import 'package:calendar_shedular_local/database/drift_database.dart';
import 'package:get_it/get_it.dart';

class HomeScreen extends StatefulWidget{

  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>{

  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context){
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible: true, //배경을 탭했을때 BottomSheet 닫기
              builder: (_) => ScheduleBottomSheet(selectedDate: selectedDate,),
              isScrollControlled: true
          );
        },
        child: Icon(
          Icons.add,
        ),
      ),
      body: SafeArea(
        child: Column(
          children: [

            MainCalander(selectedDate: selectedDate, onDaySelected: onDaySelected,),

            SizedBox(height: 8.0,),

            TodayBanner(selectedDate: selectedDate, count: 1),

            SizedBox(height: 8.0,),

            Expanded(child: StreamBuilder<List<Schedule>>(
              stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
              builder: (context, snapshot){ //snapshot이 데이터
                if(!snapshot.hasData){ //데이터가 없을때
                  return Container();
                }

                return ListView.builder(itemCount: snapshot.data!.length,
                itemBuilder: (context, index){ //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;
  });
  }
}

 

- 일정 개수 반영하기

이제 일정을 선택하면 Today_banner 부에 일정 개수를 반영해보겠습니다.

import 'package:flutter/material.dart';
import 'package:calendar_shedular_local/component/main_calaner.dart';
import 'package:calendar_shedular_local/component/schedule_card.dart';
import 'package:calendar_shedular_local/component/today_banner.dart';
import 'package:calendar_shedular_local/component/schedule_bottom_sheet.dart';
import 'package:calendar_shedular_local/const/colors.dart';
import 'package:calendar_shedular_local/database/drift_database.dart';
import 'package:get_it/get_it.dart';

class HomeScreen extends StatefulWidget{

  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>{

  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context){
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible: true, //배경을 탭했을때 BottomSheet 닫기
              builder: (_) => ScheduleBottomSheet(selectedDate: selectedDate,),
              isScrollControlled: true
          );
        },
        child: Icon(
          Icons.add,
        ),
      ),
      body: SafeArea(
        child: Column(
          children: [

            MainCalander(selectedDate: selectedDate, onDaySelected: onDaySelected,),

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

            SizedBox(height: 8.0,),

            Expanded(child: StreamBuilder<List<Schedule>>(
              stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
              builder: (context, snapshot){ //snapshot이 데이터
                if(!snapshot.hasData){ //데이터가 없을때
                  return Container();
                }

                return ListView.builder(itemCount: snapshot.data!.length,
                itemBuilder: (context, index){ //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;
  });
  }
}

 

- 최종 완성

 

Comments