Flutter - setState not updating the child widget variable passed as parameter - flutter

I'm new to flutter and trying to use two Stateful Widgets first one calling the second in build() method and I just want to update the child widget variable that is passed from parent in the constructor.
Here is the code I'm trying with.
Parent Widget
class Parent extends StatefulWidget {
Parent({Key? key}) : super(key: key);
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
List appointments = [];
#override
void initState() {
super.initState();
fetchAppointments();
}
#override
Widget build(BuildContext context) {
return Builder(builder: (BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
_pullRefresh();
},
child: ListView(
children: [
AppointmentsWidget(appointments: appointments) // <----- I passed the variable in constructor and this one is updating in setState and I want it to update in the child widget too
],
),
),
);
});
}
_pullRefresh() {
fetchAppointments();
}
fetchAppointments() {
setState(() {
// Stuff to do
appointments = ......
......
......
});
}
}
Child Widget
class AppointmentsWidget extends StatefulWidget {
var appointments;
AppointmentsWidget({this.appointments});
#override
_AppointmentsWidgetState createState() =>
_AppointmentsWidgetState(appointments: appointments); // <--- Constructor 1
}
class _AppointmentsWidgetState extends State<AppointmentsWidget> {
var appointments;
_AppointmentsWidgetState({this.appointments}); // <--- Constructor 2
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: appointments.length,
itemBuilder: (BuildContext context, int index) {
return Text(appointments[index].toString()); // <--- This is where I use it
},
);
}
}
I know the constructor calls once but I couldn't find a way to either recall the constructor OR somehow pass the updated value to the constructor.
Your help is really appreciated.

You should make your child widget stateless, as its state (the appointments) are handled by the parent. What happens currently is that your child widget is constructed, where the empty list is used as its widget.appointments value. Then when the appointments have been fetched, the widget.appointments rebuilds, but since the state of the child is maintained, this value is not passed on (initState of the child is not re-run).
class AppointmentsWidget extends StatelessWidget {
final List appointments;
const AppointmentsWidget({Key? key, required this.appointments}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: appointments.length,
itemBuilder: (BuildContext context, int index) {
return Text(appointments[index].toString()); // <--- This is where I use it
},
);
}
}
Also, take a look at the flutter docs on handling state:
https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro
These also state that it's good to keep your state high up (in the parent in this case), and make child widgets use the state of the parents to render themselves appropriately.
Rectification
As you mention in the comments, you need the child widget to be stateful (for maintaining state on some other data). In that case, you can simply get rid of the appointments state variable and use widget.appointments instead, which will update when the parent rebuilds with a new value.
class AppointmentsWidget extends StatefulWidget {
var appointments;
AppointmentsWidget({this.appointments});
#override
_AppointmentsWidgetState createState() =>
_AppointmentsWidgetState(); // <--- Constructor 1
}
class _AppointmentsWidgetState extends State<AppointmentsWidget> {
_AppointmentsWidgetState(); // <--- Constructor 2
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.appointments.length,
itemBuilder: (BuildContext context, int index) {
return Text(widget.appointments[index].toString()); // <--- This is where I use it
},
);
}
}

You need some listenable to update the widget because the context of parent and child are diferents. In that case the correct way to do it (without a state managment package) is with a InheritedWidget.
Inherited class:
class ExampleInherited extends InheritedWidget {
final Widget child;
final ExampleBloc exampleBloc; // <-- change notifier that can do changes and notify their children
const ExampleInherited({Key? key, required this.child, required this.exampleBloc}) : super(key: key, child: child);
static ExampleInherited? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ExampleInherited>();
#override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
ChangeNotifier class:
class ExampleBloc extends ChangeNotifier {
static final ExampleBloc _exampleBloc = ExampleBloc._internal();
factory ExampleBloc() {
return _exampleBloc;
}
ExampleBloc._internal();
exampleMethod(){
// here you can do whatever you need (update vars)
notifyListeners(); // <-- this notifies the children that they need to be rebuilded
}
}
Then set this in your parent view:
ExampleInherited(
exampleBloc: ExampleBloc(),
child: // content
}
And then in your child view:
#override
Widget build(BuildContext context) {
ExampleBloc exampleBloc = ExampleInherited.of(context)!.exampleBloc;
return AnimatedBuilder(
animation: exampleBloc,
builder: (context, child) {
return //your content
// this will rebuild every time you call notifyListeners() in your bloc
})
}

Related

Are unchanged ListView-Items reused when the ListView gets rebuild?

I've got a List<Data> which is diplayed in a ListView that uses Riverpod to watch any changes to the list. When I add or remove an item from that list, the ListView rebuilds as intended, but it appears like every ListViewItem and its descending widgets are rebuild - even though they show the same content as before. Here's a simplified version of my code:
class MyApp extends ConsumerWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final listLength = ref.watch(dataLengthProvider);
return MaterialApp(
home: Scaffold(
body: Column(
children: [
ElevatedButton(
child: const Icon(Icons.add),
onPressed: () => ref.read(dataListProvider.notifier).add(),
),
Expanded(
child: ListView.builder(
itemCount: listLength,
itemBuilder: (context, index) {
return MyListItem(index);
},
),
),
],
),
),
);
}
}
class MyListItem extends ConsumerWidget {
final int index;
const MyListItem(this.index, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final countValue =
ref.watch(dataItemProvider(index).select((dataItem) => dataItem.value));
return Text('Value: ${countValue.toString()}');
}
}
// Providers -------------------------------------------------------------------
final dataListProvider = StateNotifierProvider<DataListNotifier, List<Data>>(
(ref) => DataListNotifier());
final dataLengthProvider =
Provider<int>((ref) => ref.watch(dataListProvider).length);
final dataItemProvider = Provider.family<Data, int>(
(ref, index) => ref.watch(dataListProvider)[index]);
// Notifier --------------------------------------------------------------------
class DataListNotifier extends StateNotifier<List<Data>> {
DataListNotifier() : super([const Data(), const Data()]);
void add() {
state = [...state, const Data(value: 0)];
}
}
// Data model ------------------------------------------------------------------
#immutable
class Data {
final int value;
const Data({this.value = 0});
Data copyWith({int? newValue}) => Data(value: newValue ?? value);
}
Now my question: Is Flutter smart enough to automatically re-use those unchanged widgets?
If not, what can I do to avoid unneccessary builds?
You can check something. To do this, remake your class MyListItem in to have access to dispose():
class MyListItem extends ConsumerStatefulWidget {
final int index;
const MyListItem(
this.index, {
Key? key,
}) : super(key: key);
#override
ConsumerState createState() => _MyListItemState();
}
class _MyListItemState extends ConsumerState<MyListItem> {
#override
Widget build(BuildContext context) {
print(widget.index);
final countValue = ref.watch(
dataItemProvider(widget.index).select((dataItem) => dataItem.value));
return Text('Value: ${countValue.toString()}');
}
#override
void dispose() {
print('dispose: ${widget.index}');
super.dispose();
}
}
and add method delete() near add():
void delete() {
state.removeLast();
state = List.of(state);
}
and add button in MyApp:
ElevatedButton(
child: const Icon(Icons.delete),
onPressed: () => ref.read(dataListProvider.notifier).delete(),
),
And check this code again. There, of course, the RangeError (index) error will be raised, but this is not the point. But on the other hand, you can see that the dispose() method is not called when the element is added, which means that the object is not removed from the tree. At the same time, when the last element is removed, we can see the call to the dispose() method, but only for the last element! So you are on the right track :)
You can use the select for getting the reference of the provider for stopping unnecessary rebuilds in the list item.
https://riverpod.dev/docs/concepts/reading/#using-select-to-filter-rebuilds

Pass Index from Carousel Widget to Another Widget Flutter

I am new to Flutter and stumped on how to do this. I have a screen that has a Carousel Slider widget on it that I am holding in a separate file/widget to keep the code as clean as possible. To that Carousel I am passing a List which are urls of images and videos. I have already implemented an indicator bar and have the index of the list held within activeIndex variable within the Carousel widget. I then need to pass that index value to a separate widget held in a variable on the main page of my app (one with the clean code).
I basically need help on where to define variables in one widget that I can then define and pass to multiple other widgets. Please let me know if you need more context as I am new to coding in general. Thanks!
Carousel Widget
class AssetCarouselBuilder extends StatefulWidget {
const AssetCarouselBuilder({
#required this.assets,
this.activeIndex
});
final List<String> assets;
final int activeIndex;
#override
State<AssetCarouselBuilder> createState() => _AssetCarouselBuilderState();
}
class _AssetCarouselBuilderState extends State<AssetCarouselBuilder> {
int activeIndex = 0;
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CarouselSlider.builder(itemCount: widget.assets.length,
itemBuilder: (context, index, realIndex){
final assetUrl = widget.assets[index];
options: CarouselOptions(
onPageChanged: (index, reason) =>
setState (() => activeIndex = index)
//this is where I am holding the variable 'activeIndex' that I need elsewhere
if (assetUrl.contains('jpg')) {
return buildImage(assetUrl, index);
}
const SizedBox(height: 5),
buildIndicator(),
Widget buildImage(String imageUrl, int index) => Image(),
Widget buildIndicator() => AnimatedSmoothIndicator(
activeIndex: activeIndex,
count: widget.assets.length,
effect: ColorTransitionEffect()
Implementation of Carousel on "main page"
class FeedPageWidget extends StatefulWidget {
const FeedPageWidget({
Key key,
}) : super(key: key);
#override
_FeedPageWidgetState createState() => _FeedPageWidgetState();
}
class _FeedPageWidgetState extends State<FeedPageWidget>
int _currentIndex = 0;
AssetCarouselBuilder(assets: listViewPostsRecord.postAssets.asList())
And then widget I need to pass the index to another widget on the "main page".
ShareMenuWidget(
postRef: listViewPostsRecord,
assetIndex: _currentIndex)
Any help on how I get the "activeIndex" value on the setState function in the Carousel slider is very appreciated!
You can use callback method like Function(int activeIndex)? onIndexChanged;.
class CarouselCW extends StatefulWidget {
final Function(int activeIndex)? onIndexChanged;
const CarouselCW({
Key? key,
this.onIndexChanged,
}) : super(key: key);
#override
State<CarouselCW> createState() => _CarouselCWState();
}
class _CarouselCWState extends State<CarouselCW> {
final CarouselController carouselController = CarouselController();
#override
Widget build(BuildContext context) {
return CarouselSlider.builder(
itemCount: 4,
itemBuilder: (BuildContext context, int index, int realIndex) {
return Text(
index.toString(),
);
},
options: CarouselOptions(
onPageChanged: (index, reason) {
if (widget.onIndexChanged != null) widget.onIndexChanged!(index);
},
),
);
}
}
And while using this widget you will get
CarouselCW(
onIndexChanged: (activeIndex) {
print(activeIndex.toString());
},
)

ChangeNotifierProvider not re-rendering the UI (Flutter,Dart,Provider)

I have a BaseView that contains ChangeNotifieProvider and Consumer which will be common to use anywhere. This Widget also receives Generic types of ViewModel. It has onModelReady that to be called inside init state.
Also using get_it for Dependency injection.
Issue: Whenever the user inserts a new entry and calls fetch data, data gets loaded but UI still remains as it is.
If I remove the ChangeNotifierProvider and use only Consumer then it's re-rendering UI in a proper way. But I cannot pass the onModelReady function that is to be called in initState()
:::::::::::CODE:::::::::::::::::::::::::
base_view.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:businesshub/injections/injection_container.dart';
import 'package:businesshub/features/views/viewmodels/base_model.dart';
class BaseView<T extends BaseModel> extends StatefulWidget {
const BaseView({
Key? key,
this.onModelReady,
required this.builder,
}) : super(key: key);
final Function(T)? onModelReady;
final Widget Function(BuildContext context, T model, Widget? child) builder;
#override
_BaseViewState<T> createState() => _BaseViewState();
}
class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
T model = locator<T>();
#override
void initState() {
super.initState();
if (widget.onModelReady != null) {
widget.onModelReady!(model);
}
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
create: (context) => model,
child: Consumer<T>(
builder: widget.builder,
),
);
}
}
USING::::::::::::::::HERE::::::::::::::::::::::
class RecentBillBuilder extends StatelessWidget {
const RecentBillBuilder({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BaseView<SalesBillViewModel>(
onModelReady: (model) {
model.fetchAndSetSalesBills(currentUser!.uid);
model.searchController.clear();
},
builder: (ctx, model, _) {
if (model.state == ViewState.busy) {
return Center(
child: CircularProgressIndicator.adaptive(),
);
}
return model.bills.fold(
(l) => ResourceNotFound(title: l.message!),
(r) => (r.isEmpty)
? ResourceNotFound(title: "Sales Bills not created yet!")
: ListView.builder(
itemCount: min(r.length, 7),
shrinkWrap: true,
reverse: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (ctx, index) {
return RecentBillsCard(bill: r[index]);
},
),
);
},
);
}
}

Is there a way to rebuild AnimatedList in Flutter?

I have the following issue with my 'workout' App using multiple workoutlists with various workoutitems:
I select a workoutlist with 12 workoutitems.
The 'activity' screen with the AnimatedList is shown.
Afterwards, I select a different workoutlist with 80 workoutitems.
The AnimatedList is now showing the new workoutlist but only the first 12 workoutitems.
Why?
I thought that the AnimatedList inside the build Widget is rebuild every time (I am not using GlobalKey).
class WorkoutListView extends StatelessWidget {
const WorkoutListView({this.filename});
final String filename;
#override
Widget build(BuildContext context) {
return Selector<WorkoutListModel, List<Workout>>(
selector: (_, model) => model.filterWorkouts(filename),
builder: (context, workouts, _) {
return AnimatedWorkoutList(
list: workouts,
);
},
);
}
}
class AnimatedWorkoutList extends StatefulWidget {
const AnimatedWorkoutList({
Key key,
#required List<Workout> list,
}) : _list = list,
super(key: key);
final List<Workout> _list;
#override
_AnimatedWorkoutListState createState() => _AnimatedWorkoutListState();
}
class _AnimatedWorkoutListState extends State<AnimatedWorkoutList> {
#override
Widget build(BuildContext context) {
return AnimatedList(
initialItemCount: widget._list.length,
itemBuilder: (context, index, animation) {
final workout = widget._list[index];
return Column(
children: [
// Using AnimatedList.of(context).removeItem() for list manipulation
],
);
},
);
}
}
try this:
class AnimatedWorkoutList extends StatefulWidget {
const AnimatedWorkoutList({
#required List<Workout> list,
});
final List<Workout> list;
#override
_AnimatedWorkoutListState createState() => _AnimatedWorkoutListState();
}
class _AnimatedWorkoutListState extends State<AnimatedWorkoutList> {
#override
Widget build(BuildContext context) {
return AnimatedList(
initialItemCount: widget.list.length,
itemBuilder: (context, index, animation) {
final workout = widget.list[index];
return Column(
children: [
// Using AnimatedList.of(context).removeItem() for list manipulation
],
);
},
);
}
}

Flutter PageView keep rebuilding the main widget on setState value

currently flutter app structure
StackedHome has a pageview with 2 children
Pageview(parent):
HomeScreen(child#1)
Vertical PageView
bottom navigation bar
UserProfilePage(child#2)
HomeScreen should pass the index value to UserProfilePage, so when scrolling horizontally, we will get user profilescreen with id passed to that received from HomeScreen. based on the id passed i will display related user profile
Here is sample video showing the problem :
https://drive.google.com/file/d/1tIypNOHewcFSo2Pf-F97hsQGfDgNVqfW/view?usp=sharing
Problem:
i managed to do that and its working fine, but my problem on setState of that variable
setState(() {
_postIndex = postIndex;
});
on each HomeScreen > onPageChanged call i am updating the index value pass it to the parent (StackedHome) class, and since there is a setState to update profile index (UserProfilePage)...the whole app will be rebuild on each pageview change...
What i need is to disable that main widget to be rebuilt again and again on value update..
StackedHome
class StackedHome extends StatefulWidget {
final int data;
final Function(int) onDataChange;
const StackedHome({
this.data,
this.onDataChange,
Key key,
}) : super(key: key);
#override
_StackedHomeState createState() => _StackedHomeState();
}
class _StackedHomeState extends State<StackedHome>{
PageController pageController;
int _count = 0;
int _postIndex = 0;
void _postId(int postIndex) {
//This cuasing main screen to be rebuilt everytime on pageview scroll
//but getting the value correctly
setState(() {
_postIndex = postIndex;
});
}
#override
void initState() {
super.initState();
pageController = PageController();
}
#override
void dispose() {
pageController.dispose();
super.dispose();
}
int index = 0;
#override
Future<void> _refreshPosts() async {
PostApi postApi = PostApi();
setState(() {
postApi.fetchAllPosts();
});
}
Widget build(BuildContext context) {
PostApi postApi = PostApi();
return FutureBuilder(
future: postApi.fetchAllPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return apiError('No Connection Made');
break;
case ConnectionState.waiting:
case ConnectionState.active:
return ApiLoading(color:0xff000000);
break;
case ConnectionState.done:
if (snapshot.hasError) {
return apiError(snapshot.error.toString());
}
if (snapshot.hasData) {
return _drawPostsList(snapshot.data, context);
}
break;
}
return Container();
},
);
}
Widget _drawPostsList(List<Post> posts, BuildContext context) {
return PageView(
reverse: true,
children: <Widget>[
HomeScreen(
posts: posts,
index: index,
postId: _postId,//function Passed
),
UserProfilePage(
posts: posts,
index: _postIndex,
)
],
);
}
}
HomeScreen
class HomeScreen extends StatefulWidget {
#override
final List posts;
final int index;
final Function(int) postId;
int getPage() {
return value;
}
void setPage(int page) {
value = page;
}
HomeScreen({Key key, this.posts, this.index, this.postId}) : super(key: key);
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
final PageController _controller = PageController();
PageController _pageController = PageController();
int index = 0;
#override
void initState() {
super.initState();
//Set pageview inital page
_pageController = PageController(
keepPage: true,
initialPage: widget.getPage(),
);
}
#override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _refreshPosts,
child: Stack(children: <Widget>[
PageView.builder(
controller: _pageController,
onPageChanged: (index) => setState(() {
.
widget.postId(index);//I am calling parent class and updating the vlaue with new index value
.
}),
scrollDirection: Axis.vertical,
itemBuilder: (context, position) {
//Build image lists
return _homeList(widget.posts, position);
},
),
BottomNavigation("light"),
]),
);
}
}
i hope my problem is clear enough....i need to pass the value to parent so i can pass it to second child which is the profile screen so it will show user profile realted to that post
Ohh wow, managed to solve this problem using provider and consumer, by listening to any update on index id... this post helped me to solve it https://medium.com/flutter-nyc/a-closer-look-at-the-provider-package-993922d3a5a5