Provider in ListView not getting disposed? - flutter

I have ListView with a list (values) containing ~600 objects:
ListView.builder(
itemCount: values.length,
itemBuilder: (context, index) {
return ChangeNotifierProxyProvider<DatabaseRepository,
AlbumViewModel>(
initialBuilder: (context) => AlbumViewModel(album: values[index]),
builder: (context, dbRepo, vm) => vm..databaseRepository = dbRepo,
child: const AlbumEntryView(),
);
With the according AlbumEntryView
class AlbumEntryView extends StatelessWidget {
const AlbumEntryView({Key key}) : super(key: key);
Widget _flagIconButton(AlbumViewModel model) {
IconData icon = model.album.flagged ? Icons.flag : Icons.outlined_flag;
return IconButton(icon: Icon(icon), onPressed: () => model.toggleFlagged());
}
#override
Widget build(BuildContext context) {
AlbumViewModel model = Provider.of(context);
return ListTile(
title: Text(model.album.title),
trailing: _flagIconButton(model),;
}
I checked the memory in my app for the amount of ChangeNofifierProxyProvider objects and it keeps increasing when I scroll through the list (it holds the AlbumEntryView object).
As far as I know, the ListView releases widgets when they are not in the viewPort and the Provider is getting disposed when it's not in the view hierarchy anymore. Does that mean, that the list items are not getting disposed/reused resulting the objects to grow in the memory view or did I misunderstand something? If so, how can I properly use the provider as a list item without facing the issue of a potential leak of the app?

Related

How to avoid streambuilder executes unnecessarytimes

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

Navigte back with params in flutter using GetX and accessing the results in the previous page

I am creating a flutter app which enables a user to select a contact and display the selected contact before navigating back from the list of contact.
class ContactsList extends StatelessWidget {
final List<AppContact> contacts;
Function() reloadContacts;
ContactsList({Key? key, required this.contacts, required this.reloadContacts})
: super(key: key);
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: contacts.length,
itemBuilder: (context, index) {
AppContact contact = contacts[index];
return ListTile(
onTap: () {
Get.back(result:contact.info.phones!.elementAt(0).value!);
},
title: Text(contact.info.displayName!),
subtitle: Text(contact.info.phones!.length > 0
? contact.info.phones!.elementAt(0).value!
: ''),
leading: ContactAvatar(contact, 36));
},
),
);
}
}
I'm not sure you want this but, you can just use await keyword with navigate function.
For example;
final data = await Get.to(Payment());
if you want to learn some information about navigation with getx you can visit documentation

How to select only one item in ListView and render it?

I started studying flutter and I'm having a doubt about LsitViewBuilder.
I have this ListView that accesses the JSON data locally by rootBundle, but I would like that when I click on some item it would only open it on the second page.
I wanted so much to know how you can select.
My ListView
List<dynamic> buy;
#override
void initState() {
super.initState();
rootBundle.loadString('assets/dados.json').then((jsonData) {
this.setState(() {
buy = jsonDecode(jsonData);
});
});
}
........
ListView.builder(
itemCount: buy?.length ?? 0,
itemBuilder: (_, index) {
return buildCardBuy(context, index, buy);
}
),
You can wrap your list view item with the GestureDetector widget, making the Tap event to navigate to another page with the item tapped.
ListView.builder(
itemCount: buy?.length ?? 0,
itemBuilder: (_, index) {
return GestureDetector(
child: buildCardBuy(context, index, buy),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
// the first refeers to the property on your detail DetailScreen
// and the second refers to the current buy being render on
// this list view builder
builder: (context) => DetailScreen(buy: buy),
),
);
);
}
),
And in your DetailScreen something like
class DetailScreen extends StatelessWidget {
final dynamic buy;
DetailScreen({Key key, #required this.buy}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
),
body: Container()
);
}
}
I would build a second widget (let's call it ItemWidget) which represents the detail page for the object you want to 'open'.
Then I would add to that ItemWidget a property for the object data that you need to pass.
After that I would implement the logic so that when the list item is clicked, it switches the current list widget with a new ItemWidget and passes to it the properties of the clicked object.

How to update content of a ModalBottomSheet from parent while active

I have a custom button which takes in a list of items. When it is pressed, it opens a modal bottom sheet and passes that list to the bottom sheet.
However, When the buttons items change it doesn't update the items in the bottom sheet.
How can I achieve this effect.
Simple example
ButtonsPage
|
Button(items: initialItems)
|
BottomSheet(items: initialItems)
** After a delay, setState is called in ButtonsPage with newItems, thereby sending newItems to the button
ButtonsPage
|
Button(items: newItems)
| ## Here, the bottom sheet is open. I want it to update initialItems to be newItems in the bottom sheet
BottomSheet(items: initialItems -- should be newItems)
Code Sample
This is my select field which, as shown, receives a list items and when it is pressed it opens a bottom sheet and sends the items received to the bottom sheet.
class PaperSelect extends StatefulWidget {
final List<dynamic> items;
PaperSelect({
this.items,
}) : super(key: key);
#override
_PaperSelectState createState() => _PaperSelectState();
}
class _PaperSelectState extends State<PaperSelect> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.disabled ? null : () => _showBottomSheet(context),
child: Container(
),
);
}
void _showBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) => BottomSheet(
items: widget.items,
),
)
);
}
}
After some time (a network call), items is updated in the parent Widget of PaperSelect. PaperSelect then updates and receives the new items.
class BottomSheet extends StatefulWidget {
final dynamic items;
BottomSheet({
this.items,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BottomSheetState();
}
}
class _BottomSheetState extends State<BottomSheet> {
dynamic items;
#override
void initState() {
print(widget.items);
items = widget.items;
super.initState();
}
#override
Widget build(BuildContext context) {
if(items==null) return Center(
child: SizedBox(
width: 140.0,
height: 140.0,
child: PaperLoader()
),
);
if(items==-1) return Text("Network Error");
return Column(
children: <Widget>
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (BuildContext context, int i) => ListTile(
onTap: () => Navigator.pop(context),
title: Text('')
)
),
),
],
);
}
}
Now, I want to send the updated data to the bottom sheet. However, it doesn't work because the ModalBottomSheet is already open.
How can I get around this?
I assume here that items is a List<String>, since you did not specify that at all. (You should generally not use dynamic in most cases, because it does not do any type checking at all). Anyway,
One thing you could do (beside countless others) is pass in a ValueNotifier<List<String>> instead of a List<String> and then user that with a ValueListenableBuilder in your bottom sheet. like:
// in the caller
final itemsNotifier = ValueNotifier(items);
// in your bottom sheet
ValueListenableBuilder<List<String>>(
valueListenable: itemsNotifier,
builder: (context, snapshot, child) {
final items = snapshot.data;
// build your list
},
)
then every time you would change itemsNotifier.value = [...] your items would rebuild.
Note when updating itemsNotifier.value: It must not be done inside build/didUpdateWidget/etc. so if this is the case you might use WidgetsBinding.instance.addPostFrameCallback(() => itemsNotifier.value = ... ) to postpone updating the value until after the current build phase.

How to properly initialize a Future in Flutter Provider

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.