I'm having problem when trying to display data on the UI where it should rebuild the widget prior to changes on menus table. I'm using GetX for the state management and Drift, a.k.a Moor as the database.
My presentation logic looks like this
categories_panel.dart
class CategoriesPanel extends StatelessWidget {
const CategoriesPanel({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
// unrelated codes ommitted...
_GridView(),
],
),
);
}
}
class _GridView extends GetView<HomeController> {
const _GridView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Obx(
() => GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
physics: const BouncingScrollPhysics(),
childAspectRatio: 0.8,
padding: const EdgeInsets.all(30),
children: controller.categories.map((e) {
return CategoryCard(
color: e.category.labelColor,
name: e.category.name,
itemCount: e.menus.length, // This should be updated prior to changes on menus table
onTap: () => controller.selectedCategory(e),
onLongPress: () => controller.showCategoryActionDialog(e),
);
}).toList(),
),
),
);
}
}
In HomeController, I declared a variable named categories and binded the stream in onInit lifecycle.
home_controller.dart
class HomeController extends GetxController {
HomeController({
required CategoryRepository categoryRepository,
required MenuRepository menuRepository,
}) : _categoryRepository = categoryRepository,
_menuRepository = menuRepository;
final CategoryRepository _categoryRepository;
final MenuRepository _menuRepository;
final categories = <CategoryWithMenus>[].obs; // HERE
// unrelated codes ommitted...
@override
void onInit() {
categories.bindStream(_categoryRepository.stream()); // BINDED HERE
super.onInit();
}
// another unrelated codes ommitted...
}
The stream itself, looks like this... I've tried to listen to the menusStream to print each changes to make sure it was triggerred, but it doesn't.
category_repository.dart
class CategoryRepository extends Database {
Stream<List<CategoryWithMenus>> stream() {
final categoriesQuery = select(categories);
return categoriesQuery.watch().switchMap((categories) {
final idToCategory = {for (var c in categories) c.id: c};
final ids = idToCategory.keys;
final menusStream =
(select(menus)..where((tbl) => tbl.categoryId.isIn(ids))).watch();
menusStream.listen(print); // This does not print anything on create, update or delete
return menusStream.map((menus) {
final idToMenus = <int, List<Menu>>{};
for (final menu in menus) {
idToMenus.putIfAbsent(menu.categoryId, () => []).add(menu);
}
return [
for (var id in ids)
CategoryWithMenus(
category: idToCategory[id]!,
menus: idToMenus[id] ?? [],
),
];
});
});
}
// unrelated codes ommitted...
}
Finally, my model, migrations and database config looks like this
models/category_with_menus.dart
class CategoryWithMenus {
const CategoryWithMenus({
required this.category,
this.menus = const <Menu>[],
});
final Category category;
final List<Menu> menus;
}
database/migrations/categories.dart
@DataClassName('Category')
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(max: 100)();
IntColumn get labelColor => integer().nullable()();
}
database/migrations/menus.dart
class Menus extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(max: 255)();
RealColumn get price => real().withDefault(const Constant(0.0))();
IntColumn get categoryId =>
integer().references(Categories, #id, onDelete: KeyAction.cascade)();
}
database/database.dart
@DriftDatabase(tables: [
Categories,
Menus,
// unrelated codes ommitted...
])
class Database extends _$Database {
Database() : super(_openConnection());
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (OpeningDetails details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final databaseFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(databaseFolder.path, 'myapp.sqlite'));
return NativeDatabase(file);
});
}
I've tried to change my stream logic too but it still not working.
Stream<List<CategoryWithMenus>> stream() {
final categoriesStream = select(categories).watch();
final menusStream = select(menus).watch();
return Rx.combineLatest2(
categoriesStream,
menusStream,
(List<Category> a, List<Menu> b) {
return a.map((category) {
return CategoryWithMenus(
category: category,
menus: b.where((m) => m.categoryId == category.id).toList(),
);
}).toList();
},
);
}
Please help!
It turns out that this behavior occurs after I do an extraction of the database logic into the repositories, the problem is not on the stream logic.
When I did that, I put each repositories into the bindings of
HomeBindinghome_binding.dartThose
putstatements I think, creates new database instances for each repository so the watchers are unable to figure out that some data has been changed.I fixed this by reverting my refactor commit and re-implementing my stream logic the exact same way as I did on my question. But now, I put all my database logic inside of
database/database.dartinstead of making a repository.It works perfectly.