인프런 커뮤니티 질문&답변

최우인님의 프로필 이미지
최우인

작성한 질문수

[2024 최신] [코드팩토리] [초급] Flutter 3.0 앱 개발 - 10개의 프로젝트로 오늘 초보 탈출!

섹션 21. 캘린더 스케쥴러 오류 문의드립니다.

작성

·

413

0

색상 상태관리 부분을 듣고 있습니다. 똑 같이 따라했는데요.

아래 내용과 같이 FutureBuilder를 사용하여 똑같이 따라 했는데도 불구하고 null이 리턴되어 오류가 납니다.

future: GetIt.I<LocalDatabase>().getCategoryColors() 에서 강사님 강의에서는 데이터를 가지고 오는데, 제가 만든 코드에서는 null이 리턴되네요. 혹시 몰라 main.dart에서 GetIt.I<LocalDatabase>().getCategoryColors() 를 실행해 보고, 결과를 보면 정상적인 데이터가 들어옵니다.

뭐가 문제일까요?

main.dart

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

const DEFAULT_COLORS = [
  // 빨강
  'F44336',
  // 주황
  'FF9800',
  // 노랑
  'FFEB3B',
  // 초록
  'FCAF50',
  // 파랑
  '2196F3',
  // 남
  '3F51B5',
  // 보라
  '9C27B0',
];

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDateFormatting();

  final database = LocalDatabase();
  GetIt.I.registerSingleton<LocalDatabase>(database);
  final result = GetIt.I<LocalDatabase>().getCategoryColors();

  final colors = await database.getCategoryColors();
  if (colors.isEmpty) {
    for (String hexCode in DEFAULT_COLORS) {
      await database.createCategoryColor(
        CategoryColorsCompanion(
          hexCode: Value(hexCode),
        ),
      );
    }
  }

  runApp(MaterialApp(
    theme: ThemeData(
      fontFamily: 'NotoSans',
    ),
    home: HomeScreen(),
  ));
}

schedule_bottom_sheet.dart

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

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({super.key});

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

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  final GlobalKey<FormState> formKey = GlobalKey();
  int? startTime;
  int? endTime;
  String? content;
  int? selectedColorId;

  @override
  Widget build(BuildContext context) {
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;
    return SafeArea(
      bottom: true,
      child: GestureDetector(
        onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
        child: Container(
          height: MediaQuery.of(context).size.height / 2 + bottomInset,
          color: Colors.white,
          child: Padding(
            padding: EdgeInsets.only(bottom: bottomInset),
            child: Padding(
              padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
              child: Form(
                key: formKey,
                autovalidateMode: AutovalidateMode.always,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _Time(
                      onStartSaved: (newValue) {
                        startTime = int.parse(newValue!);
                      },
                      onEndSaved: (newValue) {
                        endTime = int.parse(newValue!);
                      },
                    ),
                    SizedBox(height: 16.0),
                    _Content(
                      onSaved: (newValue) {
                        content = newValue;
                      },
                    ),
                    SizedBox(height: 16.0),
                    FutureBuilder<List<CategoryColor>>(
                        future: GetIt.I<LocalDatabase>().getCategoryColors(),
                        builder: (context, snapshot) {
                          print(snapshot.data);
                          if (snapshot.hasData &&
                              selectedColorId == null &&
                              snapshot.data!.isNotEmpty) {
                            selectedColorId = snapshot.data![0].id;
                          }
                          return _ColorPicker(
                            colors: snapshot.hasData ? snapshot.data! : [],
                            selectColorId: selectedColorId!,
                          );
                        }),
                    SizedBox(height: 16.0),
                    _SaveButton(
                      onPressed: onSavePressed,
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  void onSavePressed() {
    if (formKey.currentState == null) {
      return;
    }

    if (formKey.currentState!.validate()) {
      formKey.currentState!.save();
    } else {
      print('에러가 있습니다.');
    }
  }
}

class _Time extends StatelessWidget {
  final FormFieldSetter<String> onStartSaved;
  final FormFieldSetter<String> onEndSaved;

  const _Time(
      {super.key, required this.onStartSaved, required this.onEndSaved});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
            child: CustomTextField(
          label: '시작 시간',
          isTime: true,
          onSaved: onStartSaved,
        )),
        SizedBox(width: 16.0),
        Expanded(
          child: CustomTextField(
            label: '마감 시간',
            isTime: true,
            onSaved: onEndSaved,
          ),
        ),
      ],
    );
  }
}

class _Content extends StatelessWidget {
  final FormFieldSetter<String> onSaved;

  const _Content({super.key, required this.onSaved});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: CustomTextField(
        label: '내용',
        isTime: false,
        onSaved: onSaved,
      ),
    );
  }
}

class _ColorPicker extends StatelessWidget {
  final List<CategoryColor> colors;
  final int selectColorId;

  const _ColorPicker(
      {super.key, required this.colors, required this.selectColorId});

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8.0,
      runSpacing: 10.0,
      children: colors.map((e) => rendColor(e, selectColorId == e.id)).toList(),
    );
  }

  Widget rendColor(CategoryColor color, bool isSelected) {
    return Container(
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Color(int.parse('FF${color.hexCode}', radix: 16)),
          border:
              isSelected ? Border.all(color: Colors.black, width: 1.0) : null,
        ),
        height: 32,
        width: 32);
  }
}

class _SaveButton extends StatelessWidget {
  final VoidCallback onPressed;

  const _SaveButton({super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: PRIMARY_COLOR,
        ),
        onPressed: onPressed,
        child: Text('Save'),
      ),
    );
  }
}

drift_database.dart

import 'dart:io';

import 'package:calendar_schedule_exam/model/category_color.dart';
import 'package:calendar_schedule_exam/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';

part 'drift_database.g.dart';

@DriftDatabase(
  tables: [
    Schedules,
    CategoryColors,
  ],
)
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());

  // insert schedules
  Future<int> createSchedule(SchedulesCompanion data) => into(schedules).insert(data);
  // insert categoryColors
  Future<int> createCategoryColor(CategoryColorsCompanion data) => into(categoryColors).insert(data);
  // select all categoryColors
  Future<List<CategoryColor>> getCategoryColors() => select(categoryColors).get();

  @override
  // TODO: implement schemaVersion
  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);
  });
}

오류내용

Launching lib/main.dart on iPhone 14 Pro Max in debug mode...
Running Xcode build...
Xcode build done.                                            7.8s
[VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(35)] Using the Impeller rendering backend.
Debug service listening on ws://127.0.0.1:61505/HeTRp4ZQphA=/ws
Syncing files to device iPhone 14 Pro Max...
flutter: null

======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building FutureBuilder<List<CategoryColor>>(dirty, state: _FutureBuilderState<List<CategoryColor>>#41541):
Null check operator used on a null value

The relevant error-causing widget was: 
  FutureBuilder<List<CategoryColor>> FutureBuilder:file:///Users/choiwooin/dev/flutterProject/calendar_schedule_exam/lib/component/schedule_bottom_sheet.dart:57:21
When the exception was thrown, this was the stack: 
#0      _ScheduleBottomSheetState.build.<anonymous closure> (package:calendar_schedule_exam/component/schedule_bottom_sheet.dart:68:59)
#1      _FutureBuilderState.build (package:flutter/src/widgets/async.dart:612:55)
#2      StatefulElement.build (package:flutter/src/widgets/framework.dart:5198:27)
#3      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5086:15)
#4      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5251:11)
#5      Element.rebuild (package:flutter/src/widgets/framework.dart:4805:7)
#6      ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5068:5)
#7      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5242:11)

 

답변 2

2

코드팩토리님의 프로필 이미지
코드팩토리
지식공유자

안녕하세요!

데이터 마이그레이션이 잘못 됐을 수 있습니다.

에뮬레이터/시뮬레이터에서 앱을 삭제한 후 다시 실행해보세요!

그래도 안되면 다시 질문 부탁드립니다.

감사합니다!

와 저도 앱삭제하고 다시하니 문제가 해결됬어요! 감사합니다.

0

최우인님의 프로필 이미지
최우인
질문자

앱 다 지우고 터미널에서 Flutter Clean 한 다음 해보니까 되네요. 감사합니다. 열공하겠습니다.

최우인님의 프로필 이미지
최우인

작성한 질문수

질문하기