flutter: BehaviorSubject not rebuild widget - flutter

I am trying to use Rxdart on my statless widget:
class SimplePrioritySelectWidget extends StatelessWidget {
BehaviorSubject<List<String>> _valueNotifier =
BehaviorSubject<List<String>>.seeded([]);
I wrap my widget by StreamBuilder:
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _valueNotifier.stream,
initialData: options,
builder: (context, snapshot) {
print("rebuild");
return Padding(
padding: const EdgeInsets.only(top: 25),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 16.h,
),
I have a custom drop down widget, I don't know why, when I add a string inside _valueNotifier the builder method not called? and my widget not rebuilded? What is wrong?
CustomDropdown(
dropdownMenuItemList: options,
enableBorderColor: Color(PRIMARY_COLOR_2),
onChanged: (value) {
_valueNotifier.value.add(value);
},
),

I totally agree that the
you need to use sink in _valueNotifier
CustomDropdown(
dropdownMenuItemList: options,
enableBorderColor: Color(PRIMARY_COLOR_2),
onChanged: (value) {
_valueNotifier.sink.add([value]);
},
),

Mutating the value won't notify BehaviorSubject of anything. In order to make BehaviorSubject notify its listeners, you need to provide a different state object.
CustomDropdown(
dropdownMenuItemList: options,
enableBorderColor: Color(PRIMARY_COLOR_2),
onChanged: (value) {
Sta
_valueNotifier.value = [..._valueNotifier.value, value];
// or _valueNotifier.add([..._valueNotifier.value, value]);
},
),
Also, a BehaviorSubject is state and should not be created in a StatelessWidget. If you try anyway, the subject will be created (with the same initial value) every time your widget is rebuilt.

Related

How to display a Cubit state modified in a Widget into another Widget?

Im developing a Shopping cart using the BLoC pattern and I got stuck trying to learn the subset Cubit. My main question is how can I display the state of a previously updated Cubit? My flow is the next...
On the Product Screen I increase/decrease the items I want to use.
To push to change the state, I click a button and send the items as a parameter to the Cubit function.
The item list gets updated and I want to get it into another widget that is outside of the Product Screen.
Here is the code:
main.dart
void main() {HttpOverrides.global = new MyHttpOverrides();
runApp(
RepositoryProvider<AuthenticationService>(
create: (context) {
return AuthService();
},
child: MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>(
create: (context) {
final authService = RepositoryProvider.of<AuthenticationService>(context);
return AuthenticationBloc(authService)..add(AppLoaded());
}
),
BlocProvider<CartCubit>(create: (context) => CartCubit())
],
child: MyApp(),
),
)
);
}
product_screen.dart
BlocBuilder<CartCubit, List<Item>>(
builder: (context, state) {
return Row(
children: [
_shoppingItem(0),
SizedBox(
width: SizeConfig.blockSizeHorizontal * 2,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Color(0xFF48AD71),
),
child: IconButton(
onPressed: () {
for (int i = 1; i <= counter; i++) {
items.add(widget.item);
print('something');
}
context.read<CartCubit>().addToList(items);
},
icon: Icon(
Icons.shopping_bag),
color: Colors.white,
),
),
],
);
},
)
cart_cubit.dart
class CartCubit extends Cubit<List<Item>> {
CartCubit() : super([]);
void addToList(List<Item> items) {
state.addAll(items);
emit(state);
print(state);
}
}
What I should add on my Cart Screen so I can get the value of the Cubit State? Also, do this should be better handled by using a bloc instead of cubit?
Edit: Based on the comment of Loren.A I removed the BlocBuilder of my ProductScreen and I added it to my CartScreen.
class _CartScreenState extends State<CartScreen> {
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return SingleChildScrollView(
child: BlocBuilder<CartCubit, List<Item>>(
builder: (context, state) {
return Column()
...
...
Align(
alignment: Alignment.topRight,
child: Text(
state.length.toString(), // this is not updating
textAlign: TextAlign.end,
style: TextStyle(
fontFamily: 'SinkinSans',
fontSize: 12.0,
fontWeight: FontWeight.w400,
color: Color(0xFFC9C9C9)),
),
),
Cubit is fine for this. You can update widgets anywhere in your app based on that list just by adding another BlocBuilder<CartCubit, List<Item>>. So just add another one to your Cart page and do what ya gotta do inside of it. It will reflect the previous changes.
Edit: Just noticed that it doesn't appear that you're actually rebuilding any widgets in your product screen so that BlocBuilder you have is not really doing anything. You can remove that and just use it where you need to reflect that value of the list. You can still fire that addToList method without being inside a BlocBuilder.

Flutter : using changeNotifier and provider when the context is not available

I'm trying to use the simple state management described in the Flutter docs, using a ChangeNotifier, a Consumer, and a ChangeNotifierProvider.
My problem is that I can't get a hold a on valid context to update my model (details below...). I get an error:
Error: Error: Could not find the correct Provider above this CreateOrganizationDialog Widget
This likely happens because you used a BuildContext that does not include the provider of your choice. There are a few common scenarios:
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that CreateOrganizationDialog is under your MultiProvider/Provider.
This usually happen when you are creating a provider and trying to read it immediately.
Here are extracts of my code:
class OrganizationModel extends ChangeNotifier {
final List<Organization> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Organization> get items => UnmodifiableListView(_items);
void addList(List<Organization> items) {
_items.addAll(items);
notifyListeners();
}
}
This is my model.
class OrganizationBodyLayout extends StatelessWidget {
Future<void> _showCreateOrganizationDialog() async {
return showDialog<void>(
context: navigatorKey.currentState.overlay.context,
barrierDismissible: false,
child: CreateOrganizationDialog());
}
_onCreateOrganizationPressed() {
_showCreateOrganizationDialog();
}
_onDeleteOrganizationPressed() {
//TODO something
}
_onEditOrganizationPressed() {
//TODO something
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(mainAxisSize: MainAxisSize.max, children: [
ButtonBar(
alignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
onPressed: _onCreateOrganizationPressed,
child: Text("New Organization"),
),
],
),
Expanded(
child: Container(
color: Colors.pink,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: ChangeNotifierProvider(
create: (context) => OrganizationModel(),
child: OrganizationListView(),
)),
Expanded(child: Container(color: Colors.brown))
]))),
]));
}
}
A stateless widget that contains a ChangeNotifierProvider just on top of the list widget using the model.
On a button click, a modal dialog is shown, then data is fetched from the network. I should then update my model calling the addList operation.
Below is the code for the stateful dialog box.
class CreateOrganizationDialog extends StatefulWidget {
#override
_CreateOrganizationDialogState createState() =>
_CreateOrganizationDialogState();
}
class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
TextEditingController _nametextController;
TextEditingController _descriptionTextController;
#override
initState() {
_nametextController = new TextEditingController();
_descriptionTextController = new TextEditingController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 200,
height: 220,
child: Column(
children: [
Text('New organization',
style: Theme.of(context).textTheme.headline6),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
decoration: new InputDecoration(hintText: "Organization name"),
controller: _nametextController,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
decoration:
new InputDecoration(hintText: "Organization description"),
controller: _descriptionTextController,
),
),
ButtonBar(
children: [
FlatButton(
child: new Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: new Text("Create"),
onPressed: () {
setState(() {
Future<Organization> organization =
backendCreateOrganization(_nametextController.text,
_descriptionTextController.text);
organization.then((value) {
Future<List<Organization>> organizations =
backendReloadOrganizations();
organizations.then((value) {
var model = context.read<OrganizationModel>();
// var model = navigatorKey.currentState.overlay.context.read<OrganizationModel>();
//model.addList(value);
});
});
});
Navigator.of(context).pop();
//context is the one for the create dialog here
},
)
],
)
],
),
));
}
}
My problem happens at the line
var model = context.read<OrganizationModel>();
Thinking of it, the context available here is the modal dialog box context - so it's kind of logical that the Provider is not found in the widget tree.
However, I can't see how to retrieve the proper context (which would be the one for the result list view, where the Provider is located) in order to get the model and then update it.
Any idea is welcome :-)
Solved (kind of).
The only way I've found to solve this is by making my model a global variable:
var globalModel = OrganizationModel();
And referencing this global model in all widgets that consume it. I can't find a way to find the context of a stateless widget from within a callback in another stateful widget.
It works, but it's ugly. Still open to elegant solutions here :-)
Get_it seems to be elegant way of sharing models across the application. Please check the documentation for the different use cases they provide.
You could do something like the following
GetIt getIt = GetIt.instance;
getIt.registerSingleton<AppModel>(AppModelImplementation());
getIt.registerLazySingleton<RESTAPI>(() =>RestAPIImplementation());
And in other parts of your code, you could do something like
var myAppModel = getIt.get<AppModel>();

TextField focus stuck when dropdown button is selected

I have a text field and a drop-down menu, both controlled by a Bloc. The problem I have is that as soon as the text field gets selected, it won't give up the focus if the user then tries to select something from the dropdown menu. The menu appears and then disappears an instant later and the focus is still on the text field.
Here is a basic app that demonstrates the problem:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Textfield Focus Example',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
FormBloc formBloc = FormBloc();
final List<DropdownMenuItem> userMenuItems = ['Bob', 'Frank']
.map((String name) => DropdownMenuItem(
value: name,
child: Text(name),
))
.toList();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// user - drop down menu
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('To: '),
StreamBuilder<String>(
stream: formBloc.selectedUser,
builder: (context, snapshot) {
return DropdownButton(
items: userMenuItems,
value: snapshot.data,
onChanged: formBloc.selectUser);
}),
],
),
// amount - text field
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Amount: '),
Container(
width: 100.0,
child: StreamBuilder<double>(
stream: formBloc.billAmount,
builder: (context, snapshot) {
return TextField(
keyboardType: TextInputType.number,
onChanged: formBloc.newBillAmount,
);
})),
],
),
],
),
),
);
}
}
class FormBloc {
StreamController<String> _selectedUserController = StreamController<String>();
Stream<String> get selectedUser =>
_selectedUserController.stream;
Function get selectUser => _selectedUserController.sink.add;
//Amount
StreamController<double> _billAmountController = StreamController<double>();
Stream<double> get billAmount =>
_billAmountController.stream;
void newBillAmount(String amt) =>
_billAmountController.sink.add(double.parse(amt));
void dispose() {
_selectedUserController.close();
_billAmountController.close();
}
}
Do I manually need to declare the FocusNode for the textField and tell it when to give up focus? Or is there some other reason that the text field is hogging all the attention?
Add this line of code to your TextField: focusNode: FocusNode(canRequestFocus: false).
This should prevent your TextField from requesting focus after clicking on the dropdown.
Code:
// amount - text field
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Amount: '),
Container(
width: 100.0,
child: StreamBuilder<double>(
stream: formBloc.billAmount,
builder: (context, snapshot) {
return TextField(
focusNode: FocusNode(canRequestFocus: false)
keyboardType: TextInputType.number,
onChanged: formBloc.newBillAmount,
);
})),
],
)
Sorry for late answer you need to add following code in onTapListener of DropDownButton Widget.It will remove focus on text field when you select in drop down menu or click outside of screen.Thanks
FocusScope.of(context).requestFocus(new FocusNode());
This issue has solved and merged to flutter master channel https://github.com/flutter/flutter/pull/42482
This gets around the fact that we can't currently have a dropdown and a text field on the
same page because the keyboard disappearing when the dropdown gets focus causes a metrics change, and the dropdown immediately disappears when activated.
The answer above is correct only in the case you don't want to call requestFocus() method. But in my case it was a chatting app and I wanted the textfield to get focused when the message is swiped. And if set the boolean parameter canRequestFocus, false. Then I am not able to do it.
In the chatPage appbar i was using a popupmenu which was causing the same problem (getting focused unintentionally.)
So, what worked for me is, in the top most of the method onSelected(String str) of popupmenu I called this statement :
messageFocusNode.nextFocus(); //messageFocusNode is the focusNode of the TextField.
Although I don't why and how, this worked for me. I am new to flutter, if you know the reason please update my answer.

Flutter - How to flip the previous card back using FlipCard

After days of search I'm getting help.
I work on a flutter application.
Context:
A grid view feeded with Json
-childs : GridTile with Flipcard in (https://pub.dev/packages/flip_card)
-On tap on GridTile there is a callback to get the selected Item and an animation because of the flipcard onTap
What I would:
When an item is aleready selected (flipcard flipped so we show the back of the card),
And I selected another item of the grid te(so flipcard of this itme also flipped)
I would like to flip back the old selected item Flipcard without rebuild the tree because I would lost the state of the new selected item.
I tried many thing. For example I tried to use GlobalKey on GridTiles to interract with after build but currentState is always null when I want to interact with.
I wonder what is the good practice in this case ?
I hope I was clear :) (I'm french)
Thank you the community!
.
Something to know...
It is possible to interract with the flipcard (child of gridtile) like this
(GlobalKey)
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
#override
Widget build(BuildContext context) {
return FlipCard(
key: cardKey,
flipOnTouch: false,
front: Container(
child: RaisedButton(
onPressed: () => cardKey.currentState.toggleCard(),
child: Text('Toggle'),
),
),
back: Container(
child: Text('Back'),
),
);
}
I'm not sure if I understood your question, but here is an example of how you could use a GridView with FlipCards:
var cardKeys = Map<int, GlobalKey<FlipCardState>>();
GlobalKey<FlipCardState> lastFlipped;
Widget _buildFlipCard(String text, Color color, int index) {
return SizedBox(
height: 120.0,
child: Card(
color: color,
child: Center(
child:
Text(text, style: TextStyle(color: Colors.white, fontSize: 20.0)),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FlipCards")),
body: GridView.builder(
itemCount: 20,
itemBuilder: (context, index) {
cardKeys.putIfAbsent(index, () => GlobalKey<FlipCardState>());
GlobalKey<FlipCardState> thisCard = cardKeys[index];
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlipCardWithKeepAlive(
child: FlipCard(
flipOnTouch: false,
key: thisCard,
front: _buildFlipCard("$index", Colors.blue, index),
back: _buildFlipCard("$index", Colors.green, index),
onFlip: () {
if (lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
},
),
),
RaisedButton(
child: Text("Flip Card"),
onPressed: () => cardKeys[index].currentState.toggleCard(),
)
],
);
},
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
),
);
}
class FlipCardWithKeepAlive extends StatefulWidget {
final FlipCard child;
FlipCardWithKeepAlive({Key key, this.child}) : super(key: key);
#override
State<StatefulWidget> createState() => FlipCardWithKeepAliveState();
}
class FlipCardWithKeepAliveState extends State<FlipCardWithKeepAlive>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
#override
bool get wantKeepAlive => true;
}
You need to use a different key for each element of the list, I used a Map in this case.
I also wrapped the FlipCard with a custom FlipCardWithKeepAlive stateful widget that uses AutomaticKeepAliveClientMixin to keep alive the FlipCard while scrolling.
Edit: I updated the code so when you flip one card, the previous card flipped gets flipped back. Basically you need to save the last flipped card and when a new one is flipped, flip the last one and put the new one as last flipped.
The code will make both cards flip at the same time, if you want one card to wait the other use onFlipDone() instead of onFlip(), like this:
onFlipDone: (isFront) {
bool isFlipped = !isFront;
if (isFlipped && lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
}

How can I change the state of widget based on the action of another widget?

I am having trouble with the following problem:
In this page, I have a CheckBoxListTile and two radioButtons.
I need to implement some rules, like these one:
If any checkbox gets checked, disabled and unselect all radiobuttons
If any radiobuttons gets selected, uncheck and disable all checkboxes
However, I can't get this to work. I've tried using statefulWidgets for both checkbox and radiobutton, but the logic was getting very messy.
I've tried to use a stateful widget for the PostView class, but since I'm using a futureBuilder there, whenever I called setState(), the whole page would be rebuilt and the checkbox would not get checked.
I believe there is a cleaner solution to this, but right now I can't see it.
Here is the snippet of the code so you can see ( this is using a stateless widget, I know I cannot do it this way, but It is just to show you guys what I am trying to do ):
Widget _buildFooter(Post postInfo) {
return Container(
child: Column(
children: <Widget>[
_buildRadioButtons(postInfo),
RaisedButton(
child: Text('press'),
onPressed: () {},
),
],
),
);
}
class PostView extends StatelessWidget {
final String id;
PostView(this.id);
Post postInfo;
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppBar(),
body: FutureBuilder(
future: ApiService.getpostInfo(id),
builder: (BuildContext context, AsyncSnapshot snapShot) {
if (snapShot.connectionState == ConnectionState.done) {
if (snapShot.hasError) {
return Center(
child: Text('error'),
);
}
postInfo = postInfo.fromJson(snapShot.data);
return ListView.separated(
separatorBuilder: (BuildContext context, int index) {
return Divider();
},
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return _buildHeader(postInfo);
}
if (index == (postInfo.authors.length + 1)) {
return _buildFooter(postInfo);
}
index = index - 1;
return _buildCheckboxListTile(postInfo, index);
},
itemCount: (postInfo.authors.length) + 2,
);
}
return CircularProgressIndicator();
},
),
);
}
}
_buildCheckboxListTile(Post postInfo, index) {
return CheckboxListTile(
title: Text(postInfo.authors[index].name),
value: postInfo.authors[index].checked,
onChanged: (bool value) {
***Here I need to disabled the radiobuttons in case I get a true
},
);
}
_buildRadioButtons(Post postInfo) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: CheckboxListTile(
value: postInfo.value1,
title: Text('value1'),
onChanged: (bool value) {
*** here I need to uncheck all the checkboxes mentioned above
in case the radio button is selected
},
),
),
Expanded(
child: CheckboxListTile(
value: postInfo.value2,
title: Text('value2'),
onChanged: (bool value) {},
),
),
],
);
}
Would this behaviour be possible without a statefulWidget?
Since I'm learning Flutter, any other advices regarding the code are welcomed!
Thanks!
You could try State Management libraries to easily manage state without the code getting messy. Here is a great article on the best state management libraries to use:
https://medium.com/flutter-community/flutter-app-architecture-101-vanilla-scoped-model-bloc-7eff7b2baf7e
In the case of Scoped Model, which for me is the simplest,and which I personally use, you can set both to be a child of a ScopedModel widget and have them notify each other once the state changes.