flutterdartblocdart-null-safetyflutter-moor

"Unhandled error Null check operator used on a null value" after await on a Future from a moor database


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.


Solution

  • The problem

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

    The solution

    This problem can be fixed by removing ${effectivePrefix} from the key used in the data Map.