diff --git a/docs/entities.md b/docs/entities.md index 596bd08a..10776d56 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -45,6 +45,7 @@ Floor entities can hold values of the following Dart types which map to their co - `String` - TEXT - `bool` - INTEGER (0 = false, 1 = true) - `Uint8List` - BLOB +- `enum` - INTEGER (records by the index 0..n) In case you want to store sophisticated Dart objects that can be represented by one of the above types, take a look at [Type Converters](type-converters.md). diff --git a/example/lib/database.g.dart b/example/lib/database.g.dart index 2635d580..5e57e919 100644 --- a/example/lib/database.g.dart +++ b/example/lib/database.g.dart @@ -82,7 +82,7 @@ class _$FlutterDatabase extends FlutterDatabase { }, onCreate: (database, version) async { await database.execute( - 'CREATE TABLE IF NOT EXISTS `Task` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)'); + 'CREATE TABLE IF NOT EXISTS `Task` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` TEXT NOT NULL, `isRead` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL)'); await callback?.onCreate?.call(database, version); }, @@ -105,7 +105,9 @@ class _$TaskDao extends TaskDao { (Task item) => { 'id': item.id, 'message': item.message, - 'timestamp': _dateTimeConverter.encode(item.timestamp) + 'isRead': item.isRead ? 1 : 0, + 'timestamp': _dateTimeConverter.encode(item.timestamp), + 'type': item.type.index }, changeListener), _taskUpdateAdapter = UpdateAdapter( @@ -115,7 +117,9 @@ class _$TaskDao extends TaskDao { (Task item) => { 'id': item.id, 'message': item.message, - 'timestamp': _dateTimeConverter.encode(item.timestamp) + 'isRead': item.isRead ? 1 : 0, + 'timestamp': _dateTimeConverter.encode(item.timestamp), + 'type': item.type.index }, changeListener), _taskDeletionAdapter = DeletionAdapter( @@ -125,7 +129,9 @@ class _$TaskDao extends TaskDao { (Task item) => { 'id': item.id, 'message': item.message, - 'timestamp': _dateTimeConverter.encode(item.timestamp) + 'isRead': item.isRead ? 1 : 0, + 'timestamp': _dateTimeConverter.encode(item.timestamp), + 'type': item.type.index }, changeListener); @@ -146,8 +152,10 @@ class _$TaskDao extends TaskDao { return _queryAdapter.query('SELECT * FROM task WHERE id = ?1', mapper: (Map row) => Task( row['id'] as int?, + (row['isRead'] as int) != 0, row['message'] as String, - _dateTimeConverter.decode(row['timestamp'] as int)), + _dateTimeConverter.decode(row['timestamp'] as int), + TaskType.values[row['type'] as int]), arguments: [id]); } @@ -156,8 +164,10 @@ class _$TaskDao extends TaskDao { return _queryAdapter.queryList('SELECT * FROM task', mapper: (Map row) => Task( row['id'] as int?, + (row['isRead'] as int) != 0, row['message'] as String, - _dateTimeConverter.decode(row['timestamp'] as int))); + _dateTimeConverter.decode(row['timestamp'] as int), + TaskType.values[row['type'] as int])); } @override @@ -165,8 +175,24 @@ class _$TaskDao extends TaskDao { return _queryAdapter.queryListStream('SELECT * FROM task', mapper: (Map row) => Task( row['id'] as int?, + (row['isRead'] as int) != 0, row['message'] as String, - _dateTimeConverter.decode(row['timestamp'] as int)), + _dateTimeConverter.decode(row['timestamp'] as int), + TaskType.values[row['type'] as int]), + queryableName: 'Task', + isView: false); + } + + @override + Stream> findAllTasksByTypeAsStream(TaskType type) { + return _queryAdapter.queryListStream('SELECT * FROM task WHERE type = ?1', + mapper: (Map row) => Task( + row['id'] as int?, + (row['isRead'] as int) != 0, + row['message'] as String, + _dateTimeConverter.decode(row['timestamp'] as int), + TaskType.values[row['type'] as int]), + arguments: [type.index], queryableName: 'Task', isView: false); } diff --git a/example/lib/main.dart b/example/lib/main.dart index dc3ccaf9..3dfd4ff0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -32,7 +32,7 @@ class FloorApp extends StatelessWidget { } } -class TasksWidget extends StatelessWidget { +class TasksWidget extends StatefulWidget { final String title; final TaskDao dao; @@ -42,35 +42,75 @@ class TasksWidget extends StatelessWidget { required this.dao, }) : super(key: key); + @override + State createState() => TasksWidgetState(); +} + +class TasksWidgetState extends State { + TaskType? _selectedType; + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(title)), + appBar: AppBar( + title: Text(widget.title), + actions: [ + PopupMenuButton( + itemBuilder: (context) { + return List.generate( + TaskType.values.length + 1, //Uses increment to handle All types + (index) { + return PopupMenuItem( + value: index, + child: Text( + index == 0 ? 'All' : _getMenuType(index).title, + ), + ); + }, + ); + }, + onSelected: (index) { + setState(() { + _selectedType = index == 0 ? null : _getMenuType(index); + }); + }, + ) + ], + ), body: SafeArea( child: Column( children: [ - TasksListView(dao: dao), - TasksTextField(dao: dao), + TasksListView( + dao: widget.dao, + selectedType: _selectedType, + ), + TasksTextField(dao: widget.dao), ], ), ), ); } + + TaskType _getMenuType(int index) => TaskType.values[index - 1]; } class TasksListView extends StatelessWidget { final TaskDao dao; + final TaskType? selectedType; const TasksListView({ Key? key, required this.dao, + required this.selectedType, }) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: StreamBuilder>( - stream: dao.findAllTasksAsStream(), + stream: selectedType == null + ? dao.findAllTasksAsStream() + : dao.findAllTasksByTypeAsStream(selectedType!), builder: (_, snapshot) { if (!snapshot.hasData) return Container(); @@ -105,20 +145,60 @@ class TaskListCell extends StatelessWidget { Widget build(BuildContext context) { return Dismissible( key: Key('${task.hashCode}'), - background: Container(color: Colors.red), - direction: DismissDirection.endToStart, + background: Container( + padding: const EdgeInsets.only(left: 16), + color: Colors.green, + child: const Align( + child: Text( + 'Change status', + style: TextStyle(color: Colors.white), + ), + alignment: Alignment.centerLeft, + ), + ), + secondaryBackground: Container( + padding: const EdgeInsets.only(right: 16), + color: Colors.red, + child: const Align( + child: Text( + 'Delete', + style: TextStyle(color: Colors.white), + ), + alignment: Alignment.centerRight, + ), + ), + direction: DismissDirection.horizontal, child: ListTile( - leading: Text(task.message), + title: Text(task.message), + subtitle: Text('Status: ${task.type.title}'), trailing: Text(task.timestamp.toIso8601String()), ), - onDismissed: (_) async { - await dao.deleteTask(task); - - final scaffoldMessengerState = ScaffoldMessenger.of(context); - scaffoldMessengerState.hideCurrentSnackBar(); - scaffoldMessengerState.showSnackBar( - const SnackBar(content: Text('Removed task')), - ); + confirmDismiss: (direction) async { + String? statusMessage; + switch (direction) { + case DismissDirection.endToStart: + await dao.deleteTask(task); + statusMessage = 'Removed task'; + break; + case DismissDirection.startToEnd: + final tasksLength = TaskType.values.length; + final nextIndex = (tasksLength + task.type.index + 1) % tasksLength; + final taskCopy = task.copyWith(type: TaskType.values[nextIndex]); + await dao.updateTask(taskCopy); + statusMessage = 'Updated task status by: ${taskCopy.type.title}'; + break; + default: + break; + } + + if (statusMessage != null) { + final scaffoldMessengerState = ScaffoldMessenger.of(context); + scaffoldMessengerState.hideCurrentSnackBar(); + scaffoldMessengerState.showSnackBar( + SnackBar(content: Text(statusMessage)), + ); + } + return statusMessage != null; }, ); } @@ -174,7 +254,7 @@ class TasksTextField extends StatelessWidget { if (message.trim().isEmpty) { _textEditingController.clear(); } else { - final task = Task(null, message, DateTime.now()); + final task = Task.optional(message: message); await dao.insertTask(task); _textEditingController.clear(); } diff --git a/example/lib/task.dart b/example/lib/task.dart index e353f750..267e6d8b 100644 --- a/example/lib/task.dart +++ b/example/lib/task.dart @@ -1,5 +1,15 @@ import 'package:floor/floor.dart'; +enum TaskType { + open('Open'), + inProgress('In Progress'), + done('Done'); + + final String title; + + const TaskType(this.title); +} + @entity class Task { @PrimaryKey(autoGenerate: true) @@ -7,9 +17,49 @@ class Task { final String message; + final bool isRead; + final DateTime timestamp; - Task(this.id, this.message, this.timestamp); + final TaskType type; + + Task(this.id, this.isRead, this.message, this.timestamp, this.type); + + factory Task.optional({ + int? id, + DateTime? timestamp, + String? message, + bool? isRead, + TaskType? type, + }) => + Task( + id, + isRead ?? false, + message ?? 'empty', + timestamp ?? DateTime.now(), + type ?? TaskType.open, + ); + + @override + String toString() { + return 'Task{id: $id, message: $message, read: $isRead, timestamp: $timestamp, type: $type}'; + } + + Task copyWith({ + int? id, + String? message, + bool? isRead, + DateTime? timestamp, + TaskType? type, + }) { + return Task( + id ?? this.id, + isRead ?? this.isRead, + message ?? this.message, + timestamp ?? this.timestamp, + type ?? this.type, + ); + } @override bool operator ==(Object other) => @@ -18,13 +68,15 @@ class Task { runtimeType == other.runtimeType && id == other.id && message == other.message && - timestamp == other.timestamp; + isRead == other.isRead && + timestamp == other.timestamp && + type == other.type; @override - int get hashCode => id.hashCode ^ message.hashCode ^ timestamp.hashCode; - - @override - String toString() { - return 'Task{id: $id, message: $message, timestamp: $timestamp}'; - } + int get hashCode => + id.hashCode ^ + message.hashCode ^ + isRead.hashCode ^ + timestamp.hashCode ^ + type.hashCode; } diff --git a/example/lib/task_dao.dart b/example/lib/task_dao.dart index b1ce8e6d..6d4f14c3 100644 --- a/example/lib/task_dao.dart +++ b/example/lib/task_dao.dart @@ -12,6 +12,9 @@ abstract class TaskDao { @Query('SELECT * FROM task') Stream> findAllTasksAsStream(); + @Query('SELECT * FROM task WHERE type = :type') + Stream> findAllTasksByTypeAsStream(TaskType type); + @insert Future insertTask(Task task); diff --git a/example/pubspec.lock b/example/pubspec.lock index 7b9e4cf7..d2f5f209 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "41.0.0" analyzer: dependency: "direct dev" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" async: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" build_daemon: dependency: transitive description: @@ -70,7 +70,7 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.2.0" build_runner_core: dependency: transitive description: @@ -91,7 +91,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.4" + version: "8.4.0" characters: dependency: transitive description: @@ -140,14 +140,14 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" dart_style: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" file: dependency: transitive description: @@ -182,7 +182,7 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" floor: dependency: "direct main" description: @@ -220,14 +220,14 @@ packages: name: frontend_server_client url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" graphs: dependency: transitive description: @@ -241,14 +241,14 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" io: dependency: transitive description: @@ -262,14 +262,14 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.6.0" lists: dependency: transitive description: @@ -311,14 +311,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -332,14 +332,14 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" pubspec_parse: dependency: transitive description: @@ -353,14 +353,14 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -386,28 +386,28 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1+1" sqflite_common_ffi: dependency: transitive description: name: sqflite_common_ffi url: "https://pub.dartlang.org" source: hosted - version: "2.1.0+1" + version: "2.1.1+1" sqlite3: dependency: transitive description: name: sqlite3 url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.7.2" stack_trace: dependency: transitive description: @@ -449,7 +449,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -477,7 +477,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" unicode: dependency: transitive description: @@ -505,14 +505,14 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=1.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e02d8c3a..7951f635 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,7 +7,7 @@ homepage: https://pinchbv.github.io/floor/ publish_to: none environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: floor: diff --git a/floor/pubspec.lock b/floor/pubspec.lock index 2ef19281..feb2d9ae 100644 --- a/floor/pubspec.lock +++ b/floor/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "41.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.3.1" async: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" build_daemon: dependency: transitive description: @@ -70,28 +70,28 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.2" + version: "7.2.3" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.1" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.3.3" + version: "8.4.0" characters: dependency: transitive description: @@ -140,14 +140,14 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" dart_style: dependency: transitive description: @@ -168,21 +168,21 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.1" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.2" fixnum: dependency: transitive description: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" floor_annotation: dependency: "direct main" description: @@ -213,70 +213,70 @@ packages: name: frontend_server_client url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.3" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.6.0" lists: dependency: transitive description: name: lists url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" matcher: dependency: "direct dev" description: @@ -304,7 +304,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.2" mockito: dependency: "direct dev" description: @@ -318,7 +318,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" path: dependency: "direct main" description: @@ -326,48 +326,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.2.0" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.3.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -393,28 +386,28 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+4" + version: "2.0.3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.0.1+1" + version: "2.2.1+1" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+3" + version: "2.1.1+1" sqlite3: dependency: transitive description: name: sqlite3 url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.7.2" stack_trace: dependency: transitive description: @@ -449,14 +442,14 @@ packages: name: strings url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.2" synchronized: dependency: transitive description: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -484,14 +477,14 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" unicode: dependency: transitive description: name: unicode url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" vector_math: dependency: transitive description: @@ -505,21 +498,21 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.2.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=1.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/floor_generator/lib/misc/type_utils.dart b/floor_generator/lib/misc/type_utils.dart index 5c4e3ea2..8c3f9918 100644 --- a/floor_generator/lib/misc/type_utils.dart +++ b/floor_generator/lib/misc/type_utils.dart @@ -5,7 +5,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:source_gen/source_gen.dart'; -extension SupportedTypeChecker on DartType { +extension DartTypeChecker on DartType { /// Whether this [DartType] is either /// - String /// - bool @@ -21,14 +21,11 @@ extension SupportedTypeChecker on DartType { _uint8ListTypeChecker, ]).isExactlyType(this); } -} -extension Uint8ListTypeChecker on DartType { - bool get isUint8List => - getDisplayString(withNullability: false) == 'Uint8List'; -} + bool get isEnumType => _enumTypeChecker.isSuperTypeOf(this); + + bool get isUint8List => _uint8ListTypeChecker.isExactlyType(this); -extension StreamTypeChecker on DartType { bool get isStream => _streamTypeChecker.isExactlyType(this); } @@ -63,3 +60,5 @@ final _doubleTypeChecker = _typeChecker(double); final _uint8ListTypeChecker = _typeChecker(Uint8List); final _streamTypeChecker = _typeChecker(Stream); + +final _enumTypeChecker = _typeChecker(Enum); diff --git a/floor_generator/lib/processor/entity_processor.dart b/floor_generator/lib/processor/entity_processor.dart index a4f59dd8..1e5685e6 100644 --- a/floor_generator/lib/processor/entity_processor.dart +++ b/floor_generator/lib/processor/entity_processor.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:floor_annotation/floor_annotation.dart' as annotations; import 'package:floor_generator/misc/constants.dart'; @@ -276,30 +277,51 @@ class EntityProcessor extends QueryableProcessor { final fieldElement = field.fieldElement; final parameterName = fieldElement.displayName; final fieldType = fieldElement.type; + String attributeValue = 'item.$parameterName'; - String attributeValue; - - if (fieldType.isDefaultSqlType) { - attributeValue = 'item.$parameterName'; - } else { - final typeConverter = [ - ...queryableTypeConverters, - field.typeConverter, - ].whereNotNull().getClosest(fieldType); - attributeValue = - '_${typeConverter.name.decapitalize()}.encode(item.$parameterName)'; + if (fieldType.isDartCoreBool) { + attributeValue = _serializeBoolean(field, attributeValue); + } else if (fieldType.isEnumType) { + attributeValue = _serializeEnum(attributeValue, field); + } else if (!fieldType.isDefaultSqlType) { + attributeValue = _typeConverterSerialization( + field, + fieldType, + attributeValue, + parameterName, + ); } + return attributeValue; + } - if (fieldType.isDartCoreBool) { - if (field.isNullable) { + String _serializeBoolean(Field field, String attributeValue) { + return field.isNullable // force! underlying non-nullable type as null check has been done - return '$attributeValue == null ? null : ($attributeValue! ? 1 : 0)'; - } else { - return '$attributeValue ? 1 : 0'; - } - } else { - return attributeValue; - } + ? '$attributeValue == null ? null : ($attributeValue! ? 1 : 0)' + : '$attributeValue ? 1 : 0'; + } + + String _serializeEnum(String attributeValue, Field field) { + final enumSerializer = '$attributeValue.index'; + return field.isNullable + // force! underlying non-nullable type as null check has been done + ? '$attributeValue == null ? null : $enumSerializer' + : enumSerializer; + } + + String _typeConverterSerialization( + Field field, + DartType fieldType, + String attributeValue, + String parameterName, + ) { + final typeConverter = [ + ...queryableTypeConverters, + field.typeConverter, + ].whereNotNull().getClosest(fieldType); + attributeValue = + '_${typeConverter.name.decapitalize()}.encode(item.$parameterName)'; + return attributeValue; } annotations.ForeignKeyAction _getForeignKeyAction( diff --git a/floor_generator/lib/processor/field_processor.dart b/floor_generator/lib/processor/field_processor.dart index 760b658e..8852401b 100644 --- a/floor_generator/lib/processor/field_processor.dart +++ b/floor_generator/lib/processor/field_processor.dart @@ -54,7 +54,7 @@ class FieldProcessor extends Processor { String _getSqlType(final TypeConverter? typeConverter) { final type = _fieldElement.type; - if (type.isDefaultSqlType) { + if (type.isDefaultSqlType || type.isEnumType) { return type.asSqlType(); } else if (typeConverter != null) { return typeConverter.databaseType.asSqlType(); @@ -80,6 +80,8 @@ extension on DartType { return SqlType.real; } else if (isUint8List) { return SqlType.blob; + } else if (isEnumType) { + return SqlType.integer; } throw StateError('This should really be unreachable'); } diff --git a/floor_generator/lib/processor/queryable_processor.dart b/floor_generator/lib/processor/queryable_processor.dart index 95b0a13a..8716369d 100644 --- a/floor_generator/lib/processor/queryable_processor.dart +++ b/floor_generator/lib/processor/queryable_processor.dart @@ -77,7 +77,8 @@ abstract class QueryableProcessor extends Processor { String parameterValue; - if (parameterElement.type.isDefaultSqlType) { + if (parameterElement.type.isDefaultSqlType || + parameterElement.type.isEnumType) { parameterValue = databaseValue.cast( parameterElement.type, parameterElement, @@ -108,12 +109,21 @@ abstract class QueryableProcessor extends Processor { extension on String { String cast(DartType dartType, ParameterElement parameterElement) { if (dartType.isDartCoreBool) { + final booleanDeserializer = '($this as int) != 0'; if (dartType.isNullable) { // if the value is null, return null // if the value is not null, interpret 1 as true and 0 as false - return '$this == null ? null : ($this as int) != 0'; + return '$this == null ? null : $booleanDeserializer'; } else { - return '($this as int) != 0'; + return booleanDeserializer; + } + } else if (dartType.isEnumType) { + final typeString = dartType.getDisplayString(withNullability: false); + final enumDeserializer = '$typeString.values[$this as int]'; + if (dartType.isNullable) { + return '$this == null ? null : $enumDeserializer'; + } else { + return enumDeserializer; } } else if (dartType.isDartCoreString || dartType.isDartCoreInt || diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index bb42de25..24454b86 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -111,17 +111,19 @@ class QueryMethodWriter implements Writer { ..._queryMethod.parameters .where((parameter) => !parameter.type.isDartCoreList) .map((parameter) { - if (parameter.type.isDefaultSqlType) { - if (parameter.type.isDartCoreBool) { - // query method parameters can't be null - return '${parameter.displayName} ? 1 : 0'; - } else { - return parameter.displayName; - } + final type = parameter.type; + final displayName = parameter.displayName; + + if (type.isDartCoreBool) { + // query method parameters can't be null + return '$displayName ? 1 : 0'; + } else if (type.isEnumType) { + return '$displayName.index'; + } else if (type.isDefaultSqlType) { + return displayName; } else { - final typeConverter = - _queryMethod.typeConverters.getClosest(parameter.type); - return '_${typeConverter.name.decapitalize()}.encode(${parameter.displayName})'; + final typeConverter = _queryMethod.typeConverters.getClosest(type); + return '_${typeConverter.name.decapitalize()}.encode($displayName)'; } }), ..._queryMethod.parameters @@ -129,12 +131,14 @@ class QueryMethodWriter implements Writer { .map((parameter) { // TODO #403 what about type converters that map between e.g. string and list? final DartType flatType = parameter.type.flatten(); - if (flatType.isDefaultSqlType) { - return '...${parameter.displayName}'; + final displayName = parameter.displayName; + + if (flatType.isDefaultSqlType || flatType.isEnumType) { + return '...$displayName'; } else { final typeConverter = _queryMethod.typeConverters.getClosest(flatType); - return '...${parameter.displayName}.map((element) => _${typeConverter.name.decapitalize()}.encode(element))'; + return '...$displayName.map((element) => _${typeConverter.name.decapitalize()}.encode(element))'; } }) ]; diff --git a/floor_generator/test/processor/entity_processor_test.dart b/floor_generator/test/processor/entity_processor_test.dart index 0b34f9a6..d6a5c880 100644 --- a/floor_generator/test/processor/entity_processor_test.dart +++ b/floor_generator/test/processor/entity_processor_test.dart @@ -411,6 +411,56 @@ void main() { '}'; expect(actual, equals(expected)); }); + + test('Non-nullable enum value mapping', () async { + final classElement = await createClassElement(''' + + $characterType + + @entity + class Person { + @primaryKey + final int id; + + final CharacterType someType; + + Person(this.id, this.someType); + } + '''); + + final actual = EntityProcessor(classElement, {}).process().valueMapping; + + const expected = '{' + "'id': item.id, " + "'someType': item.someType.index" + '}'; + expect(actual, equals(expected)); + }); + + test('Nullable enum value mapping', () async { + final classElement = await createClassElement(''' + + $characterType + + @entity + class Person { + @primaryKey + final int id; + + final CharacterType? someType; + + Person(this.id, this.someType); + } + '''); + + final actual = EntityProcessor(classElement, {}).process().valueMapping; + + const expected = '{' + "'id': item.id, " + "'someType': item.someType == null ? null : item.someType.index" + '}'; + expect(actual, equals(expected)); + }); }); group('expected errors', () { diff --git a/floor_generator/test/processor/queryable_processor_test.dart b/floor_generator/test/processor/queryable_processor_test.dart index 1cc6ebca..3c5176ed 100644 --- a/floor_generator/test/processor/queryable_processor_test.dart +++ b/floor_generator/test/processor/queryable_processor_test.dart @@ -451,6 +451,35 @@ void main() { expect(actual, equals(expected)); }); + test('generate constructor with enum arguments', () async { + final classElement = await createClassElement(''' + + $characterType + + class Person { + final int id; + + final String name; + + final CharacterType bar; + + final CharacterType? foo; + + Person(this.id, this.name, {required this.bar, this.foo}); + } + '''); + + final actual = TestProcessor(classElement).process().constructor; + + const expected = 'Person(' + "row['id'] as int, " + "row['name'] as String, " + "bar: CharacterType.values[row['bar'] as int], " + "foo: row['foo'] == null ? null : CharacterType.values[row['foo'] as int]" + ')'; + expect(actual, equals(expected)); + }); + test('generate constructor with named arguments', () async { final classElement = await createClassElement(''' class Person { @@ -514,6 +543,9 @@ void main() { group('nullability', () { test('generates constructor with only nullable types', () async { final classElement = await createClassElement(''' + + $characterType + class Person { final int? id; @@ -524,20 +556,31 @@ void main() { final bool? bar; final Uint8List? blob; + + final CharacterType? character; - Person(this.id, this.doubleId, this.name, this.bar, this.blob); + Person(this.id, this.doubleId, this.name, this.bar, this.blob, this.character); } '''); final actual = TestProcessor(classElement).process().constructor; - const expected = - "Person(row['id'] as int?, row['doubleId'] as double?, row['name'] as String?, row['bar'] == null ? null : (row['bar'] as int) != 0, row['blob'] as Uint8List?)"; + const expected = 'Person(' + "row['id'] as int?, " + "row['doubleId'] as double?, " + "row['name'] as String?, " + "row['bar'] == null ? null : (row['bar'] as int) != 0, " + "row['blob'] as Uint8List?, " + "row['character'] == null ? null : CharacterType.values[row['character'] as int]" + ')'; expect(actual, equals(expected)); }); test('generates constructor with only non-nullable types', () async { final classElement = await createClassElement(''' + + $characterType + class Person { final int id; @@ -548,15 +591,23 @@ void main() { final bool bar; final Uint8List blob; + + final CharacterType character; - Person(this.id, this.doubleId, this.name, this.bar, this.blob); + Person(this.id, this.doubleId, this.name, this.bar, this.blob, this.character); } '''); final actual = TestProcessor(classElement).process().constructor; - const expected = - "Person(row['id'] as int, row['doubleId'] as double, row['name'] as String, (row['bar'] as int) != 0, row['blob'] as Uint8List)"; + const expected = 'Person(' + "row['id'] as int, " + "row['doubleId'] as double, " + "row['name'] as String, " + "(row['bar'] as int) != 0, " + "row['blob'] as Uint8List, " + "CharacterType.values[row['character'] as int]" + ')'; expect(actual, equals(expected)); }); }); diff --git a/floor_generator/test/test_utils.dart b/floor_generator/test/test_utils.dart index 38b72e5c..fc19215f 100644 --- a/floor_generator/test/test_utils.dart +++ b/floor_generator/test/test_utils.dart @@ -168,6 +168,8 @@ Future createDao(final String methodSignature) async { $methodSignature } + $characterType + $_personEntity $_nameView @@ -301,3 +303,7 @@ const _nameView = ''' } '''; + +const characterType = ''' + enum CharacterType { generous, honest, faithful, faithful, loving, kind, sincere } +'''; diff --git a/floor_generator/test/writer/query_method_writer_test.dart b/floor_generator/test/writer/query_method_writer_test.dart index 388ad3aa..c307640c 100644 --- a/floor_generator/test/writer/query_method_writer_test.dart +++ b/floor_generator/test/writer/query_method_writer_test.dart @@ -213,6 +213,25 @@ void main() { ''')); }); + test('query enum parameter', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT * FROM Person WHERE characterType = :type') + Future> findByType(CharacterType type); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> findByType(CharacterType type) async { + return _queryAdapter.queryList( + 'SELECT * FROM Person WHERE characterType = ?1', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + arguments: [type.index]); + } + ''')); + }); + test('query item multiple parameters', () async { final queryMethod = await _createQueryMethod(''' @Query('SELECT * FROM Person WHERE id = :id AND name = :name') @@ -224,7 +243,10 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id, String name) async { - return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1 AND name = ?2', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), arguments: [id, name]); + return _queryAdapter.query( + 'SELECT * FROM Person WHERE id = ?1 AND name = ?2', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + arguments: [id, name]); } ''')); }); @@ -240,7 +262,10 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id, String name, String bar) async { - return _queryAdapter.query('SELECT * FROM Person WHERE foo = ?3 AND id = ?1 AND name = ?2 AND name = ?3', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), arguments: [id, name, bar]); + return _queryAdapter.query( + 'SELECT * FROM Person WHERE foo = ?3 AND id = ?1 AND name = ?2 AND name = ?3', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + arguments: [id, name, bar]); } ''')); }); @@ -256,7 +281,9 @@ void main() { expect(actual, equalsDart(''' @override Future> findAll() async { - return _queryAdapter.queryList('SELECT * FROM Person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + return _queryAdapter.queryList( + 'SELECT * FROM Person', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -272,7 +299,10 @@ void main() { expect(actual, equalsDart(r''' @override Stream findByIdAsStream(int id) { - return _queryAdapter.queryStream('SELECT * FROM Person WHERE id = ?1', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), arguments: [id], queryableName: 'Person', isView: false); + return _queryAdapter.queryStream( + 'SELECT * FROM Person WHERE id = ?1', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + arguments: [id], queryableName: 'Person', isView: false); } ''')); }); @@ -288,7 +318,10 @@ void main() { expect(actual, equalsDart(r''' @override Stream> findAllAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM Person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'Person', isView: false); + return _queryAdapter.queryListStream( + 'SELECT * FROM Person', + mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + queryableName: 'Person', isView: false); } ''')); }); @@ -304,7 +337,10 @@ void main() { expect(actual, equalsDart(r''' @override Stream> findAllAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM Name', mapper: (Map row) => Name(row['name'] as String), queryableName: 'Name', isView: true); + return _queryAdapter.queryListStream( + 'SELECT * FROM Name', + mapper: (Map row) => Name(row['name'] as String), + queryableName: 'Name', isView: true); } ''')); });