I am trying to set the initialValue of my TextFormField from my provider. This works but not on inital load of my screen. For this to work, I need to go to previous screen, then come back. Here are the relevant source codes.
User class:
class User with ChangeNotifier {
String name;
String email;
User() {
_deserialiseUser();
}
}
void _deserialiseUser() async {
final prefs = await SharedPreferences.getInstance();
final String jsonUser = prefs.getString('user');
if (jsonUser != null) {
var ju = jsonDecode(jsonUser);
name = ju['name'];
email = ju['email'];
notifyListeners();
}
}
Main class:
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => User()),
...
],
child: MyApp(),
),
);
The affected screen:
class AccountScreen extends StatefulWidget {
#override
_AccountScreenState createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> {
User user;
#override
Widget build(BuildContext context) {
user = context.watch<User>();
return Scaffold(
appBar: AppBar(
title: Text('Account'),
),
body: TextFormField(
initialValue: user.name,
),
);
}
}
If I replace my TextFormField with Text, this works fine. What am I missing?
EDIT: Although I already have the answer, I will still accept answers that can explain the root cause of the issue.
I solved the issue by not lazily loading my User provider like so:
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => User(),
lazy: false,
),
],
child: MyApp(),
),
);
Related
I am trying to write widget tests for my Flutter application.
I have a page (MainMenuScreen) as follows (I've left out the imports):
class MainMenuScreen extends StatefulWidget {
#override
_MainMenuScreenState createState() => _MainMenuScreenState();
}
class _MainMenuScreenState extends State<MainMenuScreen> {
#override
void initState() {
super.initState();
}
late MainMenuViewModel vm;
#override
Widget build(BuildContext context) {
vm = Provider.of<MainMenuViewModel>(context);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: Column(
children: <Widget>[
generateHeightSpacer(Dimensions().heightSpace),
generateLogo(Dimensions().fractionalWidth),
generateHeightSpacer(Dimensions().fractionalWidth),
generateMenuButton(
context,
Dimensions().fractionalWidth,
Dimensions().fractionalHeight,
AppLocalizations.of(context).menuPlay,
LevelSelectionScreen()),
generateHeightSpacer(Dimensions().heightSpace),
generateMenuButton(
context,
Dimensions().fractionalWidth,
Dimensions().fractionalHeight,
AppLocalizations.of(context).menuAbout,
AboutScreen()),
],
),
)),
);
}
}
My main.dart file looks like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MainMenuViewModel()),
ChangeNotifierProvider(create: (context) => LevelSelectionViewModel()),
ChangeNotifierProvider(create: (context) => LevelViewModel()),
],
child: MaterialApp(
onGenerateTitle: (context) {
return AppLocalizations.of(context).appTitle;
},
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en', ''), // English, no country code
Locale('es', ''), // Spanish, no country code
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => MainMenuViewModel(),
child: MainMenuScreen(),
// ),
),
),
);
}
}
I have looked into how to write a test that can identify when the About screen is displayed, i.e. after pressing the last Menu Button.
So, for now I have something as follows:
testWidgets("Flutter Main Menu Test Menu Button About",
(WidgetTester tester) async {
await tester.pumpWidget(MyApp());
var button = find.text("About");
expect(button, findsOneWidget);
});
This allows me to identify that the About button is displayed. (Note: I will be changing my code and tests to do this based on a 'key' rather than text.)
From what I've read the way to test is to use Mockito and a NavigationObserver but that seems to require the ability to inject into the screen. I have also been looking at other solutions which seem to suggest wrapping the provider, i.e.:
I've been following this page(https://iiro.dev/writing-widget-tests-for-navigation-events/) to try to test but I can't quite figure out how to pass the viewmodel in.
This is my test at the moment:
class MockMainMenuViewModel extends Mock implements MainMenuViewModel {}
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
void main() {
group('MainMenuScreen navigation tests', () {
late NavigatorObserver mockObserver;
MockMainMenuViewModel? mockViewModel;
setUp(() {
mockObserver = MockNavigatorObserver();
mockViewModel = MockMainMenuViewModel();
});
Future<void> _buildMainPage(WidgetTester tester) {
return tester.pumpWidget(MaterialApp(
home: MainMenuScreen(mockViewModel),
navigatorObservers: <NavigatorObserver>[observer],
));
}
});
}
Any help is appreciated please.
I am attempting to share a ChangeNotifierProvider to my main.dart, however the value never gets updated.
How it works
main.dart uses ChangeNotifierProvider to get an instance of the class Location()
main.dart routes to the location_login.dart page where a string in Location() class is set.
The instance of Location() should update in main.dart but it DOES NOT.
Here is the main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
ChangeNotifierProvider<Location>.value( <------ CREATE CHANGENOTIFIERPROVIDER
value: Location(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
Location location = Provider.of<Location>(context, listen: false); <----- LISTEN TO PROVIDER
return MultiProvider(
providers: [
FutureProvider<List<Report>>(
create: (context) =>
Collection<Report>(path: '${location.getLocation}/data/reports') <----- USE PROVIDER STRING IN PATH
.getUsers(),
initialData: [],
),
],
child: MaterialApp(
routes: {
'/': (context) => LocationLogin(),
'/login': (context) => LoginScreen(),
'/home': (context) => HomeScreen(),
},
// Theme
theme: ThemeData(
fontFamily: 'Nunito',
bottomAppBarTheme: BottomAppBarTheme(
color: Colors.black87,
),
// your customizations here
brightness: Brightness.dark,
buttonTheme: ButtonThemeData(),
),
),
);
}
}
Here is the location_login.dart
#override
Widget build(BuildContext context) {
Location location = Provider.of<Location>(context, listen: true);
return Scaffold(
body: TextButton(
child: Text("Submit",
style: GoogleFonts.poppins(
fontSize: 15.sp, color: Colors.white)),
onPressed: () {
location.setLocation('London'); <------- SETTING LOCATION
}),
);
}
}
Here is the location.dart
class Location with ChangeNotifier {
String place = 'none';
String get getLocation => place;
setLocation(String location) {
place = location;
notifyListeners();
}
}
To reiterate, the issue is that when I click the button in the location_login.dart page to set the location to "London"; it does not update the ChangeNotifierProvider with a new instance of the Location() class containing "London". Therefore, I can not update the path in my FurtureProvider. Any ideas of what is going wrong here? I tried to make this as clear as possible but if you don't understand please ask. Thank you
I think you have not consume the ChangeNotifierProvider.
For me below simple implementation work perfectly.
my main.dart file code is as below...
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/app_provider.dart';
import 'providers/favorites_provider.dart';
import 'providers/comments_provider.dart';
import 'providers/home_provider.dart';
import 'providers/details_provider.dart';
import 'providers/gallery_provider.dart';
import 'providers/chat_provider.dart';
import 'ui/splash.dart';
import 'helper/constants.dart';
import 'ui_user/login.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AppProvider()),
ChangeNotifierProvider(create: (_) => GalleryProvider()),
ChangeNotifierProvider(create: (_) => CommentsProvider()),
ChangeNotifierProvider(create: (_) => ChatProvider()),
ChangeNotifierProvider(create: (_) => HomeProvider()),
ChangeNotifierProvider(create: (_) => DetailsProvider()),
ChangeNotifierProvider(create: (_) => FavoritesProvider()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (BuildContext context, AppProvider appProvider, Widget child) {
return MaterialApp(
key: appProvider.key,
debugShowCheckedModeBanner: false,
navigatorKey: appProvider.navigatorKey,
title: Constants.appName,
theme: appProvider.theme,
home: appProvider.isLogin == "0" ? LoginPage() : Splash(),
);
},
);
}
}
And my app_provider.dart as below...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helper/constants.dart';
class AppProvider extends ChangeNotifier {
AppProvider() {
checkTheme();
}
String isLogin = "0";
ThemeData theme = Constants.lightTheme;
Key key = UniqueKey();
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void setKey(value) {
key = value;
notifyListeners();
}
void setNavigatorKey(value) {
navigatorKey = value;
notifyListeners();
}
void setTheme(value, c) {
theme = value;
SharedPreferences.getInstance().then((prefs) {
prefs.setString("theme", c).then((val) {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor:
c == "dark" ? Constants.darkPrimary : Constants.lightPrimary,
statusBarIconBrightness:
c == "dark" ? Brightness.light : Brightness.dark,
));
});
});
notifyListeners();
}
ThemeData getTheme(value) {
return theme;
}
Future<ThemeData> checkTheme() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
ThemeData t;
String r =
prefs.getString("theme") == null ? "light" : prefs.getString("theme");
isLogin = prefs.getString("isLogin") == null? "0" : prefs.getString("isLogin");
if (r == "light") {
t = Constants.lightTheme;
setTheme(Constants.lightTheme, "light");
} else {
t = Constants.darkTheme;
setTheme(Constants.darkTheme, "dark");
}
return t;
}
}
This solution is working very well for me. Hope this will help you too...
I have a MultiProvider in the main with the following code:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => ReadPreferences(),
),
ChangeNotifierProvider(
create: (context) => ItemsCrud(),
),
],
child: MaterialApp(...
I am using shared preferences to save and updated the last opened list, so the following in my ReadPreferences file:
import 'package:flutter/foundation.dart'; //To use the "ChangeNotifier"
import 'package:shared_preferences/shared_preferences.dart'; //local store
class ReadPreferences extends ChangeNotifier {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String openedList = '';
//Constructor method
ReadPreferences() {
getPreferences();
}
void getPreferences() async {
final SharedPreferences prefs = await _prefs;
openedList = prefs.getString('openedList');
}
Future<bool> updateOpenedList({String listTitle}) async {
final SharedPreferences prefs = await _prefs;
bool result = await prefs.setString('openedList', listTitle);
if (result == true) {
openedList = listTitle;
}
notifyListeners();
return result;
}
}
When I'm trying to update the opened list it updates in the shared Preferences file normally but it never listen to the new "openedList" value in my homepage screen.
The code I use in the homepage screen like the following:
child: Text(Provider.of<ReadPreferences>(context).openedList),
I checked many times by printing the new value inside the "ReadPreferences" files, but outside it, it keeps give me the old value not the updated one at all.
I tested with a modified Flutter Counter (default app), everything seams to be working fine. Note that I'm not calling setState() anywhere, so the only refresh is coming from the ReadPreferences class.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReadPreferences extends ChangeNotifier {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String openedList = '';
//Constructor method
ReadPreferences() {
getPreferences();
}
void getPreferences() async {
final SharedPreferences prefs = await _prefs;
openedList = prefs.getString('openedList');
}
Future<bool> updateOpenedList({String listTitle}) async {
final SharedPreferences prefs = await _prefs;
bool result = await prefs.setString('openedList', listTitle);
if (result == true) {
openedList = listTitle;
}
notifyListeners();
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => ReadPreferences(),
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(Provider.of<ReadPreferences>(context).openedList)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counter++;
Provider.of<ReadPreferences>(context, listen: false).updateOpenedList(listTitle: (_counter).toString());
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I finally found the answer, many thanks for #Andrija explanation. What I was doing wrong is to create a new instance from ReadPreferences() then using it for the update method, but the correct approach is to use Provider.of<ReadPreferences>(context, listen: false).updateOpenedList(listTitle: list.title); to use the update method.
For more explanation I'll add #Andrija comment hereafter:-
You are right, you should be using Provider.of. When you add Provider using ChangeNotifierProvider(create: (context) => ReadPreferences(), ) - new instance of ReadPreferences() is created, and it is kept in WidgetTree. This is the instance you want, and you get it by using Provider.of. In your code above, you created a new instance of ReadPreferences - and this is where you added a new value. This new instance has nothing to do with the one that Provider manages, and this new instance has nothing to do with your Widget.
In my app, I have a model that store the user logged in my app.
class AuthenticationModel extends ChangeNotifier {
User _user;
User get user => _user;
void authenticate(LoginData loginData) async {
// _user = // get user from http call
notifyListeners();
}
void restoreUser() async {
//_user = // get user from shared prefs
notifyListeners();
}
}
The model is registered at the top of the widget tree :
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthenticationModel(),
child: MaterialApp(
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
),
);
}
}
Somewhere down the widget tree, I have a button that calls the Model :
child: Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
return MyCustomButton(
text: 'Connect',
onPressed: () {
authModel.authenticate(...)
},
);
},
),
Now, I would like, somewhere, listen to the changes on the AuthenticationModel to trigger a Navigator.pushReplacmentNamed('/home') when the user is not null in the model.
I tried to do it in the builder of Prehome :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
if (authModel.user != null) {
Navigator.of(context).pushReplacementNamed("/home")
}
return Container(
child: // Prehome UI
);
},
);
}
}
but I have a error when doing it like this :
════════ (2) Exception caught by widgets library ═══════════════════════════════════════════════════
setState() or markNeedsBuild() called during build.
The relevant error-causing widget was:
Consumer<AuthenticationModel> file:///Users/pierre.degand/Projects/cdc/course_du_coeur/lib/Prehome.dart:13:12
═══════════════════════════════════════════════════════════════════════════════
How can I setup such a listener ? Is it a good practice to trigger navigation on model changes like this ?
Thanks
EDIT: I found a way to make this work. Instead of using Consumer inside the PrehomeScreen builder, I used the following code :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
Provider.of<AuthenticationModel>(context).addListener(() {
Navigator.of(context).pushReplacementNamed("/home");
});
return Container(
child: // UI
);
}
}
It works fine, the navigation is executed when the model changes. But there is an error message in the console (printed 3 times) :
════════ (4) Exception caught by foundation library ════════════════════════════════════════════════
Looking up a deactivated widget's ancestor is unsafe.
════════════════════════════════════════════════════════════════════════════════════════════════════
The app does not crash so, for now, I'm ok with this.
I still want to know if this is a good approach or not.
I prefer to use Stream or rxdart PublishSubject BehaviourSubject for listening to any activity or to manage global app data.
I implement it using bloc pattern. Basically bloc pattern is just like redux for react means creating a central dataset that contains all app data and you don't have to do prop drilling.
You can create Stream like this.
import 'package:rxdart/rxdart.dart';
class AbcBloc {
BehaviorSubject<bool> _connectivity;
AbcBloc() {
_connectivity = BehaviorSubject<bool>();
}
// stream
Stream<bool> get connectivity => _connectivity.stream;
// sink
Function(bool) get updateConnectivity => _connectivity.sink.add;
dispose(){
_connectivity.close();
}
}
void createAbcBloc() {
if (abcBloc != null) {
abcBloc.dispose();
}
abcBloc = AbcBloc();
}
AbcBloc abcBloc = AbcBloc();
now you can access that abcBloc variable from anywhere and listen to connectivity variable like this
import './abcBloc.dart';
void listenConnectivity(){
abcBloc.connectivity.listen((bool connectivety){
here you can perform your operations
});
}
and you can update connectivity from abcBloc.updateConnectivity(false);
every time you perform any changes that listener will get called.
remember you have to call listenConnectivity() one time to get it activated;
void main() {
Provider.debugCheckInvalidValueType = null;
return runApp(
Provider(
create: (_) => AuthenticationModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalKey<NavigatorState>();
Provider.of<AuthenticationModel>(context).addListener(() {
final authModel = Provider.of<AuthenticationModel>(context);
if (authModel.user != null) {
navigatorKey.currentState.pushReplacementNamed("/home");
}
});
return MaterialApp(
navigatorKey: navigatorKey,
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
);
}
}
I don't think ChangeNotifier is needed.
void main() async {
final isLoggedIn = await Future.value(true); // get value from shared prefs or your model
runApp(MyApp(isLoggedIn));
}
class MyApp extends StatelessWidget {
MyApp(this.isLoggedIn);
final bool isLoggedIn;
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: isLoggedIn ? '/home' : '/',
routes: {
'/': (context) => HomeScreen(),
'/login': (context) => LoginScreen()
},
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Logout'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/login"),
);
}
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Login'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/"),
);
}
}
I recognize FutureProvider can be used when a model will be given by a Future. It is needed to initialize the model by its init() async method, so I tried FutureProvider. However FutureProvider didn't give an instance of the model. It had given null.
Additionally while using ChangeNotifierProvider successfully updates UI, FutureProvider seems to not update UI when the notifyListeners() is called. Consumer gives same result. Is there anything to be fixed? Thank you.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MyModel extends ChangeNotifier {
int _point = 0;
int get point => _point;
set point(int value) {
if (_point != value) {
_point = value;
notifyListeners();
}
}
Future<void> init() async { /* Use await doSomething(); */ }
}
void main() => runApp(
FutureProvider<MyModel>(
create: (context) async {
final model = MyModel();
await model.init();
return Future.value(model);
},
lazy: false,
updateShouldNotify: (_, __) => true,
child: MyHomePage(),
),
);
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
final model = Provider.of<MyModel>(context);
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
'${model.point}',
style: Theme.of(context).textTheme.display1,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
model.point++;
},
child: Icon(Icons.add),
),
),
);
}
}
You can use ChangeNotifierProvider. It will update your UI whenever notifyListeners is called.
ChangeNotifierProvider(
create: (context) => MyModel()..init(),
child: MyHomePage(),
)
There is actually no need to call init if you are not doing anything inside your init method.
And if your MyModel class is only there for a single int value, you don't even need to use it. You can do something like:
ChangeNotifierProvider<ValueNotifier<int>>(
create: (context) => ValueNotifier<int>(0),
child: MyHomePage(),
);
And inside your HomePage
final counter = Provider.of<ValueNotifier<int>>(context);
You can use it inside your Text widget like:
Text('${counter.value}')