I need to check the connectivity in every page inside my application using connectivity library,
So i will use a cubit inside the provider. the question is when to close the stream to make it possible to dispose it when the user close the app?
just like this:
import 'package:connectivity/connectivity.dart';
#override
dispose() {
super.dispose();
subscription.cancel();
}
1. Make sure you have imported flutter_bloc and connectivity_plus in your pubspec.yaml.
2. Create an InternetCubit files:
internet_cubit.dart
internet_state.dart
3. internet_state.dart:
Here we create enum with connection types for our cubit and cubit states:
part of 'internet_cubit.dart';
enum ConnectionType {
wifi,
mobile,
}
#immutable
abstract class InternetState {}
class InternetLoading extends InternetState {}
class InternetConnected extends InternetState {
final ConnectionType connectionType;
InternetConnected({#required this.connectionType});
}
class InternetDisconnected extends InternetState {}
4. internet_cubit.dart:
Cubit depends on connectivity plugin, so we import it and create a stream subscription to be able to react on connection changes.
Also we define two methods emitInternetConnected and emitInternetDisconnected that will change actual cubit state.
Make sure to dispose of stream subscription properly.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:meta/meta.dart';
part 'internet_state.dart';
class InternetCubit extends Cubit<InternetState> {
final Connectivity connectivity;
StreamSubscription connectivityStreamSubscription;
InternetCubit({#required this.connectivity})
: assert(connectivity != null),
super(InternetLoading()) {
connectivityStreamSubscription =
connectivity.onConnectivityChanged.listen((connectivityResult) {
if (connectivityResult == ConnectivityResult.wifi) {
emitInternetConnected(ConnectionType.wifi);
} else if (connectivityResult == ConnectivityResult.mobile) {
emitInternetConnected(ConnectionType.mobile);
} else if (connectivityResult == ConnectivityResult.none) {
emitInternetDisconnected();
}
});
}
void emitInternetConnected(ConnectionType _connectionType) =>
emit(InternetConnected(connectionType: _connectionType));
void emitInternetDisconnected() => emit(InternetDisconnected());
#override
Future<void> close() {
connectivityStreamSubscription.cancel();
return super.close();
}
}
5. In your app main file create an instance of Connectivity plugin and pass it to your BlocProvider. Set up bloc consuming with your needs:
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_4/cubit/internet_cubit.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() => runApp(MyApp(connectivity: Connectivity()));
class MyApp extends StatelessWidget {
final Connectivity connectivity;
const MyApp({Key key, this.connectivity}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => InternetCubit(connectivity: connectivity),
child: MaterialApp(
title: 'Connectivity cubit',
home: Scaffold(
appBar: AppBar(
title: Text('Connectivity cubit spotlight'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<InternetCubit, InternetState>(
builder: (context, state) {
if (state is InternetConnected &&
state.connectionType == ConnectionType.wifi) {
return Text(
'Wifi',
style: TextStyle(color: Colors.green, fontSize: 30),
);
} else if (state is InternetConnected &&
state.connectionType == ConnectionType.mobile) {
return Text(
'Mobile',
style: TextStyle(color: Colors.yellow, fontSize: 30),
);
} else if (state is InternetDisconnected) {
return Text(
'Disconnected',
style: TextStyle(color: Colors.red, fontSize: 30),
);
}
return CircularProgressIndicator();
},
),
],
),
),
),
),
);
}
}
Related
I have an app where I'm emitting budgetLoaded() in several events. I have noticed that if I don't emit a different state with my budgetLoading even the app doesn't seem to respond to the change.
import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';
part 'budget_event.dart';
part 'budget_state.dart';
class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
final BudgetRepository budgetRepository;
BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
on<AppStarted>((event, emit) async {
emit(BudgetLoading());
if (await budgetRepository.hasBudget()) {
emit(BudgetLoaded(budget: budgetRepository.budget));
} else {
emit(BudgetLoaded(budget: budgetRepository.budget));
}
});
on<Withdraw>((event, emit) {
budgetRepository.budget.withdraw(event.amount);
emit(BudgetLoaded(budget: budgetRepository.budget));
});
on<Deposit>((event, emit) {
emit(BudgetLoading());
budgetRepository.budget.deposit(event.amount);
emit(new BudgetLoaded(budget: budgetRepository.budget));
});
}
}
This is an issue with the deposit event. This code works.
import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';
part 'budget_event.dart';
part 'budget_state.dart';
class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
final BudgetRepository budgetRepository;
BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
on<AppStarted>((event, emit) async {
emit(BudgetLoading());
if (await budgetRepository.hasBudget()) {
emit(BudgetLoaded(budget: budgetRepository.budget));
} else {
emit(BudgetLoaded(budget: budgetRepository.budget));
}
});
on<Withdraw>((event, emit) {
budgetRepository.budget.withdraw(event.amount);
emit(BudgetLoaded(budget: budgetRepository.budget));
});
on<Deposit>((event, emit) {
budgetRepository.budget.deposit(event.amount);
emit(new BudgetLoaded(budget: budgetRepository.budget));
});
}
}
This code does not work the state that is emitted is correct, I have unit tests to test this and I've done debugging.
This is the main page, the BlocProvider is in the Main file above the Material App so it should be global.
import 'package:budget_app/bloc/budget_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WalletScreen extends StatefulWidget {
const WalletScreen({Key? key}) : super(key: key);
#override
State<WalletScreen> createState() => _WalletScreenState();
}
class _WalletScreenState extends State<WalletScreen> {
#override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => onPressed(context),
child: Icon(Icons.add),
),
body: BlocBuilder<BudgetBloc, BudgetState>(
bloc: BlocProvider.of<BudgetBloc>(context),
builder: (context, state) {
if (state is BudgetLoaded) {
var budget = state.budget;
return SafeArea(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
child: Center(
child: Column(
children: [
Text(
'Current Balance:',
style: TextStyle(
color: Colors.white, fontSize: 25),
),
Text(
'${budget.numberFormatted}',
style: TextStyle(
color: Colors.green, fontSize: 50),
),
],
),
),
color: Colors.blueGrey,
),
),
),
],
),
),
);
} else {
return CircularProgressIndicator();
}
},
),
);
}
onPressed(BuildContext context) {
var budgetBloc = BlocProvider.of<BudgetBloc>(context);
budgetBloc.add(Deposit(100.00));
}
}
It's not a huge issue I just want to understand why it doesn't work.
EVENT
part of 'budget_bloc.dart';
abstract class BudgetEvent extends Equatable {
const BudgetEvent();
#override
List<Object> get props => [];
}
class AppStarted extends BudgetEvent {}
class Withdraw extends BudgetEvent {
final double amount;
const Withdraw(this.amount);
}
class Deposit extends BudgetEvent {
final double amount;
const Deposit(this.amount);
}
STATE
part of 'budget_bloc.dart';
abstract class BudgetState extends Equatable {
const BudgetState();
#override
List<Object> get props => [];
}
class BudgetInitial extends BudgetState {}
class BudgetLoading extends BudgetState {}
class BudgetLoaded extends BudgetState {
final Budget budget;
BudgetLoaded({required this.budget});
}
You can see in the source of emit that it won't publish a state if it is equal to the last one:
void emit(State state) {
try {
if (isClosed) {
throw StateError('Cannot emit new states after calling close');
}
if (state == _state && _emitted) return;
onChange(Change<State>(currentState: this.state, nextState: state));
_state = state;
_stateController.add(_state);
_emitted = true;
} catch (error, stackTrace) {
onError(error, stackTrace);
rethrow;
}
}
By default, == returns true if two objects are the same instance. This could lead to redundant state change notifications, so we usually override == and hashCode, to explicitly tell what counts as equal.
In your example Equatable overrides == and hashCode for you so you don't have to write boilerplate code. The problem is, your props list is empty, so every instance of the same class will be equal to each other. This is why you don't see a state change after the first one.
You should override the props in BudgetLoaded, so Equatable knows what should be compared:
class BudgetLoaded extends BudgetState {
final Budget budget;
BudgetLoaded({required this.budget});
#override
List<Object> get props => [budget];
}
And don't forget to update the Budget class too so that == and hashCode say they are equal, when you think they should be equal.
I am trying to use the BLoC library with the connectivity_plus plugin in Flutter. I have followed this post and this post to set up an Internet Cubit for the project. The code works fine when the app is started with the internet connection turned on.
However, with the internet connection turned off, if I kill the app and re-launch it or do a hot restart, the CircularProgressIndicator() is shown instead of Text("Internet Disconnected").
Turning the internet back on correctly shows Text("Internet Connected") widget. After this, If I turn off the internet connection again, this time around it correctly shows the Text("Internet Disconnected") widget.
Also, emitInternetDisconnected is not caught as an exception in the try catch block to update the app's state.
The problem with the CircularProgressIndicator() being always displayed with the internet disconnected occurs only when the app is re-launched or hot restarted. I cannot figure out the bug in my code. What should I do to fix my code? Thanks
This is the code in the internet_cubit.dart file
import 'dart:async';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'internet_enum.dart';
part 'internet_state.dart';
class InternetCubit extends Cubit<InternetState> {
final Connectivity? connectivity;
StreamSubscription? connectivityStreamSubscription;
InternetCubit({required this.connectivity}) : super(InternetLoading()) {
monitorInternetConnection();
}
void monitorInternetConnection() async {
connectivityStreamSubscription =
connectivity!.onConnectivityChanged.listen((connectivityResult) async {
try {
final result = await InternetAddress.lookup("example.com");
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
if (connectivityResult == ConnectivityResult.wifi) {
emitInternetConnected(ConnectionType.WiFi);
} else if (connectivityResult == ConnectivityResult.mobile) {
emitInternetConnected(ConnectionType.Mobile);
} else if (connectivityResult == ConnectivityResult.none) {
emitInternetDisconnected();
}
}
} on SocketException catch (_) {
emitInternetDisconnected();
}
});
}
void emitInternetConnected(ConnectionType _connectionType) =>
emit(InternetConnected(connectionType: _connectionType));
void emitInternetDisconnected() => emit(InternetDisconnected());
#override
Future<void> close() async {
connectivityStreamSubscription!.cancel();
return super.close();
}
}
This is the code in the internet_state.dart file
part of 'internet_cubit.dart';
abstract class InternetState {}
class InternetLoading extends InternetState {}
class InternetConnected extends InternetState {
final ConnectionType? connectionType;
InternetConnected({required this.connectionType});
}
class InternetDisconnected extends InternetState {}
This is the code in my main.dart file
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'internet_cubit.dart';
void main() {
runApp(
BlocProvider<InternetCubit>(
create: (_) => InternetCubit(connectivity: Connectivity()),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter BLoC Demo',
home: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("Flutter BLoC Demo"),
centerTitle: true,
backgroundColor: Colors.blue[900],
),
body: Center(
child: Builder(
builder: (context) {
return MaterialButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => ScreenOne()));
},
color: Colors.black,
textColor: Colors.white,
child: Text("Screen 1"));
},
),
),
),
),
);
}
}
class ScreenOne extends StatelessWidget {
const ScreenOne({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("Flutter BLoC Demo"),
centerTitle: true,
backgroundColor: Colors.blue[900],
),
body: Center(
child: BlocBuilder<InternetCubit, InternetState>(
builder: (_, state) {
if (state is InternetDisconnected) {
return Text("Internet disconnected");
} else if (state is InternetConnected) {
return Text("Internet connected");
}
return CircularProgressIndicator();
},
),
),
),
);
}
}
I suspected the problem to lie with my Internet connection at the network layer, so I tried a different approach. I will post my solution here so that it can be useful to someone with a similar problem.
First, I modified the internet_cubit.dart and internet_state.dart files like so:
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'internet_enum.dart';
part 'internet_state.dart';
class InternetCubit extends Cubit<InternetConnectionTypeState> {
final Connectivity? connectivity;
// ignore: cancel_subscriptions
StreamSubscription? internetConnectionTypeStreamSubscription;
InternetCubit({required this.connectivity}) : super(InternetConnectionTypeLoading()) {
monitorConnectionType();
}
void monitorConnectionType() async {
internetConnectionTypeStreamSubscription =
connectivity!.onConnectivityChanged.listen((connectivityResult) async {
if (connectivityResult == ConnectivityResult.wifi) {
emitConnectionType(ConnectionType.WiFi);
} else if (connectivityResult == ConnectivityResult.mobile) {
emitConnectionType(ConnectionType.Mobile);
}
});
}
void emitConnectionType(ConnectionType _connectionType) =>
emit(InternetConnectionType(connectionType: _connectionType));
#override
Future<void> close() async {
internetConnectionTypeStreamSubscription!.cancel();
return super.close();
}
}
part of 'internet_cubit.dart';
abstract class InternetConnectionTypeState {}
class InternetConnectionTypeLoading extends InternetConnectionTypeState {}
class InternetConnectionType extends InternetConnectionTypeState {
final ConnectionType? connectionType;
InternetConnectionType({required this.connectionType});
}
This is the internet_enum.dart file:
enum ConnectionType {
WiFi, Mobile
}
Next, I imported the internet_connection_checker package. I created a new cubit class called ConnectionCheckerCubit. Following the above code as a guide and the documentation for the internet_connection_checker package, here is the code for the connection_cubit.dart and the connection_state.dart file.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
part 'connection_state.dart';
class ConnectionCheckerCubit extends Cubit<ConnectionCheckerState> {
final InternetConnectionChecker? internetConnectionChecker;
ConnectionCheckerCubit({required this.internetConnectionChecker}) : super(InternetConnectionLoading()) {
monitorInternetConnection();
}
// ignore: cancel_subscriptions
StreamSubscription? internetConnectionStreamSubscription;
void monitorInternetConnection() async {
internetConnectionStreamSubscription = InternetConnectionChecker().onStatusChange.listen((status) {
switch (status) {
case InternetConnectionStatus.connected:
emitInternetConnectionConnected(InternetConnectionStatus.connected);
break;
case InternetConnectionStatus.disconnected:
emitInternetConnectionDisconnected();
break;
}
});
}
void emitInternetConnectionConnected(InternetConnectionStatus _internetConnectionStatus) =>
emit(InternetConnectionConnected(internetConnectionStatus: _internetConnectionStatus));
void emitInternetConnectionDisconnected() => emit(InternetConnectionDisconnected());
#override
Future<void> close() async {
internetConnectionStreamSubscription!.cancel();
return super.close();
}
}
part of 'connection_cubit.dart';
abstract class ConnectionCheckerState {}
class InternetConnectionLoading extends ConnectionCheckerState {}
class InternetConnectionConnected extends ConnectionCheckerState {
final InternetConnectionStatus? internetConnectionStatus;
InternetConnectionConnected({required this.internetConnectionStatus});
}
class InternetConnectionDisconnected extends ConnectionCheckerState {}
In the main.dart file, I used a MultiBlocProvider in the main() function as a wrapper for runApp. In the ScreenOne widget, I used a BlocBuilder widget for the InternetCubit and used context.watch<ConnectionCheckerCubit>().state to monitor the state of the ConnectionCheckerCubit. Also, I have added a AppBlocObserver class for debugging purposes. Here is the code for the main.dart file:
import 'dart:developer';
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_bloc_api/internet_enum.dart';
import 'connection_cubit.dart';
import 'internet_cubit.dart';
void main() {
Bloc.observer = AppBlocObserver();
runApp(
MultiBlocProvider(
providers: [
BlocProvider<ConnectionCheckerCubit>(
create: (_) => ConnectionCheckerCubit(internetConnectionChecker: InternetConnectionChecker()),
),
BlocProvider<InternetCubit>(
create: (_) => InternetCubit(connectivity: Connectivity()),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter BLoC Demo',
home: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("Flutter BLoC Demo"),
centerTitle: true,
backgroundColor: Colors.blue[900],
),
body: Center(
child: Builder(
builder: (context) {
return MaterialButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => ScreenOne()));
},
color: Colors.black,
textColor: Colors.white,
child: Text("Screen 1"));
},
),
),
),
),
);
}
}
class ScreenOne extends StatelessWidget {
const ScreenOne({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("Flutter BLoC Demo"),
centerTitle: true,
backgroundColor: Colors.blue[900],
),
body: Center(
child: Builder(
builder: (context) {
final connectionState = context.watch<ConnectionCheckerCubit>().state;
final internetTypeState = context.watch<InternetCubit>().state;
if (connectionState is InternetConnectionDisconnected)
return Text("Internet Disconnected");
else if (connectionState is InternetConnectionConnected){
if (internetTypeState is InternetConnectionType && internetTypeState.connectionType == ConnectionType.WiFi)
return Text("WiFi");
else
return Text("Mobile");
}
return CircularProgressIndicator();
}
)
),
),
);
}
}
class AppBlocObserver extends BlocObserver {
#override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
log('onChange: ${bloc.runtimeType}, ${bloc.state} \nCurrent state: ${change.currentState}\nNext state: ${change.nextState}');
}
#override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
log('onError(${bloc.runtimeType}, ${bloc.state}, $error, $stackTrace)');
super.onError(bloc, error, stackTrace);
}
#override
void onEvent(Bloc bloc, Object? event) {
log('onEvent(${bloc.state}, ${bloc.runtimeType}, $event)');
super.onEvent(bloc, event);
}
#override
void onTransition(Bloc bloc, Transition transition) {
log('onTransition(${bloc.state}, ${bloc.runtimeType}, ${transition.currentState}, ${transition.nextState})');
super.onTransition(bloc, transition);
}
#override
void onCreate(BlocBase bloc) {
log('onCreate(${bloc.state}, ${bloc.runtimeType})');
super.onCreate(bloc);
}
#override
void onClose(BlocBase bloc) {
log('onTransition(${bloc.state}, ${bloc.runtimeType})');
super.onClose(bloc);
}
}
Here is a Github link for more information.
add this library connectivity: ^3.0.6
and try this code in your class:-
_startNetworkTesting(BuildContext context) async {
var result = await (Connectivity().checkConnectivity());
if (result == ConnectivityResult.none) {
setState(() {
//perform your action
});
} else if (result == ConnectivityResult.mobile) {
setState(() {
//perform your action
});
} else if (result == ConnectivityResult.wifi) {
setState(() {
//perform your action
});
}
}
I am developing a Flutter app where it acts as a client, connecting to a server via an API.
The app makes requests and depending on the response it progresses the state.
My question is the following: Can I make a request, and then depending on the response, either update the UI or open a new page?
I have used FutureBuilder as shown below. The problem is that the FutureBuilder requires to return a UI. In my case, if the response is OK I want to open a new page (see //todo line).
I tried using Navigator.pushReplacement but it does not really work.
Any ideas?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'model.dart';
class Start extends StatefulWidget {
final String title;
Start({Key key, #required this.title}) : super(key: key);
#override
State<StatefulWidget> createState() => new StartState();
}
class StartState extends State<Start> {
Future<StartReply> _startReply;
_makeRequest() {
setState(() {
_startReply = ...; // actual API request
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: widget.title,
home: Scaffold(
appBar: AppBar(
title: Text(widget.title),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(false)
),
),
body: Center(
child: FutureBuilder(
future: _startReply,
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.none) {
return ElevatedButton(
onPressed: _makeRequest,
child: Text("Make request")
);
} else if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
// todo open page here
return Text('Started!', style: TextStyle(color: Colors.green, fontStyle: FontStyle.italic));
} else if(snapshot.hasError) {
debugPrint('StartReply: ${snapshot.data}');
return Text('Error (${snapshot.error})', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic));
} else {
return CircularProgressIndicator();
}
}
)
)
)
);
}
}
Yes, you should not use a FutureBuilder if you want to do anything other than changing the UI depending on the async task. You should manage your own async. Here's some code to get you started:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool loaded;
#override
void initState() {
super.initState();
asyncInit();
}
Future<void> asyncInit() async {
final response =
await doTheNetworkRequest() //imagine that this was an http request
if (yes) {
setState(() {
loaded = true;
});
} else {
Navigator.of(context).push(...);
}
}
#override
Widget build(BuildContext context) {
return loaded == true ? Text('Loaded') : Text('Loading');
}
}
I'm trying to implement a Widget Test in order to test a login form. This test depends on a bloc which I'm mocking by using MockBloc. However, it throws the following error:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK╞════════════════════════════════════════════════════
The following StateError was thrown running a test:
Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an
extension method?
I found a similar error in the following link, but I do not see how that can help me to solve my problem.
I also looked at the following file on gitlub, which is an example of a widget test by using bloc_test. The link can be found on the official website of the Bloc Library - specifically in Todos App in Flutter using the Bloc library.
However, that example is using bloc_test: ^3.0.1 while I'm using bloc_test: ^8.0.0, which can be found here.
Here is a minimal example:
LoginForm Widget
class LoginForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Form(
key: '_loginForm',
child: Column(
children: <Widget>[
...
BlocConsumer<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
...
},
builder: (context, state) {
if (state is AuthenticationInitial) {
...
} else if (state is LoggingIn || state is LoggedIn) {
...
} else if (state is Error) {
return Column(
children: <Widget>[
...
Message(
message: state.message,
messageContainerWidth: 290,
messageContainerHeight: 51,
),
...
],
);
}
}
),
],
),
);
}
}
Message Widget
class Message extends StatelessWidget {
final String message;
final double messageContainerWidth;
final double messageContainerHeight;
...
#override
Widget build(BuildContext context) {
return Container(
width: messageContainerWidth,
height: messageContainerHeight,
child: Center(
child: message != ""
? Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Color.fromRGBO(242, 241, 240, 1),
fontSize: 15,
),
)
: child,
),
);
}
}
Widget Test (I want to test that a Message is shown when the Authentication state is Error)
...
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
...
// Mocking my LoginUser usecase
class MockLoginUser extends Mock implements LoginUser {}
// Mocking my bloc
class MockAuthenticationBloc
extends MockBloc<AuthenticationEvent, AuthenticationState>
implements AuthenticationBloc {}
class AuthenticationStateFake extends Fake implements AuthenticationState {}
void main() {
MockLoginUser mockLoginUser;
setUpAll(() {
registerFallbackValue<AuthenticationState>(AuthenticationStateFake());
});
setUp(() {
mockLoginUser = MockLoginUser();
authenticationBloc = AuthenticationBloc(loginUser: mockLoginUser);
});
group('Login', () {
testWidgets(
'should show a Message when the Authentication state is Error',
(WidgetTester tester) async {
whenListen(
authenticationBloc,
Stream.fromIterable(
[
LoggingIn(),
Error(
message: 'Some error message',
),
],
),
initialState: AuthenticationInitial(),
);
final widget = LoginForm();
await tester.pumpWidget(
BlocProvider<AuthenticationBloc>(
create: (context) => authenticationBloc,
child: MaterialApp(
title: 'Widget Test',
home: Scaffold(body: widget),
),
),
);
await tester.pumpAndSettle();
final messageWidget = find.byType(Message);
expect(messageWidget, findsOneWidget);
});
});
}
I will really appreciate it if someone can help me to solve the error, or can let me know another way to implement the widget tests.
Thanks in advance!
I solved the problem, I would like to share the answer, in case someone finds out the same problem.
First of all, this link was really helpful.
The solution was to change the Widget Test in the following way:
...
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
...
class MockAuthenticationBloc
extends MockBloc<AuthenticationEvent, AuthenticationState>
implements AuthenticationBloc {}
class AuthenticationStateFake extends Fake implements AuthenticationState {}
class AuthenticationEventFake extends Fake implements AuthenticationEvent {}
void main() {
group('Login', () {
setUpAll(() {
registerFallbackValue<AuthenticationState>(AuthenticationStateFake());
registerFallbackValue<AuthenticationEvent>(AuthenticationEventFake());
});
testWidgets(
'should show a Message when the Authentication state is Error',
(WidgetTester tester) async {
// arrange
final mockAuthenticationBloc = MockAuthenticationBloc();
when(() => mockAuthenticationBloc.state).thenReturn(
LoggingIn(), // the desired state
);
// find
final widget = LoginForm();
final messageWidget = find.byType(Message);
// test
await tester.pumpWidget(
BlocProvider<AuthenticationBloc>(
create: (context) => mockAuthenticationBloc,
child: MaterialApp(
title: 'Widget Test',
home: Scaffold(body: widget),
),
),
);
await tester.pumpAndSettle();
// expect
expect(messageWidget, findsOneWidget);
});
});
}
I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. The retrieval is done by a class called "ProductData" As soon as it's ready, I want to navigate from the splash page to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation and listens to a Provider.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Unfortunately, the code below gives me the error, "setState() or markNeedsBuild() called during build."
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';=
import 'package:provider/provider.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
bool shouldProceed = false;
#override
Widget build(BuildContext context) {
shouldProceed =
Provider.of<ProductData>(context, listen: true).shouldProceed;
if (shouldProceed) {
Navigator.pushNamed(context, 'home'); <-- The error occurs when this line is hit.
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
}
Is there a better way to navigate to a page based on listening to the results of a provider?
Instead of trying to navigate to a new view what you should do is display the loading splash screen if you are still waiting for data and once that changes display your main home view, like this:
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
bool shouldProceed = Provider.of<ProductData>(context, listen: true).shouldProceed;
#override
Widget build(BuildContext context) {
if(shouldProceed){
return Home();
}else{
return RouteSplash();
}
}
}
Use BlocListener as in this example:
BlocListener(
bloc: BlocProvider.of<DataBloc>(context),
listener: (BuildContext context, DataState state) {
if (state is Success) {
Navigator.of(context).pushNamed('/details');
}
},
child: BlocBuilder(
bloc: BlocProvider.of<DataBloc>(context),
builder: (BuildContext context, DataState state) {
if (state is Initial) {
return Text('Press the Button');
}
if (state is Loading) {
return CircularProgressIndicator();
}
if (state is Success) {
return Text('Success');
}
if (state is Failure) {
return Text('Failure');
}
},
}
)
Source: https://github.com/felangel/bloc/issues/201
I think I have a solution that does what the OP wants. If you make your splash screen to be Stateful, then you can add a PostFrameCallback. This avoids any problems with Navigator being called when build is running. Your callback can then call whatever routine Provider needs to read the data. This read data routine can be passed a further callback which contains the Navigator command.
In my solution I've added a further callback so that the splash screen is visible for at least one second (you can choose what duration you think reasonable here). Unfortunately, this creates a race condition, so I need to import the synchronized package to avoid problems.
import 'package:flutter/material.dart';
import 'package:reflect/utils/constants.dart';
import 'category_screen.dart';
import 'package:provider/provider.dart';
import 'package:reflect/data_models/app_prefs.dart';
import 'dart:async';
import 'dart:core';
import 'package:synchronized/synchronized.dart';
class LoadingScreen extends StatefulWidget {
static const id = 'LoadingScreen';
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
bool readPrefsDone = false;
bool timeFinished = false;
Lock _lock = Lock();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AppPrefs>(context, listen: false).readPrefs(readDone);
Timer(Duration(seconds: 1), () {
timerDone();
});
});
}
void timerDone() async {
_lock.synchronized(() {
if (readPrefsDone) {
pushMainScreen();
}
timeFinished = true;
});
}
void readDone() {
_lock.synchronized(() {
if (timeFinished) {
pushMainScreen();
}
readPrefsDone = true;
});
}
void pushMainScreen() {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, animation2) => CategoryScreen(),
transitionDuration: Duration(seconds: 1),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: kFishLogoTag,
child: Image(
image: AssetImage('assets/fish_logo.png'),
),
),
SizedBox(
height: 30,
),
Text(
'Reflect',
style: TextStyle(
fontSize: 30,
color: Color(0xFF0000cc),
fontWeight: FontWeight.bold,
),
),
],
),
),
));
}
}
Anyone else facing this issue can use this code
Future.delayed(Duration.zero, () => Navigate.toView(context));
This navigates to the other screen without build errors