Drift/Moor and Flutter app - database streams with arguments - flutter

I have a quiz app with sqlite base. I'm using Drift (former Moor).
drift_db.dart (in a nutshell):
class QuestionsBase extends Table {
IntColumn get questionnumber => integer()();
TextColumn get question => text()();
TextColumn get answera => text().nullable()();
TextColumn get answerb => text().nullable()();
TextColumn get answerc => text().nullable()();
TextColumn get correctanswer => text()();
TextColumn get categories => text()();
}
#DataClassName("QuestionRatingRow")
class QuestionsRating extends Table {
IntColumn get questionnumber => integer()();
IntColumn get questionrating => integer()();
DateTimeColumn get repetitiondate => dateTime().nullable()();
}
...
//watching number of new questions
Stream<QueryRow> watchNumberOfNewQuestions(List<String> categories) {
String queryString = 'SELECT COUNT(*) AS count FROM questions_rating '
'LEFT JOIN questions_base ON questions_rating.questionnumber = questions_base.questionnumber '
'WHERE (questions_rating.repetitiondate IS NULL) AND (questions_base.kategorie LIKE ';
queryString += '\'%${categories[0]},%\' ';
if (categories.length > 1) {
for (var i = 1; i < categories.length; i++) {
queryString +=
'OR questions_base.kategorie LIKE \'%${categories[i]},%\' ';
}
}
queryString += ');';
return customSelect(queryString, readsFrom: {questionsRating}).watchSingle();
}
drift_repository.dart (in a nutshell):
import 'drift_db.dart';
class DriftRepository {
late DLTDatabase dltDatabase;
Stream<Map<String, dynamic>>? statisticsStream;
Stream<Map<String, int>>? datesStream;
Stream<int>? newQuestionsStream;
...
//unknown questions number stream
Stream<int> watchQuestionsNumber(List<String> categories) {
if (newQuestionsStream == null) {
final stream = dltDatabase.watchNumberOfNewQuestion(categories);
newQuestionsStream = stream.map((row) {
return row.data['count'];
});
}
return newQuestionsStream!;
}
}
Code works fine, but when user changes question category (categories), nothing happens (only after restarting the application).
Of course, I know the problem is in this part:
if (newQuestionsStream == null) {
I can remove it and code works just fine. But it calls the method watchNumberOfNewQuestions(List categories), then saves an instance so I don’t create multiple streams.
So far I've used streams in methods without arguments, so it was a good solution (I think). Now I have no idea what to do with this case.
Bonus question:-)
I have another stream Stream<Map<String, int>> watchDates(List categories). It returns quiz repetition dates (number of repetitions of questions for today, tomorrow, etc.). In addition to the problem described above, I am facing the fact that information about dates does not refresh after midnight.
I have an idea to add another argument to watchDates: Stream<Map<String, int>> watchDates(List categories, DateTime dateTime) and then in the widget use Timer:
Timer.periodic(const Duration(hours: 1), (timer) {
var dateTime = DateTime.now();
});
Is it a good idea?
EDIT: My solution
Since my app users very rarely change the category of questions (this assumes a usage scenario), I added a variable that stores a list of previous categories. When watchQuestionsNumber(List categories) is called, I check if the categories passed as an argument are different from the previous ones, and only then I return a new stream.
class DriftRepository {
late DLTDatabase dltDatabase;
Stream<Map<String, dynamic>>? statisticsStream;
Stream<Map<String, int>>? datesStream;
Stream<int>? newQuestionsStream;
List<String> _newQuestionsStreamLastCategories = [];
...
//unknown questions number stream
Stream<int> watchQuestionsNumber(List<String> categories) {
var _areCategoriesChanged =
!listEquals(categories, _newQuestionsStreamLastCategories);
if (newQuestionsStream == null || _areCategoriesChanged) {
final stream = dltDatabase.watchNumberOfNewQuestion(categories);
newQuestionsStream = stream.map((row) {
return row.data['count'];
});
_newQuestionsStreamLastCategories = categories;
}
return newQuestionsStream!;
}

Related

Dart / Flutter Sort List by Multiple Fields

I was writing my question but had found the solution before posting it. There are many examples about how to sort a list in Dart by comparing two fields. However, I still found it wasn't straight forward, at least for me, to figure out the sorting by more than two fields. I thought it would be worth sharing it under a separate topic.
Here's how I am sorting lists in Dart by three or more fields:
class Student {
String name;
String course;
int age;
Student(this.name, this.course, this.age);
#override
String toString() {
return '{$name, $course, $age}';
}
}
main() {
List<Student> students = [];
students.add(Student('Katherin', 'Dart Potions', 21));
students.add(Student('Adam Sr', 'Dart Magic', 40));
students.add(Student('Adam Jr', 'Dart Magic', 15));
students.sort(
(a, b) {
final int sortByCourse = -a.course.compareTo(b.course); // the minus '-' for descending
if (sortByCourse == 0) {
final int sortByName = a.name.compareTo(b.name);
if (sortByName == 0) {
return a.age.compareTo(b.age);
}
return sortByName;
}
return sortByCourse;
},
);
print('Sort DESC by Course, then ASC by Name and then ASC by Age:\n ${students.toString()}');
}

Existing state editing in Riverpod / Flutter

How I can manipulate the existing state in Riverpod. 'm a beginner about Flutter and Riverpod. When I try add one to Order error pops up and says:
Error: A value of type 'int' can't be assigned to a variable of type
'List'.
final OrderPaperProvider = StateNotifierProvider<OrderPaper, List<Order>>((ref) {
return OrderPaper();
});
#immutable
class Order {
final String product_id;
final String product_name;
final int product_price;
final int product_count;
Order({required this.product_id, required this.product_name, required this.product_price, required this.product_count});
Order copyWith({product_id, product_name, product_price, product_count}) {
return Order(
product_id: product_id,
product_name: product_name,
product_price: product_price,
product_count: product_count,
);
}
}
class OrderPaper extends StateNotifier<List<Order>> {
OrderPaper() : super([]);
void addOrder(Order order) {
for (var x = 0; x < state.length; x++) {
if (state[x].product_id == order.product_id) {
addOneToExistingOrder(x, order);
return;
}
}
state = [...state, order];
}
void removeOrder(String product_id) {
state = [
for (final order in state)
if (order.product_id != order) order,
];
}
void addOneToExistingOrder(index, Order order) {
state = state[index].product_count + 1; // <--------- Error in this line
}
void clearOrderPaper() {
state = [];
}
}
What is happening in the code you posted is basically this:
You are telling it to update the state which is a List<Order> with an integer.
This is because state[index].product_count + 1 actually equals a number, for example 1+2 = 3.
So you are telling it, state = 3, and this causes your error.
What you need to do, is create a new state with the list of items and the edited item, like this (you don't need to pass the index, you can get it in the function):
void addOrder(Order order) {
//find if the product is in the list. The index will not be -1.
final index = state.indexWhere((entry) => entry.product_count == order. product_count);
if (index != -1) {
//if the item is in the list, we just edit the count
state = [
...state.sublist(0, index),
order.copyWith(product_count: state[index].product_count + 1),
...state.sublist(index + 1),
];
}else {
//otherwise, just add it as a new entry.
state = [...state, order],
}
}

How to get aggregated data in moor_flutter?

Let's say I have 2 simple tables Users and Orders:
Users has columns Id
Orders has columns Id and UserId
How do I get all orders of a user easily using moor_flutter and return it as a stream of the following model?
class UserModel {
final String id;
final List<OrderModel> orderModels;
UserModel(this.id, this.orders);
}
class OrderModel {
final String id;
OrderModel(this.id);
}
This is the official documentation but it is not covering this use case.
Equivalent call with EFCore and C# would be:
public async Task<IEnumerable<UserModel>> GetUserOrders(String userId)
{
return await _db.Users
.Include(user => user.Orders)
.Where(user => user.Id == userId)
.Select(user => new UserModel
{
Id = user.Id,
OrderModels = user.Orders.Select(order => new OrderModel
{
Id = null,
}).ToList()
})
.ToListAsync();
}
I faced the same problem recently. I was confused to use Joins as per the given example in documentation.
What I have done is:
The created class (in database file only. see below example) has two objects which you want to join(combine). I wrote the query in a class of database.
You will get better understanding with example:
#UseMoor(
tables: [OfflineProductMasters, OfflineProductWiseStocks],
)
class MyDatabase extends _$MyDatabase {
// we tell the database where to store the data with this constructor
MyDatabase() : super(_openConnection());
// you should bump this number whenever you change or add a table definition. Migrations
// are covered later in this readme.
#override
int get schemaVersion => 1;
Future<List<ProductWithStock>> getProductWiseStock(String searchString, int mappingOfflineSalesTypeId) async {
try {
final rows = await (select(offlineProductMasters).join([innerJoin(offlineProductWiseStocks, offlineProductMasters.itemId.equalsExp(offlineProductWiseStocks.itemId))])
..where(offlineProductWiseStocks.salesTypeId.equals(mappingOfflineSalesTypeId) & offlineProductMasters.productName.like('$searchString%'))
..limit(50)
).get();
return rows.map((row) {
return ProductWithStock(
row.readTable(offlineProductMasters),
row.readTableOrNull(offlineProductWiseStocks),
);
}).toList();
}catch(exception){
print(exception);
return Future.value([]);
}
}
}
class ProductWithStock {
final OfflineProductMaster offlineProductMaster;
final OfflineProductWiseStock? productWithStock;
ProductWithStock(this.offlineProductMaster, this.productWithStock);
}
Now you got the structure that how you can use this type of query. Hope you will write your query in this way.
I don't know whether you have solved it or not. If solved then please post the answer so others can get help.
Thank you.
This feels like a hacky workaround but what I ended up doing is that I created 2 classes called HabitWithLogs and HabitModel. I put my query result into HabitWithLogs instances and then group them into HabitModel instances.
Data classes:
class HabitWithLog {
final Habit habit;
final HabitLog? habitLog;
HabitWithLog({required this.habit, required this.habitLog}) : assert(habitLog == null || habitLog.habitId == habit.id);
}
class HabitModel {
final Habit habit;
final List<HabitLog> habitLogs;
HabitModel({required this.habit, required this.habitLogs});
}
Dao method:
Future<List<HabitModel>> getAllHabits() async {
// Get habits and order
final query0 = (_db.select(_db.habits)..orderBy([(t) => OrderingTerm(expression: t.order, mode: OrderingMode.asc)]));
// Join with habit logs
final query1 = query0.join([
leftOuterJoin(_db.habitLogs, _db.habitLogs.habitId.equalsExp(_db.habits.id)),
]);
// Naive way that return the same habit multiple times
final hwlList = query1.map((rows) => HabitWithLog(
habit: rows.readTable(_db.habits),
habitLog: rows.readTableOrNull(_db.habitLogs),
));
// Group hwlList by habits
final groups = (await hwlList.get()).groupListsBy((hwl) => hwl.habit);
// Map grouping
return groups.entries
.map((group) => HabitModel(
habit: group.key,
habitLogs: (group.value[0].habitLog == null) ? List<HabitLog>.empty() : group.value.map((hwl) => hwl.habitLog!).toList(),
))
.toList();
}
Mapping the stream feels terrible and should not be the only way to achieve this.

How to convert List<Object> flutter

i'm new in flutter and need to help:
I have already got
final List<Genres> genres = [{1,"comedy"}, {2,"drama"},{3,"horror"}]
from api.
class Genres {
final int id;
final String value;
Genres({this.id,this.value});
}
In another method I get genres.id.(2) How can I convert it to genres.value ("drama")?
Getting a Genre from an id is inconvenient when your data structure is a List. You have no choice but to iterate over the list and compare the id value to the id of each element in the list:
final id = 2;
final genre = genres.firstWhere((g) => g.id == id, orElse: () => null);
The problem with this code is that it's slow and there could be multiple matches (where the duplicates after the first found would be ignored).
A better approach would be to convert your list to a Map when you first create it. Afterwards, you can simply use an indexer to get a Genre for an ID quickly and safely.
final genresMap = Map.fromIterable(genres, (item) => item.id, (item) => item);
// later...
final id = 2;
final genre = genresMap[id];
This way, there is guaranteed to not be any duplicates, and if an ID doesn't exist then the indexer will simply return null.
you could iterate over the json result of the api and map them to the Gener class like so,
void fn(id) {
final gener = geners.firstWhere((gener) => gener['id'] == id);
// now you have access to your gener
}
You can find the item inside the List<Genres> like this
Genres element = list.firstWhere((element) => element.id == 2); // 2 being the id you give in the question as an exaple. You should make it dynamic
print(element.value);

Check whether a list contain an attribute of an object in dart

I need to check whether myItemsList contains myitem.itemId or not, If it exists need to add itemQuantity, if it not exists need to add myitem object to myItemsList.
List<MyItem> myItemsList = new List();
MyItem myitem = new MyItem (
itemId: id,
itemName: name,
itemQuantity: qty,
);
if (myItemsList.contains(myitem.itemId)) {
print('Already exists!');
} else {
print('Added!');
setState(() {
myItemsList.add(myitem);
});
}
MyItem class
class MyItem {
final String itemId;
final String itemName;
int itemQuantity;
MyItem ({
this.itemId,
this.itemName,
this.itemQuantity,
});
}
above code is not working as expected, please help me to figure out the issue.
Contains() compares the whole objects.
Besides overriding == operator or looping over, you can use list's singleWhere method:
if ((myItemsList.singleWhere((it) => it.itemId == myitem.itemId,
orElse: () => null)) != null) {
Edit:
As Dharaneshvar experienced and YoApps mentioned in the comments .singleWhere raises StateError when more elements are found.
This is desired when you expect unique elements such as in the case of comparing IDs.
Raised error is the friend here as it shows that there is something wrong with the data.
For other cases .firstWhere() is the right tool:
if ((myItemsList.firstWhere((it) => it.itemName == myitem.itemName,
orElse: () => null)) != null) {
// EO Edit
Whole example:
List<MyItem> myItemsList = new List();
​
class MyItem {
final String itemId;
final String itemName;
int itemQuantity;
​
MyItem({
this.itemId,
this.itemName,
this.itemQuantity,
});
}
​
void main() {
MyItem myitem = new MyItem(
itemId: "id00",
itemName: "name",
itemQuantity: 50,
);
​
myItemsList.add(myitem);
​
String idToCheck = "id00";
​
if ((myItemsList.singleWhere((it) => it.itemId == idToCheck,
orElse: () => null)) != null) {
print('Already exists!');
} else {
print('Added!');
}
}
As already said before, contains compares two Objects with the == operator. So you currently compare MyItem with String itemId, which will never be the same.
To check whether myItemsList contains myitem.itemId you can use one of the following:
myItemsList.map((item) => item.itemId).contains(myitem.itemId);
or
myItemsList.any((item) => item.itemId == myitem.itemId);
You're using contains slightly wrong.
From: https://api.dartlang.org/stable/2.2.0/dart-core/Iterable/contains.html
bool contains(Object element) {
for (E e in this) {
if (e == element) return true;
}
return false;
}
You can either override the == operator, see: https://dart-lang.github.io/linter/lints/hash_and_equals.html
#override
bool operator ==(Object other) => other is Better && other.value == value;
Or you can loop over your list and search the normal way one by one, which seems slightly easier.
One more way to check does list contain object with property or not
if (myList.firstWhereOrNull((val) => val.id == someItem.id) != null) {}