I am trying to learn how to implement a moor database in flutter and I got stuck getting this error:
I/flutter ( 5303): Moor: Sent SELECT * FROM tasks; with args []
E/flutter ( 5303): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Unhandled error Null check operator used on a null value occurred in Instance of 'TodoBloc'.
E/flutter ( 5303): #0 $TasksTable.map
package:todo_app_orm/…/local/database.g.dart:153
E/flutter ( 5303): #1 MappedListIterable.elementAt (dart:_internal/iterable.dart:412:31)
E/flutter ( 5303): #2 ListIterator.moveNext (dart:_internal/iterable.dart:341:26)
E/flutter ( 5303): #3 new _GrowableList._ofEfficientLengthIterable (dart:core-patch/growable_array.dart:188:27)
E/flutter ( 5303): #4 new _GrowableList.of (dart:core-patch/growable_array.dart:150:28)
E/flutter ( 5303): #5 new List.of (dart:core-patch/array_patch.dart:50:28)
E/flutter ( 5303): #6 ListIterable.toList (dart:_internal/iterable.dart:212:44)
E/flutter ( 5303): #7 SimpleSelectStatement._mapResponse
package:moor/…/select/select.dart:70
I am getting this error after awaitng the future I'm getting from the moor database.
It doesn't appear if I only get the Future object but only after awaiting the List<Task>
object. It happens after awaiting the _database.getAllTasks()
here in the todo_bloc.dart:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc(this._database) : super(TodoInitial());
final Database _database;
@override
Stream<TodoState> mapEventToState(
TodoEvent event,
) async* {
if (event is LoadTasks) {
List<Task> tasks = await _database.getAllTasks();
yield TasksUpdated(tasks);
}
if (event is AddTask) {
_database.insertTask(event.task);
final tasks = state.tasks;
yield TasksUpdated([...tasks, event.task]);
}
if (event is RemoveTask) {
await _database.deleteTask(event.task);
}
}
}
database.dart:
@UseRowClass(Task)
class Tasks extends Table {
TextColumn get id => text()();
TextColumn get title => text()();
BoolColumn get isHighPriority => boolean()();
Set<Column> get primaryKey => {id};
}
@UseMoor(tables: [Tasks])
class Database extends _$Database {
Database()
: super(FlutterQueryExecutor.inDatabaseFolder(
path: 'db.sqlite', logStatements: true));
@override
int get schemaVersion => 1;
Future<List<Task>> getAllTasks() => select(tasks).get();
Future insertTask(Task task) => into(tasks).insert(task);
Future deleteTask(Task task) => delete(tasks).delete(task);
}
task.dart:
class Task implements Insertable<Task> {
Task({
required this.id,
required this.title,
this.isHighPriority = false,
});
final String id;
final String title;
final bool isHighPriority;
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
return TasksCompanion(
id: Value(id),
title: Value(title),
isHighPriority: Value(isHighPriority),
).toColumns(nullToAbsent);
}
}
database.g.dart:
class TasksCompanion extends UpdateCompanion<Task> {
final Value<String> id;
final Value<String> title;
final Value<bool> isHighPriority;
const TasksCompanion({
this.id = const Value.absent(),
this.title = const Value.absent(),
this.isHighPriority = const Value.absent(),
});
TasksCompanion.insert({
required String id,
required String title,
required bool isHighPriority,
}) : id = Value(id),
title = Value(title),
isHighPriority = Value(isHighPriority);
static Insertable<Task> custom({
Expression<String>? id,
Expression<String>? title,
Expression<bool>? isHighPriority,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (title != null) 'title': title,
if (isHighPriority != null) 'is_high_priority': isHighPriority,
});
}
TasksCompanion copyWith(
{Value<String>? id, Value<String>? title, Value<bool>? isHighPriority}) {
return TasksCompanion(
id: id ?? this.id,
title: title ?? this.title,
isHighPriority: isHighPriority ?? this.isHighPriority,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (title.present) {
map['title'] = Variable<String>(title.value);
}
if (isHighPriority.present) {
map['is_high_priority'] = Variable<bool>(isHighPriority.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TasksCompanion(')
..write('id: $id, ')
..write('title: $title, ')
..write('isHighPriority: $isHighPriority')
..write(')'))
.toString();
}
}
class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> {
final GeneratedDatabase _db;
final String? _alias;
$TasksTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedTextColumn id = _constructId();
GeneratedTextColumn _constructId() {
return GeneratedTextColumn(
'id',
$tableName,
false,
);
}
final VerificationMeta _titleMeta = const VerificationMeta('title');
@override
late final GeneratedTextColumn title = _constructTitle();
GeneratedTextColumn _constructTitle() {
return GeneratedTextColumn(
'title',
$tableName,
false,
);
}
final VerificationMeta _isHighPriorityMeta =
const VerificationMeta('isHighPriority');
@override
late final GeneratedBoolColumn isHighPriority = _constructIsHighPriority();
GeneratedBoolColumn _constructIsHighPriority() {
return GeneratedBoolColumn(
'is_high_priority',
$tableName,
false,
);
}
@override
List<GeneratedColumn> get $columns => [id, title, isHighPriority];
@override
$TasksTable get asDslTable => this;
@override
String get $tableName => _alias ?? 'tasks';
@override
final String actualTableName = 'tasks';
@override
VerificationContext validateIntegrity(Insertable<Task> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('title')) {
context.handle(
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
} else if (isInserting) {
context.missing(_titleMeta);
}
if (data.containsKey('is_high_priority')) {
context.handle(
_isHighPriorityMeta,
isHighPriority.isAcceptableOrUnknown(
data['is_high_priority']!, _isHighPriorityMeta));
} else if (isInserting) {
context.missing(_isHighPriorityMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Task map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return Task(
id: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
title: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}title'])!,
isHighPriority: const BoolType()
.mapFromDatabaseResponse(data['${effectivePrefix}is_high_priority'])!,
);
}
@override
$TasksTable createAlias(String alias) {
return $TasksTable(_db, alias);
}
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
late final $TasksTable tasks = $TasksTable(this);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [tasks];
}
Can anyone see where is the problem? I can provide more information if needed.
This was caused by a bug within moor_generator 4.3.0. It is now fixed as of version 4.3.1. It only occurred when custom classes were used for code generation.
The problem was in the database.g.dart file generated by the moor_generator.
Variable effectivePrefix
had a value of null
and because of that data['${effectivePrefix}id']
called .toString()
method on null
and returned a String
with value of "null". So basically data['${effectivePrefix}id']
was trying to access the data['nullid']
which doesn't exist and returns another null
. Then StringType().mapFromDatabaseResponse(data['${effectivePrefix}title'])!
tried to use a bang
operator (!)
on null
which throws an error.
Original code that caused this problem is below:
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Task map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return Task(
id: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
title: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}title'])!,
isHighPriority: const BoolType()
.mapFromDatabaseResponse(data['${effectivePrefix}is_high_priority'])!,
);
}
This problem can be fixed by removing ${effectivePrefix}
from the key used in the data Map
.