I am making a todo list app. I wrote the code and used the dart migrate feature for null safety. I don't know how to fix this, can someone please help me.
Error displayed running the app on a device
main.dart
import 'package:flutter/material.dart';
import 'package:todo_list/screens/todo_list_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TODO List',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: TodoListScreen(),
);
}
}
database_helpers.dart
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:todo_list/models/task_model.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._instance();
static Database? _db;
DatabaseHelper._instance();
String tasksTable = 'task_table';
String colId = 'id';
String colTitle = 'title';
String colDate = 'date';
String colPriority = 'priority';
String colStatus = 'status';
//Task Table
//id | Title | Date | Priority | Status
Future<Database?> get db async {
if (_db == null) {
_db = await _initDb();
}
return _db;
}
Future<Database> _initDb() async {
Directory dir = await getApplicationDocumentsDirectory();
String path = dir.path + 'todo_list.db';
final todoListDb =
await openDatabase(path, version: 1, onCreate: _createDb);
return todoListDb;
}
void _createDb(Database db, int version) async {
await db.execute(
'CREATE TABLE $tasksTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, $colDate TEXT, $colPriority TEXT, $colStatus INTEGER');
}
Future<List<Map<String, dynamic>>> getTaskMapList() async {
Database db = await (this.db as FutureOr<Database>);
final List<Map<String, dynamic>> result = await db.query(tasksTable);
return result;
}
Future<List<Task>> getTaskList() async {
final List<Map<String, dynamic>> taskMapList = await getTaskMapList();
final List<Task> taskList = [];
taskMapList.forEach((taskMap) {
taskList.add(Task.fromMap(taskMap));
});
taskList.sort((taskA, taskB) => taskA.date!.compareTo(taskB.date!));
return taskList;
}
Future<int> insertTask(Task task) async {
Database db = await (this.db as FutureOr<Database>);
final int result = await db.insert(tasksTable, task.toMap());
return result;
}
Future<int> updateTask(Task task) async {
Database db = await (this.db as FutureOr<Database>);
final int result = await db.update(
tasksTable,
task.toMap(),
where: '$colId=',
whereArgs: [task.id],
);
return result;
}
Future<int> deleteTask(int? id) async {
Database db = await (this.db as FutureOr<Database>);
final int result = await db.delete(
tasksTable,
where: '$colId = ',
whereArgs: [id],
);
return result;
}
}
task_model.dart
class Task {
int? id;
String? title;
DateTime? date;
String? priority;
int? status;
Task({this.date, this.priority, this.status, this.title});
Task.withId({this.id, this.date, this.priority, this.status, this.title});
Map<String, dynamic> toMap() {
final map = Map<String, dynamic>();
// if (id = null) {
// map['id'] = id;
// }
map['id'] = id;
map['title'] = title;
map['date'] = date!.toIso8601String();
map['priority'] = priority;
map['status'] = status;
return map;
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task.withId(
id: map['id'],
date: DateTime.parse(map['date']),
priority: map['priority'],
status: map['status'],
title: map['title'],
);
}
}
add_task_screen.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:todo_list/helpers/database_helpers.dart';
import 'package:todo_list/models/task_model.dart';
class AddTaskScreen extends StatefulWidget {
final Function? updateTaskList;
final Task? task;
AddTaskScreen({this.task, this.updateTaskList});
@override
_AddTaskScreenState createState() => _AddTaskScreenState();
}
class _AddTaskScreenState extends State<AddTaskScreen> {
final _formKey = GlobalKey<FormState>();
String? _title = '';
String? _priority = '';
DateTime? _date = DateTime.now();
int? _status = 0;
TextEditingController _dateController = TextEditingController();
final DateFormat _dateFormatter = DateFormat('MMM dd, yyyy');
final List<String> _priorities = ['Low', 'Medium', 'High'];
@override
void initState() {
super.initState();
_title = widget.task!.title;
_date = widget.task!.date;
_priority = widget.task!.priority;
_status = widget.task!.status;
_dateController.text = _dateFormatter.format(_date!);
}
@override
void dispose() {
_dateController.dispose();
super.dispose();
}
_handleDatePicker() async {
final DateTime? date = await showDatePicker(
context: context,
initialDate: _date!,
firstDate: DateTime.now(),
lastDate: DateTime(2100),
// currentDate: DateTime.now(),
);
if (date != null && date != _date) {
setState(() {
_date = date;
});
_dateController.text = _dateFormatter.format(date);
}
}
_delete() {
DatabaseHelper.instance.deleteTask(widget.task!.id);
widget.updateTaskList!();
Navigator.pop(context);
}
_submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
Task task = Task(
date: _date, title: _title, priority: _priority, status: _status);
if (task.status == 0)
DatabaseHelper.instance.insertTask(task);
else {
task.id = widget.task!.id;
task.status = widget.task!.status;
DatabaseHelper.instance.updateTask(task);
}
widget.updateTaskList!();
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back_ios_new,
size: 30,
color: Theme.of(context).primaryColor,
),
),
SizedBox(height: 20),
Text(
widget.task == null ? 'Add Task' : 'Update Task',
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
SizedBox(height: 10),
Form(
key: _formKey,
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: TextFormField(
style: TextStyle(fontSize: 18),
decoration: InputDecoration(
labelText: 'Title',
labelStyle: TextStyle(fontSize: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
validator: (input) =>
input != null && input.trim().isEmpty
? 'Please enter a task title!'
: null,
onSaved: (input) {
if (input != null) _title = input;
},
initialValue: _title,
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: TextFormField(
style: TextStyle(fontSize: 18),
controller: _dateController,
onTap: _handleDatePicker,
readOnly: true,
decoration: InputDecoration(
labelText: 'Date',
labelStyle: TextStyle(fontSize: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: DropdownButtonFormField(
isDense: true,
icon: Icon(Icons.arrow_drop_down_circle),
iconSize: 22,
iconEnabledColor: Theme.of(context).primaryColor,
items: _priorities.map((String priority) {
return DropdownMenuItem(
value: priority,
child: Text(
priority,
style: TextStyle(
color: Colors.black,
fontSize: 18,
),
),
);
}).toList(),
style: TextStyle(fontSize: 18),
decoration: InputDecoration(
labelText: 'Priority',
labelStyle: TextStyle(fontSize: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
validator: (dynamic input) => _priority == null
? 'Please select a priority level!'
: null,
onChanged: (dynamic value) {
setState(() {
_priority = value.toString();
});
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20),
height: 60,
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: TextButton(
onPressed: _submit,
child: Text(
widget.task == null ? 'Add' : 'Update',
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
),
widget.task != null
? Container(
margin: EdgeInsets.symmetric(vertical: 20),
height: 60,
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: TextButton(
onPressed: _delete,
child: Text(
'Delete',
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
)
: SizedBox(height: 0),
],
),
)
],
),
),
),
),
);
}
}
todo_list_screen.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:todo_list/helpers/database_helpers.dart';
import 'package:todo_list/models/task_model.dart';
import 'package:todo_list/screens/add_task_sreen.dart';
class TodoListScreen extends StatefulWidget {
@override
_TodoListScreenState createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
Future<List<Task>>? _taskList;
final DateFormat _dateFormatter = DateFormat('MMM dd, yyyy');
@override
void initState() {
super.initState();
_updateTaskList();
}
_updateTaskList() {
setState(() {
_taskList = DatabaseHelper.instance.getTaskList();
});
}
Widget _buildTask(Task task) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
ListTile(
title: Text(
task.title!,
style: TextStyle(
fontSize: 18,
decoration: task.status == 0
? TextDecoration.none
: TextDecoration.lineThrough),
),
subtitle: Text(
'${_dateFormatter.format(task.date!)}ยท${task.priority}',
style: TextStyle(
fontSize: 15,
decoration: task.status == 0
? TextDecoration.none
: TextDecoration.lineThrough),
),
trailing: Checkbox(
value: task.status == 1 ? true : false,
onChanged: (value) {
// task.status = value ? 1 : 0;
if (value == false)
task.status = 0;
else
task.status = 1;
DatabaseHelper.instance.updateTask(task);
_updateTaskList();
},
activeColor: Theme.of(context).primaryColor,
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AddTaskScreen(
updateTaskList: _updateTaskList(),
task: task,
),
),
),
),
Divider(),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor,
child: Icon(Icons.add),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AddTaskScreen(
updateTaskList: _updateTaskList(),
),
),
),
),
body: FutureBuilder(
future: _taskList,
builder: (context, AsyncSnapshot<List<Task>> snapshot) {
// if (!snapshot.hasData) {
// return Center(
// child: CircularProgressIndicator(),
// );
// }
final int completedTaskCount = snapshot.data!
.where((Task task) => task.status == 1)
.toList()
.length;
return ListView.builder(
padding: EdgeInsets.symmetric(
vertical: 80,
),
itemCount: 1 + snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'My Tasks',
style: TextStyle(
color: Colors.black,
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 10,
),
Text(
'$completedTaskCount of ${snapshot.data!.length}',
style: TextStyle(
color: Colors.grey,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
return _buildTask(snapshot.data![index - 1]);
},
);
},
),
);
}
}
FutureBuilder
is designed in a way to take aFuture
object from you and build yourWidget
using thebuilder
function whenever the state of thatFuture
changes.Since
Future
s are asynchronous, your data will not be available immediately. This is where it is possibly breaking.You used,
snapshot.data!.where
without checking if data is actually present or not and sincesnapshot.data
will be null until the Future is completed, you will have this error.Uncomment this code, since this was the one performing the check whether data is actually present or not.