Possible to provide a "resolved" StreamProvider to child widgets via nested ProviderScope? - flutter

I am trying to inject a "resolved" Riverpod StreamProvider object into the tree below to remove some unnecessary async calls. If my interpretation of the docs is correct, a nested ProviderScope should help with this but I am getting a runtime exception.
My use case: I need to access a user-specific specs object high in the widget tree. Some of the data from that object is required all over the remainder of the app, including as a parameter for any DB operation. The specs object comes from firebase and is retrieved async with a StreamProvider.
Once execution is inside the HomePage widget I know the specs object must be loaded and valid so I don't want to fetch it again as a Stream Provider that needs to handle load and error cases. This is especially true where the specs provider is input to other combined providers as the additional load and error cases add lots of unnecessary complexity.
// Called at the root of the tree to retieve some firestore object
final specsStreamProvider = StreamProvider<Specs?>((ref) {
return ref.read(baseDatabaseProvider).currentSpecs();
});
// Called further down to provide the object that was retrieved
final specsProvider = Provider<Specs>((ref) {
throw UnimplementedError('should have been overwritten');
});
// An example of how content will be retrieved from firestore at HomePage widget and below.
// Having to use specsStreamProvider here quickly turns into a mess.
final recordStreamProvider = StreamProvider.autoDispose<List<Record>>((ref) {
final specs = ref.read<Specs>(specsProvider);
final database = ref.read(contentDatabaseProvider(specs.current!));
return database.recordsStream();
});
class SetupWidget extends ConsumerWidget {
const SetupWidget({Key? key, required this.setupBuilder, required this.homeBuilder}) : super(key: key);
final WidgetBuilder setupBuilder;
final WidgetBuilder homeBuilder;
#override
Widget build(BuildContext context, ScopedReader watch) {
final specsAsyncValue = watch(specsStreamProvider);
return specsAsyncValue.when(
data: (specs) => _data(context, specs),
loading: () => const Scaffold(/.../),
error: (e, __) => Scaffold(/.../),
));
}
Widget _data(BuildContext context, Specs? specs) {
if (specs != null) {
return ProviderScope(
// The plan here is to introduce the resolved specs into the tree below
overrides: [specsProvider.overrideWithValue(specs)],
child: homeBuilder(context),
);
}
return setupBuilder(context);
}
}
According to the Riverpod API a nested ProviderScope is a valid tool to overwrite providers for part of the widget tree. Unfortunately, in my case I get a runtime error 'Unsupported operation: Cannot override providers on a non-root ProviderContainer/ProviderScope'
I also tried to make specsProvider a ScopedProvider but then the combined recordStreamProvider doesn't compile. ('error: The argument type 'ScopedProvider' can't be assigned to the parameter type 'RootProvider<Object?, Specs>'.'

I think I figured it out. I made specsProvider a ScopedProvider set in the parent and changed recordStreamProvider (the one that is called in the children only) to not depend on the scoped provider directly.
However, I would still love to hear from one of the Riverpod experts if what I am doing here is acceptable and no anti-pattern.
Parent setting scoped provider:
final specsStreamProvider = StreamProvider<Specs?>((ref) {
return ref.read(baseDatabaseProvider).currentSpecs();
});
// Called further down to provide the object that was retrieved
// This MUST be a ScopedProvider
final specsProvider = ScopedProvider<Specs>((ref) {
throw UnimplementedError('should have been overwritten');
});
class SetupWidget extends ConsumerWidget {/* as before */}
Children consuming scoped provider
// no dependency on specsProvider here
final recordStreamProvider = StreamProvider.family.autoDispose<List<Record>, String>((ref, storeId) {
final database = ref.read(contentDatabaseProvider(storeId));
return database.recordsStream();
});
class HomePage extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final specs = watch(specsProvider);
final recordsAsyncValue = watch(recordsStreamProvider(specs.storeId!));
return recordsAsyncValue.when(
data: (records) => /* build a list */
loading: () => /* show a progress indicator */,
error: (e, __) => /* show an alert dialog */,
));
}
}

Related

How to read the data from AsyncData<void> in Riverpod

I am trying to implement simple authentication using Riverpod package in flutter. State of my controller after calling the sign in function prints this: "AsyncData(value: Instance of 'Response')". How can I extract the body of response from this kind of data type?
You can do it like this:
/// A provider that asynchronously exposes the current user
final userProvider = StreamProvider<User>((_) async* {
// fetch the user
});
/// or example
final userProvider = FutureProvider<User>((_) async {
// fetch the user
});
class UserAuthenticationWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Oops, something unexpected happened'),
data: (user) => Text('Hello ${user.name}'),
);
}
}
Note AsyncValueX. You can use any method that suits you.

Flutter + Firebase - Mutate in-memory data model or rely on stream for rebuilds

In the example below, CandidateData is deserialized from a Firebase Realtime database using a Stream. Properties from it are used in building a HiringManagerButton.
class CandidateData {
bool isEmployed;
String name;
}
class HiringManagerButton extends StatelessWidget {
final CandidateData data;
const HiringManagerButton({Key? key, required this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('${data.isEmployed ? 'Fire' : 'Hire'} ${data.name}'),
onPressed: onPressedFn
);
}
}
class SomeClass extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<CandidateData>(
stream: fromDatabaseStream,
builder: (context, state) => HiringManagerButton(data: state.data!));
}
}
How should onPressedFn operate? Should it mutate the CandidateData model by changing the value of isEmployed, which causes the Widget to instantly rebuild, then make the service call to update the backend? Or should it just make the service call to update the backend, and rely on the Stream to provide an updated value to rebuild the HiringManagerButton with?
EDIT: Code for the two options above:
Option 1:
onPressedFn = () async {
data.isEmployed = !data.isEmployed;
await context.read<DatabaseService>().update(data);
}
Option 2:
onPressedFn = () async {
await context.read<DatabaseService>().update(CandidateData(isEmployed: !data.isEmployed, name: data.name));
}
Decided to go with option 2 as the database is the source of truth. Moreover, Flutter Firebase seems to have some form of offline consistency, which results in performance being a non-issue - DB writes are reflected instantly locally.

What is the efficient way to pass arguments to a Riverpod provider each time it gets initialized in Flutter?

I am currently trying to create an instance of a widget's state (ChangeNotifier) using a global auto-disposable ChangeNotifierProvider. The notifier instance takes in a few arguments to initialize each time the UI is built from scratch.
Let's assume we have the following simple state (or notifier):
class SomeState extends ChangeNotifier {
int _someValue;
SomeState({required int initialValue})
: _someValue = initialValue;
int get someValue => _someValue;
set someValue(int someValue) {
_someValue = someValue;
notifyListeners();
}
}
I used to use the Provider package before switching to Riverpod, where this could've easily been done like so:
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
// Passing 2 into state initializer, which may be
// obtained from a different state, but not necessarily.
create: (_) => SomeState(initialValue: 2),
builder: (context, child) => Consumer<SomeState>(
builder: (context, state, child) {
// Will print 2, as it's currently the default value.
return Text('${state.someValue}');
},
),
);
}
}
So with Provider, you can manually call to SomeState constructor with arbitrary arguments when the state is being set up (i.e. provided). However, with Riverpod, it doesn't seem as intuitive to me, mainly because the provider is made to be declared globally:
static final someProvider = ChangeNotifierProvider.autoDispose((ref) => SomeState(2));
Which would end up being used like so:
class SomeWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(someProvider);
return Text('${state.someValue}');
}
}
However, with this approach I can't pass parameters like I did in the example using Provider. I also don't want to use the family modifier because I would need to pass the same parameter each time I read/watch the state, even if it's already created.
If it helps, in my current situation I am trying to pass a function (say String Function()? func) into my state on initialization. It's also not feasible to depend on a different provider in this case which would provide such function.
How could I replicate the same functionality in the Provider example, but with Riverpod?
P.S. Apologies if code has syntax errors, as I hand-typed this and don't have an editor with me at the moment. Also, this is my first post so apologies for lack of clarity or format.
Use provider overrides with the param that you need:
First, let's ensure the ProviderScope in the root of the widget-tree.
// Root
ProviderScope(
child: MaterialApp(...)
)
After, create another one in some widget:
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
someProvider.overrideWithProvider(
ChangeNotifierProvider.autoDispose((ref) => SomeState(5)),
),
],
child: Consumer(
builder: (context, ref, child) {
final notifier = ref.watch(someProvider);
final value = notifier.someValue;
return Text('$value'); // shows 5 instead of 2
}
),
);
}
If you do not want to use family then you can put value in another way by combining two providers.
final someValue = StateProvider((ref) => 0);
final someProvider = ChangeNotifierProvider.autoDispose((ref) {
final value = ref.watch(someValue);
return SomeState(value);
});
class SomeState extends ChangeNotifier {
int _someValue;
SomeState(int initialValue) : _someValue = initialValue;
int get someValue => _someValue;
set someValue(int someValue) {
_someValue = someValue;
notifyListeners();
}
}
USAGE:
// From everywhere you can put new value to your ChangeNotifier.
ref.read(someValue.notifier).state++;
But in your case, it's better to use the `family method. It's cleaner and less complicated.

Access Providers from Dialogs for Flutter hooks

I am new to Flutter hooks and riverpod
Basically I have a provider that stores the list of books in a book shelf.
class BookList extends StateNotifier<List<BookModel>> {
BookList() : super([]);
void setBookList(List<BookModel> bookList) =>
{state = bookList};
}
final bookListProvider = StateNotifierProvider<BookList>((_) => BookList());
Then I have a page which display the books and a create button which will shows the create a new book dialog:
class BookShelfPage extends HookWidget {
#override
Widget build(BuildContext context) {
final bookList = useProvider(bookListProvider.state);
useEffect(() {
//API to get list of books
context.read(bookListProvider).setBookList(//data from API);
},[]);
final Function() onCreateBookButtonClicked = () {
showDialog(
context: context,
builder: (context) => ProviderScope(
child: (new BookCreateDialog())));
};
//Data is available for this
print("book list length 1: " + bookList.length.toString());
}
However, I am unable to access the provider values in the dialog:
class BookCreateDialog extends HookWidget {
#override
Widget build(BuildContext context) {
final bookList = useProvider(bookListProvider.state);
//Data is not available for this
print("book list length 2: " + bookList.length.toString());
}
}
Things to note:
I have a ProviderScope wrapping my application.
I have no problems persist or access the providers across different PAGES or any child widget that resides on the PAGES but I am not able to access the provider values from dialogs.
Of course, I can pass the providers' values as parameters to the dialogs but I would like to know if there is any way that I can avoid this as I got a lot values to get from providers.
May I know how to fix this? Much thanks!
You only want to use ProviderScope in two cases. The first is wrapping your app as you mentioned. The other case is when using ScopedProvider.
What you're essentially doing here:
builder: (context) => ProviderScope(child: BookCreateDialog());
is creating a new scope where the value of your StateNotifierProvider is not available (as that value lies within the ProviderScope at the root of your app).
Remove that ProviderScope and you should get the results you are expecting.

Controlling State from outside of a StatefulWidget

I'm trying to understand the best practice for controlling a StatefulWidget's state outside of that Widgets State.
I have the following interface defined.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:
When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this event and perform some action ( DB call, service request, etc ).
The controller can call showActivity or set message to have the view show progress with a message.
Because a Stateful Widget does not expose its State as a property, I don't know the best approach for accessing and modifying the State's attributes.
The way I would expect to use this would be something like this:
Widget createStartupPage() {
var page = new StartupPage();
page.onAppSelected.listen((app) {
page.showActivity = true;
//Do some work
page.showActivity = false;
});
}
I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.
Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability, and forward-thinking towards Flutter, we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.
There are multiple ways to interact with other stateful widgets.
1. findAncestorStateOfType
The first and most straightforward is through context.findAncestorStateOfType method.
Usually wrapped in a static method of the Stateful subclass like this :
class MyState extends StatefulWidget {
static of(BuildContext context, {bool root = false}) => root
? context.findRootAncestorStateOfType<_MyStateState>()
: context.findAncestorStateOfType<_MyStateState>();
#override
_MyStateState createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return Container();
}
}
This is how Navigator works for example.
Pro:
Easiest solution
Con:
Tempted to access State properties or manually call setState
Requires to expose State subclass
Don't use this method when you want to access a variable. As your widget may not reload when that variable change.
2. Listenable, Stream and/or InheritedWidget
Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.
In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.
This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :
First, we have an InheritedWidget that expose a count :
class Count extends InheritedWidget {
static of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Count>();
final int count;
Count({Key key, #required Widget child, #required this.count})
: assert(count != null),
super(key: key, child: child);
#override
bool updateShouldNotify(Count oldWidget) {
return this.count != oldWidget.count;
}
}
Then we have our State that instantiate this InheritedWidget
class _MyStateState extends State<MyState> {
int count = 0;
#override
Widget build(BuildContext context) {
return Count(
count: count,
child: Scaffold(
body: CountBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
),
),
);
}
}
Finally, we have our CountBody that fetch this exposed count
class CountBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text(Count.of(context).count.toString()),
);
}
}
Pros:
More performant than findAncestorStateOfType
Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
Automic reload of the children when the value change
Cons:
More boilerplate
Stream can be complicated
3. Notifications
Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.
An example of Notification would be :
class MyNotification extends Notification {
final String title;
const MyNotification({this.title});
}
To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.
MyNotification(title: "Foo")..dispatch(context)
Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.
Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onTitlePush,
child: Container(),
);
}
bool onTitlePush(MyNotification notification) {
print("New item ${notification.title}");
// true meaning processed, no following notification bubbling.
return true;
}
}
An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController
Pros:
Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
More than one widget can subscribe to that same notification
Prevents children from accessing unwanted State properties
Cons:
May not fit your use-case
Requires more boilerplate
You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:
class StartupPage extends StatefulWidget {
static StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<StartupPageState>());
#override
StartupPageState createState() => new StartupPageState();
}
class StartupPageState extends State<StartupPage> {
...
}
You can then access the state by calling StartupPage.of(context).doSomething();.
The caveat here is that you need to have a BuildContext with that page somewhere in its tree.
There is another common used approach to have access to State's properties/methods:
class StartupPage extends StatefulWidget {
StartupPage({Key key}) : super(key: key);
#override
StartupPageState createState() => StartupPageState();
}
// Make class public!
class StartupPageState extends State<StartupPage> {
int someStateProperty;
void someStateMethod() {}
}
// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();
// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();
I do:
class StartupPage extends StatefulWidget {
StartupPageState state;
#override
StartupPageState createState() {
this.state = new StartupPageState();
return this.state;
}
}
class DetectedAnimationState extends State<DetectedAnimation> {
And outside just startupPage.state
While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.
In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):
class StartupPage extends StatefulWidget {
static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
#override
_StartupPageState createState() => new _StartupPageState();
}
class _StartupPageState extends State<StartupPage> {
...
}
The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.
You can use eventify
This library provide mechanism to register for event notifications with emitter
or publisher and get notified in the event of an event.
You can do something like:
// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();
var controlNumber = 50;
List<Widget> buttonsGenerator() {
final List<Widget> buttons = new List<Widget>();
for (var i = 0; i < controlNumber; i++) {
widgets.add(new MaterialButton(
// Generate 10 Buttons afterwards
onPressed: () {
controlNumber = 10;
emitter.emit("updateButtonsList", null, "");
},
);
}
}
class AState extends State<ofYourWidget> {
#override
Widget build(BuildContext context) {
List<Widget> buttons_list = buttonsGenerator();
emitter.on('updateButtonsList', null, (event, event_context) {
setState(() {
buttons_list = buttonsGenerator();
});
});
}
...
}
I can't think of anything which can't be achieved by event driven programming. You are limitless!
"Freedom cannot be bestowed — it must be achieved."
- Elbert Hubbard
Have you considered lifting the state to the parent widget? It is a common, though less ideal than Redux, way to manage state in React as far as I know, and this repository shows how to apply the concept to a Flutter app.