So in my flutter app, when it's open, i get the type of the user from my RestApi (without account, with account type1, account type2 account type3), and each one of them has a special home_page (same appBar but different bodies with some similarities).
How can i do that ?
Do i have to create a specific route for each one ?
Yes the best practice is to create a route for each user, otherwise you will end up with 4 if blocks in the same file and things will get messy fast
You can create a generic widget how should a home_page should look, then create a model what information is different between users, create another file with all the data from where you get information that is related to the previous model.
Example:
Model:
class Model {
final String id;
final String name;
final String surname;
Model({this.id, this.name, this.surname});
}
Data:
const User_Data = [
Model(
id: 'user1',
name: 'Name1',
surname: 'Surname1',
),
Model(
id: 'user2',
name: 'Name1',
surname: 'Surname1',
),
];
In the data you can fetch the user by id by using User_Data.firstWhere() to get user id and after get all data for that user id.
final userId = ModalRoute.of(context).settings.arguments as String;
final selectedUser =
User_Data.firstWhere((user) => userId == user.id);
Related
I'm learning domain driven design for Flutter apps. I understand that the model is used between the infrastructure layer and the use-case, and the entity is used between in the use case and the UI.
Let's say that my app is dealing with books and I'm storing my books in Cloud Firestore. I have defined a very simple BookEntity with an id and a title.
#freezed
class BookEntity with _$BookEntity {
const factory BookEntity({
required String firestoreId, // This is the ID of the document in firestore
required String title,
}) = _BookEntity;
}
I believe that the ID of the document should be in the entity, because if I need to modify this book in the Firestore I will need to know the reference of the document, right?
As you know, in firestore the id is not part of the data themselves. When I read my database, I would be using the code below.
// code not tested
FirebaseFirestore
.instance
.collection('books')
.doc('uXSin0z3gqPHwhVLCP98')
.get()
.then((DocumentSnapshot<Map<String, dynamic>> snapshot) {
snapshot.id; // -> 'uXSin0z3gqPHwhVLCP98'
snapshot.data(); // -> {title: 'To Kill a Mockingbird', price: 11.69, year: 1960}
});
Somewhere, I will need to put the id together with the data. I think the right place to do it is in the model, which is created in the repository. I think my model would probably look very similar to the Entity:
#freezed
class BookModel with _$BookModel {
const factory BookModel({
required String firestoreId,
required String title,
}) = _BookModel;
}
And, when fetching data from Firestore I would create a model with:
BookModel(
firestoreId: snapshot.id,
title: snapshot.data()?['title'],
);
This can then be converted to a BookEntity which will be consumed by the UI.
The problem is that when I am reversing the flow, when I am creating a new book, the ID of the firestore document is not known in the presentation and domain layers. Therefore my BookEntity and BookModel must be updated so that the id is optional. The entity and the model now look like this
#freezed
class BookEntity with _$BookEntity {
const factory BookEntity({
String? firestoreId,
required String title,
}) = _BookEntity;
}
#freezed
class BookModel with _$BookModel {
const factory BookModel({
String? firestoreId,
required String title,
}) = _BookModel;
}
The problem is that now, every time I need to access the firestoreId field of my BookEntity, whose data originate from Firestore, I need to test whether the firestoreId field is null or not. But it cannot be null because the data come from Firestore, so there is always an ID. So I will either write a lot of null-checks, or use the ! (which I don't like).
In short, the "upstream" and "downstream" flows have different requirements for the firestoreId field. The Firebase -> UI flow needs a String, and the UI -> Firebase flow needs a String?.
So the question is what is the best and cleanest way to handle this?
firestoreId should be nullable, because it make sense for BookEntity to have firestoreId sometime and not have firestoreId sometime.
You probably don't need to use firestoreId in the UI, and It's only needed when writing to the Firestore. So you can have a writeToFirestore method and only use a single null check there.
You can also generate a new random id locally whenever a new BookEntity is created. Using your own document id when creating a document in Firestore
One more solution is to use late final String firestoreId, but it's skipping null check making debuging harder and doesn't work with Freezed.
When a new user is created in Firebase, I want to create a collection of user preferences and save it to Cloud Firestore as well. All defaulted to username String 'John Doe', thememode ThemeMode.light and themecolor FlexScheme.money (using flexcolorscheme package).
I can store Strings and int's (works fine with my code), but not ThemeModes apparently and I don't know how to solve this correctly.
I created a PrefService class:
class PrefService {
//Declare a nullable String to hold user unique ID
final String? uid;
//When we call PrefService, require to insert user unique ID
PrefService({this.uid});
final CollectionReference<Object?> userPreferences =
FirebaseFirestore.instance.collection('userPreferences');
Future<dynamic> updateUserPreferences(
String name,
ThemeMode themeMode,
FlexScheme themeColor,
) async {
//If document doesn't exist yet in Firebase, it will be created under user unique ID
return userPreferences.doc(uid).set(<String, dynamic>{
'name': name,
'themeMode': themeMode,
'themeColor': themeColor,
});
}
}
And an AuthService class:
class AuthService {
//Sign UP user only with email and password
Future<void> signUp({
required String email,
required String password,
required BuildContext context,
}) async {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
//After creating a user, create a database file under user unique ID as well
//Store default name, themeMode and themeColor, which can be adjusted later by user.
Logger().i('Database record created for user');
await PrefService(uid: FirebaseAuth.instance.currentUser!.uid)
.updateUserPreferences(
'John Doe',
ThemeMode.light,
FlexScheme.money,
);
etc...
When I set all parameters of updateUserPreferences() to type String, everything works and I get a nice collection under a unique user ID. But it does not let me store parameter of type ThemeMode:
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument: Instance of 'ThemeMode'
Why can I store a String, but not a ThemeMode? I want users to be able to change these settings in a SettingsScreen(), but how should I store them correctly?
I am aware of the existence of SharedPreferences, but I want to do it this way. If anyone can show me how (and why) I should handle this, it would be greatly appreciated.
Objects cannot be saved in firebase directly. If it is Model you can store it in map format.
In your case shore the them mode as String or int i.e. 0 for light and 1 for dark and convert it when you read the data.
I'm making a Next JS application with prisma and postgres.
I have 2 tables: User and Profile
Their prisma schema structure is as follows:
model User {
id String #id #default(cuid())
name String?
email String? #unique
emailVerified DateTime?
image String?
// foreign keys
sessions Session[]
profile Profile?
}
model Profile {
id Int #id #default(autoincrement())
isAdmin Boolean #default(false)
firstName String
lastName String
email String #unique
phone String
address String
gender String
image Bytes
guardianName1 String
guardianPhone1 String
guardianRelation1 String
guardianName2 String?
guardianPhone2 String?
guardianRelation2 String?
guardianName3 String?
guardianPhone3 String?
guardianRelation3 String?
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
// foreign keys
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
userId String #default(cuid()) // relation scalar field (used in the `#relation` attribute above)
requests Request[]
}
I'm also using next-auth for the authentication part of this application. So when a user signs up then upon his email verification, next-auth itself adds the user's record to the User table.
Till here, there's no issue.
Then, when the user opens his dashboard for the first time, then he's shown a form to fill, upon submission of that form, a record needs to be inserted in the Profile table. As the Profile and User table's are linked, they also need to be connected.
So when the user submits profile details form, I do this:
try {
const newProfileData = {
// other fields data here...
user: {
connect: { id: '1' } // where User table already has a record with - 'id': 1
}
};
const profile = await prisma.profile.create({ data: newProfileData, include: { user: true } });
if(profile) {
console.log("Created: ", profile);
res.status(200).json({ msg: 'Successfully Created Profile!' });
}
}
catch(err)
{
console.log(err);
}
But upon running this code, I get the error:
The change you are trying to make would violate the required relation 'ProfileToUser' between the `Profile` and `User` models.
...
code: 'P2014',
clientVersion: '2.30.3',
meta: {
relation_name: 'ProfileToUser',
model_a_name: 'Profile',
model_b_name: 'User'
}
How can this be solved?
I even tried it the other way (i.e. updating the existing User and creating the Profile record connected to it):
const user = await prisma.user.update({
where: {
email: req.body.email,
},
data: {
profile: {
create: {
// data fields here... (without the user field)
},
},
},
});
But this also gives the same error...
I want to understand why the error comes. Is this not the correct way to create a record for a 1 to 1 relation using prisma-client?
The fix:
I think you need to remove #default(cuid()) from the Profile's userId field definition.
model Profile {
//...
// foreign keys
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
userId String // relation scalar field (used in the `#relation` attribute above)
//...
}
And also get rid of include: { user: true }:
const profile = await prisma.profile.create({ data: newProfileData});
The explanation:
Profile's user and userId fields don't directly translate to actual columns on the db but are fields that let Prisma handle the link between the relations. It ends up translated to PostgreSQL's
create table profile(
--...
userId text references user (id),
--...
);
And later Prisma will populate that field with your User's id when you issue a user:{connect:{id:'1'}}. What could've happened is when you used #default(cuid()) in userId field definition, you interfered with that process. Now the column ends up as
userId text default gen_random_uuid() references user (id)
and whenever you create a Profile, a new row gets entered without specifying your own userId (which Prisma probably attempts to do before it'll try to link your User), a random id gets generated that doesn't correspond to any existing User, which violates the reference constraint.
It's that and/or your usage of include: { user: true } messes something up spawning a separate, new user, even though you tried to link your Profile to an existing one. But I would expect that to be just an unwanted side-effect making your code spawn a useless User object and row each time you create a Profile.
Once you get rid of the #default(cuid()) you can also just spawn a standalone, unlinked Profile and then link it to the appropriate User later with an update statement.
Merge the two tables into one, something like:
model User {
id String #id #default(cuid())
name String?
email String? #unique
emailVerified DateTime?
image String?
isAdmin Boolean #default(false)
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
// foreign keys
sessions Session[]
}
If you absolutely must have a Profile relation, create a database view:
create view Profile as
select
id,
isAdmin,
name,
email,
createdAt,
updatedAt,
userId
from user
and map it as a read only relation, but I can’t see the point.
I had a doubt like how to handle field does not exists situation like suppose i have released my app and in future updates i added a new field in doc then how can i handle if field does not exists.
For example, in shared preferences we use ??to handle data existence with the specified key value.
int val=prefs.getInt("myKey")??0;
as you can see that above code will set value of val to 0 if there's no value associated with the key- myKey. Similarly i would like to know is there any way of doing it for firestore document fields.
MyCode:-
class UserModel
{
final String? id;
final String? username;
final String? email;
UserModel({
this.id,
this.username,
this.email,
});
factory UserModel.fromDocument(DocumentSnapshot doc)
{
return UserModel(
id: doc['id'],
username: doc['username'],//suppose the username does not exist in the field then how can i assign the value "User" to the username?
email: doc['email'],
);
}
}
This is the problem when developing an app which use Firebase. If you add a field after there is already a data and if you try to get new field in dart, you are getting this error. I think the only solution is deleting all the old data which doesn't have new field. Or put this new field to all of your old data. It's kinda annoying. So make sure that you have to add all possible field in the beginning.
Unfortunately, you can't use like int val=prefs.getInt("myKey")??0; because it isn't even null.
I'm making an app using Flutter, with Cloud Firestore for the backend. I have a stream which retrieves a list of user documents for all users and want to filter the users whose favorite food is "pasta". I don't want to load the other documents. Here is my stream, and the function which maps it to my user model.
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('Users');``
List<MyAppUser> _userListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((DocumentSnapshot doc) {
return MyAppUser(
uid: doc.id ?? '',
name: (doc['name']).toString() ?? '',
email: (doc['email']).toString() ?? '',
favorite_food: (doc['favorite food']).toString() ?? '',
);
}).toList();
}
Stream<List<MyAppUser>> get users {
return usersCollection.snapshots().map(_userListFromSnapshot);
}
Here is my user model if needed:
class MyAppUser{
final String uid;
final String name;
final String email;
final String favorite_food;
MyAppUser({
this.name,
this.email,
this.uid,
this.favorite_food,
});
}
Should I use a where function after mapping or before?
If I filter before mapping, I will have to do a where on the original stream like
usersCollection.where('favorite food', isEqualTo: 'pasta')
If I filter after mapping, I can get type safety:
I listen to the stream with Provider: final users = Provider.of<List<MyAppUser>>(context);
Then query like this:
users.where((user) => user.favorite_food == 'pasta');
I would prefer to use typesafety, but, will I be billed for reading only the filtered documents or all documents?
I got this answer from Aurimas Deimantas, after commenting on their article on medium.com. Below, I have adapted their answer to suit this question.
Firestore bills you based on how many document reads you have.
It will be better to filter before mapping, with
usersCollection.where('favorite food', isEqualTo: 'pasta')
because this will only read the documents where favorite food is pasta.
If you filter after mapping, like this:
users.where((user) => user.favorite_food == 'pasta');
then all user documents will be read, and after that, filtered. So Firestore will bill you for all the document reads instead of only those whose favorite food is pasta.
This is why it saves money to filter on the userscollection directly, before mapping it to your model.
If you want to map the stream to your model, you can do it after the where filter, by adding the .map(...) function after the .where(...) function, and this will map (& read) only the documents that pass the where filter, saving money.
You can use where clause just after collection calling like
... Collection('Users').where(field, conditions)
With this, you don't have filter list using collection