Generic filter Flutter - flutter

Goodmorning,
I'm developing an app with flutter but I'm facing some problems with Provider (I think something miss in my knowledge).
My app fetch data from my API and displays them in listview.
In whole app I have different screen which displays different data type in listview and now I want to create filtering logic.
To avoid rewrite same code multiple times I thought to create one screen to reuse for filtering purposes but I'm facing problems with state management.
What I did:
create base model for filter information
`
enum FilterWidget { TEXT_FIELD, DROPDOWN } //to resolve necessary Widget with getWidget() (to implement)
class FilterBaseModel with ChangeNotifier {
String? value= 'Hello';
FilterWidget? widgetType;
FilterBaseModel(this.value, this.widgetType);
onChange() {
value= value== 'Hello' ? 'HelloChange' : 'Hello';
notifyListeners();
}
}
`
One screen for display filters depending on request
List<FilterBaseModel> filters = [];
FilterScreen() {
//Provided from caller. Now here for test purposes
filters.add(FilterBaseModel('Filter1', FilterWidget.TEXT_FIELD));
filters.add(FilterBaseModel('Filter2', FilterWidget.TEXT_FIELD));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
minimum: EdgeInsets.symmetric(vertical: 15, horizontal: 15),
child: SingleChildScrollView(
child: Container(
height: 400,
child: Column(
children: filters
.map(
(e) => Consumer<FilterBaseModel>(
builder: (_, filter, child) =>
ChangeNotifierProvider.value(
value: filter,
child: CustomTextField(
`your text` initialText: e.value,
onTap: () {
e.onChange();
filter.onChange();
},
),
),
),
)
.toList(),
))),
),
);
}
`
The problem is in Consumer and ChangeNotifier.value.
Screen works quite well: widget are displayed and callback are called, what is wrong? I need to use onChange method of both instance to update UI otherwhise method was called but widget is not rebuilt.
I know that probably putting consumer there is not right way but I tried also to put outside but doesn't work.
I expect to have one filter screen which receives in input filters list information, display them, handle their state managment and return their value.
P.S: this code now works, but I know is not the right way
Thank you for help!
EDIT
Have same behaviour without ChangeNotifierProvider.value. Therefore I'm more confused than before because still persist the double call to onChange for correct rebuilding.
More bit confused about ChangeNotifierProvider.value using...

Related

Flutter: scrolling a list of DropdownButtons each containing 1k selections is too slow

I have a list of items (5-6 items) that are displayed using a ListView.builder. Each item contains a DropdownButton widget for the user to pick a number from 1 - 1000, thus containing 1000 DropdownMenuItems.
I implemented it as shown below, but the problem is that scrolling down the ListView is too slow and stutters. Even if the listView has 5 or 6 items, but note that each of them has an embedded DropdownButton containing 1000 DropdownMenuItems.
Is there a fix? Or another way to achieve my requirement?
N.B: Even if I reduce the number of DropdownMenuItems to 100, it still stutters when scrolling down the ListView.
class List extends StatelessWidget {
final List<Item> // Contains 5 items.
final List<int> quantityList = List<int>.generate(1000, (int i) => i);
//--
child: ListView.builder(
itemBuilder: (buildContext, i) {
return MyItem(
quantityList,
);
},
itemCount: items.length(),
)
class MyItem extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: DropdownButton<int>(
items: quantityList
.map(
(int e) =>
DropdownMenuItem<int>(
value: e,
child: Text(e.toString()),
),
)
.toList(),
),
),
);
}
Edit
I changed MyItem class to be as below, but still, the same problem exists.
Tried using ListView and ListView.custom instead of ListView.builder, to build the whole list in advance instead of lazily according to this, but still same issue.
I also tried running the app using --profile configuration to simulate a release version. The performance is better but still suffers from terrible stuttering and lag. Tested on emulator and physical device.
class MyItem extends StatelessWidget {
List<DropDownMenuItem> quantityList; // contains 1k
#override
Widget build(BuildContext context) {
return Container(
width:300,
height:300,
child: DropdownButton<int>(
items: quantityList,
),
),
);
}
ListView will create and destroy widgets as they come in and out of view. Your MyItem widget is a very heavy widget (lots of stuff to create and destroy).
You probably don't want each MyItem to be a Scaffold(). Normally you only have 1 Scaffold() visible as it's a fancy root view for an app. It has an app bar, snack bar, drawer, etc. Try having just your Container(...) that's currently under body: as the return from your MyItem.build().
In the items: of your DropdownButton, you build and destroy the list of items when the DropdownButton scrolls in and out of view. If this list is the same for every widget in your list like in your test code above, create one List<Widget>[] that contains your DropdownMenuItem widgets and pass it in to your MyItem() widgets. Something like:
//In your widget with the ListView
List<Widget> myItems;
//In the initState() of your widget with the ListView
...
myItems = quantitySelection.map(
(int e) => DropdownMenuItem<int>(
value: e,
child: Text(e.toString()),
),
).toList(),
...
//In your ListView.builder()
return MyItem(
...
items: myItems,
...
);
//In your MyItem.build() -> DropdownButton()
...
DropDownButton(
items: items
),
...
FWIW - we have a ListView with complex children that we test with 10,000 items. There's a significant difference in performance between the debug and release builds in Flutter. It stutters a little in the debug builds but is very smooth in the release builds.
I was able to solve the issue by only using the cacheExtent property of ListView.builder, setting it to list.length*200. This is kind of a workaround knowing that in my case the list length will always be small.
Pre-building the DropDownMenuItems had no sensed performance enhancement by a user, but it is a good recommended practice regardless, as instead of building the same DropDownMenuItems over and over again for every list item.
Although according to the docs: ListView and ListView.separated does not lazy load items rather build them all up at the beginning, I kept experiencing the same stuttering and lag during scrolling as with ListView.builder.

How to change displayed data the best way in Flutter

i want to change displayed data in Flutter? I wrote a function changeDataForTest (only a function for testing the event), which should change the data displayed in Text.
But if I click on this, it isn't changed. The value of the displayed string only changes, if i add (context as Element).reassemble(); after calling the method. Is this the normal way to go, or is there a smoother way to solve my problem?
dynamic changeDataForTest(neuerWert) {
this.data = neuerWert;
}
Column(
children: [
Center(
child: Text(
this.data + this.wiegehts,
),
),
FlatButton(
textColor: Color(0xFF6200EE),
onPressed: () {
changeDataForTest('neuerWert');
(context as Element).reassemble();
},
)
],
)
Thanks
Lukas
If you're using only a small widget, you could use a StatefulWidget using the method:
setState(() {
// change your variable
})
If your widget is complex and has lots of different possible variables, I'll not recommend using setState as this method calls the build method every time is being used.
One simple and fast option, is to use ValueNotifier:
final myVariable = ValueNotifier(false); // where you can replace 'false' with any Object
and then, using it this way:
ValueListenableBuilder(
valueListenable: myVariable,
builder: (context, value, child) {
return Text(value); // or any other use of Widgets
},
);
myVariable.value = true; // if you're looking for to change the current value
finally, if you logic is truly complex and you need to scale, I'll recommend to use a StateManagement library like:
Provider
Riverpod
BloC
Others
You can find those libraries and examples over: https://pub.dev

Avoid Bloc state-sharing with multiple Widget instances in Flutter

Good day everyone, I'm using Flutter with bloc and I'm trying to show a list of Channels which you can "Follow" o "Unfollow".
I have a custom ChannelContainer Widget that loads a list with channel titles and a custom widget JoinButton which shows whether the user can "Follow/Unfollow" depending on the state (If the channel is already being followed or not).
Widget _getChannelsList() {
List<Post> channelList = ContentService().getAllChannels();
return ListView.separated(
itemBuilder: (_, i) => ChannelContainer(
title: channelList[i].title,
description: channelList[i].description,
idChannel: channelList[i].id),
separatorBuilder: (_, i) => SizedBox(height: 5.0),
itemCount: channelList.length,
);
}
This is my custom JoinButton Widget:
#override
Widget build(BuildContext context) {
final channelBloc = BlocProvider.of<ChannelBloc>(context);
channelBloc.add(GetJoinStatus(idChannel));
return Container(
child: BlocBuilder<ChannelBloc, ChannelState>(builder: (context, state) {
var userIsJoined = state.userIsJoined;
if (userIsJoined) {
return ElevatedButton(
child: Text('Unfollow'),
onPressed: () {},
);
} else {
return ElevatedButton(
child: Text('Follow'),
onPressed: () {},
);
}
}),
);
}
I made a test checking the idChannel and returning true to even numbers and false to odd ones and thought that as I was creating multiple JoinButtons, each one would keep their state (followed or not) but as I'm firing the GetJoinStatus() event each time a JoinButton is instantiated and the state changes all the buttons get rebuilded and get the same state.
So, at the end I have a list of JoinButtons that are all getting just Follow or Unfollow but not mixed results.
What would be the best way to get the expected result? I've tried a lot of things that could work but anything compatible with bloc workflow... Thank you in advance!
I think the best way to do this is to have a map in your bloc state whose key is the channel id and the value is a boolean true if joined and false if not.

Flutter: setState((){} fails to redraw DropdownButton value

I am new to flutter, and am trying to code mobile game. So far I have been able to manage through previous questions on forums, youtube tutorials and example apps. However I have hit a wall that I cannot solve.
Many of the features in my UI are supposed to change based on user behavior but do not update, I am using this DropdownButton as my example but it is a problem I am having elsewhere in the code as well. When the user makes a selection from the DropdownButton it should update the value of the button, but instead only the hint is displayed. I have been able to determine through print statements that the selection is registered.
Based on what I have learned so far I suspect that the issue entails my widget's statefulness or lack thereof. I found a few similar questions which suggested using a StatefulBuilder, however when I tried to implement it the code had errors or else duplicated my entire ListTile group. I also saw suggestions about creating a stateful widget instead but the instructions were too vague for me to follow and implement without errors. I am including snippets of the code below, I am hesitant to paste the whole thing because the app is nearly 2000 lines at this point. Please let me know if any further information or code is needed.
this is the problematic code, see below for the code in context.
DropdownButton(
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hunters Guild',
theme: ThemeData(
primaryColor: Colors.grey,
),
home: Main(),
);
}
}
final List<Combatant> _combatants = List<Combatant>();
//combatant is a class that holds stats like HP which may change during a battle
//they also have a Fighter and a Monster and one of those will be null and the other useful
//depending if faction = "good" or "evil
//I am aware that this may not be the most elegant implementation but that is a problem for another day.
class MainState extends State<Main> {
final List<Fighter> _squad = List<Fighter>();
//Fighter is a class which stores info like HP, name, skills, etc for game character,
//this List is populated in the page previous to _fight
//where the user picks which characters they are using in a given battle
void _fight(Quest theQuest){
for(Fighter guy in _squad){
_combatants.add(Combatant("good", guy, null));
}
_combatants.add(Combatant("evil", null, theQuest.boss));
//quests are a class which store a boss Monster, later on I will add other things like rewards and prerequisites
final tiles = _combatants.map((Combatant comba){ //this structure is from the build your first app codelab
List<DropdownMenuItem<String>> listDrop = [];
String skillChoice = null;
if (comba.faction == "good"){
for (Skill skill in comba.guy.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
else if (comba.faction == "evil"){
for (Skill skill in comba.boss.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
//^this code populates each ListTile (one for each combatant) with a drop down button of all their combat skills
return ListTile(
leading: comba.port,
title: Text(comba.info),
onTap: () {
if(comba.faction == "good"){
_fighterFightInfo(comba.guy, theQuest.boss); //separate page with more detailed info, not finished
}
else{
_monsterFightInfo(theQuest.boss); //same but for monsters
}
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton( //HERE IS WHERE THE ERROR LIES
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
IconButton(icon: Icon(Icons.check), onPressed: (){
//eventually this button will be used to execute the user's skill choice
})
],
),
subtitle: Text(comba.hp.toString()),
);
},
);
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
//I have tried moving most of the above code into here,
//and also implementing StatelessBuilders here and other places in the below code to no effect
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text("Slay "+theQuest.boss.name+" "+theQuest.boss.level.toString()+" "+theQuest.boss.type.name, style: TextStyle(fontSize: 14),),
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: () {
_squadSelect(theQuest);
}),
),
body: ListView(children: divided),
);
},
),
);
}
//there are a lot more methods here which I haven't included
#override
Widget build(BuildContext context) {
//I can show this if you need it but there's a lot of UI building on the main page
//Also might not be the most efficiently implemented
//what you need to know is that there are buttons which call _fight and go to that page
}
class Main extends StatefulWidget {
#override
MainState createState() => MainState();
}
Thank you for any help or advice you can offer.
Its a bit difficult to understand what is going on here, so if possible add the problem areas in a new widget and post it here.
In any case, for now it seems your skillShare variable is local, so every time you are setting state it does not update. Try something like this:
class MainState extends State<Main> {
String skillShare = ""; //i.e make skillShare a global variable
final List<Fighter> _squad = List<Fighter>();
The following question had information which helped me find the answer. Different question but the solution applies. Thanks user:4576996 Sven. Basically I need to reformat from doing my constructing in the void _fight to a new stateful page. This may be useful for anyone else who is using the beginning tutorial as a framework to build their app.
flutter delete item from listview

Flutter Bloc , Bloc state , navigate?

what I’m facing now is after I implemented bloc following one of the tutorials, I'm stuck now in place where after I'm getting the response and the state is changed, I want to navigate to another widget
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(APP_TITLE),
),
body: buildBody(context));
}
}
BlocProvider<SignInBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => sl<SignInBloc>(),
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: <Widget>[
BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
if(state is Empty)
return MessageDisplay(message: 'Sign In please.',);
else if(state is Loaded)
return HomePage();
else
return MessageDisplay(message: 'Sign In please.',);
}
),
SignInControls(),
],
),
),
),
);
}
in state of loaded I want to navigate to another widget.
so how to achieve that, and what is the best way for it?
You can't use the navigator or change the state while the widget is being built (your case).
There're two ways
1. The old fashioned way
WidgetsBinding.instance.addPostFrameCallback((_){
// Your code goes here
});
2. Since you already implemented the BLOC library you have a more elegant way to achieve this by using BlocListener. you can learn more about it in the documentation
Hope i helped!
Navigation can be used like Inherited widgets:
Navigator nav = Navigator.of(this.context);
then you can use somthing like:
nav.push(MaterialPageRoute(builder: (context) => YourSecondPage()))
in flutter, you can't just move to some page directly. you should use a route.
I think the cleanest way to use named routes. this is an example:
// here you put a class of names to use later in all of your project.
class RouteNames{
static String homepage = "/";
static String otherPage= "/otherpage";
}
// in your main file , MyApp class
var routes = {
RouteNames.homepage: (context)=> new MyHomePage(),
RouteNames.otherPage: (context)=> new MyOtherPage()
};
// then use routes variable in your MaterialApp constructor
// and later on in your project you can use this syntax:
Navigator.of(context).pushNamed(RouteNames.otherPage);
I think this way is clean and it's centralized, it's good if you want to send arguments to routes.
To learn more about navigation: navigation official documentation is pretty good
A note about the Bloc builder & listener:
Since BlocBuilder is going to be called lots of times. it should only contain widgets and widgets only. if you put navigation code inside it, this code would be called multiple times.
As Ayham Orfali said You definitely should use BlocListener for that. Inside it you can listen to changes in state. here is an example
// some code
children: <Widget>[
BlocListener(
bloc: BlocProvider.of<SignInBloc>(context),
listener: (context, state) {
if(state is Loaded){
Navigator.of(context).pushNamed("some other page");
}
// else do nothing!
},
child:// just bloc builder which contains widgets only. ,
SignInControls(),
]
// some other code