How to Test a StateNotifiers reaction to a Stream? - flutter

My Setup
I have a StateNotifier whose state is a #freezed class:
#freezed
class MyFreezedState with _$MyFreezedState{
const factory MyFreezedState({
required AsyncValue<int> asyncFreezedStateInt,
}) = MyFreezedState;
}
class MyStateNotifier extends StateNotifier<MyFreezedState>{
const MyStateNotifier(MyFreezedState state) : super(state);
}
Inside the StateNotifier I want to listen to a stream, and set the state's data accordingly, so my final setup looks like this :
final myStateNotifierProvider = Provider<MyStateNotifier, MyFreezedState>((ref) {
final intStream = ref.watch(myIntStreamProvider);
return MyStateNotifier(
MyFreezedState(
asyncFreezedStateInt: AsyncValue.loading(),
),
asyncIntStreamValue : intStream
);
});
class MyStateNotifier extends StateNotifier<MyFreezedState>{
MyStateNotifier (
MyFreezedState state, {
required this.asyncIntStreamValue,
}) : super(state) {
reactToIntStreamChanges();
}
final AsyncValue<int> asyncIntStreamValue;
void reactToIntStreamChanges(){
asyncIntStreamValue.when(
data: (intData) {
state = state.copyWith(asyncFreezedStateInt : AsyncValue.data(intData);
},
error: (err, stk) {
state = state.copyWith(asyncFreezedStateInt : AsyncValue.error(err,stk);
},
loading () {
state = state.copyWith(asyncFreezedStateInt : AsyncValue.loading();
}
}
}
Everything works as expected until here.
The Problem
The problem here is, that I don't know how to properly test the reaction to the stream.
This is my test setup:
void main() {
late ProviderContainer container;
late MockIntService mockIntService;
setUp((){
mockIntService = MockIntService();
container = ProviderContainer(
overrides: [myIntServiceProvider.overrideWithValue(mockIntService)],
);
when(() => mockIntService.myIntStream).thenAnswer((invocation) {
return Stream<int>.fromIterable([1]);
});
})
What I have tried
Doesn't work
//This test fails, the value is still an AsyncValue.loading()
test('should set the states value if stream emits value',(){
final stateValue = container.read(myStateNotifierProvider).asyncFreezedStateInt;
expect(stateValue, AsyncValue.data(1));
});
Works but is hacky and ugly
After doing some research I came up with this solution but I feel like there has to be a better way.
//This test passes, but I feel like this is a bad approach.
test('should set the states value if stream emits value',(){
fakeAsync((async){
container.read(myStateNotifierProvider);
async.elapse(const Duration(milliseconds:100);
final stateValue =
container.read(myStateControllerProvider).asyncFreezedStateInt;
expect(stateValue, AsyncValue.data(1));
});
});
}
EDIT
This is the condensed version of the service where the stream comes from :
final myIntStreamProvider = StreamProvider((ref) {
return ref.read(intServiceProvider).intStream;
});
final intServiceProvider = Provider<BaseTemplateService>((ref) {
return IntService();
});
class IntService {
Stream<Int> get intStream => firestore
.collection("intValues")
.snapshots();
}
Any suggestions are appreciated, thanks!

The firsts container.read() return always loading, because the container.watch() cannot be implemented.
I think it's cleaner this way:
test('should set the states value if stream emits value', () async {
var stateValue = container.read(myStateNotifierProvider);
expect(stateValue, const MyFreezedState.loading());
await Future.delayed(const Duration(seconds: 1));
stateValue = container.read(myStateNotifierProvider);
expect(stateValue, const MyFreezedState.data(AsyncValue.data(1)));
});
State:
#freezed
abstract class MyFreezedState with _$MyFreezedState {
const factory MyFreezedState.loading() = _Loading;
const factory MyFreezedState.data(AsyncValue<int> asyncValue) = _Data;
const factory MyFreezedState.error() = _Error;
}
I hope it's help you!

Related

How to prevent api being called multiple times on scroll controller

I have a scroll controller. I am making a request when the scroll position passes a defined value. The problem is, it is making thousands of requests when it overpasses the position. To prevent this I tried implementig a loading but it doesn't seem to be working.
Here's my scroll controller
#override
void initState() {
super.initState();
PetsBloc petsBloc = BlocProvider.of<PetsBloc>(context);
_scrollController.addListener(() {
final ScrollPosition position = _scrollController.position;
if (position.pixels >= position.maxScrollExtent - 500 &&
selectedView.isNotEmpty &&
!petsBloc.state.loading) {
//* Make the requeset
petsBloc.add(CollectionRequested(collection, page));
setState(() {
page++;
});
}
});
}
This is my pets_bloc.dart:
class PetsBloc extends Bloc<PetsEvent, PetsState> {
PetsRepository petsRepository = PetsRepository();
AlertsRepository alertsRepository = AlertsRepository();
InfoRepository infoRepository = InfoRepository();
PetsBloc() : super(const PetsState()) {
on<CollectionRequested>((event, emit) async {
if (!state.loading) {
emit(state.copyWith(loading: true));
final List<PetModel> result =
await petsRepository.getCollection(event.collection, event.page * 10);
switch (event.collection) {
case 'lost':
emit(state.copyWith(lostPets: [...state.lostPets, ...result]));
break;
case 'transit':
emit(state.copyWith(foundPets: [...state.foundPets, ...result]));
break;
case 'adoption':
emit(state.copyWith(adoptionPets: [...state.adoptionPets, ...result]));
break;
}
}
});
}
}
This is my pets_state.dart
part of 'pets_bloc.dart';
class PetsState extends Equatable {
final List<PetModel> lostPets;
final List<PetModel> adoptionPets;
final List<PetModel> foundPets;
final List<AlertModel> alertPets;
final List<UserPost> userPosts;
final bool loading;
final bool fetched;
const PetsState({
this.lostPets = const [],
this.adoptionPets = const [],
this.foundPets = const [],
this.alertPets = const [],
this.userPosts = const [],
this.loading = false,
this.fetched = false,
});
PetsState copyWith({
List<PetModel>? lostPets,
List<PetModel>? adoptionPets,
List<PetModel>? foundPets,
List<AlertModel>? alertPets,
List<UserPost>? userPosts,
bool? loading,
bool? fetched,
}) =>
PetsState(
lostPets: lostPets ?? this.lostPets,
adoptionPets: adoptionPets ?? this.adoptionPets,
foundPets: foundPets ?? this.foundPets,
alertPets: alertPets ?? this.alertPets,
userPosts: userPosts ?? this.userPosts,
loading: loading ?? this.loading,
fetched: fetched ?? this.fetched,
);
#override
List<Object> get props => [lostPets, adoptionPets, foundPets, alertPets, userPosts];
}
The request is still being made even though I have an if with petsBloc.state.loading in my initstate and inside the on an if(!state.loading)
I hope you can help me! Thanks in advance!
Here you could find an official example of how to throttle your events so that only a single event will be fired in a short period.
The main idea is to implement a custom event transformer:
EventTransformer<E> throttleDroppable<E>(Duration duration) {
return (events, mapper) {
return droppable<E>().call(events.throttle(duration), mapper);
};
}
Notice, that you need to add a bloc_concurrency package to your project dependencies.
Then, use this event transformer in your bloc:
class PetsBloc extends Bloc<PetsEvent, PetsState> {
PetsRepository petsRepository = PetsRepository();
AlertsRepository alertsRepository = AlertsRepository();
InfoRepository infoRepository = InfoRepository();
PetsBloc() : super(const PetsState()) {
on<CollectionRequested>((event, emit) async {
if (!state.loading) {
emit(state.copyWith(loading: true));
final List<PetModel> result =
await petsRepository.getCollection(event.collection, event.page * 10);
switch (event.collection) {
case 'lost':
emit(state.copyWith(lostPets: [...state.lostPets, ...result]));
break;
case 'transit':
emit(state.copyWith(foundPets: [...state.foundPets, ...result]));
break;
case 'adoption':
emit(state.copyWith(adoptionPets: [...state.adoptionPets, ...result]));
break;
}
}
},
transformer: throttleDroppable(Duration(milliseconds: 100)), // Use the new transformer
);
}
}
At the moment, all of the events within 100 milliseconds will be dismissed and only a single event will be fired. Feel free to adjust this duration value based on your needs.

How to compose async action and StateNotifierProvider?

I have some stream source (from FlutterReactiveBle library) and reflect it to state managed by StateNotifier.
But I can't sure whether it is right way from the following source. I'm especially afraid of _setState invalidates connectionProvider. And it looks like a bit complicated.
How can I improve this?
It may not work because I wrote it just for illustration.
#freezed
class DeviceConnections with _$DeviceConnections {
const DeviceConnections._();
const factory DeviceConnections({
Map<String, StreamSubscription<void>> connectings,
MapEntry<String, StreamSubscription<void>>? connected,
}) = _DeviceConnections;
}
class SimpleStateNotifier<T> extends StateNotifier<T> {
SimpleStateNotifier(super.state);
void update(T newState) {
state = newState;
}
}
StateNotifierProvider<SimpleStateNotifier<T>, T> simpleStateNotifierProvider<T>(
T initialState,
) {
return StateNotifierProvider<SimpleStateNotifier<T>, T>((ref) {
return SimpleStateNotifier(initialState);
});
}
class DeviceConnector {
DeviceConnector({
required FlutterReactiveBle ble,
required DeviceConnections state,
required Function(DeviceConnections) setState,
required Iterable<String> deviceIds,
}) : _ble = ble,
_state = state,
_setState = setState,
_deviceIds = deviceIds;
final FlutterReactiveBle _ble;
final DeviceConnections _state;
final Function(DeviceConnections) _setState;
final Iterable<String> _deviceIds;
void connect() {
final subscriptions = <String, StreamSubscription<void>>{};
for (final id in _deviceIds) {
subscriptions[id] = _connectInterval(id).listen((event) {});
}
_setState(_state.copyWith(connectings: subscriptions));
}
void disconnect() {
for (final subscription in _state.connectings.values) {
subscription.cancel();
}
_state.connected?.value.cancel();
_setState(DeviceConnections());
}
Stream<void> _connectInterval(String id) async* {
while (true) {
final connection = _ble.connectToDevice(
id: id,
connectionTimeout: Duration(seconds: 10),
);
await for (final update in connection) {
switch (update.connectionState) {
case DeviceConnectionState.connected:
final subscription = _state.connectings[id];
if (subscription != null) {
final others =
_state.connectings.entries.where((x) => x.key != id).toList();
for (final connection in others) {
connection.value.cancel();
}
_setState(
DeviceConnections(connected: MapEntry(id, subscription)),
);
}
break;
default:
break;
}
}
}
}
}
final connectionStateProvider = simpleStateNotifierProvider(
DeviceConnections(),
);
final bleProvider = Provider((_) => FlutterReactiveBle());
class AnotherState extends StateNotifier<List<String>> {
AnotherState(super.state);
}
final anotherStateNotifierProvider = StateNotifierProvider<AnotherState, List<String>>((ref) {
return AnotherState([]);
});
final connectionProvider = Provider((ref) {
final ble = ref.watch(bleProvider);
final connectorState = ref.watch(connectionStateProvider);
final connectorNotifier = ref.watch(connectionStateProvider.notifier);
final deviceIds = ref.watch(anotherStateNotifierProvider);
final connector = DeviceConnector(
ble: ble,
deviceIds: deviceIds,
state: connectorState,
setState: connectorNotifier.update,
);
ref.onDispose(connector.disconnect);
return connector;
});

how to mock the state of a StateNotifierProvider flutter

my test is throwing an exception because there is a StateNotifierProvider inside which is not overridden. for a regular Provider, i can override it using providerContainer, but for the state of a stateNotifierProvider, I don't know how to do it. I tried my best but I reached the limit of my best. I already saw this and this but it didn't help.
Appreciate much if someone could help me out of this. Thanks
My service File
class ReportService {
final Ref ref;
ReportService({
required this.ref,
});
Future<void> testReport() async {
//* How can i override this provider ?
final connection = ref.read(connectivityServiceProvider);
if (connection) {
try {
await ref.read(reportRepositoryProvider).testFunction();
} on FirebaseException catch (e, st) {
ref.read(errorLoggerProvider).logError(e, st);
throw Exception(e.message);
}
} else {
throw Exception('Check internet connection...');
}
}
}
final reportServiceProvider = Provider<ReportService>((ref) => ReportService(
ref: ref,
));
My test file
void main() {
WidgetsFlutterBinding.ensureInitialized();
final reportRepository = MockReportRepository();
ReportService makeReportService() {
final container = ProviderContainer(overrides: [
reportRepositoryProvider.overrideWithValue(reportRepository),
]);
addTearDown(container.dispose);
return container.read(reportServiceProvider);
}
test('test test', () async {
//How to stub the connectivityServiceProvider here ?
when(reportRepository.testFunction)
.thenAnswer((invocation) => Future.value());
final service = makeReportService();
await service.testReport();
verify(reportRepository.testFunction).called(1);
});
My StateNotifierProvider
class ConnectivityService extends StateNotifier<bool> {
ConnectivityService() : super(false);
}
final connectivityServiceProvider =
StateNotifierProvider<ConnectivityService, bool>(
(ref) => ConnectivityService());

Is there a way to access data coming from BLE device from single file which keeps updating?

I want to access characteristic values of BLE from one dart file, What I am doing is that I am connecting the device from one activity and then sending the device info to all other activities. But to get values I have to write the same code again and again to all activities/dart files.
For example i am connecting device in an activity like this:
StreamBuilder<List<ScanResult>>(
stream: FlutterBlue.instance.scanResults,
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map(
(r) => ScanResultTile(
result: r,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
r.device.connect();
print('DEVICE CONNECTED');
return BluetoothConnectedSuccess(device: r.device);
Here device: r.device is the device that i have connected to my Flutter App. Now if i want to display device data i have to initilaze these lines of code everytime i jump to any screen/activity:
class BluetoothConnectedSuccess extends StatefulWidget {
const BluetoothConnectedSuccess({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
#override
_BluetoothConnectedSuccessState createState() =>
_BluetoothConnectedSuccessState();
}
class _BluetoothConnectedSuccessState extends State<BluetoothConnectedSuccess> {
// BLE
final String SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
final String CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
bool isReady;
Stream<List<int>> stream;
List<int> lastValue;
List<double> traceDust = List();
#override
void initState() {
super.initState();
isReady = false;
connectToDevice();
}
connectToDevice() async {
await widget.device.connect();
discoverServices();
}
discoverServices() async {
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) {
characteristic.setNotifyValue(!characteristic.isNotifying);
stream = characteristic.value;
print(stream);
lastValue = characteristic.lastValue;
print(lastValue);
setState(() {
isReady = true;
});
}
});
}
});
}
_dataParser(List<int> data) {
var value = Uint8List.fromList(data);
print("stream.value: $value"); // stream.value: [33]
var hr = ByteData.sublistView(value, 0, 1);
print("Heart rate: ${hr.getUint8(0)}");
return hr.getUint8(0); // Heart rate: 33
}
It's creating a lot of mess to write the same code again and again to the activities where BLE data is needed.
Is there a way to only call this connected device from a single file instead of initializing the same code in every activity?
This is the link to my repo for a look at what I am doing on every activity/screen with BLE device data.
Please help me out as I am new to Flutter. Thank you
Firstly learn basic of state management using Get you can refer to my code here what happens is every time something changes or upadtes it will immediate show in UI using specific Get Widgets like Obx and GetX , these widgets listen to changes in value which are marked with obs (observable).
for exmaple :
Obx(
() => ble.isScan.value ? LinearProgressIndicator() : SizedBox(),
),
this will observe the changes in isScan value .
class BleServices extends GetxController {
FlutterBlue blue = FlutterBlue.instance;
BluetoothDevice d;
var connectedDevices = List<BluetoothDevice>().obs;
var scanResults = List<ScanResult>().obs;
var bleState = BluetoothState.off.obs;
var isScan = false.obs;
var scanRequire = false.obs;
var bluetoothServices = List<BluetoothService>().obs;
var discoveringServices = false.obs;
var characteristics = List<BluetoothCharacteristic>().obs;
#override
void onInit() async {
super.onInit();
final perb = await Permission.bluetooth.status.isGranted;
final perL = await Permission.location.status.isGranted;
if (perb && perL) {
getConnectedDevices();
} else {
await Permission.bluetooth.request();
await Permission.location.request();
}
isScanning();
state();
}
isDiscovering() async {}
getConnectedDevices() async {
final connectedDevice = await blue.connectedDevices;
connectedDevices.value = connectedDevice;
AppLogger.print('connected devices : $connectedDevice');
if (connectedDevice.length == 0) {
scanRequire.value = true;
searchDevices();
}
return connectedDevice;
}
searchDevices() {
// AppLogger.print('pppppppppppppp');
blue
.scan(timeout: Duration(seconds: 20))
.distinct()
.asBroadcastStream()
.listen((event) {
AppLogger.print(event.toString());
scanResults.addIf(!scanResults.contains(event), event);
});
Future.delayed(Duration(seconds: 20), () {
blue.stopScan();
Get.showSnackbar(GetBar(
message: 'scan is finished',
));
});
}
isScanning() {
blue.isScanning.listen((event) {
AppLogger.print(event.toString());
isScan.value = event;
});
}
state() {
blue.state.listen((event) {
AppLogger.print(event.toString());
bleState.value = event;
});
}
}

Flutter Riverpod : How to Implement FutureProvider?

I using Flutter Riverpod package to handling http request. I have simple Http get request to show all user from server, and i using manage it using FutureProvider from Flutter Riverpod package.
API
class UserGoogleApi {
Future<List<UserGoogleModel>> getAllUser() async {
final result = await reusableRequestServer.requestServer(() async {
final response =
await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
}
User Provider
class UserProvider extends StateNotifier<UserGoogleModel> {
UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
Future<UserGoogleModel> searchUserByIdOrEmail({
String idUser,
String emailuser,
String idOrEmail = 'email_user',
}) async {
final result = await _userGoogleApi.getUserByIdOrEmail(
idUser: idUser,
emailUser: emailuser,
idOrEmail: idOrEmail,
);
UserGoogleModel temp;
for (var item in result) {
temp = item;
}
state = UserGoogleModel(
idUser: temp.idUser,
createdDate: temp.createdDate,
emailUser: temp.emailUser,
imageUser: temp.emailUser,
nameUser: temp.nameUser,
tokenFcm: temp.tokenFcm,
listUser: state.listUser,
);
return temp;
}
Future<List<UserGoogleModel>> showAllUser() async {
final result = await _userGoogleApi.getAllUser();
state.listUser = result;
return result;
}
}
final userProvider = StateNotifierProvider((ref) => UserProvider());
final showAllUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.showAllUser();
return result;
});
After that setup, i simply can call showAllUser like this :
Consumer((ctx, read) {
final provider = read(showAllUser);
return provider.when(
data: (value) {
return ListView.builder(
itemCount: value.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final result = value[index];
return Text(result.nameUser);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error $error'),
);
}),
it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.
Let's say , i have another http get to show specific user from id user or email user. Then API look like :
API
Future<List<UserGoogleModel>> getUserByIdOrEmail({
#required String idUser,
#required String emailUser,
#required String idOrEmail,
}) async {
final result = await reusableRequestServer.requestServer(() async {
final baseUrl =
'${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getUserByIdOrEmail';
final chooseURL = idOrEmail == 'id_user'
? '$baseUrl?id_or_email=$idOrEmail&id_user=$idUser'
: '$baseUrl?id_or_email=$idOrEmail&email_user=$emailUser';
final response = await http.get(chooseURL);
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
User Provider
final showSpecificUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.searchUserByIdOrEmail(
idOrEmail: 'id_user',
idUser: usrProvider.state.idUser, // => warning on "state"
);
return result;
});
When i access idUser from userProvider using usrProvider.state.idUser , i got this warning.
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
It's similiar problem with my question on this, but on that problem i already know to solved using read(userProvider.state) , but in FutureProvider i can't achieved same result using ref(userProvider).
I missed something ?
Warning: This is not a long-term solution
Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.
class A extends StateNotifier<B> {
...
static final provider = StateNotifierProvider((ref) => A());
getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
final a = ref.read(A.provider);
final t = a.getState();
print(t);
});
Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.