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

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

Related

Flutter - Provider - Different ChangeNotifier instance for different context

I created two widgets from same custom StatefulWidget class. I want them to use separate ChangeNotifier instance from same ChangeNotifier derived class because they need to consume different data set. Unfortunately just like below example, it's not working like I want it to. Both read() and watch() respectively write and read data on the same ChangeNotifier instance.
Wait a minute. Isn't that what Provider supposed to do?. Yes I know. I'm aware that. But now I just need a little flexibility. I think I'm just using Provider the wrong way if I'm not wrong.
Thank you for your help. Greatly appreciate it.
MultiProvider App() => MultiProvider(
...
providers : [
...
ChangeNotifierProvider(create : (_) => Notifier()),
]
);
class TestState extends State<Test>{
GlobalKey<CounterState> gk1 = GlobalKey<CounterState>();
GlobalKey<CounterState> gk2 = GlobalKey<CounterState>();
#override
Widget build(BuildContext context){
...
.. [Counter(gk1), Counter(gk2)]
...
.. onPressed: (){
.. gk1.currentState?.increment(1);
.. gk2.currentState?.increment(2);
.. },
...
}
}
class CounterState extends State<Counter>{
#override
Widget build(BuildContext context){
...
.. context.watch<Notifier>().count
...
}
void increment(int v){
context.read<Notifier>().count += v;
}
}
class Notifier with ChangeNotifier{
int _count = 0;
int get count => _count;
void set count(int v){
_count = v;
notifyListeners();
}
}
I remember facing this exact issue when I was using this package.
I did a little search but couldn't find much info about it, so I decided to change approach.
Instead of having N providers of the same type, I now create 1 provider containing all the info for the N widgets.
In your case I would do something like:
class Notifier with ChangeNotifier{
List<int> _counts = [0, 0];
int getCountAt(int index) {
return _counts[index]; //Control list lenght of course
}
void set count(int index, int v){
_count[index] = v;
notifyListeners();
}
}
Relying on index to access the correct counter might not work.
If that's the case, you can create a more complex object to access it's counter (maybe an id? a UniqueKey?).
I guess there's no other way yet to solve this problem easily. Either I was missing something or Provider doesn't yet support binding with ChangeNotifier by any passed instance directly not by only a single instance of the class. So I marked Axel's as the answer for now.
But based on the example in the question, for my case I use state instance for the key to identify the different data set, and binding it with a "change notifier instance" exactly.
class CounterState extends State<Counter>{
#override
Widget build(BuildContext context){
...
.. context.watch<Notifier>().instance(this).count
...
}
void increment(int v){
context.read<Notifier>().instance(this).count += v;
}
}
class Notifier with ChangeNotifier{
Map<State, NotifierInstance> _instance = {};
NotifierInstance instance(State state){
if(_instance[state] == null) _instance[state] = NotifierInstance(this);
return _instance[state]!;
}
}
class NotifierInstance{
Notifier notifier;
int _count = 0;
NotifierInstance(this.notifier);
int get count => _count;
void set count(int v){
_count = v;
notifier.notifyListeners();
}
}

flutter : run provider function to load data before/while widget is loading the layout

Structure of code:
I have a function, that on a Button click returns a stateful widget A.
In a separate file, I have a ChangeNotifier class B, which A
needs (it needs the decoded json file) and I use the ChangeNotifier class as a general memory container which all the different widgets have access to.
B:
class Database_Controller extends ChangeNotifier {
Future<void> _readVeryLargeJsonFrame() async {
final String response =
await rootBundle.loadString('assets/tables/largejson.json');
final data = await json.decode(response);
return data;
}
Other functions to give back entries of data
}
Problem:
I would like to execute _readVeryLargeJsonFrame as soon as A is called (potentially with a loading spinner for the user) and before A is loaded (or at least in parallel).
How do I call the ChangeNotifier function in the "init" part of a stateful widget? Peter Koltai mentioned the initState method. But how do I call the ChangeNotifier function from this?
(2. Context problem: So far, I would be using Provider.of<Database_Controller>(context,listen: false)._readVeryLargeJsonFrame(); but how do I get the context argument here?)
(3. Is the Future<void> ... async nature of the _readVeryLargeJsonFrame function a problem here?)
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}

How can I get a changenotifier using a futureprovider with riverpod?

I am using shared_preferences for my app and I have made a Settings class with helper methods.
As part of the Settings method I use Settings.create to generate my SharedPreferences.
It looks like this:
class Settings extends ChangeNotifier {
final SharedPreferences prefs;
Settings(this.prefs);
static Future<Settings> create() async {
var prefs = await SharedPreferences.getInstance();
return Settings(prefs);
}
// ### Helper Methods ###
}
I have used this post to try a solution and have come up with this:
FutureProvider<Settings> createSettings = FutureProvider<Settings>((ref) {
return Settings.create();
});
ChangeNotifierProvider<Settings> settingsProvider = ChangeNotifierProvider<Settings> ((ref) {
final settingsInstance = ref.watch(createSettings).maybeWhen(
data: (value) => value,
orElse: () => null,
);
return Settings(settingsInstance.prefs);
});
The problem I have now run into is that I get an Error because null is returned as long as the future has not completed.
I have hit a wall and am out of ideas. Does anyone have an idea on how to solve this?
Yeah, two options. Depending on the trade-offs you want to make.
Options
Option 1 (just wait)
In main.dart inside the Future<void> main() async {} function, just wait for the call to get shared prefs and then manually set the state in a your providers (using a state provider).
So that looks like:
(providers.dart)
final settingsProvider = StateProvider<Settings>((_) => defaultValue);
// ^ You can also make the above nullable if you don't have a reasonable default. But to be fair, this default will never be called if you're always setting it in main.
(main.dart)
Future<void> main() async {
final settings = await Settings.create();
final container = ProviderContainer();
container.read(settingsProvider.state).state = settings;
}
Option 2 (same idea, just don't wait)
The same as the above code, but don't wait in main. Just don't wait. Here's the difference:
(main.dart)
Future<void> main() async {
final container = ProviderContainer();
Settings.create().then((settings){
container.read(settingsProvider.state).state = settings;
});
// The rest of your code...
runApp();
}
Overview / Comparison
Option #1 is more simple to work with, but you may not be okay with waiting if you want a fast startup. But I doubt that it matters. In that case, if you have reasonable defaults, then use option #2
For the code on using Riverpod in main(), please reference this Github issue comment:
https://github.com/rrousselGit/riverpod/issues/202#issuecomment-731585273

Flutter Riverpod listen not being invoked

I'm trying to implement just a basic listener in a widget (I will want to show a snackbar) but it just isnt being invoked by the provider. Cant see what Im doing wrong here.
I've tried from other widgets and the listener still doesn't hear the event.
Any ideas?
int foo = 1;
final FooProvider = Provider<int>((ref) {
foo = foo + 1;
return foo;
});
class showSnack extends ConsumerWidget {
final int taskID;
const showSnack(this.taskID);
#override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(FooProvider, (int? previousCount, int newCount) {
logger.d("Fooo event");
});
return TaskInfo(taskID);
}
}
The basic Provider is not a state-holding type of provider. It's basically a static provider of some sort of data or a service class, meaning that it can't be used to watch for state changes or for listening.
You should probably use the StateProvider, StateNotifierProvider or the ChangeNotifierProvider. You can read more about the different providers in the documentation.

Flutter provider in initState

I'm currently trying Provider as a state management solution, and I understand that it can't be used inside the initState function.
All examples that I've seen call a method inside a derived ChangeNotifier class upon user action (user clicks a button, for example), but what if I need to call a method when initialising my state?
Motivation:
Creating a screen which loads assets (async) and shows progress
An example for the ChangeNotifier class (can't call add from initState):
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
}
You can call such methods from the constructor of your ChangeNotifier:
class MyNotifier with ChangeNotifier {
MyNotifier() {
someMethod();
}
void someMethod() {
// TODO: do something
}
}
Change your code to this
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) async {
// Loading Assets maybe async process with its network call, etc.
_progress += dProgress;
notifyListeners();
}
ProgressData() {
add();
}
}
In initState all the of(context) things don't work correctly, because the widget is not fully wired up with every thing in initState.
You can use this code:
Provider.of<ProgressData>(context, listen: false).add(progress)
Or this code:
Future.delayed(Duration.zero).then(_){
Provider.of<ProgressData>(context).add(progress)
}):
So an AssetLoader class which reports on its progress will look something like this, I guess:
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
ProgressData() {
_loadFake();
}
Future<void> _loadFake() async {
await _delayed(true, Duration(seconds: 1));
_add(1.0);
await _delayed(true, Duration(seconds: 2));
_add(2.0);
await _delayed(true, Duration(seconds: 3));
_add(3.0);
}
// progress
double get progress => _progress;
// add
void _add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
// _delayed
Future<dynamic> _delayed(dynamic returnVal, Duration duration) {
return Future.delayed(duration, () => returnVal);
}
}
As Fateme said:
the widget is not fully wired up with everything in initState
Also, you can use something like this in your initState
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Provider.of<ProgressData>(context, listen: false).add(5);
});
I think it's more standard!
Be aware that you should use the correct context! I mean the context of the Builder!
The problem here lies with the fact that context does not exist yet in initState as extensively explained by the other answers. It doesn't exist because it hasn't yet been made a part of the widget tree.
Calling a method
If you're not assigning any state and only calling a method then initState would be the best place to get this done.
// The key here is the listen: false
Provider.of<MyProvider>(context, listen: false).mymethod();
The code above is allowed by Flutter because it doesn't have to listen for anything. In short, it's a one off. Use it where you only want to do something instead of read/listen to something.
Listening to changes
Alternatively, if you need to listen to changes from Provider then the use of didChangeDependencies would be the best place to do so as context would exist here as in the docs.
This method is also called immediately after initState.
int? myState;
#override
void didChangeDependencies() {
// No listen: false
myState = Provider.of<MyProvider>(context).data;
super.didChangeDependencies();
}
If you've never used didChangeDependencies before, what it does is get called whenever updateShouldNotify() returns true. This in turn lets any widgets that requested an inherited widget in build() respond as needed.
I'd usually use this method in a FutureBuilder to prevent reloading data when data already exists in Provider after switching screens. This way I can just check Provider for myState and skip the preloader (if any) entirely.
Hope this helps.