Flutter-getx, widget does not re-render when RxMap set to a new value - flutter

I'm building a widget that renders based on the contents of an RxMap. I start with the RxMap initialized to empty, and upon clicking a button set the RxMap to contain a new value. After setting the new value for the RxMap, the widget does not re-render to display the new map values.
Here's my code:
(EDIT: included all the boilerplate as well to prevent confusion. Each class is in a separate file with relevant imports in all the other files.)
class MyController extends GetxController {
var selectedItem = {}.obs;
}
class MyBinding extends Bindings {
#override
void dependencies() {
Get.put(MyController());
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext highContext) {
return GetMaterialApp(
title: 'App',
getPages: [
GetPage(
name: "/test",
page: () => MyClass(),
binding: MyBinding()),
],
initialRoute: "/test",
);
}
}
final saController = Get.find<MyController>();
class MyClass extends StatelessWidget {
Widget build(context) {
print(saController.selectedItem);
return Scaffold(
child: Column(
children: [
Obx(() => Container(
child: Text(saController.selectedItem.toString())
)),
TextButton(
child: Text("click"),
onPressed: (() {
saController.selectedItem = RxMap({"test": "item"});
saController.selectedItem.refresh();
saController.update();
print(saController.selectedItem);
})
)
]
)
);
}
}
The first print statement correctly prints the empty object to the console and the onPressed print statement correctly prints the new object with its key and value included to the console, but the widget does not re-render with the new value. refresh() and update() don't appear to do anything.
This seems to be specific to RxMap, I've been able to get other datatypes to update on change successfully, e.g. swapping out the RxMap value for a boolean value and toggling it from true to false on click causes the widget to re-render correctly without changing any other parts of the code.

Your UI will not trigger an update unless the value of a Rx variable (selectedItem.value) is changed. But you are updating the Rx variable (selectedItem) itself.
Therefore,you need to change saController.selectedItem = RxMap({"test": "item"}); to saController.selectedItem.value = {"test": "item"};
And I think you don't actually need to call refresh() and update() anymore.

Related

Is it possible to share and update one screen's reactive value in another screen without Provider?

So I have this block of code in a widget that navigates to another screen:
screen_one.dart
class ScreenOne extends StatefulWidget {
const ScreenOne({ super.key });
#override
State<ScreenOne> createState() => _ScreenOneState();
}
class _ScreenOneState extends State<ScreenOne> {
List<String> state = [''];
#override
Widget build(BuildContext context) {
return Column(
MaterialButton(
onPressed: () => Navigator.pushNamed(context, '/screen-two'),
child: Text('Click here.')
),
Text(state[0]),
);
}
}
screen_two.dart
class ScreenTwo extends StatelessWidget {
const ScreenTwo({ super.key });
#override
Widget build(BuildContext context) {
return Container();
}
}
Basically I need to pass the state variable from ScreenOne to ScreenTwo and then update it there (in ScreenTwo)
ScreenTwo needs to display the same thing as ScreenOne and add() a new item to the state list when some button is clicked which should show on both the screens.
Its just one simple List so I am trying to avoid using provider.
Is it possible to do though?
I'm currently just passing it through the Navigator:
screen_one.dart
Navigator.pushNamed(
context,
'/post-info',
arguments: state,
),
screen_two.dart
Widget build(BuildContext context) {
final List<String> post = ModalRoute.of(context)!.settings.arguments as List<String>;
// ...
}
first I want to recommend you when things go bigger and more complex, it's better to use a state management approach, However since you did say that you have only one List you can simply use a ValueNotifier, with ValueListenableBuilder:
// this should be outside widget classes, maybe in a custom-made class or just in a global scope.
ValueNotifier stateNotifier = ValueNotifier([""]);
now in the places you want to use that state, you can use ValueListenableWidget like this:
ValueListenableBuilder(
valueListenable: stateNotifier,
builder: (context, value, child) {
return Column(
children: [
Text('${state[0]}'),
MaterialButton(
onPressed: () {
Navigator.pushNamed(context, '/screen-two'),
},
child: Text('click'),
),
],
);
},
);
}
}
and any other place where you want to see that state get updates, you need to use ValueListenableWidget.
Now, for executing a method like add() on the List and notify the widgets, you need to assign a new value for it like this:
void addInTheList(String elem) {
List current = stateNotifier.value;
current.add(elem);
// this exactly what will be responsible for updating.
stateNotifier.value = current;
}
now, you can just call addInTheList and expect it to update in all of them:
addInTheList("Example");

Best way to pass widgets to child widget in flutter

I'm new to flutter but I have a widget that wraps a custom painter. I am trying to get it to work so I can supply a Widget to this child widget's constructor and then use that widget as the child of the custom painter.
For example:
class MyPainterWrapper extends StatefulWidget {
Widget _childWidget;
SceneRender([this._childWidget]);
#override
State<StatefulWidget> createState() {
return new MyPainterWrapperState(_childWidget);
}
}
class MyPainterWrapperState extends State<SceneRender> {
Widget _childWidget;
MyPainterWrapperState(this._childWidget);
#override
Widget build(BuildContext context) {
return Column(
children: [
CustomPaint(painter: MyPainter(), child: _childWidget)
],
);
}
}
And in another widget (called testWidget):
bool _answerCorrect = false;
bool _answerInputted = false;
var _msgController = TextEditingController();
FocusNode _answerFieldFocus = new FocusNode();
DictionaryEntry _currentEntry;
void _checkIfCorrect(String answerGiven) {
setState(() {
_answerCorrect = false;
if (_currentEntry.Full_Word == answerGiven)
_answerCorrect = true;
else if (_currentEntry.dictReadings.isNotEmpty) {
for (AlternateDictionaryEntryReading entryReading in _currentEntry
.dictReadings) {
if (entryReading.Alternate_Reading == answerGiven) {
_answerCorrect = true;
break;
}
}
}
_answerInputted = true;
_msgController.clear();
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: MyPainterWrapper(Center(Container(Column(children: <Widget>[
if (_answerCorrect && _answerInputted) Text('CORRECT!'),
if (!_answerCorrect && _answerInputted) Text('WRONG:'),
if (_answerInputted)
Text(_currentEntry.Full_Word),
if (_answerInputted)
for(AlternateDictionaryEntryReading reading in _currentEntry.dictReadings)
Text(reading.Alternate_Reading),
Container(
constraints: BoxConstraints.expand(
height: 100,
width: 1000
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (DictionaryTranslation translation in _currentEntry.dictTranslations)
Text(translation.Translation),
],
)
),
),
Text('Enter Answer:',),
TextField(
controller: _msgController,
focusNode: _answerFieldFocus,
onSubmitted: (String value) {
_checkIfCorrect(value);
_answerFieldFocus.requestFocus();
},
)
This works to render the first time correctly, but any setState calls from checkIfCorrect from testWidget do not force the child widget to rebuild. I've tried testing it this way and it works, so that leads me to believe that I'm passing the widget incorrectly to have it redrawn via setState
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: CustomPaint(painter: TestPainter(), child: Center(
child: Container(...))
Your MyPainterWrapperState class reads like you are creating a new _childWidget property inside your state (which has a default value of null). You are then using it to initialize a new instance of MyPainterWrapperState, then throwing away the instance of MyPainterWrapper() that you just created.
You're not actually using the stateful part of your stateful widget at all; You're just calling a method that returns something once.
That said, your approach is basically right, but the implementation is off a little.
My advice:
First, you can use named properties to supply constructor arguments to your class. I've made that change in the code snippet shown below.
The State class of a stateful widget supplies a widget property that should be used to reference the properties of the widget that created it. The State widget should also have a solitary initializer that accepts no arguments.
Also good to know is that the State class provides an initState() method that you can override to initialize any class local variables that you declare. This should be used to give a value to your _childWidget property.
Finally, anything you expect to be rebuilt should be inside the MyPainterWrapperState() class. Your SceneRender() method doesn't appear in your code, but you might want to move it into MyPainterWrapperState() if you expect the scene to change based on the value of the child.
I suggest these changes.
Pass arguments to MyPainterWrapper via named arguments.
Remove the argument to MyPainterWrapperState() and reference the child through the widget property supplied to the state.
Initialize _childWidget by overriding initState()
If SceneRender() does anything that depends on the value of _childWidget, move it to the build() method of MyPainterWrapperState().
The Flutter docs are great, and the Flutter team has created a ton of helpful YouTube videos that explain in a couple of minutes examples of how to use dozens of them. For a better understanding of StatefulWidget, you can read about it here.
If you make these changes, your solution would look something like the code snippet below.
Presuming you make those changes, you would alter your call to MyPainterWrapper() to use named properties.
Change this line
body: MyPainterWrapper(Center(Container(Column(children: <Widget>[
To this
body: MyPainterWrapper(child: Center(Container(Column(children: <Widget>[
This won't get you to done, but it will get you closer. I also haven't run this through a compiler, so there are probably errors in the snippet, but this should serve to illustrate the approach.
class MyPainterWrapper extends StatefulWidget {
MyPainterWrapper(
{
#required child: this.childWidget,
}
);
final Widget childWidget;
// Not sure what this does, but I'm pretty sure that it doesn't
// provide anything into the widget tree.
// If it mutates its arguments, then you might still need it.
// SceneRender([this._childWidget]);
#override
State<StatefulWidget> createState() {
// Note no arguments.
return new MyPainterWrapperState();
}
}
class MyPainterWrapperState extends State<MyPainterWrapper> {
// This is an uninitialized variable inside this class.
Widget _childWidget;
// MyPainterWrapperState(this._childWidget);
// Initialize state variables here.
#override initState() {
// Assigns the widget class initializer to your state variable.
_childWidget = widget.childWidget;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
CustomPaint(painter: MyPainter(), child: _childWidget)
],
);
}
}```

Flutter- Passing Data between Custom Widgets inside column

I'm stuck with a WidgetA() which calculates a String but I'd like to pass this string to WidgetB() - how do I pass the updated data to WidgetB ?
return Column(
children: [
WidgetA() // calculates value
WidgetB() // needs to use this value
],
);
Widget A is a costum Class Stateful Widget
The most obvious way, to me, to do this is to store the data in the state of the parent widget. You could then provide WidgetA with a callback to set this state, and provide WidgetB with the value:
int value;
setValue(int newValue) {
setState(() {
value = newValue;
});
}
return Column(
children: [
WidgetA(callback: (int newValue) => this.setValue(newValue)) // calculates value
WidgetB(value: this.value) // needs to use this value
],
);
WidgetA and WidgetB could then look something like the following:
class WidgetA extends StatelessWidget {
final Function callback;
// get the callback as a named argument
WidgetA({required this.callback});
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
print('The button was pressed!');
// get the value and call the callback with it
callback(42);
},
child: Text('Press me!'));
}
}
class WidgetB extends StatelessWidget {
final int value;
WidgetB({required this.value});
#override
Widget build(BuildContext context) {
return Text('The number is $value');
}
}
Depending on the value and how often it's updated, using some sort of storage like shared preferences for the value is also possible.
Create a shared memory, for example, a class, and save the value to a class variable.
class SharedMemory {
String stringCalculatedByWidgetA;
}
Then make sure that WidgetB updates the value in the shared memory.

Flutter BLoC can't update my list of boolean

So, I tried to learn flutter especially in BLoC method and I made a simple ToggleButtons with BLoC. Here it looks like
ToggleUI.dart
class Flutter501 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 50 With Bloc Package',
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocProvider<ToggleBloc>(
builder: (context) => ToggleBloc(maxToggles: 4),
child: MyToggle(),
)
],
),
),
),
);
}
}
class MyToggle extends StatelessWidget {
const MyToggle({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
ToggleBloc bloc = BlocProvider.of<ToggleBloc>(context);
return BlocBuilder<ToggleBloc, List<bool>>(
bloc: bloc,
builder: (context, state) {
return ToggleButtons(
children: [
Icon(Icons.arrow_back),
Icon(Icons.arrow_upward),
Icon(Icons.arrow_forward),
Icon(Icons.arrow_downward),
],
onPressed: (idx) {
bloc.dispatch(ToggleTap(index: idx));
},
isSelected: state,
);
},
);
}
}
ToogleBloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
abstract class ToggleEvent extends Equatable {
const ToggleEvent();
}
class ToggleTap extends ToggleEvent {
final int index;
ToggleTap({this.index});
#override
// TODO: implement props
List<Object> get props => [];
}
class ToggleBloc extends Bloc<ToggleEvent, List<bool>> {
final List<bool> toggles = [];
ToggleBloc({
#required int maxToggles,
}) {
for (int i = 0; i < maxToggles; i++) {
this.toggles.add(false);
}
}
#override
// TODO: implement initialState
List<bool> get initialState => this.toggles;
#override
Stream<List<bool>> mapEventToState(ToggleEvent event) async* {
// TODO: implement mapEventToState
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
}
yield this.toggles;
}
}
The problem came when I tried to Tap/Press one of the buttons, but it doesn't want to change into the active button. But it works whenever I tried to press the "Hot Reload". It likes I have to make a setState whenever the button pressed.
The BlocBuilder.builder method is only executed if the State changes. So in your case the State is a List<bool> of which you only change a specific index and yield the same object. Because of this, BlocBuilder can't determine if the List changed and therefore doesn't trigger a rebuild of the UI.
See https://github.com/felangel/bloc/blob/master/docs/faqs.md for the explanation in the flutter_bloc docs:
Equatable properties should always be copied rather than modified. If an Equatable class contains a List or Map as properties, be sure to use List.from or Map.from respectively to ensure that equality is evaluated based on the values of the properties rather than the reference.
Solution
In your ToggleBloc, change the List like this, so it creates a completely new List object:
#override
Stream<List<bool>> mapEventToState(ToggleEvent event) async* {
// TODO: implement mapEventToState
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
this.toggles = List.from(this.toggles);
}
yield this.toggles;
}
Also, make sure to set the props for your event, although it won't really matter for this specific question.
BlocBuilder will ignore the update if a new state was equal to the old state. When comparing two lists in Dart language, if they are the same instance, they are equal, otherwise, they are not equal.
So, in your case, you would have to create a new instance of list for every state change, or define a state object and send your list as property of it.
Here is how you would create new list instance for every state:
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
}
yield List.from(this.toggles);
You can read more about bloc library and equality here:
https://bloclibrary.dev/#/faqs?id=when-to-use-equatable

Flutter interaction with change in memory

I am trying to understand how a flutter Widget populated by a memory item behaves when the underlying memory item changes. My use case is that: by trigger of some event(using EventBus package), this underlying memory item may or may not change. If it does, then I would like to trigger a setState(), but not otherwise as I wouldn't want to unnecessarily call it. Kindly help me as to how I can achieve this.Currently the onclick changes the str but the Text widget doesn't change. My example -
button click(acting as event trigger) changes underlying str item that populates the Text item. Thanks in advance.
class _MainApp extends State<MainApp>{
#override
String str = "1";
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
CustomText(),
RaisedButton(onPressed: (){
print(CustomTextS.str);
CustomTextS.setStr("f");
print(CustomTextS.str);
},)
],
),
),
);
}
}
class CustomText extends StatefulWidget{
CustomTextS createState() => CustomTextS();
}
class CustomTextS extends State<CustomText>{
static String str = "1";
static setStr(String a){
str = a;
}
Widget build(BuildContext context){
return Text(str);
}
}
Currently I proceeded to use the fact that Strings and integers are passed by value. Thus one can use that fact to get effectively a copy which one can use to compare with the updated value to determine whether or not on event action a widget's properties have changed or not.