How to implement socket_io_client to flutter_bloc with freezed?
socket_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:socket_io_client/socket_io_client.dart';
part 'socket_bloc.freezed.dart';
part 'socket_event.dart';
part 'socket_state.dart';
class SocketBloc extends Bloc<SocketEvent, SocketState> {
late final Socket _socket;
SocketBloc() : super(SocketState.initial()) {
_socket = io(
'http://localhost:1337',
OptionBuilder()
.setTimeout(3000)
.setReconnectionDelay(5000)
.disableAutoConnect()
.build(),
);
_socket.onConnecting((data) => add(_SocketConnectingEvent()));
_socket.onConnect((_) => add(_SocketOnConnect()));
_socket.onConnectError((data) => add(_SocketConnectErrorEvent()));
_socket.onConnectTimeout((data) => add(_SocketConnectTimeoutEvent()));
_socket.onDisconnect((_) => add(_SocketOnDisconnect()));
_socket.onError((data) => add(_SocketErrorEvent()));
_socket.on('joined', (data) => add(_SocketJoinedEvent()));
// User events
on<_SocketConnect>((event, emit) {
_socket.connect();
});
on<_SocketDisconnect>((event, emit) {
_socket.disconnect();
});
// Socket events
on<_SocketConnectingEvent>((event, emit) {
emit(SocketState.connected("Connecting"));
});
on<_SocketOnConnect>((event, emit) {
emit(SocketState.connected(_socket.id!));
});
on<_SocketConnectErrorEvent>((event, emit) {
emit(SocketState.connected("Connection Error"));
});
on<_SocketConnectTimeoutEvent>((event, emit) {
emit(SocketState.connected("Connection timeout"));
});
on<_SocketOnDisconnect>((event, emit) {
emit(SocketState.disconnected());
});
on<_SocketErrorEvent>((event, emit) {
emit(SocketState.connected("ErrorEvent"));
});
on<_SocketJoinedEvent>((event, emit) {
emit(SocketState.connected("JoinedEvent"));
});
}
#override
Future<void> close() {
_socket.dispose();
return super.close();
}
}
socket_event.dart
part of 'socket_bloc.dart';
#freezed
class SocketEvent with _$SocketEvent {
const factory SocketEvent.connect() = _SocketConnect;
const factory SocketEvent.connecting() = _SocketConnectingEvent;
const factory SocketEvent.onConnect() = _SocketOnConnect;
const factory SocketEvent.onConnectError() = _SocketConnectErrorEvent;
const factory SocketEvent.onConnectTimeout() = _SocketConnectTimeoutEvent;
const factory SocketEvent.onError() = _SocketErrorEvent;
const factory SocketEvent.onJoined() = _SocketJoinedEvent;
const factory SocketEvent.disconnect() = _SocketDisconnect;
const factory SocketEvent.onDisconnect() = _SocketOnDisconnect;
}
socket_state.dart
part of 'socket_bloc.dart';
#freezed
class SocketState with _$SocketState {
const factory SocketState.initial() = _SocketInitial;
const factory SocketState.connected(String status) = _SocketConnected;
const factory SocketState.disconnected() = _SocketDisonnected;
}
Also dont forget to run flutter pub run build_runner build --delete-conflicting-outputs
Related
I'm using mockito for testing, riverpod for state management. I'm trying to test the method in my controller class but getting the FakeUsedError:
FakeUsedError: 'execute' No stub was found which matches the argument
of this method call: execute(Instance of 'AuthUseCaseInput').
I'm calling AuthUseCase class method from the AuthController class.
class AuthController extends StateNotifier<AuthState> {
final AuthUseCase authUseCase;
AuthController(this.authUseCase) : super(const AuthState.initial());
Future<void> mapAuthEventToAuthState(AuthEvent event) async {
state = const AuthState.loading();
await event.map(
signInWithEmailAndPassword: (signInWithEmailAndPassword) async {
final result = await authUseCase.execute(AuthUseCaseInput(
signInWithEmailAndPassword.email,
signInWithEmailAndPassword.password));
await result.fold(
(failure) async => state = AuthState.error(failure),
(login) async => state = const AuthState.loggedIn(),
);
});
}
The test class code is given below
void main() {
late AuthUseCase mockAuthUseCase;
late Login login;
late AuthUseCaseInput authUseCaseInput;
late AuthController authController;
setUpAll(() {
mockAuthUseCase = MockAuthUseCase();
login = LoginModel.fromJson(
json.decode(
jsonReader('helpers/dummy_data/login_success_response.json'),
),
).toEntity();
authUseCaseInput = AuthUseCaseInput(email, password);
when(mockAuthUseCase.execute(authUseCaseInput)).thenAnswer(
(_) async => Right(login),
);
authController = AuthController(mockAuthUseCase);
});
group('Auth Controller', () {
stateNotifierTest<AuthController, AuthState>(
'[AuthState.loggedIn] when sign in is success',
setUp: () async {
when(mockAuthUseCase.execute(authUseCaseInput))
.thenAnswer(
(_) async => Right(login),
);
},
actions: (notifier) => notifier.mapAuthEventToAuthState(
const SignInWithEmailAndPassword(email, password)),
expect: () => [const AuthState.loading(), const AuthState.loggedIn()],
build: () {
return authController;
});
});
}
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());
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!
I am trying to learn bloc and I am writing simple unit tests. I am trying to test the auth events but I am facing the error below. Inside my app when I trigger an event, I don't get any errors and everything seems to work fine, so why am I getting error here? Am I missing something, could anyone advise?
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
final AuthenticationRepository _authRepository;
late StreamSubscription<AuthStatus> _authSubscription;
AuthenticationBloc(
{required AuthenticationRepository authenticationRepository})
: _authRepository = authenticationRepository,
super(const AuthenticationState()) {
on<AuthStateChanged>(_onAuthStatusChanged);
on<AuthenticationLogoutRequested>(_onLogoutRequested);
_authSubscription = _authRepository.status
.listen((status) => add(AuthStateChanged(authStatus: status)));
}
enum AuthStatus { unknown, authenticated, unauthenticated }
class AuthenticationRepository {
final _controller = StreamController<AuthStatus>();
Stream<AuthStatus> get status => _controller.stream;
Future<void> logIn({
required String username,
required String password,
}) async {
await Future.delayed(
const Duration(milliseconds: 300),
() => _controller.add(AuthStatus.authenticated),
);
}
void logOut() {
_controller.add(AuthStatus.unauthenticated);
}
void dispose() => _controller.close();
}
class AuthenticationState extends Equatable {
final AuthStatus status;
final User? user;
const AuthenticationState({this.status = AuthStatus.unknown, this.user});
#override
List<Object?> get props => [user, status];
}
void main() {
late AuthenticationBloc authenticationBloc;
MockAuthenticationRepository authenticationRepository = MockAuthenticationRepository();
setUp((){
authenticationBloc = AuthenticationBloc(authenticationRepository: authenticationRepository);
});
group('AuthenticationEvent', () {
group('Auth status changes', () {
test('User is unknown', () {
expect(authenticationBloc.state.status, AuthStatus.unknown);
});
test('User is authorized', () {
authenticationBloc.add(AuthStateChanged(authStatus: AuthStatus.authenticated));
expect(authenticationBloc.state.status, AuthStatus.authenticated);
});
test('User is unauthorized', () {
authenticationBloc.add(AuthStateChanged(authStatus: AuthStatus.unauthenticated));
expect(authenticationBloc.state.status, AuthStatus.unknown);
});
});
});
}
It is most likely that you have not created a stub for a function in your mock class. This is generally the cause of the NULL return. This can be achieved by using when() from the mockito package. https://pub.dev/packages/mockito.
Also there is a bloc testing package that may be useful to look into. https://pub.dev/packages/bloc_test
Below is an example of how I implemented it. Hope this helps.
blocTest<ValidateUserBloc, ValidateUserState>('Validate User - Success',
build: () {
when(mockSettingsRepo.validateUser(url, authCode)).thenAnswer(
(_) async => User(
success: true, valid: true, url: url, authCode: authCode));
return validateUserBloc;
},
seed: () => ValidateUserState(user: User(url: url, authCode: authCode)),
act: (bloc) => bloc.add(ValidateUserAuthoriseEvent()),
expect: () => <ValidateUserState>[
ValidateUserState(
status: ValidUserStatus.success,
user: User(
success: true, valid: true, url: url, authCode: authCode))
]);
so I'm having this really weird bug connecting to the backend ws server.
flutter 2.10.3
dart 2.16.1
socket_io_client ^2.0.0-beta.4-nullsafety.0
If I start the app, and go through the login process, it doesn't connect, doesn't show any error, or any event. there's nothing at all. (but it sometimes works, it's totally random but let's say it does 1/10 times)
And if I reload the app, it connects successfully. also when I add breakpoints in the code, it works fine.
// ignore_for_file: avoid_print, cascade_invocations
import 'package:bloc/bloc.dart';
import 'package:dealize/core/helpers/flutter_secure_storage_helper.dart';
import 'package:dealize/features/chat/data/models/channel_model.dart';
import 'package:dealize/features/chat/data/models/message_model.dart';
import 'package:dealize/features/chat/domain/entities/channel.dart';
import 'package:dealize/features/chat/domain/entities/message.dart';
import 'package:equatable/equatable.dart';
import 'package:socket_io_client/socket_io_client.dart' as io;
part 'chat_event.dart';
part 'chat_state.dart';
class ChatBloc extends Bloc<ChatEvent, ChatState> {
ChatBloc({
required this.socketUrl,
required this.secureStorageHelper,
}) : super(
const ChatState(
channels: <Channel>[],
messages: <Message?>[],
),
) {
on<Connect>(
(event, emit) async {
try {
final accessToken = await secureStorageHelper.read('accessToken');
_socket = io.io(
socketUrl,
io.OptionBuilder()
.setTransports(['websocket'])
.setAuth(
<String, dynamic>{
'token': accessToken,
},
)
.disableAutoConnect()
.build(),
);
_socket
..onAny((event, dynamic data) {
print('EVENT ===> $event');
print('DATA ===> $data');
})
..onDisconnect((dynamic data) {
print('Disconnected: $data');
})
..on('channel', (dynamic data) async {
final channel =
ChannelModel.fromJson(data as Map<String, dynamic>);
add(AddedChannel(channel: channel));
})
..on('participant', (dynamic data) async {
print('Participant ===> $data');
// todo find participant and update it
// todo add updatedChannel event
})
..on('message', (dynamic data) async {
final message =
MessageModel.fromJson(data as Map<String, dynamic>);
add(AddedMessage(message: message));
add(MessageDelivered());
})
..on('exception', (dynamic data) async {
print('Error ===> $data');
// todo show toast message
});
_socket.connect();
} catch (e) {
print(e.toString());
}
},
);
on<AddedChannel>(
(event, emit) =>
emit(state.copyWith(channels: [...state.channels, event.channel])),
);
on<AddedMessage>(
(event, emit) {
final currentChannelIndex = state.channels.indexWhere(
(channel) => channel.id == event.message.channel,
);
final currentChannel = state.channels.removeAt(currentChannelIndex);
final messageIndex = currentChannel.messages
.indexWhere((element) => element!.id == event.message.id);
if (messageIndex == -1) {
currentChannel.messages.add(event.message);
} else {
currentChannel.messages[messageIndex] = event.message;
}
emit(
state.copyWith(
channels: [...state.channels, currentChannel],
),
);
},
);
on<CreateChannel>((event, emit) {
final phone = '+${state.dialCode}${state.phone}';
_socket.emit('create_channel', {
'phone': phone,
});
});
on<CreateMessage>((event, emit) {
_socket.emit('create_message', {
'channelId': event.channelId,
'type': event.type.name,
'text': state.newMessage,
});
});
on<MessageDelivered>((event, emit) {
_socket.emit('message_delivered', {
'': '',
});
});
on<MessageRead>((event, emit) {
_socket.emit('message_read', {
'': '',
});
});
on<PhoneNumberUpdated>(
(event, emit) => emit(state.copyWith(phone: event.phone)),
);
on<DialCodeUpdated>(
(event, emit) => emit(state.copyWith(dialCode: event.dialCode)),
);
on<NewMessageUpdated>(
(event, emit) => emit(state.copyWith(newMessage: event.message)),
);
on<EnterChannel>((event, emit) {
emit(
state.copyWith(
channelId: event.channelId,
messages: state.channels
.firstWhere((element) => element.id == event.channelId)
.messages,
),
);
add(MessageRead());
});
on<ExitChannel>(
(event, emit) => emit(
state.copyWith(
channelId: '',
messages: [],
),
),
);
}
final String socketUrl;
final FlutterSecureStorageHelper secureStorageHelper;
late final io.Socket _socket;
}
If anyone has an idea it would be really helpful, or a better way to implement socket io connection in flutter through BLoC