I'm trying to implement Provider instead of using State so after reading some tutorials, that are little hard to me to understand, because I didn't find something clear to say: "You're going to use Firebase, then here you should use ChangeNotifierProvider or Provider or StreamProvider etc."
So I found a tutorial by Jeff Delaney at Fireship that I tried to apply to my use case, but despite it works I'm not sure I did it properly, mostly because I get this message saying "The getter was called on null". I can suppress it by putting an "?" like user?.name but I want to understand what is going on and why despite the error the code still works fine.
Here's the code I have:
First I have a normal User model that works just fine creating model and returning name, tlf and other stuff from Firebase. This part is ok.
Then I have this database service from the Jeff's code:
class DatabaseService {
final Firestore _db = Firestore.instance;
/// Get a stream of a single document
Stream<User> streamUser (String id) {
return _db
.collection('profiles')
.document(id)
.snapshots()
.map((snap) => User.fromMap(snap.data));
}
}
Then I have the screen where I implemented the Provider:
class GestorScreen extends StatelessWidget {
final db = DatabaseService();
final FirebaseUser firebaseUser;
GestorScreen({Key key, #required this.firebaseUser});
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: db.streamUser(firebaseUser.uid),
child: DashboardButtons(),
);
}
}
And the DashboardButtons class:
class DashboardButtons extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<User>(context);
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
user?.updated != null ? _button(CustomColors.emerald, () {
Navigator.push(context,
MaterialPageRoute(builder: (context) =>
Dashboard(user: user)));
}, 'Gestionar Perfil') : _button(CustomColors.emerald, () {
Navigator.push(context,
MaterialPageRoute(builder: (context) =>
UserAlta(user: user,)));
}, ' Dar de Alta (ßeta)'),
SizedBox(height: SizeConfig.blockSizeHorizontal * 7,),
.................
.................
.................
],
)
),
backgroundColor: CustomColors.newCreme,
)
}
}
The code works fine but if delete the "?" from user.updated it says that The getter 'updated' was called on null... still it works fine.
Can someone help me with this? Where is the problem with the getter? And did I implemented the Provider the right way?
Get the error when the user is null.
user?.updated is equal to user == null ? null : user.updated
its a safe navigation operator, when the user object is null, the whole expression returns null. Otherwise updated() method is run.
safe navigation operator
Related
CURRENT BEHAVIOR: When I log out of my app and then log back in as a different user, I am taken to a dashboard page with no data. I have to restart the app from the IDE in order to load the user's data.
DESIRED BEHAVIOR: When I log in as a given user, the dashboard page should show that user's data without having to reload/restart/refresh anything.
Based on feedback in this thread, I've condensed my code as much as possible while trying not to remove anything that might help identify my issue. Apologies for the ugliness of the code - I removed as much white space and formatting as I could in order to shorten the paste.
I am working on an app that uses the Firebase Realtime Database as a back-end. The app is user-based, so each user will have a directory, with several subdirectories within each user directory. I'm trying to display a simple list of items returned from the database. Currently I have to restart the app each time I log out and log back in as a different user, which isn't what I'm looking for. I need a given user's data to appear upon login. I don't quite understand what all is happening here (I stumbled across a functional solution after several days of trial and error and googling), but I thought a Stream was more or less a 'live' stream of data from a particular source.
The code snippet below is actually taken from three or four different files in my project; I've put everything in one file and stripped out formatting and white space to make it more compact. I don't think I removed anything material to my problem.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const FlipBooks());}
class FlipBooks extends StatelessWidget {const FlipBooks({super.key});
#override
Widget build(BuildContext context) => const MaterialApp(home: AuthService());}
class AuthService extends StatelessWidget {const AuthService({super.key});
static String getUid() => FirebaseAuth.instance.currentUser!.uid;
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {return const DashboardPage();
} else {return const LoginPage();}}))}}
class DashboardPage extends StatelessWidget {const DashboardPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
child: GestureDetector(onTap: () {FirebaseAuth.instance.signOut();},
child: const Icon(Icons.logout))]),
body: StreamBuilder(
// kPAYEES_NODE is defined in constants.dart as
// kUSER_NODE.child('payees')
// kUSER_NODE is defined as FirebaseDatabase.instance.ref('users/${AuthService.getUid()}')
stream: kPAYEES_NODE.onValue,
builder: (context, snapshot) {
final payees = <Payee>[];
if (!snapshot.hasData) {return Center(child: Column(children: const [Text('No Data')]));
} else {
final payeeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>;
payeeData.forEach((key, value) {
final dataLast = Map<String, dynamic>.from(value);
final payee = Payee(id: dataLast['id'], name: dataLast['name'], note: dataLast['note']);
payees.add(payee);});
return ListView.builder(
shrinkWrap: true,
itemCount: payees.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(payees[index].name), subtitle: Text(payees[index].id));});}}),
floatingActionButton: FloatingActionButton(
onPressed: () {Navigator.push(context, MaterialPageRoute(
builder: (context) => AddThing(), fullscreenDialog: true));},
child: const Icon(Icons.add));}}
class LoginPage extends StatefulWidget {const LoginPage({super.key});
#override
State<LoginPage> createState() => _LoginPageState();}
class _LoginPageState extends State<LoginPage> {
// variables for FocusNodes, TextEditingControllers, FormKey
Future signIn() async {
try {await FirebaseAuth.instance.signInWithEmailAndPassword(email, password);
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
Future signUp() async {
try {await FirebaseAuth.instance.createUserWithEmailAndPassword(email, password);
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
Future sendEm() async {
var methods = await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
if (methods.contains('password')) {return signIn();
} else {showDialog(...); // give user option to register or try again
return;}}
Future passwordReset() async {
try {await FirebaseAuth.instance.sendPasswordResetEmail(email);
showDialog(...); // show reset email sent dialog
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
#override
void dispose() {...}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(...), // logo, welcome text
Form(...), // email+pw fields, forgot pw link => passwordReset(), submit button => sendEm()
]))));}}
As I said in my answer to your previous question, you're defining static String getUid() => FirebaseAuth.instance.currentUser!.uid; which means that is only evaluates the current user once. Since the user can sign in and out, the UID is a stream and that requires that you use authStateChanges to expose it to your DashboardPage, just as you already do in the AuthService itself.
class AuthService extends StatelessWidget {const AuthService({super.key});
static Stream<String?> getUid() => FirebaseAuth.instance. authStateChanges().map<String?>((user) => user?.uid);
...
}
I didn't run the above code, so there might be some typos or minor errors in it.
Now you can use a StreamBuilder when you call getUid() and get a stream of UID values (or null when no one is signed in).
I am trying to take a Screanshot of a Stak with a list of iteams in it. It displays normaly and works, but when i try to take screenshot of the Widget I resive:
NoSuchMethodError (NoSuchMethodError: The getter 'stateWidget' was called on null.
Receiver: null
Tried calling: stateWidget)
(I use a Inhereted widget)
Her is the Widget I am trying to take a Screenshot of
class BlinkSkjerm extends StatelessWidget {
#override
Widget build(BuildContext context) {
final provider = InheritedDataProvider.of(context);
final data = provider.historikken[provider.index];
return SizedBox(
height: 400,
child: Stack(
children: data.inMoveableItemsList,
));
}
}
and her is the onPress funtion:
onPressed: () async {
final controler = ScreenshotController();
final bytes = await controler.captureFromWidget(BlinkSkjerm());
setState(() {
this.bytes = bytes;
});
}
you used InheritedDataProvider in wrong way. you did not provide data that needed in BlinkSkjerm.
you want to take screen shot from widget that not in the tree, but that widget need data that should provide before build it which you did not provide it.
this approach work this way:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InheritedDataProvider(
child: BlinkSkjerm(),
data:'some string',
)),
);
this way you can use
final provider = InheritedDataProvider.of(context);
and make sure it is not null.
for your situation I recommended to do something like this:
onPressed: () async {
final controler = ScreenshotController();
final bytes = await controler.captureFromWidget(InheritedDataProvider(
child: BlinkSkjerm(),
data:'some string',
));
setState(() {
this.bytes = bytes;
});
}
for more information see this page
I have got a problem with the provider package.
I want to be able to clean an attribute (_user = null) of a provider ChangeNotifier class (it is a logout feature).
The problem is when I am doing that from a Widget that use info from this Provider.
My main app is like :
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
builder: (context, _) => App(),
),
);
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (_, auth, __) {
Widget displayedWidget;
switch (auth.loginState) {
case ApplicationLoginState.initializing:
displayedWidget = LoadingAppScreen();
break;
case ApplicationLoginState.loggedIn:
displayedWidget = HomeScreen();
break;
case ApplicationLoginState.loggedOut:
default:
displayedWidget = AuthenticationScreen(
signInWithEmailAndPassword: auth.signInWithEmailAndPassword,
registerAccount: auth.registerAccount,
);
}
return MaterialApp(
title: 'My App',
home: displayedWidget,
routes: {
ProfileScreen.routeName: (_) => ProfileScreen(),
},
);
});
}
}
My Provider class (simplified) :
class AuthProvider extends ChangeNotifier {
ApplicationLoginState _loginState;
ApplicationLoginState get loginState => _loginState;
bool get loggedIn => _loginState == ApplicationLoginState.loggedIn;
User _user;
User get user => _user;
void signOut() async {
// Cleaning the user which lead to the error later
_user = null;
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
My Profile screen which is accessible via named Route
class ProfileScreen extends StatelessWidget {
static const routeName = '/profile';
#override
Widget build(BuildContext context) {
final User user = Provider.of<AuthProvider>(context).user;
return Scaffold(
// drawer: AppDrawer(),
appBar: AppBar(
title: Text('Profile'),
),
body: Column(
children: [
Text(user.displayName),
FlatButton(
child: Text('logout'),
onPressed: () {
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(builder: (BuildContext context) => App()),
// ModalRoute.withName('/'),
// );
Provider.of<AuthProvider>(context, listen: false).signOut();
},
)
],
),
);
}
}
When I click the logout button from the profile screen, I don't understand why i get the error :
As I am using a Consumer<AuthProvider> at the top level of my app (this one includes my route (ProfileScreen), I thought it would redirect to the AuthenticationScreen due to the displayedWidget computed from the switch.
But it seems to rebuild the ProfileScreen first leading to the error. the change of displayedWidget do not seems to have any effect.
I'm pretty new to Provider. I don't understand what I am missing in the Provider pattern here ? Is my App / Consumer wrongly used ?
I hope you can help me understand what I've done wrong here ! Thank you.
Note : the commented Navigator.pushAndRemoveUntil redirect correctly to the login screen but I can see the error screen within a few milliseconds.
Your user is null, and you tried to get the name of him. You need to check it before using it. It will look like this:
user == null ?
Text("User Not Found!"),
Text(user.displayName),
From the provider API reference of Provider.of :
Obtains the nearest Provider up its widget tree and returns its
value.
If listen is true, later value changes will trigger a new State.build
to widgets, and State.didChangeDependencies for StatefulWidget.
So I think the line final User user = Provider.of<AuthProvider>(context).user; in your profile screen calls a rebuild when the _user variable is modified, and then the _user can be null in your ProfileScreen.
Have you tried to Navigator.pop the profile screen before clearing the _user variable?
I have this stateless widget called myPage.dart. which contains a Stack of Texts and Stateful List View Builder.
here is the code (I commented out the 2nd group of Text and Stateful List View Builder for now:
Widget content(BuildContext context) =>
Container(
child: Stack(
children: <Widget>[
sameDayText(context),
SameDayWorkManagement(context),
// nextDayText(),
// nextDay.NextDayWorkManagement(),
],
),
);
The sameDayText is no problem. probably because the class for that is inside the myPage.dart but I can't seem to pass the context to sameDayWorkManagement.dart which is a stateful widget that contains a listview builder. keep in mind that everything worked in the past. its just that when I tried to add localization now, It seems that the context is null for some reason in the sameDayWorkManagement. Localization requires context. and I keep getting error on snippet of codes in the sameDayWorkManagement that localizes text. and again because of the context being null:
here is the sample code of the context being null in the sameDayWorkManagement.dart
Localization.of(widget.buildContext).getTranslatedValue('wakeup')
and here is the script for the sameDayWorkManagement.dart
class SameDayWorkManagement extends StatefulWidget {
BuildContext buildContext;
SameDayWorkManagement(buildContext);
#override
_SameDayWorkManagementState createState() => _SameDayWorkManagementState();
}
class _SameDayWorkManagementState extends State<SameDayWorkManagement>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return Container(
// backgroundColor: Color(app_background_color_blue),
child: LayoutBuilder(
builder: (context, constraints) => SafeArea(
child: Container(
child: new StoreConnector<AppState, MainPageViewModel>(
converter: (store) => MainPageViewModel.fromStore(store),
builder: ( _, viewModel) => content(viewModel, constraints),
),
),
),
),
);
}
#override
void initState () {
super.initState();
if(widget.buildContext != null) {
print("is true");
} else {
print("is not true");
}
}
In initState the result is is not true
to be more precise. here is the image of myPage that does not have Localization and instead uses static Japanese Text
The first dot and Japanese Text with a telephone icon in the right is the sameDayText widget. the card below it is the sameDayWorkManagement its a list view and its scrollable.
and then the rest bellow are those that I commented out ( for now) called next day
I created a really ugly work around, so I'm still hoping this would be fixed. my work around is I created a map of all the necessary translated text in myPage using the localization which again is working in there. and pass that map to the sameDayWorkManagement as a parameter. and use that map to populate my needed text. yes it is very ugly. but for now it is working.
So I am implementing something like below:
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
int _count = 0;
int get count => _count;
Future<List<Widget>> getList() async{
addToList(Text('$count'));
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
increment(){
_count += 1;
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return Container(
child: futureProvider == null
? Text('Loading...'
: ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index){
return futureProvider[index];
}
),
));
}
}
Basically, what this does is that a List of Widgets from a Future is the content of ListView Builder that I have as its objects are generated from a database query. Those widgets are buttons that when pressed should update the "Count" value and should update the Text Widget displaying the latest "Count" value.
I was able to test the buttons and they seem to work and are incrementing the _count value via backend, however, the displayed "Count" on the Text Widget is not updating even if the Provider values are updated.
I'd like to ask for your help for what's wrong here, with my understanding, things should just update whenever the value changes, is this a Provider anti-pattern, do I have to rebuild the entire ListView, or I missed something else?
I'm still getting myself acquainted with this package and dart/flutter in general, hoping you can share me your expertise on this. Thank you very much in advance.
so I have been on a lot of research and a lot of trial and errors last night and this morning, and I just accidentally bumped into an idea that worked!
You just have to have put the listening value on a consumer widget making sure it listens to the nearest Provider that we have already implemented higher in the widget tree. (Considering that I have already finished drawing my ListView builder below the FutureProvider Widget)
..getList() async{
Consumer<ChallengeViewProvider>(
builder: (_, foo, __) => Text(
'${foo.count}',
),
);
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
I have also refactored my widgets and pulled out the Button as a stateless widget for reuse. Though make sure that referenced Buttons are subscribed to the same parent provider having the Counter value and have the onTap property call out the increment() function through Provider<>.of
Hoping this will help anyone in the future!