How to listen to variable changes in a provider class? - flutter

When using providers, how can I listen to a value change in the provider class?
In the code below, when _myData changes, I want to change _allData as well. (I know I can just change them both together, but the code below is a stripped version of what I have)
class CoolProvider extends ChangeNotifier {
List<String> _allData = [];
List<String> _myData = [];
List<OrdersResponse> get allData => _allData;
List<OrdersResponse> get myData => _myData;
void changeData() {
_allData = ['yo', 'bro'];
notifyListeners();
}
}

You can use addListener for instance of your class.
CoolProvider coolProvider = CoolProvider();
void f() {
// access new data
}
coolProvider.addListener(f);

Related

Flutter Provider: Should I create a new Provider for every class?

Lets say I have multiple settings a user can set. Should I have one Provider which manages all settings like so:
class Settings with ChangeNotifier {
SettingsA _settingsA;
SettingsB _settingsB;
List<String> _settingsC;
SettingsA get settingsA => _settingsA;
SettingsB get settingsB => _settingsB;
List<String> get settingsC => _settingsC;
// Setters
void updateA(SettingsA settingsA) {
_settingsA = settingsA;
notifyListeners();
}
void updateB(SettingsB settingsB) {
_settingsB = settingsB;
notifyListeners();
}
void addToC(String setting) {
_settingsC.add(setting);
notifyListeners();
}
}
Or should I rather make a Provider for every object like so:
class SettingsAProvider with ChangeNotifier {
SettingsA _settingsA;
SettingsA get settingsA => _settingsA;
// Setters
void update(SettingsA settingsA) {
_settingsA = settingsA;
notifyListeners();
}
}
What is the best practise of using ChangeNotifierProviders?
In my opinion, you should use SettingAProvider,SettingBProvider,...
If you use Settings Class...
When you call updateA, it will notify all value _settingA,_settingB,_settingC,... even if unecessary.

Read provider inside a global or static method in Flutter

I have a question, regarding reading providers from inside static methods or global methods. I am using riverpod and awesome_notification packages, and I need to alter the state the app, from the action of the notification, for this, the package uses static methods inside a controller class.
class NotificationController{
...
static Future<void> onActionReceivedMethod(ReceivedAction receivedAction) async {
...//some way to access a provider, to call methods on it
}
...
}
If there is another way of doing this that I am not seeing, please let me know.
I have not been able to find a way to do this.
You can:
Pass to the ref function as a parameter.
static Future<void> onActionReceivedMethod(ReceivedAction receivedAction, Ref ref) async {
final some = ref.read(someProvider);
}
Create a class that accepts the ref field in the constructor.
final notificationProvider = Provider((ref) => NotificationController(ref));
// or use tear-off
final notificationProvider = Provider(NotificationController.new);
class NotificationController {
NotificationController(Ref ref) {
_ref = ref;
}
static late final Ref _ref;
static Future<void> onActionReceivedMethod(ReceivedAction receivedAction) async {
final some = _ref.read(someProvider);
}
}
An additional example:
import 'package:riverpod/riverpod.dart';
final valueProvider = Provider<int>((_) => 5);
final managerProvider = Provider(ManagerProvider.new);
class ManagerProvider {
ManagerProvider(Ref ref) {
_ref = ref;
}
static late final Ref _ref;
static int getValue() => _ref.read(valueProvider);
}
void main() {
final container = ProviderContainer();
container.read(managerProvider);
final value = ManagerProvider.getValue();
print(value); // 5
}
Either way, you should always have access to `Ref'.
Update:
As #OppositeDragon and #Eran Ravid pointed out, we really can't access _ref in a static method. However, if you define _ref in the constructor, it is possible. I think it's a terrible anti-pattern, though. Use method 1 and you will be fine.

How to unit test a class that is created by provider?

So let's say I have a Counter class like this
class Counter extends ChangeNotifier {
int _i = 0;
int get myCounter => _i;
void increment() {
_i++;
notifyListeners();
}
void decrement() {
_i--;
notifyListeners();
}
}
I want to write a test file for it, so I expose its instance like this. The problem is, after I expose it, how do I access the instance of the class I just created? Like say, I increased _i value through a button, how will I access the instance that is created by Provider in order to test it?
I was looking to do the same but then I found this answer https://stackoverflow.com/a/67704136/8111212
Basically, you can get the context from a widget, then you can use the context to get the provider state
Btw, you should test a public variable like i instead of _i
Code sample:
testWidgets('showDialog', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Material(child: Container())));
final BuildContext context = tester.element(find.byType(Scaffold)); // It could be final BuildContext context = tester.element(find.byType(Container)) depending on your app
final Counter provider = Provider.of<Counter>(context, listen: false);
expect(provider.i, equals(3));
});
You first initialize the Provider in your main.dart file using
ChangeNotifierProvider
after that you can use the class anywhere in your code by either using the Consumer widget or by using:
final counter = Provider.of<Counter>(context)
Here is a good post/tutorial about how to use Provider

The instance member 'params' can't be accessed in an initializer

class LevelUp extends GetxController {
Map<String, String> params = Get.arguments;
var myTest = params.[comLevel];
}
Error report--"The instance member 'params' can't be accessed in an initializer." I am new to programming and this is being called directly from a widget. I checked the LevelUp map and it has contents. The error occurs where I am trying to assign the param value to myTest. It doesn't matter if I put the key in quotes or provide an integer. Any advice would be greatly appreciated.
You can't access params before you've initialized the object. To fix your example, move your myTest initialization into a constructor.
Also, I don't believe you should have a period before [comLevel].
class LevelUp extends GetxController {
Map<String, String> params = Get.arguments;
String myTest;
LevelUp() {
myTest = params[comLevel];
}
}
Null safety update:
Use late keyword: Dart 2.12 comes with late keyword which helps you do the lazy initialization which means until the field bar is used it would remain uninitialized.
class Test {
int foo = 0;
late int bar = foo; // No error
}
Although this question has been answered for the OP's case, I want to offer a solution to those receiving this error in a StatefulWidget scenario.
Consider a situation where you would want to have a list of selectable items that dictate which category to display. In this case, the constructor might look something like this:
CategoryScrollView({
this.categories,
this.defaultSelection = 0,
});
final List<String> categories;
final int defaultSelection;
Note the property defaultSelection is responsible for specifying which category should be selected by default. You would probably also want to keep track of which category is selected after initialization, so I will create selectedCategory. I want to assign selectedCategory to defaultSelection so that the default selection takes effect. In _CategoryScrollViewState, you cannot do the following:
class _CategoryScrollViewState extends State<CategoryScrollView> {
int selectedCategory = widget.defaultSelection; // ERROR
#override
Widget build(BuildContext context) {
...
}
}
The line, int selectedCategory = widget.defaultSelection; will not work because defaultSelection it hasn't been initialized yet (mentioned in other answer). Therefore, the error is raised:
The instance member 'widget' can't be accessed in an initializer.
The solution is to assign selectedCategory to defaultSelection inside of the initState method, initializing it outside of the method:
class _CategoryScrollView extends State<CategoryScrollView> {
int selectedCategory;
void initState() {
selectedCategory = widget.defaultSelection;
super.initState();
}
A simple example, where it shows how we can resolve the above issue,
Example: Create an instance of class B, and pass an instance of class A in the parameter of it
WRONG(Compile time error of initializer):
final A _a = A();
final B _b = B(_a);
shows error: The instance member '_a' can't be accessed in an initializer.
Right:
final A _a = A();
late final B _b;
AppointmentRepository() {
_b = B(_a);
}
#100% working solution
:
Juts place var myTest = params.[comLevel];
below your Build method.
eg.
class LevelUp extends GetxController {
Map<String, String> params = Get.arguments;
Widget build(BuildContext context) {
var myTest = params.[comLevel];
}
}
For me it happened Because i was trying to access a Property of a class instance (Lets Say Class A ) And Use this property to initialize Another Class (Class B) , The Property Was Integer Number and Was Defined
However , Since i didn't Make an Object from "Class A" I can access those propertied Belong to it !
I tried to use this property inside the "Build" Method so that an object is "Created/Built" And it Worked !
I also got the similar error.
And I found the solution as follows.
My first code:
final BuildContext mycontext = GlobalContextClass.navigatorKey.currentContext;
final PsValueHolder psValueHolder = Provider.of<PsValueHolder>(mycontext, listen: false);
Next is the code where the error is fixed:
final PsValueHolder psValueHolder = Provider.of<PsValueHolder>(GlobalContextClass.navigatorKey.currentContext, listen: false);
Instead of defining 2 variables in a row, I placed the first variable directly in the place of the 2nd variable.
Another solution is making your variable, a GetX parameter.
int count_myProducts = cartItems.length; //The instance member 'cartItems' can't be accessed in an initializer. (Documentation)
int get count_myProducts => cartItems.length;
see this video at 27:34
GetX State Management tutorial with Flutter
https://www.youtube.com/watch?v=ZnevdXDH25Q&ab_channel=CodeX
Just carry
var myTest = params.[comLevel];
into Widget build{} below.
like this :
class LevelUp extends GetxController {
Map<String, String> params = Get.arguments;
Widget build(BuildContext context) {
var myTest = params.[comLevel];
}
}

Dart, override many getters and setters in a DRY or synthetic way in a Proxy Pattern

Lets take this dart class:
class Subject {
String a;
String b;
String c;
}
Now I want to use it trough a Proxy, to manage lazy loading and synchronization.
I want as well to have default values to use as placeholders while I'm loading the real data from the net. To keep thighs neat and isolated I added another class:
class Fallback implements Subject {
#override String a = 'a';
#override String b = 'b';
#override String c = 'c';
}
Now I have all the bricks I need to write down the "proxy with fallback" class:
class Proxy implements Subject {
Subject model;
Subject fallback = Fallback();
Future<void> slowlyPopulateModel() async => if (model == null) ... // do some async stuff that valorize model
#override
String get a {
slowlyPopulateModel();
return model?.a ?? fallback.a;
}
#override
set a(String _a) {
model?.a = a;
notifyListener();
}
// same getters and setters for b and c
}
By overriding get a I can call the slow I/O method if needed and return the placeholder value of my Fallback class. Once the new value is set my overridden set a(String _a) will call notifyListener() that will update my interface.
It is working fine, but I have manually override getter and setter for each field of my class (and they are many).
Does Dart have any trick to do this in a more DRY way?
E.g. some way to inject code to be executed before or after each getter or setter?
I would suggest to have a look at Streams for this.
This code example will return an initial value, fetch a new value and notify the listener of it through a stream.
import 'dart:async';
class Subject {
// Current value, initially at "a"
String _a = "a";
StreamController<String> _aController = StreamController<String>();
String get a {
_updateA();
return _a;
}
Stream<String> get aStream => _aController.stream;
void _updateA() async {
String newValue = await _fetchA();
_a = newValue; // update the current value
_aController.add(newValue); // push the new value onto the stream for listeners
}
// return a String after two seconds
Future<String> _fetchA() async {
return Future.delayed(
Duration(seconds: 2),
() => "New value for _a",
);
}
// This closes the stream
void dispose() {
_aController.close();
}
}
main(args) {
final subject = Subject();
// listen to changes
subject.aStream.listen((value) {
print("new value of a: $value");
});
// get current value
final currentValue = subject.a;
print("current value of a: $currentValue");
}
Output of this example
current value of a: a
(after two seconds) new value of a: New value for _a
Use it in Flutter with a StreamBuilder
StreamBuilder<String>(
stream: subject.aStream,
initialData: subject.a,
builder: (context, snapshot) {
final valueOfA = snapshot.data;
return Text("value is $valueOfA");
}
)
Some of the boilerplate code could be replaced by the BehaviorSubject in RxDart. But this would require another dependency to be imported into the project.