I am not able to figure out the right architecture of getting this done using firestore streams
and flutter_bloc
.
I have a logged in
user
and I want to store the user details
in the collection
with the document id
same as logged in user id
and want to listen to changes, Throughout the program.
Meanwhile i would want to even store user details (if doesn't exist) and update user details.
I am having a usermodel
in UserState
and i am listening to userModel
via state using BlocBuilder.
As i am using BlocBuilder my setState
isn't working and TextFormField
isn't working as it says beginBatchEdit on inactive InputConnection
UserCubit.dart
class UserCubit extends Cubit<UserState> {
final _firestore = FirebaseFirestore.instance;
final User? _currentUser = FirebaseAuth.instance.currentUser;
UserCubit() : super(UserInitialState()) {
emit(UserMainLoadingState());
_firestore --> listening to stream and updating state
.collection("sample")
.doc(_currentUser?.uid)
.snapshots()
.listen((event) {
event.exists
? emit(UserExists(sample: SampleModel.fromjson(event.data()!, event.id)))
: {emit(UserNotExists())};
});
}
Future<void> addSampleUser({required SampleModel sample}) async {
emit(UserSideLoadingState());
_firestore
.collection('sample')
.doc(_currentUser?.uid)
.set(sample.toJson())
.then((value) => emit(UserSavedUpdatedState()));
}
}
UserState.dart
abstract class UserState extends Equatable {
final SampleModel? sampleModel;
const UserState({this.sampleModel});
@override
List<Object?> get props => [sampleModel];
}
class UserExists extends UserState {
const UserExists({required SampleModel sample}) : super(sampleModel: sample);
}
Widget.dart (Save/Update User Details)
class _MyWidgetState extends State<MyWidget> {
// @override
var fullNameKey;
TextEditingController? fullNameController;
bool _formChecked = false;
TextEditingController? phoneNumberController;
Avatar? selectedAvatar;
@override
void initState() {
fullNameController = TextEditingController();
fullNameKey = GlobalKey<FormState>();
super.initState();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<UserCubit, UserState>(listener: (context, state) {
if (state is UserSavedUpdatedState) {
print("Saved/Updated User");
context.goNamed(Routes.profileMain);
}
}, builder: (context, state) {
// Here i am trying to get details from the state
selectedAvatar = state.sampleModel?.avatar == "boy"
? Avatar.boy
: state.sampleModel?.avatar == "boy"
? Avatar.girl
: null;
fullNameController!.text = state.sampleModel?.name ?? "";
if (state is UserMainLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.girl; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.girl)),
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.boy; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.boy))
],
),
Form(
key: fullNameKey,
child: TextFormField(
// BlocBuilder even freezes TextFormField
autovalidateMode: _formChecked
? AutovalidateMode.always
: AutovalidateMode.disabled,
validator: (value) {
if (value == null ||
fullNameController?.text.trim() == "") {
return "Name cannot be empty";
}
if (value.length < 3) {
return "Username must be greater than 3 characters";
}
return null;
},
controller: fullNameController,
decoration: const InputDecoration(
labelText: "Full Name",
),
)).marginDown(),
FilledButton(
onPressed: () {
setState(() {
_formChecked = true;
});
if (fullNameKey.currentState!.validate() &&
selectedAvatar != null) {
SampleModel sample = SampleModel(
name: fullNameController!.text,
avatar: selectedAvatar == Avatar.boy ? "boy" : "girl");
BlocProvider.of<UserCubit>(context)
.addSampleUser(sample: sample);
}
},
child: const Text("Submit"),
)
],
));
}),
);
}
}
As soon as the submit button is clicked it erases the entire text and validator gets activated. Avatar selection doesn't work as well.
What is the best way to achieve the desired function using streams
, flutter_bloc
, Suggestions would be greatly appreciated
as far as I can see you pre-select the avatar based on the emitted state. However, I do not see that you return the selection via an event/function to the bloc/cubit. So this is needed in order to send the updated avatar with the next emit.
From what I can see, I would also possibly exchange the abstract class state with a class state implementing Equatable and the simply always copyWith the state for any updates. This way you always have the same UserState - no need for if and else if for state selection, however, the data of the state changes based on the situation. I think for a user bloc/cubit this makes the lifecycle a bit easier
UPDATE:
IconButton(
onPressed: () {
setState(() {
context.read<UserCubit>.updateUser(selectedAvatar: Avatar.boy);
selectedAvatar = Avatar.boy; -- > possibly no longer needed if returned from Cubit
});
},
icon: const Icon(Icons.boy))
As for the state management, a state can look like this:
class TaskListState extends Equatable {
const TaskListState({
this.status = DataTransStatus.initial,
this.taskList = const [],
this.filter,
this.error,
this.editThisTaskId,
});
final DataTransStatus status;
final List<TaskListViewmodel> taskList;
final TaskListFilter? filter;
final String? error;
final String? editThisTaskId;
TaskListState copyWith({
DataTransStatus Function()? status,
List<TaskListViewmodel> Function()? taskList,
TaskListFilter Function()? filter,
String Function()? error,
String? Function()? editThisTaskId,
}) {
return TaskListState(
status: status != null ? status() : this.status,
taskList: taskList != null ? taskList() : this.taskList,
filter: filter != null ? filter() : this.filter,
error: error != null ? error() : this.error,
editThisTaskId: editThisTaskId != null
? editThisTaskId() : this.editThisTaskId,
);
}
@override
List<Object?> get props => [
status,
taskList,
filter,
error,
editThisTaskId,
];
}
which you use - in this case with a Stream - like this:
await emit.forEach<dynamic>(
_propertiesRepository.streamTasks(event.propertyId),
onData: (tasks) {
return state.copyWith(
status: () => DataTransStatus.success,
taskList: () => List.from(
tasks.map((t) => TaskListViewmodel.fromDomain(t))),
);
},
onError: (_, __) {
return state.copyWith(
status: () => DataTransStatus.failure,
);
},
);