I have quite the problem wrapping my head around the whole state management of GetX so naturally I'm facing some issues.
I'm getting a collection from firebase which I put into a listview populated with Card widgets (CustomCard()), each document into a Card widget. In this Card widget I have a boolean that controls whether the card should be expanded (simply by Adding a Row()) or not if Card is tapped. The issue I'm facing is that if use GetX for this boolean, all the cards will trigger and not each individual card. In a way this seems logical because I only have one controller that manages this boolean.
So to clarify, bool isCardExpanded seem to be global for all Card widgets meaning that if I tap one card, ALL cards will expand, which is not what I want. I need them to act individually.
Do I need one separate controller for every Card in the list view or is this solvable in another way?
Controller
class Controller extends GetxController {
RxBool isCardExpanded = false.obs;
void changeExpanded() {
isCardExpanded.value = !isCardExpanded.value;
update();
}
}
ListView:
class CustomScreen extends State<CustomScreen>
implements ItemScreenInterface {
Controller ctrl = Get.find();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(someCol)
.doc(SomeDoc)
.collection(anotherCol).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.docs.length,
itemBuilder: (_, i) {
return CustomCard( // <------------ Card widget
document: snapshot.data.docs[i]);
}),
)
],
);
},
);
}
Card class
class CustomCard extends StatefulWidget {
CustomCard({required this.doc});
var doc;
#override
_CustomCardState createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
Controller ctrl = Get.find();
RxBool _isCardExpanded = false.obs;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Card(
child: GestureDetector(
onTap: () {
ctrl.changeExpanded(); // <-- change bool
},
child: Padding(
padding: EdgeInsets.all(2),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Text('Top Part')
]),
if (_isCardExpanded.value) Divider(thickness: 2),
if (_isCardExpanded.value) // Controlled by _isCardExpanded. Add Row if true
Row(
children: <Widget>[
Text('EXPANDED'),
],
),
],
),
),
),
);
}
}
Desired outcome:
Actual outcome:
Because your controller instance initializes only once.
To solve your problem, you need to make a List that has the status of the cards in the Controller.
Or don't use the State management tool, but separately place the _isExpanded value in the _CustomCardState class. And use the setState() function.
EDIT
There is another way by using the tag argument when creating the controller like Get.create(someController, tag: TAG_NAME);
Related
I am creating a form (not using the Form Widget) in Flutter where the user can add an arbitrary amount of items (treatments) which are rendered as InputChip widgets list in a Wrap widget.
The form uses a button (AddButton widget) which opens a form dialog which itself returns the newly created item (treatment) that is added to selectedItems:
class TreatmentsWidget extends StatefulWidget {
const TreatmentsWidget({super.key, required this.selectedItems});
final List<Treatment> selectedItems;
#override
State<TreatmentsWidget> createState() => _TreatmentsWidgetState();
}
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = widget.selectedItems.map(
(item) {
return InputChip(
label: Text('${item.name} - ${item.frequency}/${item.frequencyUnit.name})',
);
},
).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return const TreatmentDialog();
}).then((value) {
if (value != null) {
Treatment item = value;
setState(() {
widget.selectedItems.add(item);
});
}
});
}),
],
);
}
}
For some reason, when a new item is added to selectedItem and that the item overflows the current line, the layout is not recomputed such that the Wrap widget overflows the button:
However, as soon as the user scroll (the whole screen content is inside a SingleChildScrollView), the layout is recomputed and the Wrap takes the right amount of space:
How can I force a redraw when a new item is added to prevent this glitch?
The issue seems to be that the Column does not recompute its size on the current frame when one of his child size changes.
I ended up forcing rebuilding the Column using a ValueKey whenever chips.length changes:
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = ...;
return Column(
key: ValueKey(chips.length),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(...),
],
);
}
}
I am still interested in a cleaner solution if it exists. Using a provider seems overkill for this case.
I am new to Flutter Bloc and must be missing how State changes are processed by the UI widgets. At the top level I have a BlocConsumer and under that I have nested BlocBuilder widgets with buildWhen methods to indicate when and how the Bloc widget should be rebuilt. Based on print statements,it looks like the Bloc state is consumed in the top level BlocConsumer widget and never makes it down to the lower level BlocBuilder widgets.
The code below should
Display circular progress bar on startup - this works ok
Call a bunch of APIs - This is happening
In the meantime display the initial screen with default text values in various widgets - this happens
As API returns and Bloc passes states on the stream, the appropriate UI widget should be rebuilt replacing default text with the data in the stream object. -- this doesn't happen.
Code snippets:
RaspDataStates issued by Bloc (Just showing for reference. Not showing all subclasses of RaspDataState):
#immutable
abstract class RaspDataState {}
class RaspInitialState extends RaspDataState {
#override
String toString() => "RaspInitialState";
}
class RaspForecastModels extends RaspDataState {
final List<String> modelNames;
final String selectedModelName;
RaspForecastModels(this.modelNames, this.selectedModelName);
}
...
Bloc just to show how initialized. Code all seems to work fine and isn't shown.
class RaspDataBloc extends Bloc<RaspDataEvent, RaspDataState> {
RaspDataBloc({required this.repository}) : super(RaspInitialState());
#override
RaspDataState get initialState => RaspInitialState();
...
Now to the UI widget.
class SoaringForecast extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<RaspDataBloc>(
create: (BuildContext context) =>
RaspDataBloc(repository: RepositoryProvider.of<Repository>(context)),
child: RaspScreen(repositoryContext: context),
);
}
}
class RaspScreen extends StatefulWidget {
final BuildContext repositoryContext;
RaspScreen({Key? key, required this.repositoryContext}) : super(key: key);
#override
_RaspScreenState createState() => _RaspScreenState();
}
class _RaspScreenState extends State<RaspScreen>
with SingleTickerProviderStateMixin, AfterLayoutMixin<RaspScreen> {
// Executed only when class created
#override
void initState() {
super.initState();
_firstLayoutComplete = false;
print('Calling series of APIs');
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
_mapController = MapController();
}
#override
void afterFirstLayout(BuildContext context) {
_firstLayoutComplete = true;
print(
"First layout complete. mapcontroller is set ${_mapController != null}");
_setMapLatLngBounds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: AppDrawer.getDrawer(context),
appBar: AppBar(
title: Text('RASP'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: null),
],
),
body: BlocConsumer<RaspDataBloc, RaspDataState>(
listener: (context, state) {
print('In forecastLayout State: $state'); << Can see all streamed states here
if (state is RaspDataLoadErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text(state.error),
),
);
}
}, builder: (context, state) {
print('state is $state'); << Only see last streamed state here
if (state is RaspInitialState || state is RaspDataLoadErrorState) {
print('returning CircularProgressIndicator');
return Center(
child: CircularProgressIndicator(),
);
}
print('creating main screen'); << Only see this when all streams complete
return Padding(
padding: EdgeInsets.all(8.0),
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: [
getForecastModelsAndDates(),
getForecastTypes(),
displayForecastTimes(),
returnMap()
]));
}));
}
Widget getForecastModelsAndDates() {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: forecastModelDropDownList(), // ForecastModelsWidget()
),
Expanded(
flex: 7,
child: Padding(
padding: EdgeInsets.only(left: 16.0),
child: forecastDatesDropDownList(),
)),
],
);
}
// Display GFS, NAM, ....
Widget forecastModelDropDownList() {
return BlocBuilder<RaspDataBloc, RaspDataState>(
buildWhen: (previous, current) {
return current is RaspInitialState || current is RaspForecastModels;
}, builder: (context, state) {
if (state is RaspInitialState || !(state is RaspForecastModels)) {
return Text("Getting Forecast Models");
}
var raspForecastModels = state;
print('Creating dropdown for models');
return DropdownButton<String>(
value: (raspForecastModels.selectedModelName),
isExpanded: true,
iconSize: 24,
elevation: 16,
onChanged: (String? newValue) {
BlocProvider.of<RaspDataBloc>(context)
.add(SelectedRaspModel(newValue!));
},
items: raspForecastModels.modelNames
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value.toUpperCase()),
);
}).toList(),
);
});
}
... more BlocBuilder child widgets similar to the one above
The print statements in the console are:
Calling series of APIs
state is RaspInitialState
returning CircularProgressIndicator
First layout complete. mapcontroller is set true
... (First of bunch of API output displays - all successful)
state is RaspInitialState << Not sure why this occurs again
returning CircularProgressIndicator
... (More API output displays - all successful)
streamed RaspForecastModels
In forecastLayout State: Instance of 'RaspForecastModels' << Doesn't cause widget to be rebuild
streamed RaspForecastDates << Other states being produced by Bloc
In forecastLayout State: Instance of 'RaspForecastDates'
streamed RaspForecasts
In forecastLayout State: Instance of 'RaspForecasts'
In forecastLayout State: Instance of 'RaspForecastTime'
streamed RaspMapLatLngBounds
In forecastLayout State: Instance of 'RaspMapLatLngBounds'
state is Instance of 'RaspMapLatLngBounds'
creating main screen
Any words of wisdom on the errors of my way would be appreciated.
I added this earlier as a comment but then found Stackoverflow didn't initially show my comment (I needed to click on show more). So here it is in better readable form.
Problem solved. I needed to move the line:
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
from the initState() method to afterFirstLayout().
All blocbuilders then executed and the UI was built appropriately . And to answer my title question, the bloc states are broadcast and can be picked up by different BlocBuilders.
I'm trying to display a list of documents which works, but I read that one good practice is to manage states (which I'm trying currently to understand too). In this case every time I change of screen using the bottomNavigationBar the streamBuilder executes (I always see the CircularProgressIndicator).
I tried call the collection reference in the intState but still the same issue, my code:
class Deparments extends StatefulWidget {
Deparments({Key? key, required this.auth}) : super(key: key);
final AuthBase auth;
#override
_DeparmentsState createState() => _DeparmentsState();
}
class _DeparmentsState extends State<Deparments> {
late final Stream<QuerySnapshot<Object?>> _widget;
Stream<QuerySnapshot<Object?>> getProds(){
CollectionReference ref = FirebaseFirestore.instance.collection("Departamentos");
return ref.snapshots();
}
#override
void initState() {
super.initState();
_widget = getProds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: SideMenu(auth: widget.auth),
appBar: AppBar(
title: Text("Departamentos"),
centerTitle: true,
backgroundColor: Colors.green,
),
body: Container(
child: StreamBuilder<QuerySnapshot> (
stream: _widget,
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
List deparments =
snapshot.data!.docs.map((doc) => doc.id).toList();
return Column(
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(top: 10),
scrollDirection: Axis.vertical,
itemCount: deparments.length,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: Card(
child: Text(deparments[index]),
),
);
}),
)
],
);
}
}),
),
);
}
}
Update: for those who are facing the same issue Tayan provides a useful solution and he has a video showing the solution https://stackoverflow.com/a/64057210/9429407
Init state will not help you to avoid rebuilds because on changing tabs Flutter rebuilds your Screen. So we need some way to keep our screen alive, so here comes AutomaticKeepAliveClientMixin.
class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin<Home> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
//Make sure to include the below method
super.build(context);
return SomeWidget();
}
}
The above implementation keeps all of your tab state persists and does not rebuilds the tabs again. Well this may serve your purpose but it may not be idle because this loads all the tabs at once even if the user actually didnt visited a tab, so to avoid the build unless a tab is clicked, use the above method in combination with pageview.
Check out pageView implementation
Also, if you want a better way to manage state and save some of your read calls to Firestore, then you should store data locally and fetch only those needed and/or use paginations.
Initialize your stream in initState just like this answer:
StreamBuilder being called numerous times when in build
I am new in Flutter and I am facing a problem.
What I have so far is that I am getting the data from a web service asynchronously (Json). So in the build method I use a FutureBuilder widget for Scaffold's body argument. And its 'builder' argument (of FutureBuilder widget) returns a ListView.builder to show the data.
Also I use ScrollController for scrolling.
Its time I reach the maximum scroll point I call once again the service to get the next page of data and so on...
The problem is that when I "change page" by scrolling it brings the data correctly, but it starts from the very first line of data and not from the point where i ve reached the maxScrollExtent point.
So if the first 'snapshot' has 25 rows and the second has 15, when I go to the second it starts from row 1 and not from row 26 although the total amount of data row is 40 correctly .
As a result i don't have a smooth scrolling. I have been stuck for enough time to this point and i do not know what I am missing. Do I have to use Page Storage keys (i saw video from flutter team but i haven't found an edge to my problem). Any hints? A sample of code follows.
class _MyAppState extends State<MyApp> {
final ScrollController _controller = ScrollController();
#override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
}
void _scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
if (nextDataSet != null) {
print('nextDataSet = ' + nextDataSet);
setState(() {
inDataSet = nextDataSet;
});
} else {
print('nextDataSet = NULL');
}
}
Future<Post> fetchPost([String dataSet]) async {
...
return Post.fromJson(json.decode(utf8.decode(response.bodyBytes)));
}
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => Scaffold(
...
body: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
),
),
),
);
}
Widget _buildList(BuildContext context, AsyncSnapshot snapshot) {
... /*code that eventually fills the lists of strings
progressivePartyListSec, progressivePartyListThird*/
...
return ListData(controller: _controller, listEidosPerigr:
progressivePartyListSec, listPerigr: progressivePartyListThird );
}
}
class ListData extends StatelessWidget {
final ScrollController controller;
final List<String> listEidosPerigr;
final List<String> listPerigr;
ListData({this.controller,
this.listEidosPerigr,
this.listPerigr});
#override
Widget build(BuildContext context) {
return ListView.builder(
controller: controller,
itemCount: rowsSelected,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
removeAllHtmlTags(listEidosPerigr[i]),
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.blue),
),
new RichText(
text: new TextSpan(
text: listPerigr[i].replaceAllMapped('<BR>', (Match m) => '').replaceAllMapped('<b>', (Match m) => '').replaceAllMapped('</b>', (Match m) => ''),
style: DefaultTextStyle.of(context).style,
),
),
]);
});
}
}
after some research i found a solution in the following link: Flutter: Creating a ListView that loads one page at a time
Thanks a lot AbdulRahman AlHamali.
Basically, on the above code that i have posted, i used another argument for the ListView.builder and it is the following key: PageStorageKey('offset'),
Finally as AbdulRahman writes on his article i used the KeepAliveFutureBuilder as a wrapper of FutureBuilder in other worlds in my build method i did...
body: KeepAliveFutureBuilder(
//child: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
//),
),
so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.