How to get aggregated data in moor_flutter? - 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.

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()}');
}

Drift/Moor and Flutter app - database streams with arguments

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!;
}

iterate trough map<List<dataModel>> in Dart Flutter

I have 2 datamodels. the userDataModel and the PostDataModel.
Users may have multiple posts
so I would like to have the following fetching structure:
Map<userDataModel,List<PostDataModel>> = data
Can I iterate through data using indexes? (user index and then posts indexes ?)
something like for(user, data in data){}
or something like data[0][0]
In the end, I would like to have a listView of all the posts, one user at the time
I also would like to know how to print the data since printing data only returns instances.
First, using the structure you provided, Map<userDataModel,List<PostDataModel>>, you have to have two nested loops. To be able to iterate through all of them you will need to have some code like the following:
void main() {
var data = Map<UserDataModel, List<PostDataModel>>();
data.forEach((user, posts) {
for (var post in posts) {
print(post.name);
print(user.id);
}
});
}
class UserDataModel {
int id;
}
class PostDataModel {
String name;
}
However, since, as you mentioned, your posts belong to users, I would put the PostDataModel inside the UserDataModel. Then, your data will eventually look a little different:
void main() {
var data = List<UserDataModel>();
data.add(UserDataModel(1, [PostDataModel("one"),PostDataModel("two")]));
data.add(UserDataModel(2, [PostDataModel("three"),PostDataModel("four")]));
for (var user in data) {
for (var post in user.posts) {
print(user);
print(post);
}
}
}
class UserDataModel {
int id;
UserDataModel(this.id, this.posts);
List<PostDataModel> posts;
#override
String toString() {
var result = "";
result += "Id: $id\n";
var count = 0;
for(var post in posts) {
result += "Post $count, $post";
}
return result;
}
}
class PostDataModel {
String name;
PostDataModel(this.name);
#override
String toString() {
return "Name: $name\n";
}
}
It depends on your backend structure but I would suggest the second way.
As an answer to the second part of your question, look at the method toString. If you override it, you can customize the way your class is displayed when, for example, you use print(userDataModel. I created those classes as for example, so customize them for your own classes.
In fact, with the last example, you can access your posts by data[0].posts[0] as you requested.
I guess you could do something like
var data = userDataModel.map((user) => postDataModel.map((post) { post.useriD == user.userId ? post : null}).toList()).toList();
One possible solution is to iterate over the Map and over the List in a nested forEach:
data.forEach((user, posts) {
print(user);
posts.forEach((post) {
print(post);
});
print('---'); // end of the user's posts
});

How do to update an object within Flutter MobX ObservableList?

I'm looking for the best approach to update an ObservableList with objects.
I've created a dummy example below with Car and a store with Cars.
In JavaScript I can simply call
const carData = {id: 1, name: "Chrysler"};
update(carData);
and in the update method:
#action
updateCar(carData) {
cars = cars.map(car => car.id === carData.id ? {...car, ...carData} : car);
}
How do one achieve the same in Flutter with MobX?
class Car {
int id;
String model;
DateTime year;
}
abstract class _CarCollectionStore with Store {
#observable
ObservableList<Car> cars = ObservableList<Car>();
#computed
get latestCar() => iterating and getting latest Car by year.
#action
updateCar(WHICH PARAMETERS?) {
...
}
}
How do I actually update the Name of the latestCar?
carCollectionStore = Provider.of<CarCollectionStore>(context, listen: false);
carNameController = TextEditingController(text: carCollectionStore.latestCar.name);
carNameController.addListener(() {
carCollectionStore.updateCar(carNameController.text);
});

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);