In my app I have a login screen and a home screen. When navigating from the login to the home screen I read data from a .txt file and show 4 random data points. I am getting the data from the file in my initState so that it isn't called multiple times when the state changes and then waiting on it with a future builder like...
class _HomeScreenState extends State<HomeScreen> {
Future<bool> _future;
#override
initState() {
super.initState();
print('in initState about to call _getData');
_future = _getData();
}
#override
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
case ConnectionState.done:
if (snapshot.hasError) {
return new Center(
child: Text('Error'),
);
} else {
return new ListView(
children: <Widget>[
//my view
],
);
}
}
}
);
return MaterialApp(
home: WillPopScope(
onWillPop: () async {
return Navigator.pop(context);
},
child: Scaffold(
body: futureBuilder,
),
)
);
Now when I navigate back to the login screen using the back button and then go back to the home screen initState will go off multiple times (this can be seen by the print statement I left in). As you go back and forth between these two screens (pop homescreen, push homescreen) initState will be called exponentially more times. I am so confused, any help is appreciated!
EDIT: Full code for both login and home screen can be found https://github.com/ViscousOx/Flutter-Stuff
Try that: It work
Use the default statement with the ConnectionState.waiting.
class _HomeScreenState extends State<HomeScreen> {
Future<bool> _future;
#override
initState() {
super.initState();
print('in initState about to call _getData');
_future = _getData();
}
#override
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError) {
return new Center(
child: Text('Error'),
);
} else {
return new ListView(
children: <Widget>[
//my view
],
);
}
}
}
);
return MaterialApp(
home: WillPopScope(
onWillPop: () async {
return Navigator.pop(context);
},
child: Scaffold(
body: futureBuilder,
),
)
);
Connection waiting state can be avoided. I used as:
FutureBuilder(future: _isUserLoggedIn(),
builder: (ctx, loginSnapshot) =>
loginSnapshot.data == true ? AppLandingScreen() : SignUpScreen()
),
As you go back and forth between these two screens (pop homescreen, push homescreen)
It sounds like you're creating your HomeScreen each time you enter the screen, so it'll be a brand new widget, and thus normal that initState() gets called.
If you want to keep the same screen (object) around for the lifetime of your app, there are a number of solutions such as Stack and Offstage so that your widget lives on, but isn't visible.
Related
so I'm currently working on an application that has a listview on the first screen (implemented on main.dart).
The listview fetches it's data from internet (async).
The problem is that, the listview does not get updated when the data is changed.
(I can implement this functionality simply by designing a 'reload' button and pressing it every time I want the new data. But that's not what I want right now).
In other words, how can I update the listview automatically?
EDIT1: ADDING SOME CODE
code might be messy; see the description at the end.
class RssFeed extends StatelessWidget {
String title;
String pubDate;
RssFeed(this.title, this.pubDate);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Align(
alignment: Alignment.topRight,
child: Text(title),
),
Text(pubDate)
],
),
);
}
}
class FeedsList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _FeedsListState();
}
}
class _FeedsListState extends State<FeedsList> {
List<Widget> list1 = new List<Widget>();
#override
void initState() {
super.initState();
ls();
}
Future ls() async {
list1.clear();
list.clear();
sites1.clear();
RSS_reader rss_reader = new RSS_reader();
for (var i in saver.list.items) {
sites1.add(
site(siteAdress: i.siteAdress, siteDescription: i.siteDescription));
}
var res = await rss_reader.Get_items(sites1);
for (var val in res) {
list.add(InkWell(
onTap: () => _launchURL(val.item.link),
child: Container(
height: 50,
color: Colors.amber[100],
child: Center(
child: new RssFeed(val.item.title, val.item.pubDate.toString()),
),
)));
}
print(list.length);
setState(() {
list1 = list;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
}));
}
}
DESCRIPTION:
As you can guess, this is a RSS reader.
So, I have a class RSSFeed; which makes one of the tiles of Listview.
then in the FeedsList class (stateful widget), I make the listview.
I have a class called RSS_reader and a method Get_items, which gets a bunch of sites as input and puts those sites' newest feeds in a list ('res' in the above code).
Then, I put the items in a list of 'Container's and then build the listview.
Then, in the main function, I create a container like below:
Container(
height: 500,
width: 580,
child: FeedsList(),
)
and there appears the problem; the FeedsList class does not get updated automatically. although if I put a button and navigate to FeedsList class through that button, the list is refreshed and OK.
Thanks for reading and help.
If you just want to fetch data once from your external source use a FutureBuilder, if you want to fetch data multiple times take a look to StreamBuilder. Both widgets will have the behavior you are looking for, with no refresh button.
Simple example of how to use a FutureBuilder:
Future<List<String>> _fetchData() {
return // fetch data from source
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _fetchData,
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
// This widget will be built when data is fetched
const List<String> list = snapshot.data;
return ListView(
children: list.map(
(element) => ListTile(
title: Text(element),
),
).asList(),
);
} else {
// This widget will be built while you are waiting for your data to be fetched
return Container(
child: Center(
child: Text("Loading data..."),
),
);
}
},
);
}
You have to stream data and ListView will update automatically.
In the button that you say you can re call your ls() functions, your list should update on tap button
sample:
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: () => ls(),
),
);
I am implementing a Logout widget that will only appear when user is logged in.
I used this DrawerLogout widget inside a Drawer with a ListView
class DrawerLogout extends StatefulWidget {
#override
_DrawerLogoutState createState() => _DrawerLogoutState();
}
class _DrawerLogoutState extends State<DrawerLogout> {
Stream authState = FirebaseAuth.instance.authStateChanges();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListTile(
title: Text(
'Logout',
style: TextStyle(color: Theme.of(context).errorColor),
),
onTap: () {
FirebaseAuth.instance.signOut();
},
);
}
return Container();
});
}
}
Something like this:
Scaffold(
drawer: Drawer(
child: ListView(
children:[
...
...
DrawerLogout(),
])
)
)
The problem is the logout button only shows for the first time when I open the drawer, after closing the drawer and reopen it, the logout button disappear.
This is the error code when it disappear:
[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: Bad state: Cannot add new events while doing an addStream
As discussed in comments, moving the state initialization to initState solved the issue.
The issue was caused by new events being added into a disposed stream. As per docs:
If a State's build method depends on an object that can itself change state, for example a ChangeNotifier or Stream, or some other object to which one can subscribe to receive notifications, then be sure to subscribe and unsubscribe properly in initState, didUpdateWidget, and dispose:
In initState, subscribe to the object.
In didUpdateWidget unsubscribe from the old object and subscribe to the new one if the updated widget configuration requires replacing the object.
In dispose, unsubscribe from the object.
As #JoyTerence suggested, initializing the stream in initState works!
class DrawerLogout extends StatefulWidget {
#override
_DrawerLogoutState createState() => _DrawerLogoutState();
}
class _DrawerLogoutState extends State<DrawerLogout> {
Stream authState;
#override
void initState() {
// TODO: implement initState
super.initState();
authState = FirebaseAuth.instance.authStateChanges();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListTile(
title: Text(
'Logout',
style: TextStyle(color: Theme.of(context).errorColor),
),
onTap: () {
FirebaseAuth.instance.signOut();
},
);
}
return Container();
});
}
}
I have made a logo screen for my app which redirects the user to the appropriate location (home screen if the user is logged in and the welcome/splash screen if he is not). After I reach the home screen I can go back to the previous screen by pressing the back button on the android phone. I want to prevent the user from going back to the logo screen.
Things I have tried:
WillPopScope() with onWillPop returning false
My logo screen code:
class LogoScreen extends StatefulWidget {
#override
_LogoScreenState createState() => _LogoScreenState();
}
class _LogoScreenState extends State<LogoScreen> {
var preferences;
#override
void initState() {
super.initState();
startTimer();
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"assets/images/g1177.png",
scale: 10.0,
),
],
),
),
),
);
}
startTimer() {
var _duration = Duration(milliseconds: 2000);
return Timer(_duration, navigate);
}
navigate() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
if (preferences.getBool("is_logged_in") == true) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HomeScreen();
}));
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return SplashScreen();
}));
}
}
}
Why this approach doesn't work on my device?
Use .pushReplacement(...) instead of .push(...)
pushReplacement Replace the current route of the navigator that most tightly encloses
the given context by pushing the given route and then disposing the
previous route once the new route has finished animating in.
I figured it out. If I apply WillPopScope() on the Logo Screen then when I am on the logo screen, if I press the back button then I can't go further back from that screen. Hence, if I use WillPopScope on the home screen then I can't use the back button on that screen. Here is my Home Screen build method:
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: Scaffold(
//add your UI here like your appBar, body, bottomNavBar etc..
) //Scaffold
); //WillPopScope
I would like to be able to use the back button to navigate back to the main page instead of closing the app, I have seen the WillPopScope widget option but that needs to show a dialog, is there a way to pop back using the android back button without a dialog?
You can Use Navigator.canPop() Method to avoid app from Exiting.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return Navigator.canPop(context);
},
child: Scaffold(
appBar: AppBar(title: const Text('HomePage')),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
backgroundColor: Colors.red,
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => SecondPage()));
},
),
),
);
}
}
The onWillPop callback needs a Future of bool returned. You just can do
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
return Future.value(true); // or return Future.value(false);
},
child: Container()
);
}
I'm new to Flutter.
I have an app with 2 sub widgets (2 fragments in Android), and when i clicked next button in WidgetA, I want to replace (or push) that widget into WidgetChildA, like push (or replace) fragments in Android. But instead of that, I got a fullscreen widget like a normal screen in Flutter.
Here is my code:
import 'package:flutter/material.dart';
class DemoFragment extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new DemoFragmentState();
}
}
class DemoFragmentState extends State<DemoFragment> {
#override
Widget build(BuildContext context) {
print(context.toString() + context.hashCode.toString());
return new Scaffold(
appBar: new AppBar(title: new Text("Demo fragment")),
body: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
new FragmentA(),
new FragmentB()
],
),
);
}
}
class FragmentA extends StatelessWidget {
#override
Widget build(BuildContext context) {
print(context.toString() + context.hashCode.toString());
return new Center(
child: new Column(
children: <Widget>[
new Text("Fragment A"),
new RaisedButton(
child: new Text("next"),
onPressed: () {
print(context.toString() + context.hashCode.toString());
Navigator.of(context).push(new PageRouteBuilder(
opaque: true,
transitionDuration: const Duration(milliseconds: 0),
pageBuilder: (BuildContext context, _, __) {
return new FragmentChildA();
}));
/*showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Hello world"),
content: new Text("this is my content"),
));*/
})
],
),
);
}
}
class FragmentB extends StatelessWidget {
#override
Widget build(BuildContext context) {
print(context.toString() + context.hashCode.toString());
return new Center(
child: new Column(
children: <Widget>[
new Text("Fragment B"),
new RaisedButton(
child: new Text("next"),
onPressed: () {
print(context.toString() + context.hashCode.toString());
Navigator.of(context).push(new PageRouteBuilder(
opaque: true,
transitionDuration: const Duration(milliseconds: 0),
pageBuilder: (BuildContext context, _, __) {
return new FragmentChildB();
}));
})
],
));
}
}
class FragmentChildA extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[new Text("Fragment Child A")],
)));
}
}
class FragmentChildB extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[new Text("Fragment Child B")],
)));
}
}
Screenshots:
Home page
After clicked
I'm not sure if you can use the router to replace just the part of a view; but you could conditionally change which Widget you render in the build method, like this:
children: <Widget>[
someCondition ? new FragmentA() : new FragmentChildA(),
new FragmentB()
],
Then you just need to set someCondition by using setState in the stateful widget:
setState(() => someCondition = true);
If you want to do this from inside FragmentA you could allow it to have the function passed into its constructor:
new FragmentA(
onPress: setState(() => someCondition = true)
)
However, it might be better to encapsulate all of this logic inside a single widget so this logic isn't all hanging around in the parent. You could make a single StatefulWidget for FragementA which keeps track of which stage you're on, and then in its build method renders the correct child widget, something like:
build() {
switch(stage) {
Stages.Stage1:
return new Stage1(
onNext: () => setState(() => stage = Stages.Stage2);
);
Stages.Stage2:
return new Stage1(
onPrevious: () => setState(() => stage = Stages.Stage1);
);
}
}
You could simply use a MaterialApp widget with the CupertinoPageTransitionsBuilder as pageTransitionTheme like
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: SlideRightPageTransitionsBuilder(),
}),
initialRoute: "fragment1",
routes: <String, WidgetBuilder>{
"fragment1": (BuildContext context) => Fragment1(),
"fragment2": (BuildContext context) => Fragment2(),
}
...
),
Then in fragment 1 you simply use the following to navigate to the other fragment with a slide animation
Navigator.of(context).pushReplacementNamed("fragment2");
Well, I found out the way to handle this case for a few months, but I just forgot to answer this question.
The solution is wrapping your Widget with a new Navigator.
You can see the video example here
And the simple demo for it here
The downside of this solution is sometimes, the keyboard is not showing as my intention.
ok I'm going to be doing this the same way google does it with the bottom navigation bar, I don't see this as the most performant but it works
class MainFabContainer extends StatefulWidget {
MainFabContainer({
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return MainFabContainerState();
}
}
class MainFabContainerState extends State<MainFabContainer> {
String title = "Title";
int _currentIndex = 0;
final List<int> _backstack = [0];
#override
Widget build(BuildContext context) {
//each fragment is just a widget which we pass the navigate function
List<Widget> _fragments =[Fragment1(navigate: navigateTo),Fragment2(navigate: navigateTo,),Fragment3(navigate: navigateTo,)];
//will pop scope catches the back button presses
return WillPopScope(
onWillPop: () {
customPop(context);
},
child: Scaffold(
drawer: drawer(),
appBar: AppBar(
title: Text(title),
),
body: Column(
children: <Widget>[
Expanded(
child: _fragments[_currentIndex],
),
],
),
),
);
}
void navigateTo(int index) {
_backstack.add(index);
setState(() {
_currentIndex = index;
});
}
void navigateBack(int index) {
setState(() {
_currentIndex = index;
});
}
customPop(BuildContext context) {
if (_backstack.length - 1 > 0) {
navigateBack(_backstack[_backstack.length - 1]);
} else {
_backstack.removeAt(_backstack.length - 1);
Navigator.pop(context);
}
}
//this method could be called by the navigate and navigate back methods
_setTitle(String appBarTitle) {
setState(() {
title = appBarTitle;
});
}
}