GetX UI state not changing on ListTile - flutter

I have a list of objects, but I want to change the state of one object to "isLoading" where it will have a different title, etc.
I'm building my list view:
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return saveGame.isLoading
? buildListTileIsLoading(context, saveGame)
: buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
onTap: () => controller.process(saveGame)
);
}
The controller:
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[].obs;
void process(SaveGame saveGame) {
saveGame.working = true;
update();
}
}
Where have I gone wrong here?
edits: Added more code

So despite the fact, I'm only updating one object in the list and not modifying the content of the list (adding/removing objects) I still need to call saveGames.refresh();
An oversight on my end didn't think you'd need to refresh the entire list if you're just changing the property on one of the objects.
Good to know :)

update() is used with GetBuilder()
obs() is used with obx()
you need to make a change on list to update widgets
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/get_navigation.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetMaterialApp(
onInit: () {
Get.lazyPut(() => SaveGameController());
},
home: const HomePage(),
);
}
}
class HomePage extends GetView<SaveGameController> {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(), body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
tileColor: saveGame.working ? Colors.red : Colors.yellow,
title: Text(saveGame.name),
onTap: () => controller.process(saveGame));
}
}
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[
SaveGame(id: 0, name: 'a', working: false),
SaveGame(id: 1, name: 'b', working: false),
SaveGame(id: 2, name: 'c', working: false)
].obs;
void process(SaveGame saveGame) {
final index = saveGames.indexWhere((element) => element.id == saveGame.id);
saveGames
.replaceRange(index, index + 1, [saveGame.copyWith(working: true)]);
}
}
class SaveGame {
final int id;
final String name;
final bool working;
SaveGame({required this.id, required this.name, required this.working});
SaveGame copyWith({int? id, String? name, bool? working}) {
return SaveGame(
id: id ?? this.id,
name: name ?? this.name,
working: working ?? this.working);
}
}

Related

Provider cannot be found above widget

class ListPosts extends StatefulWidget {
const ListPosts({Key? key}) : super(key: key);
#override
State<ListPosts> createState() => _ListPostsState();
}
class _ListPostsState extends State<ListPosts> {
#override
Widget build(BuildContext context) {
final posts = Provider.of<List<PostModel>>(context) ?? [];
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post.creator),
subtitle: Text(post.text),
);
},
);
}
}
It gives me this error on the provider and I checked everywhere but I cannot find any solution:
Error: Could not find the correct Provider<List> above this ListPosts Widget
This happens because you used a BuildContext that does not include the provider
of your choice.
I checked in other post and tutorials but could not find a good solutions, a lot of people are talking about to fix the widget tree but I believe mine is ok.
This could be your answer.
Just Follow the below code.
Your Model Class:
class PostModel {
final int id;
final int userId;
final String title;
final String body;
PostModel({this.id, this.userId, this.title, this.body});
factory PostModel.fromJson(Map<String, dynamic> json) {
return PostModel(
id: json['id'],
userId: json['userId'],
title: json['title'] ?? "",
body: json['body'] ?? "",
);
}
}
Your Provider Class:
class PostDataProvider with ChangeNotifier {
List<PostModel> post = [];
getPostData(context) async {
post = await getPostData(context); //This method will bring your posts data in formate of List<PostModel>
notifyListeners();
}
}
Your UI Screen:
class ListPosts extends StatefulWidget {
const ListPosts({Key? key}) : super(key: key);
#override
State<ListPosts> createState() => _ListPostsState();
}
class _ListPostsState extends State<ListPosts> {
#override
void initState() {
super.initState();
final postProvider = Provider.of<PostDataProvider>(context, listen: false);
postProvider.getPostData(context);
}
#override
Widget build(BuildContext context) {
List<PostModel> posts = Provider.of<PostDataProvider>(context).post;
return posts.isEmpty ? const Center(child:
CircularProgressIndicator()):ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post.creator),
subtitle: Text(post.text),
);
},
);
}
}

How can I change my code to allow me to correctly use BLoC without setState?

Introduction: I am encountering an unexpected problem in my code when using BLoC, which I don't know how to fix, and I was looking for a solution.
When I use the code below, an object adds to the ListView.builder whenever I click on the ListTile as expected. However, This addition only happens once and when I continue to press on the ListTile to add more items to the list, the state is not updated (which is verifiable by pressing the Hot Reload button, which then updates the table with the items).
Problem: I have converted the MyHomePage widget to a StatefulWidget and wrapped the call to my bloc in a setState method (uncomment the commented section of the code to see that it works as expected). Now, as I understand it, I should be able to use BLoC for my state management needs and not need to use a StatefulWidget.
Questions:
Why is my code not working correctly?
How can I change my code to fix my problem?
Code:
Here is the complete minimum code, which reflects the problem that I am having:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CollectionBloc(),
child: const MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return BlocBuilder<CollectionBloc, Collection>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: ListView.builder(
itemCount: state.hierarchy(state).length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
var showType = state.hierarchy(state)[index].showType;
// TODO: setState function is here.
setState(() {
if (showType == ShowType.collection) {
BlocProvider.of<CollectionBloc>(context).add(AddSeries(
series: Collection(
name:
"Series ${state.getChildrenOfNode(state.hierarchy(state)[index]).length + 1}",
showType: ShowType.series,
children: [],
)));
BlocProvider.of<CollectionBloc>(context).add(UpdateBloc(collection: state));
}
if (showType == ShowType.series) {
BlocProvider.of<CollectionBloc>(context).add(AddSeason(
series: state.hierarchy(state)[index],
season: Collection(
name:
'Season ${state.getChildrenOfNode(state.hierarchy(state)[index]).length + 1}',
showType: ShowType.season,
children: [],
)));
}
if (showType == ShowType.season) {
BlocProvider.of<CollectionBloc>(context).add(AddEpisode(
season: state.hierarchy(state)[index],
episode: Collection(
name:
"Episode ${state.getChildrenOfNode(state.hierarchy(state)[index]).length + 1}",
showType: ShowType.episode,
children: [],
)));
}
});
},
leading: Card(
child: TextWidget(name: state.hierarchy(state)[index].name),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: const Text('to json'),
onPressed: () {
var toJson = state.toJson();
var fromJson = Collection.fromJson(toJson);
print(fromJson);
},
),
);
},
);
}
}
class TextWidget extends StatelessWidget {
const TextWidget({super.key, required this.name});
final String name;
#override
Widget build(BuildContext context) {
return Text(name);
}
}
/// BLoC
class InitialState extends Collection {
InitialState(collection)
: super(
name: collection.name,
showType: collection.showType,
children: collection.children,
);
}
abstract class BLoCEvents {}
class AddSeries extends BLoCEvents {
AddSeries({required this.series});
final Collection series;
}
class AddSeason extends BLoCEvents {
AddSeason({required this.series, required this.season});
final Collection series;
final Collection season;
}
class AddEpisode extends BLoCEvents {
AddEpisode({required this.season, required this.episode});
final Collection season;
final Collection episode;
}
class UpdateBloc extends BLoCEvents {
UpdateBloc({required this.collection});
final Collection collection;
}
class CollectionBloc extends Bloc<BLoCEvents, InitialState> {
CollectionBloc()
: super(InitialState(Collection(
name: 'Collection', showType: ShowType.collection, children: []))) {
on<AddSeries>(
((event, emit) => emit(state..addSeries(series: event.series))));
on<AddSeason>(((event, emit) =>
emit(state..addSeason(series: event.series, season: event.season))));
on<AddEpisode>(((event, emit) =>
emit(state..addEpisode(season: event.season, episode: event.episode))));
///todo: update bloc here.
on<UpdateBloc>(((event, emit) => print(state.toJson())));
}
}
/// Model
enum ShowType { collection, series, season, episode }
class Collection {
Collection(
{required this.name, required this.showType, required this.children});
final String name;
final ShowType showType;
List<Collection> children = [];
void addSeries({required Collection series}) => children.add(series);
void addSeason({required Collection series, required Collection season}) =>
series.children.add(season);
void addEpisode({required Collection season, required Collection episode}) =>
season.children.add(episode);
List<Collection> hierarchy(Collection node) {
List<Collection> list = [];
list.add(node);
for (Collection child in node.children) {
list.addAll(hierarchy(child));
}
return list;
}
List<Collection> getChildrenOfNode(Collection node) {
List<Collection> list = [];
for (Collection child in node.children) {
list.add(child);
}
return list;
}
toJson() {
return {
'name': name,
'showType': showType,
'children': children.map((child) => child.toJson()).toList(),
};
}
factory Collection.fromJson(Map<String, dynamic> json) {
return Collection(
name: json['name'],
showType: json['showType'],
children: json['children']
.map<Collection>((child) => Collection.fromJson(child))
.toList());
}
#override
String toString() {
return 'Collection{name: $name, showType: $showType, children: $children}';
}
}

how to display the value that came from valueNotifier?

I have a valueNotifier that generates a list of events and takes a random string every 5 seconds and sends it to the screen. It lies in inheritedWidget. How can I display in the ListView the event that came with the valueNotifier? What is the correct way to print the answer?
My code:
class EventList extends StatelessWidget {
const EventList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return EventInherited(
child: EventListScreen(),
);
}
}
class EventListScreen extends StatefulWidget {
const EventListScreen({Key? key}) : super(key: key);
#override
State<EventListScreen> createState() => _EventListScreenState();
}
class _EventListScreenState extends State<EventListScreen> {
#override
Widget build(BuildContext context) {
final eventNotifier = EventInherited.of(context).eventNotifier;
return Scaffold(
appBar: AppBar(
title: const Text('Event List'),
centerTitle: true,
),
body: Container(
padding: const EdgeInsets.all(30),
child: ValueListenableBuilder(
valueListenable: eventNotifier,
builder: (BuildContext context, List<String> value, Widget? child) {
return ListView(
children: [
],
);
},
),
),
);
}
}
class EventNotifier extends ValueNotifier<List<String>> {
EventNotifier(List<String> value) : super(value);
final List<String> events = ['add', 'delete', 'edit'];
final stream = Stream.periodic(const Duration(seconds: 5));
late final streamSub = stream.listen((event) {
value.add(
events[Random().nextInt(4)],
);
});
}
class EventInherited extends InheritedWidget {
final EventNotifier eventNotifier = EventNotifier([]);
EventInherited({required Widget child}) : super(child: child);
static EventInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
#override
bool updateShouldNotify(EventInherited oldWidget) {
return oldWidget.eventNotifier.streamSub != eventNotifier.streamSub;
}
}
If you have correct value, you can return listview like this:
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return Text(value[index]);
},
);
After having a quick look at ValueNotifier,
It says the following:
When the value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
In your case, the value is an array. By adding items to the array, it wont recognise a change.
Also see other Stacko post.
Try something like:
value = [...value].add(...)

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
],
);
},
);
}
}

How to dynamically update a particular list view item while using a ListView.builder?

I am making a list view in Flutter. I want to update an item's property when the item is long pressed.
Following is the complete Code:
// main.dart
import 'package:LearnFlutter/MyList.dart';
import 'package:flutter/material.dart';
import 'MyList.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'My list demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: MyList(),
);
}
}
// MyList.dart
import 'package:flutter/material.dart';
class Item {
Item(String name, bool selected, Color color) {
_name = name;
_selected = selected;
_color = color;
}
String _name;
bool _selected;
Color _color;
String getName() {
return _name;
}
bool isSelected() {
return _selected;
}
void toggleSelected() {
_selected = !_selected;
}
void setColor(Color color) {
_color = color;
}
Color getColor() {
return _color;
}
}
class MyList extends StatefulWidget {
#override
_CardyState createState() => new _CardyState();
}
class _CardyState extends State<MyList> {
#override
Widget build(BuildContext context) {
var itemsList = [
Item('My item1', false, Colors.grey[200]),
Item('My item2', false, Colors.grey[200]),
Item('My item3', false, Colors.grey[200]),
];
return ListView.builder(
itemCount: itemsList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Icon(Icons.train),
title: Text(itemsList[index].getName()),
trailing: Icon(Icons.keyboard_arrow_right),
tileColor: itemsList[index].getColor(),
selected: itemsList[index].isSelected(),
onLongPress: () {
toggleSelection(itemsList[index]);
},
),
);
},
);
}
void toggleSelection(Item item) {
print(item.getName() + ' long pressed');
setState(() {
item.toggleSelected();
if (item.isSelected()) {
item.setColor(Colors.blue[200]);
} else {
item.setColor(Colors.grey[200]);
}
});
}
}
Question:
In the above code toggleSelection is getting called on long press event. But the item's color does not get updated. What am I doing wrong?
The main reason it is not functioning properly is that you have no state in your class Item, so you are not re-building/updating anything. If you would like to handle it there in the class, then you will need to extend it to the ChangeNotifier. You will also need to use the ChangeNotifierProvider, look at the docs for help: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
You will need the provider package: https://pub.dev/packages/provider
Class Item
class Item extends ChangeNotifier {
Item(String name, Color color) {
_name = name;
_color = color;
}
int selectedIndex; // to know active index
String _name;
Color _color;
String getName() {
return _name;
}
void toggleSelected(int index) {
selectedIndex = index;
notifyListeners(); // To rebuild the Widget
}
void setColor(Color color) {
_color = color;
notifyListeners();
}
Color getColor() {
return _color;
}
}
Widget List
class MyList extends StatefulWidget {
#override
_CardyState createState() => new _CardyState();
}
class _CardyState extends State<MyList> {
#override
Widget build(BuildContext context) {
final items = Provider.of<Item>(context); // Accessing the provider
bool selected = false; // default val. of bool
var itemsList = [
Item('My item1', Colors.grey[200]),
Item('My item2', Colors.grey[200]),
Item('My item3', Colors.grey[200]),
];
return ListView.builder(
itemCount: itemsList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Icon(Icons.train),
title: Text(itemsList[index].getName()),
trailing: Icon(Icons.keyboard_arrow_right),
tileColor: items.selectedIndex == index
? items.getColor()
: Colors.grey[200],
selected: items.selectedIndex == index ? true : false,
onLongPress: () {
setState(() => selected = !selected);
items.toggleSelected(index);
if (selected) {
items.setColor(Colors.red);
}
},
),
);
},
);
}
}
make MyList into a stateless widget keep all the data that it should show in the HomePage which is a statefull widget including the data about the selected items. then pass the data into MyList
here is how your MyList could be
class MyList extends StatelessWidget {
final List<Item> items;
final List<int> selectedItemIdList;
final void Function(Item) onLongClick;
MyList(this.items, this.selectedItemIdList, this.onLongClick);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, position) {
//remember all you need to do here is to create your item based on the data you have
var item = items[position];
var isSelected = items.firstWhere((element) => item.id == element.id) != null;
if (isSelected) {
//build and return a widget with selected look
} else {
return GestureDetector(
onLongPress: () => onLongClick(item), //changes data in homepage then MyList will be updated automatically
child: Container(
//rest of your widget
),
);
}
},
itemCount: items.length,
);
}
}
inside your HomePageState
//all the data the list it build from should be stored here not inside the list. and
List<Item> items = [ ... ];
List<int> selectedItemIdList = [ ... ];
//MyList is just a Stateless widget that only shows this data change (a very DUMB view as one could say)
#override
Widget build(BuildContext context) {
return MyList(items, selectedItemIdList, (item) {
setState((){
selectedItemIdList.add(item.id);
});
});
}