Here is the summary of the code I'm having a problem with:
Parent widget
class HomePage extends StatefulWidget {
#override
State<HomePage> createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
final GlobalKey<AsyncLoaderState> _asyncLoaderState = GlobalKey<AsyncLoaderState>();
List<DateTime> rounds;
List<PickupModel> pickups;
DateTime currentDate;
Widget build(BuildContext context) {
var _asyncLoader = AsyncLoader(
key: _asyncLoaderState,
initState: () async => await _getData(),
renderLoad: () => Scaffold(body: Center(child: CircularProgressIndicator())),
renderError: ([error]) => Text('Sorry, there was an error loading'),
renderSuccess: ({data}) => _buildScreen(context),
);
return _asyncLoader;
}
Widget _buildScreen(context) {
return Scaffold(
body: PickupList(pickups),
);
}
Future<Null> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
);
if (picked != null && picked != currentDate) {
currentDate = picked;
pickups = await api.fetchPickupList(currentDate);
setState(() {
});
}
}
_getData() async {
rounds = await api.fetchRoundsList();
currentDate = _getNextRound(rounds);
pickups = await api.fetchPickupList(currentDate);
}
}
Children Widget
(Listview builds tiles)
class PickupTile extends StatefulWidget{
final PickupModel pickup;
PickupTile(this.pickup);
#override
State<StatefulWidget> createState() {
return PickupTileState();
}
}
class PickupTileState extends State<PickupTile> {
Duration duration;
Timer timer;
bool _isUnavailable;
bool _isRunning = false;
bool _isDone = false;
#override
void initState() {
duration = widget.pickup.duration;
_isUnavailable = widget.pickup.customerUnavailable;
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
// UI widgets
]
}
So I have a parent widget an initial list of pickups which are displayed in the children PickupTile. One can change the date of the pickups displayed using _selectDate. This fetches a new list of Pickups which are stored in the parent State, and the children are rebuilt with their correct attributes. However, the State of the children widget (duration, isRunning, isDone...) is not reset so they stay on screen when changing the date.
If feel like I'm missing something obvious but I can't figure out how to reset the State of the children Widget or create new PickupTiles so that when changing the date I get new separate States.
Related
There is very easy way to use Hive key-value database on StatefulWidgets, for example:
class HookDemo extends StatefulWidget {
#override
_HookDemoState createState() => _HookDemoState();
}
class _HookDemoState extends State<HookDemo> {
Box user;
#override
void initState() {
super.initState();
user = Hive.box<User>('user');
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
final _u = User()
..nameFamily = 'myname'
..mobileNumber = '123456789';
_user!.add(_u);
_u.save();
},
child: Icon(Icons.add),
),
...
);
}
}
here we defined Box user property and inside initState we implemented what's user such as user = Hive.box<User>('user');
after that we can use user without any problem and getting already opened error
now in this current application we used HookWidget and when we want to use Hive we get error as box already opened
main.dart:
Future<void> initHiveDriver() async {
final appDocumentDirectory = await path_provider.getApplicationDocumentsDirectory();
await Hive.initFlutter(appDocumentDirectory.path);
await Hive.openBox<UserAdapter>('user');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
///...
initHiveDriver();
runApp(
ProviderScope(observers: [
Logger()
],
child: MyApp()),
);
}
how can i create a provider for Hive with Riverpod and use it into HookWidget?
I am using Hive with Riverpod like this.
I am using a named constructor so I can await the openBox call.
final hiveProvider = FutureProvider<HiveDB>((_) => HiveDB.create());
class HiveDB {
var _userBox;
HiveDB._create() {}
static Future<HiveDB> create() async {
final component = HiveDB._create();
await component._init();
return component;
}
_init() async {
Hive.registerAdapter(UserAdapter());
this._userBox = await Hive.openBox<User>('user');
}
storeUser(User user) {
this._userBox.put('user', user);
}
User getUser() {
return this._userBox.get('user');
}
}
Use in a ConsumerWidget:
class SomeWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(hiveProvider).data?.value;
...
}
}
I've been trying without success to load different pages according to my Shared Preference settings.
Based on several posts found in stackoverflow, i end up with the following solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:testing/screens/login.dart';
import 'package:testing/screens/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget page = Login();
Future getSharedPrefs() async {
String user = Preferences.local.getString('user');
if (user != null) {
print(user);
this.page = Home();
}
}
#override
void initState() {
super.initState();
this.getSharedPrefs();
}
#override
Widget build(BuildContext context) {
return MaterialApp(home: this.page);
}
}
class Preferences {
static SharedPreferences local;
/// Initializes the Shared Preferences and sets the info towards a global variable
static Future init() async {
local = await SharedPreferences.getInstance();
}
}
The variable user is not null because the print(user) returns a value as expected, but the login screen is always being opened.
Your problem is that your build method returns before your getSharedPrefs future is complete. The getSharedPrefs returns instantly as soon as it's called because it's async and you're treating it as a "Fire and Forget" by not awaiting. Seeing that you can't await in your initState function that makes sense.
This is where you want to use the FutureBuilder widget. Create a Future that returns a boolean (or enum if you want more states) and use a future builder as your home child to return the correct widget.
Create your future
Future<bool> showLoginPage() async {
var sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setString('user', 'hasuser');
String user = sharedPreferences.getString('user');
return user == null;
}
When user is null this will return true. Use this future in a Future builder to listen to the value changes and respond accordingly.
#override
Widget build(BuildContext context) {
return MaterialApp(home: FutureBuilder<bool>(
future: showLoginPage(),
builder: (buildContext, snapshot) {
if(snapshot.hasData) {
if(snapshot.data){
// Return your login here
return Container(color: Colors.blue);
}
// Return your home here
return Container(color: Colors.red);
} else {
// Return loading screen while reading preferences
return Center(child: CircularProgressIndicator());
}
},
));
}
I ran this code and it works fine. You should see a blue screen when login is required and a red screen when there's a user present. Uncomment the line in showLoginPage to test.
There is a much pretty way of doing this.
Assuming that you have some routes and a boolean SharedPreference key called initialized.
You need to use the WidgetsFlutterBinding.ensureInitialized() function before calling runApp() method.
void main() async {
var mapp;
var routes = <String, WidgetBuilder>{
'/initialize': (BuildContext context) => Initialize(),
'/register': (BuildContext context) => Register(),
'/home': (BuildContext context) => Home(),
};
print("Initializing.");
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesClass.restore("initialized").then((value) {
if (value) {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Home(),
);
} else {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Initialize(),
);
}
});
print("Done.");
runApp(mapp);
}
The SharedPreference Class Code :
class SharedPreferencesClass {
static Future restore(String key) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
return (sharedPrefs.get(key) ?? false);
}
static save(String key, dynamic value) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
if (value is bool) {
sharedPrefs.setBool(key, value);
} else if (value is String) {
sharedPrefs.setString(key, value);
} else if (value is int) {
sharedPrefs.setInt(key, value);
} else if (value is double) {
sharedPrefs.setDouble(key, value);
} else if (value is List<String>) {
sharedPrefs.setStringList(key, value);
}
}
}
At the HompePage, am navigating to Settings page with;
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => Settings()));
the Settings() page contains an int input to allow user specify the number of posts they want to see at the HomePage. When users input the number and form.save, the value is stored in SharedPreferences. But when the user go back to the HomePage, the initial number of post still there. I want the HomePagestate to refresh so that the number of post the user specify at the Settings Page will take effect immediately the form is saved.
Below is some snippets of my code;
This is the form _submit on Settings() page,
_submit() async {
final form = _formKey.currentState;
SharedPreferences prefs = await SharedPreferences.getInstance();
if (form.validate()) {
prefs.setInt('defaultField', newva);
form.save();
final mysb = SnackBar(
duration: Duration(seconds: 1),
content: new Text(
'Data Saved Successfully',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.red,
);
_scaffoldKey.currentState?.showSnackBar(mysb);
myHomePageState.setState(() {
newSULength = newva;
});
print('Done for $newva');
}
}
This is my MyHomePage()
MyHomePageState myHomePageState = new MyHomePageState();
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => new MyHomePageState();
static MyHomePageState of(BuildContext context){
final MyHomePageState navigator = context.ancestorStateOfType(const TypeMatcher<MyHomePageState>());
assert(() {
if(navigator == null) {
throw new FlutterError('Error occoured');
}
return true;
}());
return navigator;
}
}
class MyHomePageState extends State<MyHomePage> {
int newSULength = 0;
void initState() {
// TODO: implement initState
super.initState();
loadDF();
}
set newle(String value) => setState(() => _string = value);
loadDF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
newSULength = (prefs.getInt('defaultField') ?? 5);
for (int i = 0; i < newSULength; i++) {
\\todos
}
});
print('Done');
}
}
You can use callbacks to indicate the HomePage that the Settings page changed some value in shared preference. Refer this
I am trying to add a Image widget when I get the results of an API call. My code is:
class AnimalDetailsPage extends StatefulWidget {
final selection;
_AnimalDetailsPage createState() => new _AnimalDetailsPage();
AnimalDetailsPage({Key key, this.selection}) : super(key: key);
}
class _AnimalDetailsPage extends State<AnimalDetailsPage> {
Future<List> getphotos(horseId) async {
http.Response response = await http.get(
Uri.encodeFull(
"http://myhorses.com/api/getHorsePhotos?horse_id=" +
horseId));
return JSON.decode(response.body);
}
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
dynamic body = new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: bodyContent,
);
if (widget.selection != null) {
final horse = widget.selection;
getphotos(horse['id'].toString()).then((res) {
setState(() {
bodyContent.add(new Image.network(res[0]['image']));
});
});
}
return Scaffold(
body: body,
);
}
}
What I can't manage to understand is that the setState does not updates the view. If I move the setState out of the then statement and hard code the image src, then it works fine.
bodyContent is declared inside build()
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
...
bodyContent.add(new Image.network(res[0]['image']));
...
and setState() causes build to be executed again, which means the bodyContent that holds the image is discarded an a new one created.
Move List<Widget> bodyContent = [menu]; out of the build() method and make it a class-level field and you should get the desired result.
I've been facing some problems related to the setState function while using Stateful Widgets that updates itself with the help of Timers. The code below show 2 main classes that replicate how I came to find this error. The Text Widget "Lorem" should be inserted within 10 seconds - and it is - but it's never shown. I tried to debug the array "Items" and it does contain the "lorem" Text Widget after 5 seconds, as it should. The "build" function runs but doesn't make any difference in the UI.
class textList extends StatefulWidget {
#override
State<StatefulWidget> createState() =>
new _textListState();
}
class _textListState extends State<textList>
with TickerProviderStateMixin {
List<Widget> items = new List();
Widget lorem = new textClass("Lorem");
Timer timer;
#override
void initState() {
super.initState();
items.add(new textClass("test"));
items.add(new textClass("test"));
timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
setState(() {
items.removeAt(0);
items.add(lorem);
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
Iterable<Widget> content = ListTile.divideTiles(
context: context, tiles: items).toList();
return new Column(
children: content,
);
}
}
class textClass extends StatefulWidget {
textClass(this.word);
final String word;
#override
State<StatefulWidget> createState() =>
new _textClass(word);
}
class _textClass extends State<textClass>
with TickerProviderStateMixin {
_textClass(this.word);
String word;
Timer timer;
#override
void initState() {
super.initState();
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
word += "t";
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return new Text(word);
}
}
This is not how I came to find this error but this is the simplest way to replicate it. The main idea is: The children texts should keep updating themselves (in this case, adding "t"s in the end) and, after 5 seconds, the last of them should be replaced for the Text Widget "Lorem", what does happen in the list but not in the UI.
Here's what's wrong:
A State should never have any constructor arguments. Use the widget property to get access to final properties of the associated StatefulWidget.
Flutter is reusing your _textClass instance because the class name and keys match. This is a problem since you only set widget.word in initState so you're not picking up the new word configuration information. You can fix this either by giving the StatefulWidget instances unique keys to disambiguate them and cause the old State to be disposed, or you can keep around the old State and implement didUpdateWidget. The latter approach is shown below.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(title: new Text('Example App')),
body: new textList(),
),
));
}
class textList extends StatefulWidget {
#override
State<StatefulWidget> createState() =>
new _textListState();
}
class _textListState extends State<textList>
with TickerProviderStateMixin {
List<Widget> items = new List();
Widget lorem = new textClass("Lorem");
Timer timer;
#override
void initState() {
super.initState();
items.add(new textClass("test"));
items.add(new textClass("test"));
timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
setState(() {
items.removeAt(0);
items.add(lorem);
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
Iterable<Widget> content = ListTile.divideTiles(
context: context, tiles: items).toList();
return new Column(
children: content,
);
}
}
class textClass extends StatefulWidget {
textClass(this.word);
final String word;
#override
State<StatefulWidget> createState() =>
new _textClass();
}
class _textClass extends State<textClass>
with TickerProviderStateMixin {
_textClass();
String word;
Timer timer;
#override
void didUpdateWidget(textClass oldWidget) {
if (oldWidget.word != widget.word) {
word = widget.word;
}
super.didUpdateWidget(oldWidget);
}
#override
void initState() {
super.initState();
word = widget.word;
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
word += "t";
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return new Text(word);
}
}