I am using Provider to ensure data consistency between widgets. I just learned that the level of declared Provider object is important when accessing data in widgets. So I created one Provider variable at the top and tried to pass it as an argument to children, stateless & stateful widgets.
In a stateful widget, user selects size for a product. In a stateless widget, user taps button to add the selected item with selected specification to cart. Afterwards, API handles all stuff but to that point, when user selects other sizes, Provider does not update the initial data it holds. It always stuck with initial data or in my case initial size of a product.
Here is my: top parent widget:
#override
Widget build(BuildContext context) {
// initialize
var provider = Provider.of<ProductDetailProvider>(context, listen: true);
provider.detailModel = widget.detailModel!;
provider.seciliBeden = int.parse(widget.detailModel!.Stoks.first.Beden);
provider.stokId = -1;
return Scaffold(
backgroundColor: AppTheme.containerColor,
body: SafeArea(
bottom: false,
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
ImageViewBuilder(
pageController: imagePageController,
productDetailModel: widget.detailModel,
resims: resims,
),
DraggableScrollableSheet(
initialChildSize: 0.3,
maxChildSize: 1,
minChildSize: 0.3,
builder: (context, controller) {
return DraggableProductView(
scrollController: controller,
detailModel: widget.detailModel,
provider: provider, // StatefulWidget
);
},
),
Positioned(
child: Align(
alignment: Alignment.bottomCenter,
child: AddToCartBottomBar(
productDetailModel: widget.detailModel!,
productDetailProvider: provider, //StatelessWidget
),
),
),
],
),
),
),
);
}
StatefulWidget: this is how I change Provider object in DraggableProductView:
onTap: () {
setState(() {
seciliBeden =
widget.detailModel!.Stoks[index].Beden;
});
widget.provider!
.changeSeciliBeden(int.parse(seciliBeden));
},
Inside this StatefulWidget, I can print updated provider with updated datas. ChangeSeciliBeden also triggers ChangeStockId function inside provider.
StatelessWidget: AddToCartBottom widget holds several stateless widgets. This is the onTap method from AddToCartButton:
onTap: () async {
// This line prints only initial size & stock id of the product.
// It does not print updated data
print("yukarı: ${productDetailProvider!.stokId}");
// ...
// ...
// ...
},
Finally, this is my provider class:
class ProductDetailProvider extends ChangeNotifier {
late ProductDetailModel detailModel;
late int seciliBeden;
late int stokId;
// This method works too. It prints updated data.
changeSeciliBeden(int value) {
seciliBeden = value;
prepareStokId();
print("provider secili beden: $seciliBeden");
print("provider stok id: $stokId");
notifyListeners();
}
changeStokId(int value) {
stokId = value;
notifyListeners();
}
// This method works. I checked that. If block is true always once.
prepareStokId() {
detailModel.Stoks.forEach((element) {
if (element.Beden == seciliBeden.toString()) {
changeStokId(element.StokId);
}
});
notifyListeners();
}
}
Can somebody help me to figure out where I did wrong? Isn't this the way to pass data between widgets without using arguments?
Related
Want to ask is How to change variable value with stream flutter?
You think my question is so fundamental and I can search in everywhere on internet. But in this scenario with stream, I can't change the variable value with method. How I need to do? please guide me. I will show with example.
Here, this is bloc class code with rxDart.
class ChangePinBloc {
final ChangePinRepository _changePinRepository = ChangePinRepository();
final _isValidateConfirmNewPinController = PublishSubject();
String oldPin = '';
Stream get isValidateConfirmNewPinStream =>
_isValidateConfirmNewPinController.stream;
void checkValidateConfirmNewPin(
{required String newPinCode, required String oldPinCode}) {
if (newPinCode == oldPinCode) {
oldPin = oldPinCode;
changePin(newCode: newPinCode);
isValidateConfirmPin = true;
_isValidateConfirmNewPinController.sink.add(isValidateConfirmPin);
} else {
isValidateConfirmPin = false;
_isValidateConfirmNewPinController.sink.add(isValidateConfirmPin);
}
}
void changePin({required String newCode}) async {
changePinRequestBody['deviceId'] = oldPin;
}
dispose() {
}
}
Above code, want to change the value of oldPin value by calling checkValidateConfirmNewPin method from UI. And want to use that oldPin value in changePin method. but oldPin value in changePin always get empty string.
This is the calling method checkValidateConfirmNewPin from UI for better understanding.
PinCodeField(
pinLength: 6,
onComplete: (value) {
pinCodeFieldValue = value;
changePinBloc.checkValidateConfirmNewPin(
newPinCode: value,
oldPinCode: widget.currentPinCodeFieldValue!);
},
onChange: () {},
),
Why I always get empty String although assign a value to variable?
Lastly, this is complete code that calling state checkValidateConfirmNewPin from UI.
void main() {
final changePinBloc = ChangePinBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StreamBuilder(
stream: changePinBloc.isValidateConfirmNewPinStream,
builder: (context, AsyncSnapshot pinValidateSnapshot) {
return Stack(
children: [
Positioned.fill(
child: Column(
children: [
const PinChangeSettingTitle(
title: CONFIRM_NEW_PIN_TITLE,
subTitle: CONFIRM_NEW_PIN_SUBTITLE,
),
const SizedBox(
height: margin50,
),
Padding(
padding: const EdgeInsets.only(
left: margin50, right: margin50),
child: PinCodeField(
pinLength: 6,
onComplete: (value) {
changePinBloc.checkValidateConfirmNewPin(
newPinCode: value,
oldPinCode: widget.newCodePinValue!,
);
},
onChange: () {},
),
)
],
),
),
pinValidateSnapshot.hasData
? pinValidateDataState(pinValidateSnapshot, changePinBloc)
: const Positioned.fill(
child: SizedBox(),
),
],
);
},
),
),
);
}
}
To update the variable you should emit a new state using emit() method.
Just make sure your bloc is correct as it should inherit from Bloc object. Read flutter_bloc documentation to know how to use it.
A simple example:
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
ExampleBloc() : super(ExampleInitial()) {
on<ExampleEvent>((event, emit) {
//Do some logic here
emit(ExampleLoaded());
});
}
}
So here is the problem.
TabScreen() with 3 pages and one fabcontainer button (Stateless widget).
When pressed the fabcontainer will give you the chances of make one upload, after the upload i would like to refresh one of the page of the tabscreen.
return Container(
height: 45.0,
width: 45.0,
// ignore: missing_required_param
child: FabContainer(
icon: Ionicons.add_outline,
mini: true,
),
);
}
OnTap of the fabcontainer:
Navigator.pop(context);
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => CreatePost(),
),
);
},
Cannot add a .then(){setState... } because it is a stateless widget and i need to set the state of a precise page, not of the fabcontainer.
Any idea?
Thanks!
Define a updateUi method inside your TabScreen (which defines the pages)
TabScreen:
void updateUi(){
// here your logic to change the ui
// call setState after you made your changes
setState(() => {});
}
Pass this function as a constructor param to your FabContainer button
FabContainer(
icon: Ionicons.add_outline,
mini: true,
callback: updateUi,
),
Define it in your FabContainer class
final Function() callback;
Call it to update the ui
callback.call();
So what Ozan suggested was a very good beginning but i could not access the stateful widget in order to set the state.
What i did on top of Ozan's suggestion was giving the state a globalkey:
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Assigning it to the scaffold:
return Scaffold(
key: scaffoldKey,
Making the state public removing the _MyPizzasState -> MyPizzasState
Creating a method to refresh the data:
refreshData() {
pizzas = postService.getMyPizzas();
setState(() {
});
}
Assigning a key during the creation of the MyPizzaPage:
final myPizzasKey = GlobalKey<MyPizzasState>();
{
'title': 'My Pizza',
'icon': Ionicons.pizza_sharp,
'page': MyPizzas(key: myPizzasKey),
'index': 0,
},
And, how Ozan said once i received the callback :
buildFab() {
return Container(
height: 45.0,
width: 45.0,
// ignore: missing_required_param
child: FabContainer(
icon: Ionicons.add_outline,
mini: true,
callback: refreshMyPizzas,
),
);
}
void refreshMyPizzas() {
print("Refreshing");
myPizzasKey.currentState?.refreshData();
}
body: Container(
child: Consumer(builder: (context, watch, child) {
var wallet = watch(walletBuilderProvider);
//print(wallet.allWalletItems[0].eventName);
return WalletList(wallets: wallet.allWalletItems);
}),
)
final walletBuilderProvider =
ChangeNotifierProvider.autoDispose<WalletModel>((ref) {
final walletData = ref.watch(dataProvider);
// Create an object by calling the constructor of WalletModel
// Since we now have memory allocated and an object created, we can now call functions which depend on the state of an object, a "method"
final walletModel = WalletModel();
walletModel.buildWallet(walletItems: walletData);
return walletModel;
});
What I do initially to refresh all the data before it loads is I just call
context.refresh(dataProvider);
context.refresh(walletBuilderProvider);
Here is the List that gets called to display the data.
class WalletList extends StatelessWidget {
final List<Wallet> wallets;
WalletList({required this.wallets});
#override
Widget build(BuildContext context) {
return Container(
child: wallets.isEmpty
? Container(
height: 150,
child: Center(
child: Text(
"List is empty",
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
)
: getWalletListItems());
// return ListView(
// children: getWalletListItems(),
// );
}
ListView getWalletListItems() {
print(wallets.length);
print("afterwallets");
var walletList = wallets
.map((walletItem) => WalletListItem(wallet: walletItem))
.toList();
return ListView.builder(
itemCount: walletList.length,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
double scale = 1.0;
return Opacity(
opacity: scale,
child: Align(
heightFactor: 0.7,
alignment: Alignment.topCenter,
child: walletList[index]),
);
});
}
}
What I want to do in the end is use some form of RefreshIndictator to refresh both providers but when I have been attempting to implement that in either the Consumer or the WalletList I haven't been seeing any change at all.
First walletBuilderProvider watch dataProvider so you only need to refresh dataProvider, that will force a refresh on all providers that depend on it
Have you tried using RefreshIndicator Widget?
RefreshIndicator(
onRefresh: () async => context.refresh(dataProvider),
child: WalletList(wallets: wallet.allWalletItems),
);
I'm using CustomScrollView and I need to search data inside 3 SliverList and 2 SliverGrid. All of them listen their data with ValueListenableBuilder and also I have an info box which shows if any data found or not. There are 5 ValueNotifier<bool> variables that helps me show/hide the info box but the problem is when the builders called again I need to call ValueNotifier.value inside this builders but it gives an error:
setState() or markNeedsBuild() called during build.
I know why this error happen, but I can't find any other solution, because I need to keep track of the status of the lists and grids (Some of their items can be deleted and meanwhile I need to check again if new list or grid has a data)
One of the SliverList:
Widget _taskWidgets() {
return SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 22, 16, 16),
sliver: ValueListenableBuilder(
valueListenable: _taskService.listenable,
builder: (_, __, child) {
var tasks = _taskService.search(_keyword.value);
_tasksEmpty.value = tasks.isEmpty; // this line gives error
if (!_showCompleted.value) {
tasks = tasks.where((e) => !e.completed).toList();
}
final listIDS = tasks.map((e) => e.listID).toSet();
return SliverList(
delegate: SliverChildBuilderDelegate((_, index) {
if (index % 2 == 0) {
final widget = HierarchyCard(tasks, index ~/ 2, true);
if (index == 0) {
return Stack(
clipBehavior: Clip.none,
children: [
widget,
_text(_appLocalizations.searchTasks),
],
);
}
return widget;
}
return const SizedBox(height: 30);
}, childCount: 2 * listIDS.length - 1),
);
}
),
);
}
This is because you are rebuilding a widget which is already being build
Try
WidgetsBinding.instance.addPostFrameCallback((_){
_tasksEmpty.value = tasks.isEmpty;
});
The user can either enter the answer with InputChips or manually type it in the TextField. When I try with InputChips, the correct answer is not detected. When I try to manually type it, the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
The Future function should only be called once because it fetches a random document from Firestore, splits the String and scrambles the different pieces. It is some form of quiz.
class _buildPhrases extends State<PhrasesSession>{
TextEditingController _c;
String _text = "initial";
#override
void initState(){
_c = new TextEditingController();
super.initState();
}
#override
void dispose(){
_c?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// TODO: implement build
return Scaffold(
body: Column(
children: <Widget>[
Flexible(flex: 2, child: _buildRest(context),),
Flexible(flex: 5,
child: FutureBuilder(
future: getEverything(args.colName),
builder: (context, snapshot){
if(!snapshot.hasData){
return Center(child: CircularProgressIndicator(),);
}else{
return Column(
children: <Widget>[
Flexible(flex: 1, child: Text(snapshot.data[1]),),
Divider(),
Flexible(flex: 2, child: Container(
child: TextField(
onChanged: (t){
_text += "$t ";
if(_c.text == snapshot.data[0]){
return print("CORRECT ANSWER");
}
},
controller: _c,
textAlign: TextAlign.center,
enabled: true,
),
),),
Flexible(flex: 3,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data.length - 2,
itemBuilder: (context, index){
if(index>snapshot.data.length - 2){
return null;
}else{
return Padding(
padding: const EdgeInsets.all(4.0),
child: InputChip(
label: Text(snapshot.data[index + 2]),
onPressed: (){
_c.text += "${snapshot.data[index + 2]} ";
},
),
);
}
},
))
],
);
}
},
),)
],
)
);
}
}
Let's solve this in parts.
When I try to manually type it the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
This is hapenning because when the keyboard is showing or hidding the flutter framework calls build method of your widget and this default behavior is the reason why your FutureBuilder is realoading. You should avoid call network methods inside build method and I advise you to use BLoC pattern to handle state of your widget.
My Future needs the String that is passed from another route, though. See the Arguments args = .... Any idea how I get it in the initState?
Well if you need context instance to get this String you can't access current context inside initState method because your widget isn't full initialized yet. A simple way to solve this in your case but not the best is verify if the data was already fetched from network or not.
Future _myNetworkFuture; // declare this as member of your stateWidgetClass
Widget build(BuildContext context){
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// this line says if(_myNetworkFuture == null) do the thing.
_myNetworkFuture ??= getEverything(args.colName);
return ...
Flexible(flex: 5,
child: FutureBuilder(
future: _myNetworkFuture,
builder: (context, snapshot){
// ...
}
}
With this approach when flutter framework calls build method if you already fetched the data you don't download the data again. But I really advise you to use BLoC pattern in this kind of situation.