I've got a Stream<UserProfile> being returned form a firebase service.
I'm using MVVM architecture and have a ProfileViewModel which is extended by a freezed state class:
class ProfileModel extends StateNotifier<ProfileState> {
ProfileModel({
required this.authService,
required this.databaseService,
}) : super(const ProfileState.loading());
late AuthService authService;
late FirestoreDatabase databaseService;
Stream<UserProfile?> get userProfile {
return databaseService.profileStream();
}
}
The above results in the following view:
final profileModelProvider =
StateNotifierProvider.autoDispose<ProfileModel, ProfileState>((ref) {
final authService = ref.watch(authServiceProvider);
final databaseService = ref.watch(databaseProvider)!;
return ProfileModel(
authService: authService, databaseService: databaseService);
});
class ProfilePageBuilder extends ConsumerWidget {
const ProfilePageBuilder({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(profileModelProvider);
final model = ref.read(profileModelProvider.notifier);
final up = ref.read(userProfileProvider);
return ProfilePage(
onSubmit: () => {},
name: up.value?.uid ?? "Empty",
canSubmit: state.maybeWhen(
canSubmit: () => true,
success: () => true,
orElse: () => false,
),
isLoading: state.maybeWhen(
loading: () => true,
orElse: () => false,
),
errorText: state.maybeWhen(
error: (error) => error,
orElse: () => null,
),
);
}
}
I would like to know the correct way (using riverpod) to pass the firebase stream to the UI without mixing up UI/BL without loosing functionality of real time data.
I was able to create a StreamProvider which referenced the profile model but it doesnt feel right.
final userProfileProvider = StreamProvider.autoDispose<UserProfile?>((ref) {
return ref.watch(profileModelProvider.notifier).userProfile;
});
My alternative is to convert streams to futures within the view model and then update the state as the function runs.
I'm really quite stuck here, any help would be appreciated
My guess is you want to
listen to a stream from Firebase
When the latest value changes, you want any dependencies to update
You only want the latest value of the stream.
INTRODUCING BehaviorSubject!
You'll need package:rxdart though you may already have it installed.
import 'package:rxdart/rxdart.dart';
#riverpod
BehaviorSubject<ProfileState> userProfileSubject(
UserProfileSubjectRef ref) {
final stream = ....;
// Get the stream and wrap it in a BehaviorSubject
return BehaviorSubject()..addStream(stream);
}
#riverpod
UserProfile? userProfile(
UserProfileRef ref) {
final behaviorSubject = ref.watch(userProfileSubjectProvider);
// when the underlying stream updates,
// invalidate self so we read the new value
behaviorSubject.doOnData((newProfileState) { ref.invalidateSelf(); });
// note that value could be null while stream
// emits a value. You can wait for that
// and convert this provider to return a Future<UserProfile>
// or in the UI handle the null.
// note that firebase could also have a null value.
return behaviorSubject.value?.userProfile;
}
Related
(Update at the end of the post) I want to add my normal firebase auth with additional user information. In this example, name and goal calories. For that, I created this register function:
Future<void> signUpWithEmailAndPassword(String email, String password, BuildContext context, WidgetRef ref, widget) async {
FocusManager.instance.primaryFocus?.unfocus();
try {
await auth.createUserWithEmailAndPassword(email: email, password: password);
ref.read(isUp.notifier).state = false;
ref.read(writeItemViewModelProvider).setInitValue();
} on FirebaseAuthException catch (e) {
the function setInitValue() looks like this:
class FirestoreDb extends ChangeNotifier {
Future<void> setInitValue() async {
await firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').doc(auth.currentUser!.uid).set({
'name': null,
'calories': null,
});
}
}
Here seems to work everything fine. Inside firestore a file gets created and my user also. Without this additional user infos my auth works also fine. So I think there is a problem with my stream of the user information. Because: I have to check if the registert user has already added information or not.
I do this with a second .when function:
#override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final watcher = ref.watch(itemsProvider);
return authState.when(
data: (data) {
if (data != null) {
return watcher.when(data: (calo) {
if (calo.first.calories != null) {
return const RootPage();
} else {
return UserInformation();
}
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen());
the first .when function is for the auth, here seems to be no problem, but the secons is strange. When I login first time, it says bad state. From now on, every time I register with a different account, I only get the old data from the previous account until I hot restart.
After the user information, you get to this page:
#override
Widget build(BuildContext context, WidgetRef ref) {
final streamData = ref.watch(itemsProvider);
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(toolbarHeight: 0, backgroundColor: Colors.transparent),
resizeToAvoidBottomInset: false,
body: streamData.when( data: (calo) {
return Text(calo.first.calories.toString());
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen())
);
}
where I can see that s old information until hot restart.
So something with my stream is not updating the state correctly.
When I wrap delete the .when function and use a Streambuilder listening to the stream directly everything works.
Here is my itemsProvider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => ref.read(itemRepositoryProvider).itemsStream,
);
final itemRepositoryProvider = Provider((ref) => ReadData());
class ReadData{
Stream<List<UsersModel>> get itemsStream {
return firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
});
}
}
I check with debugging and "print points" the way of the compiler and recognised the problem but have no answer why the compiler do this:
#override
Widget build(BuildContext context, WidgetRef ref) {
print("inside UserInfoBuild");
final watcher = ref.watch(itemsProvider);
return watcher.when(data: (userInfoData) {
print("inside AsyncValue<List<UsersModel>>");
if (userInfoData.first.calories != null) {
return const RootPage();
} else {
return UserInformation(); [...]
declare provider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) {
print("inside stream provider");
return ref.read(itemRepositoryProvider).itemsStream;
},
);
so, my guess was that the print order should be:
I/flutter: inside UserInfoBuild
I/flutter: inside stream provider
I/flutter: inside AsyncValue<List<UsersModel>>
but its actually just:
I/flutter: inside UserInfoBuild
I/flutter: inside AsyncValue<List<UsersModel>>
so the compiler skips the final itemsProvider = StreamProvider.
Just after a hot restart it executes the line of code
I think the key point is 'get' itemsStream. You have two ways to try.
// 1.
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
}),
);
// 2.
You can use StreamController to get data from firebaseFirestore.collection in ReadData class, and use a Stream variable to sync that value. Update StreamProvider to the Stream variable.
I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}
I have a stateless widget which returns a column of two consumer widgets. Each of these two widgets are consuming 2 different streams from providers, both of which depend on the same another provider. Both streams return different type of data. If I leave only one of the two consumer widgets, everything works as expected. But if I show both, one of them returns error after a 5 second timeout - "Error TimeoutException after 0:00:05.000000: Future not completed" (seems random if it's the first or the second one, but usually the second).
Using also Flutter Blue Plus to communicate with BT device
Here's a very simplified version of what my code looks like:
// responsePacket here - List<int> converted to specifically to Data1 or Data2 depending on 1st byte
responseStream = _txCharacteristic.value..map(responsePacket).where((it) => it != null).asBroadcastStream()
Stream<T> getStream<T>() => responseStream.where((element) => element is T).cast<T>();
Future<bool> _sendIt(List<int> message) {
return _rxCharacteristic.write(requestPacket, withoutResponse: false);
}
Future<bool> requestData1() {
return _sendIt([1]);
}
Future<bool> requestData2() {
return _sendIt([2]);
}
final streamDataProvider1 = StreamProvider.autoDispose<Data1>((ref) async* {
Object obj = ref.watch(objectStateNotifierProvider);
while (obj.data != null) {
obj.bt!.requestData1();
Data1 value = await obj.data!.getStream<Data1>().first.timeout(const Duration(seconds: 5));
yield value;
}
});
final streamDataProvider2 = StreamProvider.autoDispose<Data2>((ref) async* {
Object obj = ref.watch(objectStateNotifierProvider);
while (obj.data != null) {
obj.bt!.requestData2();
Data2 value = await obj.data!.getStream<Data2>().first.timeout(const Duration(seconds: 5));
yield value;
}
});
class _MainScreenData extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: [
_TestStream1(), // Comment out one of these and it works as expected
_TestStream2(),
],
);
}
}
class _TestStream1 extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final stream = ref.watch(streamDataProvider1);
return stream.when(
data: (value) => Text(value.toString()),
error: (e, s) => Text('Error $e'),
loading: () => const CircularProgressIndicator(),
);
}
}
class _TestStream2 extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final stream = ref.watch(streamDataProvider2);
return stream.when(
data: (value) => Text(value.toString()),
error: (e, s) => Text('Error $e'),
loading: () => const CircularProgressIndicator(),
);
}
}
I found here on SO similar questions, but nothing specifically like my case. I want to watch streams separately in some cases. When I want to use both streams in one widget, I found I can nest StreamBuilder widgets and use separate snapshots (didn't try that yet though)
What am I missing, that I get one of the two widgets displaying not completing the Future?
Do not rely on .stream + StreamBuilder to listen to a StreamProvider
The correct usage is:
final value = ref.watch(streamDataProvider2);
return value.when(
data: (value) => Text('$value'),
loading: () => CircularLoadingProgressIndicator(),
error: (err, stack) => Text('error $error'),
);
I have WebSocket service which I want to use with StreamProvider. When I used it with simple StreamProvider and tried to get data it worked well as it was described in docs, but my issue is a little bit complicated: I want to make it reactive and change seconds (I get DateTime from WebSocket). So I found out that in riverpod we can use state with StateNotifier and using it I can change state. So, I decided to combine two kinds of providers (in docs it says I can easily do it), but when I placed StreamProvider inside StateNotifier, it stopped getting requests to website and retrieve data. How can I solve this issue?
My full code looks like this:
final todosProvider =
StateNotifierProvider<TickingTime, String?>((ref) => TickingTime());
class TickingTime extends StateNotifier<String?> {
static String? time;
static var websockets;
TickingTime() : super(time) {
Timer.periodic(const Duration(seconds: 1), (timer) {
state = time;
});
print('NOT HEHE1');
websockets = StreamProvider((ref) async* {
print('NOT HEHE');
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final connectionValue = HubConnectionBuilder()
.withUrl(
'http://securelink/link',
options: httpConnectionOptions,
)
.build();
print('NOT HEHE2');
await connectionValue.start();
if (connectionValue.state == HubConnectionState.Connected) {
await connectionValue.invoke('GetCurrentDateTime').then((value) {
time = value as String;
DateTime dateTime = DateTime.parse(time ?? 'no time');
time = DateFormat('HH:mm:ss').format(dateTime);
});
yield time;
}
;
ref.onDispose(() {
connectionValue.onclose(({error}) {
throw Exception('NOT HEHE, YOU KNOW');
});
});
});
}
}
UI:
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
var time = ref.watch(todosProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(time ?? 'NOTHING TO SHOW'),
),
),
);
data I get from WebSocket:
14:25:34
This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :