Cache result of transformed stream in dart using StreamStransformer - flutter

I want to cache the result of a transformed stream (with StreamTransformer.fromHandlers) to be able to access the last event without listening to stream, similar to rxdart BehaviorSubject.
I have tried setting an internal property and expose it with getter but it has no effect and I have to wait for the stream to emit a value before it changes.
class SimpleBloc {
final BehaviorSubject<String> _contentController = BehaviorSubject();
/// Emits content value or [ValidationError] if invalid
Stream<String> get content$ => _contentController.transform(StreamTransformer.fromHandlers(handleData: (data, sink) {
if (data?.isNotEmpty != true) {
sink.addError(ValidationErrorRequired());
} else {
sink.add(data!);
}
});
String? get _content => _contentController.valueOrNull;
/// Emits boolean when content changes that indicates if bloc is valid
Stream<bool> valid$ => $content.transform(
handleData: (data, sink) => sink.add(_valid = true),
handleError: (error, sink) => sink.add(_valid = false)
);
Function(String) get setContent => _contentController.add;
bool _valid = false;
bool get valid => _valid;
}
test("bloc is invalid when content is invalid", () async {
final bloc = MyBloc();
bloc.setContent("content");
// If I expectLater before the test will pass because apparently it takes some time until transformers are run
// await expectLater(bloc.valid$, emits(true));
expect(bloc.valid, equals(true)); //test fails
});
I have noticed that maybe the getters are the issue so I tried creating the streams in the constructor to the transformers are instantiated but it hadnt had any effect:
class MyBloc {
Stream<bool> get valid$ => _validStream;
Stream<String> get content$ => _contentStream;
MyBloc() {
_contentStream = _contentController.transform(...);
_validStream = _contentStream.transform(...);
}
}
Does anyone have a solution for this?

Related

rx dart combine multiple streams to emit value whenever any of the streams emit a value

In RX dart there is the RX.combineLatest method to combine the results of a stream using a callback function.
Problem is that it only emits a value when every stream has emitted a value. If one has not it does not emit.
Merges the given Streams into a single Stream sequence by using the combiner function whenever any of the stream sequences emits an item.
The Stream will not emit until all streams have emitted at least one item.
Im trying to combine multiple streams into one stream for validation which should emit false or true when the streams have not emitted or emitted an empty value.
class FormBloc {
final BehaviorSubject<bool> _result = BehaviorSubject();
final BehaviorSubject<String?> _usernameController = BehaviorSubject();
final BehaviorSubject<String?> _emailController = BehaviorSubject();
// Will only emit if each stream emitted a value
// If only username is emitted valid is not emitted
Stream<bool> get valid$ => Rx.combineLatest2(
_usernameController.stream,
_emailController.stream,
(username, email) => username != null || email != null
);
}
How can I join those streams so valid$ emits a value if any of the streams change?
Because all of the solutions here are workarounds Ive implemented my own stream class. Implementation equals the original CombineLatestStream implementation except that it does not wait for all streams to emit before emitting:
import 'dart:async';
import 'package:rxdart/src/utils/collection_extensions.dart';
import 'package:rxdart/src/utils/subscription.dart';
class CombineAnyLatestStream<T, R> extends StreamView<R> {
CombineAnyLatestStream(List<Stream<T>> streams, R Function(List<T?>) combiner) : super(_buildController(streams, combiner).stream);
static StreamController<R> _buildController<T, R>(
Iterable<Stream<T>> streams,
R Function(List<T?> values) combiner,
) {
int completed = 0;
late List<StreamSubscription<T>> subscriptions;
List<T?>? values;
final _controller = StreamController<R>(sync: true);
_controller.onListen = () {
void onDone() {
if (++completed == streams.length) {
_controller.close();
}
}
subscriptions = streams.mapIndexed((index, stream) {
return stream.listen(
(T event) {
final R combined;
if (values == null) return;
values![index] = event;
try {
combined = combiner(List<T?>.unmodifiable(values!));
} catch (e, s) {
_controller.addError(e, s);
return;
}
_controller.add(combined);
},
onError: _controller.addError,
onDone: onDone
);
}).toList(growable: false);
if (subscriptions.isEmpty) {
_controller.close();
} else {
values = List<T?>.filled(subscriptions.length, null);
}
};
_controller.onPause = () => subscriptions.pauseAll();
_controller.onResume = () => subscriptions.resumeAll();
_controller.onCancel = () {
values = null;
return subscriptions.cancelAll();
};
return _controller;
}
}
Creating new stream which emits current value and listen the stream is my best practice.
class FormBloc {
final BehaviorSubject<bool> _result = BehaviorSubject();
final BehaviorSubject<String?> _usernameController = BehaviorSubject();
final BehaviorSubject<String?> _emailController = BehaviorSubject();
final _usernameStreamController = StreamController<String?>()
..add(_usernameController.value)
..addStream(_usernameController.stream);
final _emailStreamController = StreamController<String?>()
..add(_emailController.value)
..addStream(_emailController.stream);
Stream<bool> get valid$ => Rx.combineLatest2(
_usernameStreamController.stream, // use streamController instead
_emailStreamController.stream, // use streamController instead
(username, email) => username != null || email != null
);
}
Instead of combining multiple streams, you can use one BehaviorSubject<Map<String, String?>> to emit changes in username or email.
add either changed\submitted username or email to the BehaviorSubject
_usernameEmailController.add({"uname": value},);
or
_usernameEmailController.add({"email": value},);
so that you can validate the inputs by listening to it. I used StreamBuilder to display the emitted values,
StreamBuilder<Map<String, String?>>(
stream: _usernameEmailController.stream
.map((data) {
_r = {..._r, ...data};
return _r;
}),
builder: (context, snapshot) {
return Column(
children: [
Text(snapshot.data.toString()),
if (snapshot.hasData)
Text(
"Is valid?: "
"${(snapshot.data!["uname"] != null && snapshot.data!["uname"]!.isNotEmpty) || (snapshot.data!["email"] != null && snapshot.data!["email"]!.isNotEmpty)}"
),
],
);
},
),
checkout the my solution on DartPad here.
In the DartPad I have used StreamController instead of BehaviorSubject as DartPad doesn't support rxdart Package.
But you can replace line 40 in DartPad
final StreamController<Map<String, String?>> _usernameEmailController =
StreamController();
with
final BehaviorSubject<Map<String, String?>> _usernameEmailController =
BehaviorSubject();
If you want to use BehaviorSubject.
You could seed your BehaviorSubjects with a default value of null:
final BehaviorSubject<String?> _usernameController = BehaviorSubject().seeded(null);
final BehaviorSubject<String?> _emailController = BehaviorSubject().seeded(null);
Another possibility would be to give the combined stream a seed value:
Rx.combineLatest2(
_usernameController.stream,
_emailController.stream,
(username, email) => username != null || email != null
).sharedValueSeeded(false);

Initialize a null safe variable without a default constructor?

I'm sure this problem has been asked before, but I can't figure out how to even properly word it.
I am trying to get Bluetooth Device data into my flutter app. The examples I have found either use non-null safe versions of dart code or they hide all of the important details.
I am trying to build a very simple prototype from scratch so I can get a better grasp on things.
I want to make a stateful widget that updates based on notifyListeners(). The idea is I start out with "noName bluetooth device", then once I have a device connected, I can update the object and display the name of the connected device.
I keep running into this same roadblock and I can't get past it. I want to make a default Bluetooth Device, but the device has no default constructor. The default device cannot be null because of null safety.
Can someone help me figure this out. There is something I know I am fundamentally misunderstanding, but I don't know where to start.
The code below makes a ChangeNotifierProvider the parent of my BlueTesting widget that should display details of the connected Bluetooth Device (I haven't written all of the code yet).
The BTDevices class should update the Bluetooth Device object, and notify the app to display the updated data from "the default empty device" to the new connected device.
Thank you for your help!
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:provider/provider.dart';
void main() => runApp(
ChangeNotifierProvider(create: (_) => BTDevices(), child: BlueTesting()));
class BTDevices extends ChangeNotifier {
BluetoothDevice device = BluetoothDevice();
void setDevice(BluetoothDevice newDevice) {
device = newDevice;
notifyListeners();
}
}
UPDATE:
I tried updating the above code to set:
BluetoothDevice device = null;
A value of type 'Null' can't be assigned to a variable of type 'BluetoothDevice'.
Try changing the type of the variable, or casting the right-hand type to 'BluetoothDevice'.dartinvalid_assignment
here is the information on the BluetoothDevice definition:
part of flutter_blue;
class BluetoothDevice {
final DeviceIdentifier id;
final String name;
final BluetoothDeviceType type;
BluetoothDevice.fromProto(protos.BluetoothDevice p)
: id = new DeviceIdentifier(p.remoteId),
name = p.name,
type = BluetoothDeviceType.values[p.type.value];
BehaviorSubject<bool> _isDiscoveringServices = BehaviorSubject.seeded(false);
Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;
/// Establishes a connection to the Bluetooth Device.
Future<void> connect({
Duration? timeout,
bool autoConnect = true,
}) async {
var request = protos.ConnectRequest.create()
..remoteId = id.toString()
..androidAutoConnect = autoConnect;
Timer? timer;
if (timeout != null) {
timer = Timer(timeout, () {
disconnect();
throw TimeoutException('Failed to connect in time.', timeout);
});
}
await FlutterBlue.instance._channel
.invokeMethod('connect', request.writeToBuffer());
await state.firstWhere((s) => s == BluetoothDeviceState.connected);
timer?.cancel();
return;
}
/// Cancels connection to the Bluetooth Device
Future disconnect() =>
FlutterBlue.instance._channel.invokeMethod('disconnect', id.toString());
BehaviorSubject<List<BluetoothService>> _services =
BehaviorSubject.seeded([]);
/// Discovers services offered by the remote device as well as their characteristics and descriptors
Future<List<BluetoothService>> discoverServices() async {
final s = await state.first;
if (s != BluetoothDeviceState.connected) {
return Future.error(new Exception(
'Cannot discoverServices while device is not connected. State == $s'));
}
var response = FlutterBlue.instance._methodStream
.where((m) => m.method == "DiscoverServicesResult")
.map((m) => m.arguments)
.map((buffer) => new protos.DiscoverServicesResult.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => p.services)
.map((s) => s.map((p) => new BluetoothService.fromProto(p)).toList())
.first
.then((list) {
_services.add(list);
_isDiscoveringServices.add(false);
return list;
});
await FlutterBlue.instance._channel
.invokeMethod('discoverServices', id.toString());
_isDiscoveringServices.add(true);
return response;
}
/// Returns a list of Bluetooth GATT services offered by the remote device
/// This function requires that discoverServices has been completed for this device
Stream<List<BluetoothService>> get services async* {
yield await FlutterBlue.instance._channel
.invokeMethod('services', id.toString())
.then((buffer) =>
new protos.DiscoverServicesResult.fromBuffer(buffer).services)
.then((i) => i.map((s) => new BluetoothService.fromProto(s)).toList());
yield* _services.stream;
}
/// The current connection state of the device
Stream<BluetoothDeviceState> get state async* {
yield await FlutterBlue.instance._channel
.invokeMethod('deviceState', id.toString())
.then((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
.then((p) => BluetoothDeviceState.values[p.state.value]);
yield* FlutterBlue.instance._methodStream
.where((m) => m.method == "DeviceState")
.map((m) => m.arguments)
.map((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => BluetoothDeviceState.values[p.state.value]);
}
/// The MTU size in bytes
Stream<int> get mtu async* {
yield await FlutterBlue.instance._channel
.invokeMethod('mtu', id.toString())
.then((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
.then((p) => p.mtu);
yield* FlutterBlue.instance._methodStream
.where((m) => m.method == "MtuSize")
.map((m) => m.arguments)
.map((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => p.mtu);
}
/// Request to change the MTU Size
/// Throws error if request did not complete successfully
Future<void> requestMtu(int desiredMtu) async {
var request = protos.MtuSizeRequest.create()
..remoteId = id.toString()
..mtu = desiredMtu;
return FlutterBlue.instance._channel
.invokeMethod('requestMtu', request.writeToBuffer());
}
/// Indicates whether the Bluetooth Device can send a write without response
Future<bool> get canSendWriteWithoutResponse =>
new Future.error(new UnimplementedError());
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is BluetoothDevice &&
runtimeType == other.runtimeType &&
id == other.id;
#override
int get hashCode => id.hashCode;
#override
String toString() {
return 'BluetoothDevice{id: $id, name: $name, type: $type, isDiscoveringServices: ${_isDiscoveringServices.value}, _services: ${_services.value}';
}
}
enum BluetoothDeviceType { unknown, classic, le, dual }
enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting }
The default device cannot be null because of null safety.
You got this backwards. Null-safety is never the reason something can or cannot be null. You decide whether something can or cannot be null. It has been this way forever, the programmer decides if a variable sometimes is null. All null-safety does is force the programmer to share this secret arcane knowledge with their compiler, so the compiler can do it's job and warn the programmer if their logic contains mistakes.
So if you think that having no device for the start of the program (sounds reasonable), then you can decide to make it nullable. Use BluetoothDevice? as the type and it is nullable. And now your compiler can tell you all the places where you (mistakenly) assume it never is null. That's the great thing about null-safety. It's here to help you with whatever choice you make, not constrain you in your choices.
Make your variable nullable
BluetoothDevice? device;
And check if device is null later

How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI

Well, I'm stuck on this problem. I have a code for audioservice (audioplayer.dart) which takes a queue to play. I'm getting the queue from playlist.dart in audioplayer.dart using ModalRoute and save in a global variable queue. Then, I initialize the AudioPlayerService. Now everything till here is fine but inside the AudioPlayerTask class which extends BackgroundAudioTask, when I try to access the variable (inside onStart) it comes out to be an empty list. I don't know where the problem is and I'm not very much familier with the BackgroundAudioTask class. Here's how it looks like:
import .....
List<MediaItem> queue = [];
class TempScreen extends StatefulWidget {
#override
_TempScreenState createState() => _TempScreenState();
}
class _TempScreenState extends State<TempScreen> {
#override
Widget build(BuildContext context) {
queue = ModalRoute.of(context).settings.arguments;
// NOW HERE THE QUEUE IS FINE
return Container(.....all ui code);
}
// I'm using this button to start the service
audioPlayerButton() {
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
);
AudioService.updateQueue(queue);
print('updated queue at the start');
print('queue now is $queue');
AudioService.setRepeatMode(AudioServiceRepeatMode.none);
AudioService.setShuffleMode(AudioServiceShuffleMode.none);
AudioService.play();
}
}
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
Seeker _seeker;
StreamSubscription<PlaybackEvent> _eventSubscription;
String kUrl = '';
String key = "38346591";
String decrypt = "";
String preferredQuality = '320';
int get index => _player.currentIndex == null ? 0 : _player.currentIndex;
MediaItem get mediaItem => index == null ? queue[0] : queue[index];
// This is just a function i'm using to get song URLs
fetchSongUrl(songId) async {
print('starting fetching url');
String songUrl =
"https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" +
songId;
var res = await get(songUrl, headers: {"Accept": "application/json"});
var resEdited = (res.body).split("-->");
var getMain = jsonDecode(resEdited[1]);
kUrl = await DesPlugin.decrypt(
key, getMain[songId]["more_info"]["encrypted_media_url"]);
kUrl = kUrl.replaceAll('96', '$preferredQuality');
print('fetched url');
return kUrl;
}
#override
Future<void> onStart(Map<String, dynamic> params) async {
print('inside onStart of audioPlayertask');
print('queue now is $queue');
// NOW HERE QUEUE COMES OUT TO BE AN EMPTY LIST
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
if (queue.length == 0) {
print('queue is found to be null.........');
}
_player.currentIndexStream.listen((index) {
if (index != null) AudioServiceBackground.setMediaItem(queue[index]);
});
// Propagate all events from the audio player to AudioService clients.
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
// Special processing for state transitions.
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
AudioService.currentMediaItem != queue.last
? AudioService.skipToNext()
: AudioService.stop();
break;
case ProcessingState.ready:
break;
default:
break;
}
});
// Load and broadcast the queue
print('queue is');
print(queue);
print('Index is $index');
print('MediaItem is');
print(queue[index]);
try {
if (queue[index].extras == null) {
queue[index] = queue[index].copyWith(extras: {
'URL': await fetchSongUrl(queue[index].id),
});
}
await AudioServiceBackground.setQueue(queue);
await _player.setUrl(queue[index].extras['URL']);
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
#override
Future<void> onSkipToQueueItem(String mediaId) async {
// Then default implementations of onSkipToNext and onSkipToPrevious will
// delegate to this method.
final newIndex = queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1) return;
_player.pause();
if (queue[newIndex].extras == null) {
queue[newIndex] = queue[newIndex].copyWith(extras: {
'URL': await fetchSongUrl(queue[newIndex].id),
});
await AudioServiceBackground.setQueue(queue);
// AudioService.updateQueue(queue);
}
await _player.setUrl(queue[newIndex].extras['URL']);
_player.play();
await AudioServiceBackground.setMediaItem(queue[newIndex]);
}
#override
Future<void> onUpdateQueue(List<MediaItem> queue) {
AudioServiceBackground.setQueue(queue = queue);
return super.onUpdateQueue(queue);
}
#override
Future<void> onPlay() => _player.play();
#override
Future<void> onPause() => _player.pause();
#override
Future<void> onSeekTo(Duration position) => _player.seek(position);
#override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
#override
Future<void> onRewind() => _seekRelative(-rewindInterval);
#override
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
#override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
#override
Future<void> onStop() async {
await _player.dispose();
_eventSubscription.cancel();
await _broadcastState();
// Shut down this task
await super.onStop();
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _player.position + offset;
// Make sure we don't jump out of bounds.
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
// Perform the jump via a seek.
await _player.seek(newPosition);
}
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin) {
_seeker = Seeker(_player, Duration(seconds: 10 * direction),
Duration(seconds: 1), mediaItem)
..start();
}
}
/// Broadcasts the current state to all clients.
Future<void> _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: [
MediaAction.seekTo,
MediaAction.seekForward,
MediaAction.seekBackward,
],
androidCompactActions: [0, 1, 3],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
);
}
AudioProcessingState _getProcessingState() {
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
}
This is the full code for AudioService in-case needed.
(Answer update: Since v0.18, this sort of pitfall doesn't exist since the UI and background code run in a shared isolate. The answer below is only relevant for v0.17 and earlier.)
audio_service runs your BackgroundAudioTask in a separate isolate. In the README, it is put this way:
Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService API to communicate with the background task, while your background task will only use the AudioServiceBackground API to interact with the UI and other clients.
The key point there is that isolates do not share memory. If you set a "global" variable in the UI isolate, it will not be set in the background isolate because the background isolate has its own separate block of memory. That is why your global queue variable is null. It is not actually the same variable, because now you actually have two copies of the variable: one in the UI isolate which has been set with a value, and the other in the background isolate which has not (yet) been set with a value.
Now, your background isolate does "later" set its own copy of the queue variable to something, and this happens via the message passing API where you pass the queue from the UI isolate into updateQueue and the background isolate receive that message and stores it into its own copy of the variable in onUpdateQueue. If you were to print out the queue after this point it would no longer be null.
There is also a line in your onStart where you are attempting to set the queue, although you should probably delete that code and let the queue only be set in onUpdateQueue. You should not attempt to access the queue in onStart since your queue won't receive its value until onUpdateQueue. If you want to avoid any null pointer exception before its set, you can initialise the queue in the background isolate to an empty list, and it will eventually get replaced by a non-empty list in onUpdateQueue without ever being null.
I would also suggest you avoid making queue a global variable. Global variables are generally bad, but in this case, it may actually be confusing you into thinking that that queue variable is the same in both the UI and the background isolate when in reality each isolate will have its own copy of the variable perhaps with different values. Thus, your code will be clearer if you make two separate "local" variables. One inside the UI and one inside the background task.
One more suggestion is that you should note that the methods in the message passing API are asynchronous methods. You should wait for the audio service to start before you send messages to it, such as setting the queue. AND you should wait for the queue to be set before you try to play from the queue:
await AudioService.start(....);
// Now the service has started, it is safe to send messages.
await AudioService.updateQueue(...);
// Now the queue has been updated, it is safe to play from it.

flutter firestore stream mapping to another stream

I have a menus collection on firestore and I want to perform a map operation on each document and return a new stream. So, instead of the Stream<QuerySnapShop>, I wanted Stream<VendorMenuItem>
Stream<VendorMenuItem> getAllVendorMenuItems(String vendorId) async* {
var collectionReference = fs.collection('restaurants').doc('$vendorId').collection("menus").snapshots();
collectionReference.map((event) {
print("mapping");
event.docs.forEach((element) {
return VendorMenuItem.fromMap(element.data());
});
});
}
and I am calling it within a build method just to test my approach, and I got nothing printed on the console, here is how I called it
#override
Widget build(BuildContext context) {
var fs = Provider.of<FireStoreDatabaseRoute>(context);
fs.getAllVendorMenuItems("ewP3B6XWNyqjM98GYYaq").listen((event) {
print("printing final result");
print(event.name);
});
Any clues? thank you
UPDATE:
I wasn't yielding anything, however the yield keyword didnt help
Stream<VendorMenuItem> getAllVendorMenuItems(String vendorId) async* {
var collectionReference = FirebaseFirestore.instance.collection('restaurants').doc('$vendorId').collection("menus").snapshots();
yield* collectionReference.map((event) => event.docs.map((e) => VendorMenuItem.fromMap(e.data())));
}
This is how you transform stream using the method you use.
Stream<List<VendorMenuItem>> getAllVendorMenuItems(String vendorId) async* {
var collectionReference =
FirebaseFirestore.instance.collection('Files').snapshots();
yield* collectionReference.map(
(event) => event.docs
.map(
(e) => VendorMenuItem.fromMap(e.data()),
)
.toList(), //Added to list to Match the type, other wise dart will throw an error something Like MappedList is not a sub type of List
);
}
This is a second way to achieve the same task using a stream controller.
Stream<List<VendorMenuItem>> getAllVendorMenuItems2(String vendorId) {
StreamController<List<VendorMenuItem>> controller =
StreamController<List<VendorMenuItem>>();
FirebaseFirestore.instance.collection("Files").snapshots().listen((event) {
controller.add(event.docs
.map(
(e) => VendorMenuItem.fromMap(e.data()),
)
.toList() //ToList To Match type with List
);
});
return controller.stream;
}
So the reason why it didn't work was I didnt realize the map function is only a middleware and therefore the async* is not required; here is an alternative to #Taha's solution
(without the use of a stream controller)
Stream<List<VendorMenuItem>> getAllVendorMenuItems(String vendorId) {
var snapshot = fs.collection('restaurants').doc(vendorId).collection('menus').snapshots();
return snapshot.map<List<VendorMenuItem>>((event) {
return event.docs.map((e) {
return VendorMenuItem.fromMap(e.data());
}).toList();
});
}

rxDart BehaviorSubject's value is not null on sink.addError

I'm new to using rxDart & bloc. I implement a transform to validate input. When I listen to the data on the sink it outputed correctly (null if error & value if no error), but when I print the value of the BehaviorSubject it wont represent null on error and prints the value that should be an error. Here is my code:
final _phoneNumberController = BehaviorSubject<String>();
Function(String) get setPhoneNumber => _phoneNumberController.sink.add;
Observable<String> get phoneNumberValue =>
_phoneNumberController.stream.transform(_validatePhoneNumber);
final _validatePhoneNumber = StreamTransformer<String, String>.fromHandlers(
handleData: (phoneNumber, sink) {
if (phoneNumber.length > 5 && isNumeric(phoneNumber)) {
sink.add(phoneNumber);
} else {
sink.addError(StringConstant.phoneNumberValidationErrorMessage);
}
});
void signUserIn() {
print(_phoneNumberController.stream.value); // Prints value that should be an error
}
SignInBloc() {
phoneNumberValue.listen((data) => print(data)); // Just Fine
}
You're listening to data but you don't listen to sink errors. change your code to:
SignInBloc() {
phoneNumberValue.listen((data) {
print(data);
},
onError: (_){
print(_.toString());
});
}