I have a Widget which uses bloc builder to map the different state of widget.
class BodyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<NewsBloc, NewsState>(builder: (context, state) {
return state.map(
.....
);
});
}
....
}
The BodyWidget is created in a Widget with BlocProvider.
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
And the NewsBloc is defined as
#injectable
class NewsBloc extends Bloc<NewsEvent, NewsState> {
final GetNews getNews;
NewsBloc({
#required this.getNews,
}) : super(const _Initial());
#override
Stream<NewsState> mapEventToState(
NewsEvent event,
) async* { ... }
}
I am using get_it and injectable for Dependency Injection.
Now I am trying to write a simple widget test for BodyWidget and I am not so sure how to inject all these dependency in test.
class MockBuildContext extends Mock implements BuildContext {}
class MockNewsBloc extends Mock implements NewsBloc {}
void main() {
ForYouNewsTab _widget;
MockBuildContext _context;
NewsBloc _newsBloc;
Widget makeTestableWidgets({Widget child}) {
return MaterialApp(
home: Scaffold(
// body: BlocProvider(
// create: (_context) => getIt<NewsBloc>(),
// child: child,
// ),
body: child,
),
);
}
setUp(() {
_context = MockBuildContext();
_widget = ForYouNewsTab();
});
test('ForYouNewsTab is sub class of StatelessWidget', () {
expect(_widget, isA<StatelessWidget>());
});
testWidgets('should return sized box for initial state',
(WidgetTester tester) async {
await tester.pumpWidget(makeTestableWidgets(child: _widget));
});
}
I did search in stackoverflow, but could not found a solution that works form me.
I solved my issue by following very basic steps. Not so sure if its the right way. Anyway if anyone ever comes to the same problem, it might help them.
class MainPage extends StatelessWidget {
//added line
final NewsBloc newsBloc;
const MainPage({
Key key,
#required this. newsBloc,
})
#override
Widget build(BuildContext context) {
return BlocProvider(
// changed line
// create: (context) => getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
create: (context) => newsBloc..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
Now in my test case I can create MockNewsBloc and inject it easily to the MainPage when it is under testing.
Related
I am making a list of stateless widget as shown below and passing the id as the parameter to the widgets.
Code for cartPage:-
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
bool loading=true;
List<CartTile> cartTiles=[];
#override
void initState() {
super.initState();
if(currentUser!=null)
getData();
}
getData()async
{
QuerySnapshot snapshot=await cartReference.doc(currentUser.id).collection('cartItems').limit(5).get();
snapshot.docs.forEach((doc) {
cartTiles.add(CartTile(id: doc.data()['id'],index: cartTiles.length,));
});
setState(() {
loading=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: loading?Center(child:CircularProgressIndicator():SingleChildScrollView(
child: Column(
children: cartTiles,
),
),
);
}
}
Code for CartTile:-
class CartTile extends StatelessWidget {
final String id;
CartTile({this.id,});
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productReference.doc(id).snapshots(),
builder: (context,snapshot)
{
//here am using the snapshot to build the cartTile.
},
);
}
}
So, my question is whenever I will call setState in my homepage then will the stateless widget be rebuilt and increase my document reads. Because i read somewhere that when we pass the same arguments or parameters to a stateless widget then due to its cache mechanism it doesn't re build. If it will increase my reads then is there any other way to solve this problem?
Minimal reproducible code:
void main() => runApp(FooApp());
class FooApp extends StatefulWidget {
#override
_FooAppState createState() => _FooAppState();
}
class _FooAppState extends State<FooApp> {
bool _showPage2 = false;
void _onPressed(bool value) => setState(() => _showPage2 = value);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Navigator(
onPopPage: (route, result) => route.didPop(result),
pages: [
MaterialPage(child: Page1(onPressed: _onPressed)),
if (_showPage2) MaterialPage(child: Page2()),
],
),
);
}
}
class Page1 extends StatelessWidget {
final ValueChanged<bool> onPressed;
const Page1({Key key, this.onPressed}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page1')),
body: ElevatedButton(
onPressed: () => onPressed(true),
child: Text('Page2'),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
void dispose() {
super.dispose();
print('dispose');
}
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}
didPop:
When this function returns true, the navigator removes this route from the history but does not yet call dispose. Instead, it is the route's responsibility to call NavigatorState.finalizeRoute, which will in turn call dispose on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
But in my example, you can see without having to call NavigatorState.finalizeRoute in Page2, dispose method does get called contradiciting Docs.
It's done internally when using MaterialPage/_PageBasedMaterialPageRoute. You can poke around in the code starting in the Navigator class, which appears to lead up to this OverlayRoute class. If you do want to trace through yourself, it wasn't a walk in the park for me and you'll have to pay close attention to how each class is related.
This class has the finishedWhenPopped getter, which is true by default. And if you look at the didPop override implementation right below the getter definition, didPop will internally call finalizeRoute when finishedWhenPopped is true.
Implementation from OverlayRoute class
#protected
bool get finishedWhenPopped => true;
#override
bool didPop(T? result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
if (finishedWhenPopped)
navigator!.finalizeRoute(this);
return returnValue;
}
This is true only for at least MaterialPage/_PageBasedMaterialPageRoute. Other implementations don't necessarily do this.
I have a Stream Provider (connected to firebase) that is not working. I am guessing that the problem lies in the fact that I am using a named navigator [Navigator.pushNamed(context, '/route',)]. I guess this makes the 'route' widget to not be the son of the widget that calls it. Let me show it better below.
My app structure is as follows:
My main widget which handles routing and receives the Stream with user authentication (there is no problem here):
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
routes: {
'/home': (context) => Wrapper(),
'/edit_profile': (context) => UserProfile() //This is where I am having trouble.
}
),
);
}
}
The Wrapper that validates if the user is authenticated and acts accordingly:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
// return either the Home or Authenticate widget
if (user == null){
return Authenticate();
} else {
return HomeWrapper();
}
}
}
The HomeWrapper which receives the second stream and redirects to the widget I am having trouble with:
class HomeWrapper extends StatefulWidget {
#override
_HomeWrapperState createState() => _HomeWrapperState();
}
class _HomeWrapperState extends State<HomeWrapper> {
String currentBodyName = 'home';
Widget currentBodyWidget = Home();
#override
Widget build(BuildContext context) {
Widget _drawerOptions = Row(
children: [
FlatButton(child: someChild, onPressed: () {Navigator.pushNamed(context, '/edit_profile',);},), //This is the actual call to the navigator.
],
);
return StreamProvider<Map>.value( //This is the problematic Stream!
value: DatabaseService().userDetail,
child: Scaffold(
//Body
body: currentBodyWidget,
//I am simplifying this to show the most important parts
bottomNavigationBar: myBottomNavigationBar(
buttons: <Widget>[
FlatButton(
icon: someIcon,
onPressed: () => _onItemTapped('home'),
),
FlatButton(
icon: otherIcon,
onPressed: () => _onItemTapped('second_screen'),
),
],)
//Drawer
drawer: Drawer(child: _drawerOptions,), //This one has the call to the problematic edit_profile route.
);
}
void _onItemTapped(String newBodyName) {
if (newBodyName != currentBodyName){
setState(() {
currentBodyName = newBodyName;
switch(newBodyName) {
case 'home': {
currentBodyWidget = Home();
}
break;
case 'second_screen': {
currentBodyWidget = SecondScreen();
}
break;
default: {
currentBodyWidget = Home();
}
break;
}
});
}
}
}
Finally the edit_profile route calls the UserProfile Widget which looks like this:
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
//This is where the error occurs!!
final userDocument = Provider.of<Map>(context) ?? [];
print(userDocument);
return Scaffold(body: Container());
}
}
This is the error that it throws:
The following ProviderNotFoundError was thrown building UserProfile(dirty, state: _UserProfileState#09125):
Error: Could not find the correct Provider<Map<dynamic, dynamic>> above this UserProfile Widget
Thank you very much!!
Turns out my approach was wrong.
Instead of wrapping the HomeWrapper with the StreamProvider, hoping that it would pass the data to the next route (UserProfile ), what I did was to wrap the UserProfile widget with a StreamProvider, as follows:
(Note: I changed the Map StreamProvider for a UserData StreamProvider.)
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
return Scaffold(
body: Container(
//My Widget here
);
} else
return Loading();
});
}
}
This series was very helpful: https://www.youtube.com/playlist?list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC
In my app, I have a model that store the user logged in my app.
class AuthenticationModel extends ChangeNotifier {
User _user;
User get user => _user;
void authenticate(LoginData loginData) async {
// _user = // get user from http call
notifyListeners();
}
void restoreUser() async {
//_user = // get user from shared prefs
notifyListeners();
}
}
The model is registered at the top of the widget tree :
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthenticationModel(),
child: MaterialApp(
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
),
);
}
}
Somewhere down the widget tree, I have a button that calls the Model :
child: Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
return MyCustomButton(
text: 'Connect',
onPressed: () {
authModel.authenticate(...)
},
);
},
),
Now, I would like, somewhere, listen to the changes on the AuthenticationModel to trigger a Navigator.pushReplacmentNamed('/home') when the user is not null in the model.
I tried to do it in the builder of Prehome :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
if (authModel.user != null) {
Navigator.of(context).pushReplacementNamed("/home")
}
return Container(
child: // Prehome UI
);
},
);
}
}
but I have a error when doing it like this :
════════ (2) Exception caught by widgets library ═══════════════════════════════════════════════════
setState() or markNeedsBuild() called during build.
The relevant error-causing widget was:
Consumer<AuthenticationModel> file:///Users/pierre.degand/Projects/cdc/course_du_coeur/lib/Prehome.dart:13:12
═══════════════════════════════════════════════════════════════════════════════
How can I setup such a listener ? Is it a good practice to trigger navigation on model changes like this ?
Thanks
EDIT: I found a way to make this work. Instead of using Consumer inside the PrehomeScreen builder, I used the following code :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
Provider.of<AuthenticationModel>(context).addListener(() {
Navigator.of(context).pushReplacementNamed("/home");
});
return Container(
child: // UI
);
}
}
It works fine, the navigation is executed when the model changes. But there is an error message in the console (printed 3 times) :
════════ (4) Exception caught by foundation library ════════════════════════════════════════════════
Looking up a deactivated widget's ancestor is unsafe.
════════════════════════════════════════════════════════════════════════════════════════════════════
The app does not crash so, for now, I'm ok with this.
I still want to know if this is a good approach or not.
I prefer to use Stream or rxdart PublishSubject BehaviourSubject for listening to any activity or to manage global app data.
I implement it using bloc pattern. Basically bloc pattern is just like redux for react means creating a central dataset that contains all app data and you don't have to do prop drilling.
You can create Stream like this.
import 'package:rxdart/rxdart.dart';
class AbcBloc {
BehaviorSubject<bool> _connectivity;
AbcBloc() {
_connectivity = BehaviorSubject<bool>();
}
// stream
Stream<bool> get connectivity => _connectivity.stream;
// sink
Function(bool) get updateConnectivity => _connectivity.sink.add;
dispose(){
_connectivity.close();
}
}
void createAbcBloc() {
if (abcBloc != null) {
abcBloc.dispose();
}
abcBloc = AbcBloc();
}
AbcBloc abcBloc = AbcBloc();
now you can access that abcBloc variable from anywhere and listen to connectivity variable like this
import './abcBloc.dart';
void listenConnectivity(){
abcBloc.connectivity.listen((bool connectivety){
here you can perform your operations
});
}
and you can update connectivity from abcBloc.updateConnectivity(false);
every time you perform any changes that listener will get called.
remember you have to call listenConnectivity() one time to get it activated;
void main() {
Provider.debugCheckInvalidValueType = null;
return runApp(
Provider(
create: (_) => AuthenticationModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalKey<NavigatorState>();
Provider.of<AuthenticationModel>(context).addListener(() {
final authModel = Provider.of<AuthenticationModel>(context);
if (authModel.user != null) {
navigatorKey.currentState.pushReplacementNamed("/home");
}
});
return MaterialApp(
navigatorKey: navigatorKey,
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
);
}
}
I don't think ChangeNotifier is needed.
void main() async {
final isLoggedIn = await Future.value(true); // get value from shared prefs or your model
runApp(MyApp(isLoggedIn));
}
class MyApp extends StatelessWidget {
MyApp(this.isLoggedIn);
final bool isLoggedIn;
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: isLoggedIn ? '/home' : '/',
routes: {
'/': (context) => HomeScreen(),
'/login': (context) => LoginScreen()
},
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Logout'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/login"),
);
}
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Login'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/"),
);
}
}
We want to show an AlertDialog after some asynchronous processing such as network processes.
When calling 'showAlertDialog ()' from an external class, I want to call it without context. Is there a good way?
class SplashPage extends StatelessWidget implements SplashView {
BuildContext _context;
#override
Widget build(BuildContext context) {
this._context = context;
...
}
I've considered the above method, but I'm worried about side issues.
Help
My current code
class SplashPage extends StatelessWidget implements SplashView {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyStoreColors.eats_white1_ffffff,
body: Center(
child: new SvgPicture.asset('assets/ic_splash.svg'),
),
);
}
#override
void showAlertDialog() {
showDialog<void>(
context: /*How to get context?*/,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Not in stock'),
content: const Text('This item is no longer available'),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
#override
void moveToHomeContainer() {
}
#override
void moveToLoginContainer() {
}
}
To show an AlertDialog you need the context, but in StatelessWidget you do not have access to it directly as in StatefulWidget.
Few options are [1]:
passing it as GlobalKey [2]
passing build context as parameter to any other function inside StatelessWidget
use a service to inject the dialog without context [3]
Cheers.
You should trigger rebuild when the async event complete, either convert your widget to StatefulWidget and call setState() or use a state management solution like Bloc.
For example using StatefulWidget your code will look like this:
class SplashPage extends StatefulWidget {
#override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> implements SplashView {
bool _asynOpDone = false;
/// Call this when the async operation is done.
void _onAsynOpDone() => setState(() => _asyncOpDone = true);
#override
Widget build(BuildContext context) {
if (_asyncOpDone) showAlertDialog(context);
return Scaffold(
...,
///
);
}
#override
void showAlertDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: ...,
);
}
}
You can apply Builder pattern concept to simplify this.
There is a little example here.
button_builder.dart