I have stateful widgets A, B and C. B and C are both children of A. I have created a queue Q in widget A. Both B and C show the first element of Q, but when 'dismissing' this element in either B or C, both widgets should show the next element of Q.
Currently the problem is that widgets B and C cannot detect changes and I don't think there is a way to listen for queue changes on this Queue implementation: https://api.dartlang.org/stable/2.4.1/dart-collection/Queue-class.html
Should I use a Queue? I have also read about streams, but I am not sure that will work either, because the first element should not necessarily be consumed by B or C. Any ideas?
You could implement the Provider Pattern for State Managment.
Provider Flutter Package
As the documentation says, to expose a variable using provider, wrap any widget into one of the provider widgets from this package and pass it your variable. Then, all descendants of the newly added provider widget can access this variable.
The main idea is to create a class where you will save the state you want to share (in your case it would be the queue). Then, provide access from the widgets to this provider
Example: The following class is the Provider Class. The shared variable would be "_count"
class CounterProvider with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
Then, you instiantate this Provider in the widget of your preference (In your case, A).. so all its descendants (B and C) have access to the provider and its data.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final counter = CounterProvider();
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider( // WIDGET FROM PROVIDER PACKAGE
builder: (context) => CounterProvider(), // REQUIRED LINE
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (BuildContext context) => HomePage(),
'somepage': (BuildContext context) => SomePage(),
}
),
);
}
}
Now, inside of HomePage():
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final counterProvider = Provider.of<CounterProvider>(context);
// Inject the provider in your widget. From here, you have access to the Counter properties.
return Scaffold(
appBar: AppBar(
title: Text( counterProvider.counter.toString() ), // accessing to the provider!
),
body: Center(
child: Text(counterProvider.counter.toString()), // again
),
);
}
}
Of course you can create your methods you need in the provider so you can change the state in anyway you want.
I hope it helps!
Youtube link in spanish but understable workflow
Related
I'm using Provider package, and I want to dynamically add widgets to display.
I wrote the code like below, but the widgets doesn't show anything.
No errors have occurred.
// Contains widget and related data
class WidgetData {
Widget? child; // want to show this
String data1;
int data2;
}
class Model exteds ChangeNotifier {
List<WidgetData> widget; // I want to show all of this widget.child
void addWidget(Widget child) {
print("Called1") // "Called1"
var w = widgets.toList();
w.add(child);
widgetData = w;
notifyListeners();
}
}
class Example extends StatelessWidget {
#override
build (BuildContext context) {
return Scaffold(
body: Column(children: [
for(var i in context.watch<Model>().widget) i.child!;
]);
}
}
When the button pushed, context.read<Model>().addWidget(Text("test")) will be called.
But still doesn't show widgets.
// inside of build(BuildContext context)
FloatingActionButton(
onPressed: () => context.read<Model>().addWidget(Text("test")),
child: Icon(Icons.abc)
);
Of course I built the tree of provider in main.
void main() {
runApp(MultiProvider(
ChangeNotifierProvider<Model>(
create: (_) => Model()),
));
child:
....
}
It is discouraged to create and store Widgets outside of the build function in Flutter. Your provider should not provide Widgets, but the data needed to construct widgets, and you then construct the widgets inside the build method.
For example, if instead of a list of Widgets you had a list of Strings, then in the build method you convert that list of Strings easily into Text widgets like this: Column(children: stringlist.map((e) => Text(e)).toList())
I'm experimenting with Flutter and I'm trying to build a simple app using the Providers pattern.
My Problem
I am trying to access a provider in one of the widgets and I'm getting this error once I get the required provider in the stateful widget class. I can't figure out what am I doing wrong here.
Error
Error: Could not find the correct Provider<ContactProvider> above this ContactsPage Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that ContactsPage is under your MultiProvider/Provider<ContactProvider>.
This usually happens when you are creating a provider and trying to read it immediately.
...
The Code
main.dart
import 'package:ProvidersExample/provider_list.dart';
void main() async => runApp(Home());
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providerList,
child: MaterialApp(
home: ContactsPage(),
)
);
}
}
providers_list.dart
List<SingleChildWidget> providerList = [
ChangeNotifierProvider(
create: (_) => ContactProvider(),
lazy: false,
)
];
The provider: contact_provider.dart
class ContactProvider extends ChangeNotifier{
List<Contact> _contactList = [];
// getter
List<Contact> get contacts {
return [..._contactList];
}
// setter
set contacts(List<Contact> newContacts) {
_contactList = newContacts;
notifyListeners();
}
...
The Widget contacts_page.dart
class ContactsPage extends StatefulWidget {
#override
_ContactsPageState createState() => _ContactsPageState();
}
class _ContactsPageState extends State<ContactsPage> {
#override
void initState(){
super.initState();
}
#override
Widget build(BuildContext context) {
// This line throws the error
ContactProvider _provider = Provider.of<ContactProvider>
(context, listen:false);
return Scaffold(
appBar: AppBar(
title: Text(
...
I think the reason is that you are referencing a list of providers which were created outside in provider_list.dart which does not have access to your context from your widget tree.
Try this instead:
List providerList(context) {
return [
ChangeNotifierProvider(
create: (context) => ContactProvider(),
lazy: false,
)
];
}
providerList() is now a method that takes in context, and uses that context to register your providers, and return it.
You should specify the type <T> when creating your provider so it knows which type you are looking for in the widget tree.
List<SingleChildWidget> providerList = [
ChangeNotifierProvider<ContactProvider>(
create: (_) => ContactProvider(),
lazy: false,
)
];
I am trying to use a custom statefull PageWrapper widget to wrap all my pages. The idea is to make it return a Scaffold and use the same menu drawer and bottom navigation bar, and call the appropriate page as page parameter.
My bottomNavigationBar is working well and I am setting the correct selectedIndex, but I can't find a way to access it in the child page (that is in another file), since I don't know how to access the parent's selectedIndex and display the appropriate widget from my page's list.
import 'package:flutter/material.dart';
class PageWrapper extends StatefulWidget {
final Widget page;
final AppBar appBar;
final BottomNavigationBar bottomNav;
final Color bckColor;
PageWrapper({#required this.page, this.appBar, this.bckColor, this.bottomNav});
#override
_PageWrapperState createState() => _PageWrapperState();
}
class _PageWrapperState extends State<PageWrapper> {
int _selectedIndex;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
void initState() {
super.initState();
_selectedIndex = 0;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar,
backgroundColor: widget.bckColor,
bottomNavigationBar: CustomBottomNavigation(selectedIndex: _selectedIndex, onItemTapped: _onItemTapped),
body: widget.page,
drawer: Drawer(...),
);
}
}
Named roots in my main.dart:
home: PageWrapper(page: HomeScreen()),
routes: {
'form': (context) => PageWrapper(page: RoomService()),
},
I would like to access that bottom navigation bar's current index somehow in my HomeScreen and RoomService screen. Is there a way to do it?
You can solve that by using a State Management tool like Provider or Bloc. To keep things simple, lets use Provider to do it.
Wrap MaterialApp with a ChangeNotifierProvider in your main.dart.
return MultiProvider(
providers: [
ChangeNotifierProvider<IndexModel>(
create: (context) => IndexModel()),
],
child: MaterialApp(...)
);
Create a model that will hold your index value:
Also, you have to override the getter and setter of index in order to call notifyListeners after its value is set. Here is an example:
class IndexModel extends ChangeNotifier {
int _index;
get index => _index;
set index(int index) {
_index = index;
notifyListeners(); //Notifies its listeners that the value has changed
}
}
Here is how you can display your data according to its index (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value it is listening to, changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<IndexModel, String>(
selector: (_, model) => model.index,
builder: (_, i, __) {
switch(i){
//do your returning here based on the index
}
},
);
}
)
}
Extra note. Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<IndexModel>(context,listen:false);
int index =model.index; //get index value
model.index=index; //set your index value
You have to pass listen:false when you aren't listening for changes. This is needed when you are accessing it in initState or in onPressed.
When using flutter bloc what is the recommendation, is it recomended for each page to have its own bloc or can i reuse one block for multiple pages, if so how?
I think that the best solution is to have one BLoC per page. It helps you to always know in which state each screen is just by looking at its BLoC. If you want to show the state of each of your tabs independently you should create one BLoC for each tab, and create one Repository which will handle fetching the data. But if the state of every tab will be the same, (for example you fetch data only once for all of the screens, so you don't show loading screen on every tab) then I think that you could create just one BLoC for all of this tabs.
It is also worth to add, that BLoCs can communicate with each other. So you can get state of one BLoC from another, or listen to its state changes. That could be helpful when you decide to create separate BLoCs for tabs.
I have addressed this topic in my latest article. You can check it out if you want to dive deeper.
There are no hard-set rules about this. It depends on what you want to accomplish.
An example: if each page is "radically" from each other, then yes, a BLoC per page makes sense. You can still share an "application-wide" BLoC between those pages if some kind of sharing or interaction is required between the pages.
In general, I've noticed that usually a BLoC "per page" is useful as there are always specific things related for each page that you handle within their BLoC. You can the use a general BLoC to share data or some other common thing between them.
You can combine the BLoC pattern with RxDart to handle somewhat more complex interaction scenarios between a BLoC and the UI.
Sharing a BLoC is fairly simple, just nest them or use a MultiProvider (from the provider package):
runApp(
BlocProvider(
builder: (_) => SettingsBloc(),
child: BlocProvider(
builder: (_) => ApplicationBloc(),
child: MyApp()
)
)
);
and then you can just retrieve them via the Provider:
class MyApp extends ... {
#override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
final appBloc = Provider.of<ApplicationBloc>(context);
// do something with the above BLoCs
}
}
You can share different bloc's in different pages using BlocProvider.
Let's define some RootWidget that will be responsible for holding all Bloc's.
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
NavigationBloc _navigationBloc;
ProfileBloc _profileBloc;
ThingBloc _thingBloc;
#override
void initState(){
_navigationBloc = NavigationBloc();
_thingBloc = ThingBloc();
_profileBloc = ProfileBloc();
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(
builder: (BuildContext context) => _navigationBloc
),
BlocProvider<ProfileBloc>(
builder: (BuildContext context) => _profileBloc
),
BlocProvider<ThingBloc>(
builder: (BuildContext context) => _thingBloc
),
],
child: BlocBuilder(
bloc: _navigationBloc,
builder: (context, state){
if (state is DrawProfilePage){
return ProfilePage();
} else if (state is DrawThingsPage){
return ThingsPage();
} else {
return null
}
}
)
)
}
}
And after that, we can use any of bloc from parent and all widgets will share the same state and can dispatch event on the same bloc
class ThingsPage extends StatefulWidget {
#override
_ThingsPageState createState() => _ThingsPageState();
}
class _ThingsPageState extends State<ThingsPage> {
#override
void initState(){
_profileBloc = BlocProvider.of<ProfileBloc>(context);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: BlocBuilder(
bloc: _profileBloc,
builder: (context, state){
if (state is ThingsAreUpdated){
return Container(
Text(state.count.toList())
);
} else {
return Container()
}
}
)
);
}
}
I am working on an app in Flutter and I'm pretty new to it/Dart. I already created the login, signup etc and everything works perfectly fine. Now I want to create a "Login-Wall" Template for every View that needs the user to be logged in. If the user is not logged in, he should be returned to the LoginView, if the api-call is still loading, it should not show anything but a loading screen called LoadingView(). I started by creating a Stateful Widget called AuthorizedLayout:
class AuthorizedLayout extends StatefulWidget {
final Widget view;
AuthorizedLayout({this.view});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
The state utilizes a Future Builder as follows:
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: futureToken,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return NoConnectionView();
case ConnectionState.active:
case ConnectionState.waiting:
return LoadingView();
case ConnectionState.done:
if(snapshot.data != null) {
print("User Data loaded");
return widget.view;
} else
return LoginView();
}
},
);
}
As you can see, it should load the userdata, and when it's finished it should return the view. The futureToken represents the Future that will return the User-Object from the server after an api-request. In any other case it should show the Loading/Error/Login Page.
I'm calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
view: DashboardView(),
),
);
}
In the Build method of the Dashboard view I have a "print('Dashboard View');". The problem I have is that in the output the 'Dashboard View' is printed before the 'User Data Loaded'. That means I can't access the loaded user data in that view. This means that this solution does not work the way I intended it to.
Now for my question: Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall? I hope the code I posted explains the idea I'm trying to go for.
Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall?
Absolutely! At a basic level, you're talking about state management. Once a user logs into your app, you want to store that user data so that it's accessible to any widget within the widget tree.
State management in Flutter is a hotly-debated topic and while there are a ton of options, there is no defacto state management technique that fits every app. That said, I'd start simple. One of the simplest and most popular options is the scoped_model package.
You can read all of the details here, but the gist is that it provides utilities to pass a data model from a parent widget to its descendants.
First, install the package.
Second, you'll want to create a model that can hold the user data that you want to be accessible to any widget in the tree. Here's a trivial example of what that might look like:
// user_model.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
class UserModel extends Model {
dynamic _userData;
void setUserData(dynamic userData) {
_userData = userData;
}
String getFirstName() {
return _userData['firstName'];
}
static UserModel of(BuildContext context) =>
ScopedModel.of<UserModel>(context);
}
Next, we'll need to make an instance of this UserModel available to all widgets. A contrived way of doing this would be to wrap your entire app in a ScopedModel. Example below:
// main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'login_view.dart';
import 'user_model.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<UserModel>(
model: UserModel(),
child: MaterialApp(
theme: ThemeData.light(),
home: LoginView(),
),
);
}
}
In the above code, we're wrapping our entire instance of MaterialApp in a ScopedModel<UserModel>, which will give every widget in the application access to the User model.
In your login code, you could then do something like the following when your login button is pressed:
onPressed() async {
// authenticate your user...
var userData = await someApiCall();
// set the user data in our model
UserModel.of(context).setUserData(userData);
// go to the dashboard
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DashboardView(),
),
);
}
Last but not least, you can then access that user data through the UserModel like so:
// dashboard_view.dart
import 'package:flutter/material.dart';
import 'package:scoped_model_example/user_model.dart';
class DashboardView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text(
UserModel.of(context).getFirstName(),
),
),
],
);
}
}
Check out the docs on scoped_model for more details. If you need something more advanced, there are a number of other state management patterns in Flutter such as BloC, Redux, Mobx, Provider and more.
So I just got what was happening. I was passing the already-built widget to the AuthorizedView. What I actually had to pass was a Builder instead of a Widget.
class AuthorizedLayout extends StatefulWidget {
final Builder viewBuilder;
AuthorizedLayout({this.viewBuilder});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
Calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
viewBuilder: Builder(builder: (context) => DashboardLayout()),
),
);
}
Note that I recalled the final variable to viewBuilder instead of view, compared to the example above.
This will actually build the widget AFTER the userdata is loaded.