I am building note application which stores notes in database (I am using floor).
My stream is listening data in database and every change that is occurred it will automatically rebuild the screen.
My problem is that when I implement the logic for toggleSelectAll (when I select notes they will change color) in usecase with for loop and update every note individually in database it is too slow. You can see how every note is selected when waiting for process to finish.
And now I have tried to write query for selecting all notes directly in database but my stream doesn't recognize it. When I call the usecase it will change the selection but rebuilld of the screen is not triggered. I can see the change when I restart the app.
Is there any solution for this or is there any other way to do selection which I didn't consider?
My code:
Note entity
class Note extends Equatable {
@PrimaryKey(autoGenerate: true)
final int? id;
final String title;
final String description;
final String date;
final bool isSelected;
final bool isDeleted;
final bool isFavorite;
const Note({
this.id,
required this.title,
required this.description,
required this.date,
this.isSelected = false,
this.isDeleted = false,
this.isFavorite = false,
});
Note copyWith(
{ValueGetter<int?>? id,
String? title,
String? description,
String? date,
bool? isSelected,
bool? isDeleted,
bool? isFavorite}) {
return Note(
id: id != null ? id() : this.id,
title: title ?? this.title,
description: description ?? this.description,
date: date ?? this.date,
isSelected: isSelected ?? this.isSelected,
isDeleted: isDeleted ?? this.isDeleted,
isFavorite: isFavorite ?? this.isFavorite);
}
@override
List<Object> get props =>
[id!, title, description, date, isSelected, isDeleted, isFavorite];
}
NoteDao
@dao
abstract class NoteDao {
@Query('SELECT * FROM NoteModel')
Stream<List<NoteModel>> getNotes();
@delete
Future<void> removeNote(NoteModel noteModel);
@insert
Future<void> insertNote(NoteModel noteModel);
@update
Future<void> updateNote(NoteModel noteModel);
@Query(
'UPDATE NoteModel SET isSelected=CASE WHEN isSelected=1 THEN 0 ELSE 1 END')
Future<void> toggleAllNotesSelect();
}
NoteLocalDataSourceImpl
class NoteLocalDataSourceImpl implements NoteLocalDataSource {
final AppDatabase _appDatabase;
const NoteLocalDataSourceImpl(this._appDatabase);
@override
Stream<List<NoteModel>> getNotes() {
try {
final notes = _appDatabase.noteDao.getNotes();
return notes;
} catch (error) {
log(error.toString());
throw const DatabaseException();
}
}
@override
Future<void> insertNote(NoteModel noteModel) async {
try {
return await _appDatabase.noteDao.insertNote(noteModel);
} catch (error) {
log(error.toString());
throw const DatabaseException();
}
}
@override
Future<void> updateNote(NoteModel noteModel) async {
try {
return await _appDatabase.noteDao.updateNote(noteModel);
} catch (error) {
log(error.toString());
throw const DatabaseException();
}
}
@override
Future<void> removeNote(NoteModel noteModel) async {
try {
return await _appDatabase.noteDao.removeNote(noteModel);
} catch (error) {
log(error.toString());
throw const DatabaseException();
}
}
@override
Future<void> toggleAllSelectNotes() async {
try {
return await _appDatabase.noteDao.toggleAllNotesSelect();
} catch (error) {
log(error.toString());
throw const DatabaseException();
}
}
}
NoteRepositoryImpl
class NoteRepositoryImpl implements NoteRepository {
final NoteLocalDataSource _noteLocalDataSource;
NoteRepositoryImpl(this._noteLocalDataSource);
@override
Future<Either<Failure, Stream<List<Note>>>> getNotes() async {
try {
final noteModelStream = _noteLocalDataSource.getNotes();
final noteStream = noteModelStream.map((List<NoteModel> noteModels) =>
noteModels.map((noteModel) => noteModel.toNote()).toList());
return Right(noteStream);
} on DatabaseException catch (error) {
return Left(DatabaseFailure(error.message));
}
}
@override
Future<Either<Failure, Success>> insertNote(Note note) async {
try {
final noteModel = NoteModel.fromNote(note);
await _noteLocalDataSource.insertNote(noteModel);
return const Right(Success());
} on DatabaseException catch (error) {
return Left(DatabaseFailure(error.message));
}
}
@override
Future<Either<Failure, Success>> removeNote(Note note) async {
try {
final noteModel = NoteModel.fromNote(note);
await _noteLocalDataSource.removeNote(noteModel);
return const Right(Success());
} on DatabaseException catch (error) {
return Left(DatabaseFailure(error.message));
}
}
@override
Future<Either<Failure, Success>> updateNote(Note note) async {
try {
final noteModel = NoteModel.fromNote(note);
await _noteLocalDataSource.updateNote(noteModel);
return const Right(Success());
} on DatabaseException catch (error) {
return Left(DatabaseFailure(error.message));
}
}
@override
Future<Either<Failure, Success>> toggleAllSelectNotes() async {
try {
await _noteLocalDataSource.toggleAllSelectNotes();
return const Right(Success());
} on DatabaseException catch (error) {
return Left(DatabaseFailure(error.message));
}
}
}
UseCase (commented part is where things get very slow so I tried this way)
class ToggleAllNotesSelectUseCase
implements UseCase<Success, ToggleAllNotesSelectParams> {
final NoteRepository _noteRepository;
const ToggleAllNotesSelectUseCase(this._noteRepository);
@override
Future<Either<Failure, Success>> call(
ToggleAllNotesSelectParams params) async {
// final areAllCompleted = params.notes.every((note) => note.isSelected);
// for (final note in params.notes) {
// final newNote = note.copyWith(isSelected: !areAllCompleted);
// await _noteRepository.updateNote(newNote);
// }
await _noteRepository.toggleAllSelectNotes();
return const Right(Success());
}
}
Part of the Bloc where I call UseCase
void _onToggleAllNotesSelectEvent(
ToggleAllNotesSelectEvent event, Emitter<NoteState> emit) async {
await _noteUseCases
.setAllNotesUnselectedUseCase(const ToggleAllNotesSelectParams());
}
BlocBuilder where I return ListItemWidget which is ListView.builder for building notes
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: const MenuWidget(),
floatingActionButton: const AddNoteButtonWidget(),
appBar: AppBar(
title: const Text(StringConstants.appbarHomeTitle),
actions: [
IconButton(
onPressed: () => context.go(StringConstants.searchNotePageRoute),
icon: const Icon(Icons.search)),
const SortPopUpWidget(),
const ToggleSelectPopUpWidget(),
],
),
body: BlocBuilder<NoteBloc, NoteState>(
builder: (context, state) {
if (state.status == NoteStatus.success) {
if (state.notes.isEmpty) {
return const EmptyListWidget(
message: 'Empty', iconPath: StringConstants.imageNotesEmpty);
} else {
return ListItemWidget(state: state);
}
}
if (state.status == NoteStatus.loading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state.status == NoteStatus.failure) {
return const Center(
child: Text("Fail"),
);
} else {
return const SizedBox();
}
},
),
);
}
}
ListItemWidget
class ListItemWidget extends StatelessWidget {
final NoteState state;
const ListItemWidget({
super.key,
required this.state,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: state.notes.length,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.all(8),
elevation: 5,
color: state.notes[index].isSelected
? Colors.blueGrey[200]
: Theme.of(context).cardColor,
child: ListTile(
onLongPress: () {
context
.read<NoteBloc>()
.add(ToggleNoteSelectEvent(note: state.notes[index]));
},
onTap: () {
context.go(StringConstants.updateNotePageRoute,
extra: state.notes[index]);
},
title: Text(
state.notes[index].title,
maxLines: 1,
style: Theme.of(context).textTheme.labelLarge,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.notes[index].description,
maxLines: 1,
style: Theme.of(context).textTheme.labelMedium,
),
Text(
state.notes[index].date,
maxLines: 1,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
trailing: state.notes[index].isSelected
? IconButton(
onPressed: () {
context.read<NoteBloc>().add(const SetNoteDeletedEvent());
},
icon: Icon(
Icons.delete,
color: Theme.of(context).iconTheme.color,
),
)
: IconButton(
onPressed: () {},
icon: state.notes[index].isFavorite
? Icon(
Icons.star,
color: Theme.of(context).colorScheme.primary,
)
: Icon(
Icons.star_border,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
},
);
}
}