I'm working on an app that gets the user to take a list of measurements. I use ListView to display a list of measurements. When the the user clicks on a list item it takes them to a new page, they enter the measurement, hit save and then in the save method I do Navigator.pop(context) back to the list. It all works but there is a usability problem.
If you tap a list tile and then use the app bar to go back it returns to the same scroll position in the ListView. If you enter some data then hit Save it returns to the top of the list. Even though I'm using Navigator.pop(context) in the save method. You can imagine returning to the top each time is pretty painful when the list of requirement measurements is quite long.
I guess is maybe its something to do with the fact that in the Save method I also update the model with which the list is built on so its kind of no longer the same list??
EDIT
I'm still not getting there and now I have an issue where the itemScrollController is not attached when I want to call it. Some code will hopefully help:
class ListContents extends StatefulWidget {
ListContents({
Key key,
}) : super(key: key);
#override
_ListContentsState createState() => _ListContentsState();
}
class _ListContentsState extends State<ListContents> {
ItemScrollController itemScrollController;
#override
void initState() {
super.initState();
itemScrollController = ItemScrollController();
}
I set up the scroll controller in init state. I set up a button to test this. I can run this when the page loads:
void jump(jumpIndex) {
itemScrollController.jumpTo(index: jumpIndex);
}
When I click that button it will jump to what ever index is passed.
I need to do this jumpTo when popping back from the previous page. I have this code in the list tiles. This loads an input page. - I was hoping to run the jumpTo method after it's popped.
onTapped: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider<MeasureInstance>.value(
value: _instance,
child: InputOne(
index: index,
),
),
),
);
await _database.setInstance(_instance);
print(itemScrollController.isAttached);
itemScrollController.jumpTo(index: index);
},
When I pop back to the list page I see 'false' printed in the console and then an error that _jumpTo was called on null
Related
Is there a way to navigate from one dart "page" to a specific point in another? This will get me to a given page
Navigator.push(
context,
MaterialPageRoute(builder: (context) => WK3()),
);
But I want to navigate to a specific child or row within that page (which are unfortunately fairly long, and would otherwise require a lot of scrolling).
I am used to working with html, where you just have to indicate a position within a page using a hash tag:
#here
That should be possible to do in Flutter/Dart, right?
This is not possible by just using the flutter Navigator. What I would do to tackle that issue is that I would pass an argument which contains the scroll position to the Navigator for example:
Navigator.pushNamed(
context,
'/wk3',
arguments: {'scrollTo': elementId}, // or any other logic like half of the screen or so
);
To read more about Navigator and arguments you can check out the official documentation here. You can also do that for none named routes obviously.
Inside your target widget you could then do the following approach.
Take the argument and parse it to whatever you need.
Depending on your page and your scroll behavior you could use the initState to directly scroll to your desired location. What happens next is a bit dependend on your concrete implementation or where you want to scroll. In certain situations it might be more useful to add a postFrameCallBack for your scrolling instead of doing it in the initState. I'll add it for educational reasons in the snippet below.
Assuming we have a ScrollController of a ListView for example the widget we navigated to knows where we want it to scroll to due to our passed argument. If you use for instance a position value here and we have the ScrollController to do something like this:
controller.position.animateTo(
widget.args.scrollTo, //make sure it has the correct type
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
);
There are also ways you could scroll to a certain element in a list or a column (like for example the 100th element). Check this question for more information. You can find a slight implentation with a scroll controller below:
class ScreenArguments {
final String scrollTo;
ScreenArguments(this.scrollTo);
}
class Screen extends StatefulWidget {
final ScreenArguments args;
Screen(this.args, {Key key}) : super(key: key);
#override
ScreenState createState() => ScreenState();
}
class ScreenState extends State<Screen> {
#override
void initState() {
scrollMeTo = widget.args.scrollTo;
scrollController = ScrollController();
WidgetsBinding.instance
.addPostFrameCallback((_) => scrollTo(context)); // this is probably safer than doing scrollTo(context) directly in your initState
enter code here
// if you do not use addPostFrameCallback you can call scrollTo(context) directly.
//scrollTo could use scrollControler.animateTo() etc.
}
I dont have ScrollController / ListView implementation
If thats not the case and you do not have a ScrollController and you want just to scroll to any element on your widget things get a little bit more complicated. In that case I'd recommened you to use flutters Scrollable.ensureVisible. Taken from the documentation it does the following:
Scrolls the scrollables that enclose the given context so as to make
the given context visible.
Lets assume you have Column inside a SingleChildScrollView to have a foundation for your scrolling behavior. You would then define a GlobalKey for each section of your widget you would like to scroll to. This key would be the identifier which we pass in as an argument. Assuming we have a GlobalKey in the widget which is called second we could do the following:
Scrollable.ensureVisible(
GlobalObjectKey(widget.args.scrollTo).currentContext, //this would reference second
alignment: 0.5, //
duration: Duration(seconds: 2),
curve: Curves.easeInOut);
You can read more about Scrollable.ensureVisible here.
What approach to take is dependended on your needs and on your implementation.
In my app, I have a homepage that has 5 tabs on the bottom. On each tabbed page, there is an app bar that has a '+' symbol as an action, which navigates you to a different page. The navigation with that '+' button to the new page is done with the following code, alongside the Flutter Platform Widgets package:
Navigator.of(context, rootNavigator: true)
.push(
platformPageRoute(
context: context,
builder: (context) => Page1(),
),
);
I use the platformPageRoute feature as an easy way to navigate with a native feel. Now, that works fine to navigate to a new page, but the issue comes when I use
Navigator.pop(context);
to navigate back to the original page. When I use that to navigate back to that original page, it pays no attention to the tab that was selected originally. For example, if I were originally on the second tab on the homepage and then use the '+' button on that tab and then finally use
Navigator.pop(context);
on that new page, it returns the first tab of the homepage. Is there any way of ensuring when I use the above command, it goes to the right tab? I have tried something along the lines of:
Navigator.popUntil(context, '/homepageTab2');
alongside a named route, to return to the correct tab on the homepage, although that returns a black screen. Why might that be? I have also tried using:
Navigator.pushAndRemoveUntil(
context,
platformPageRoute(
context: context,
builder: (context) =>
HomePage(selectedPage: 1),
),
(route) => false,
);
This does not work either, since it returns the selected/correct page tab content, but with the first tab selected. In addition, the other
'problem' for me is that the animation is a 'push' one and that doesn't 'match' with the animation when I have more often used
Navigator.pop(context);
to navigate back to a screen. Is there a way to maybe use pushAndRemoveUntil but then change the animation to match a pop animation?
Thanks!
EDIT:
I have just noticed that with the situation I have described above, it is actually returning the correct screen content when I use Navigator.pop(context); but the tab in the tab bar at the bottom is showing as the first tab, in the second tab's position, essentially duplicating the first tab, until I navigate to a new tab and back, at which time it shows the correct tab in the correct position. I hope that makes sense!
As it turns out, the issue wasn't related to Navigator.pop(context); being used. It was the way I was controlling the selected tab. I'm posting this as an answer incase it helps someone else.
Initially, I created late values for a tab controller and the current selected page, like so:
late TabController _tabController;
late ScrollController _scrollController;
late int _selectedPage;
Then, I created a list of widgets that represented the actual page to display for each selected tab:
List<Widget> _pageWidgets = <Widget>[
Page1();
Page2();
Page3();
Page4();
Page5();
];
Then (and I think this was the bit that wasn't working) I used initState() as follows:
void initState() {
super.initState();
// Initialising a value that allows the 'final' page selector to be changed
_selectedPage = widget.selectedPage;
// Initialising the tab controller
_tabController = TabController(
length: 5,
vsync: this,
initialIndex: _selectedPage,
);
// updating the tab index when a new item is selected
_tabController.addListener(() {
setState(() {
_selectedPage = _tabController.index;
//_tabIndex = _tabController.index;
});
});
// Creating the scroll controller
_scrollViewController = ScrollController();
// Scrolling view to top when a new tab is selected
_tabController.addListener(() {
setState(() {
_scrollViewController
.jumpTo(_scrollViewController.position.minScrollExtent);
});
});
}
I then controlled the page content like this:
body: _pageWidgets.elementAt(_selectedPage),
I'm not 100% sure why this wasn't working, although I believe it would have something to do with the fact that initState() would only be called during the build and therefore placing the functionality inside there would mean changes wouldn't be detected. Either way, my new method, which works perfectly, is:
/// Controls the screen to display first
int _index = 0;
/// Creating a navigation key to control tab bar navigation
final _navigationKey = GlobalKey<CurvedNavigationBarState>();
Then, within the Scaffold() I show the page content like this:
body: _pageWidgets.elementAt(_index),
And finally, within the navigation bar (which is the CurvedNavigationBar() package from pub.dev) I give it a key and the index:
key: _navigationKey,
index: _index,
And this controls it perfectly, showing the correct tab.
Sub-pages of a TabBarView cannot be navigated using Navigator.
You can use TabController to go to your desired tab page after awaiting Navigator.push():
await Navigator.of(context, rootNavigator: true)
.push(
platformPageRoute(
context: context,
builder: (context) => Page1(),
),
);
tabController.animateTo(<index of tab>);
I have 2 screens,
Screen one contains a list view with onPressed action on every item
screen two contains the detail of the pressed item as well as a drawer with the same list view as screen one.
What I want to do here is when the user goes to the detail screen and click on an item from the drawer the detail screen should pop and push back with new params.
Code so far,
Route
GetPage(
name: '/market-detail',
page: () => MarketDetail(),
binding: MarketDetailBinding(),
),
Binding
class MarketDetailBinding extends Bindings {
#override
void dependencies() {
Get.lazyPut(() => MarketDetailController());
}
}
Click action in screen one
onTap: () {
Get.toNamed('market-detail',
arguments: {'market': market});
},
Detail Screen Class
class MarketDetail extends GetView<MarketDetailController> {
final Market market = Get.arguments['market'];
}
Click action in detail screen sidebar
onTap: () {
Get.back();
Get.back();
Get.toNamed('market-detail',
arguments: {'market': market});
},
First Get.back() is to close the drawer, then remove the route and push the same route back again,
Expected behaviour,
MarketDetailController should be deleted from memory and placed again,
What actually happening
The controller only got delete and not getting back in memoery on drawer click action until I hot restart the app(By clicking save).
If anybody understands it, please help I am stuck here.
As I can see, you're trying to pop and push the same route with a different parameter in order to update a certain element on that route. Well, if that's the case then just let me show you a much better way.
In your MarketDetailController class you should add those:
class MarketDetailsController extends GetxController {
// A reactive variable that stores the
// instance of the market you're currently
// showing the details of.....
Rx<Market> currentMarket;
// this method will be called once a new instance
// of this controller gets created
// we will use it to initialize the controller
// with the required values
#override
void onInit() {
// some code here....
// .......
// intializing the variable with the default value
currentMarket = Market().obs;
super.onInit();
}
void updateCurrentMarket(Market market) {
// some code here if you need....
// updating the reative variable value
// this will get detected then by the Obx widgets
// and they will rebuild whatever depends on this variable
currentMarket.value = market;
}
}
Now inside your page UI, you can wrap the widget that will display the market details with the Obx widget like this:
Obx(
() {
final Market currentMarket = controller.currentMarket.value;
// now you have the market details you need
// use it and return your widget
return MyAwesomeMarketDetailsWidget();
},
)
Now for your click action, it can just be like this:
onTap: () => controller.updateCurrentMarket(myNewMarketValue)
This should be it. Also, I advise you to change GetView to GetWidget and Get.lazyPut() to Get.put()
I have a Stateful widget that I am displaying inside a stateless widget. I have reused this Widget in several parts of my application, where it worked.
The widget looks a somethin like this
class InnerWidget extends StatefulWidget {
// private keys for objects in the remote dataahase
List<int> privateKeys;
const InnerWidgt({
Key key,
this.privateKeys,
}) : super(key: key);
#override
_InnerWidgetState createState() => _InnerWidgetState();
}
class _InnerWidgetState extends State<InnerWidget> {
// data retrieved from the database
Future<List<SomeClass>> data;
#override
void initState() {
this.data = this.fetchData(widget.privateKeys);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<SomeClass>> (
future: this.data,
builder: (BuildContext contest, AsyncSnapshot<List<SomeClass>> snapshot) {
if(snapshot.hasData){
// return a widget (a list of names in this case)
}
return Text('Loading...');
}
);
}
Future<List<SomeClass>> fetchData(List<int> pKeys){
// fetches the JSON objects from the server and returns them as instances
}
}
The widget is created and given a list of keys, it makes a call to the remote API to get some objects and displays them in a list. I thought it might be nice that this widget is kind of handling itself, but maybe it's bad design?
What happens, is that the fetchData() method is never called and the "Loading..." is displayed forever. However snapshot.hasData does return true, so I'm thinking this is some kind of repainting issue.
The view I am talking about, where this is used is a detail view, which you navigate to from a list (ListView). I am using this exact same InnerWidget in the the elements inside the ListView, where it works perfectly fine. But when I click on the item to navigate to the detail page, the same InnerWidget does not work. (I know fetching the same data twice is not good, I have that in the back of my head to change it, and there's already a Cache underneath). Maybe this has something to do with the preserved state in the ElementTree? That because I am using the same InnerWidgit, it's preserving its state and not rerendering?
I hope I phrased my question well enough, I did not want to throw hundreds of lines of code at you, that's why created this minimal example of the widget at least. I can add the list and navigation part too if needed, but maybe this is something, that somebody that has worked more with Flutter, knows right away, I'm only 3 weeks in.
I am new to flutter. I have two screens. My first screen is Stateful, it consists of a list view, which is developed using FutureBuilder, when I select an item, I am pushing the app to a new screen which is also of Stateful type.
When I am moving to the new screen, the functions from previous screen are still calling and I do not know why it is happening.
This is what I have done:
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuizDetail(
quizID: featuredContests.id,
),
),
);
},
child: Card(...))// InkWell Closed
Next Screen
class QuizDetail extends StatefulWidget {
final int quizID;
// In the constructor, require a QuizID.
QuizDetail({Key key, #required this.quizID}) : super(key: key);
#override
_QuizDetailState createState() => _QuizDetailState();
}
class _QuizDetailState extends State<QuizDetail> {
#override
void initState() {
super.initState();
getQuizDetail(quizID: widget.quizID);
}
It calls this function, but then also it calls the function from the previous screen which is used to fetch data from 'A' Network and the initState consists of a function which is used to fetch data from API 'B', but the data is not received from 'B', but comes from 'A' and entire process is dismissed. Can anyone help?
If you are not going to go back to the first screen after pushing the new screen, try using Navigator.pushReplacement() instead of Navigator.push().
Navigator.push() only pushes the new screen above the current one. It does not discard the screen underneath and hence if you're rebuilding the widget tree at some point in your code, all the screens in the call stack will get rebuilt.
Navigator.pushReplacement() pops the current topmost screen and pushes the new one in its place.
Also, if you plan on going back to the first screen, you can mark the onTap method as async and use await to force your program to wait till the QuizDetail screen is popped using Navigator.pop().
More info about navigation in flutter can be found here.
Example -
onTap: () async{
await Navigator.push(
//Rest of the code is same.
);
}