Flutter: Use Navigator with TabBar and TabBarView - flutter

I'm trying to understand the class Navigator.
A Navigator Route seem to always return a new widget but what if I want to manage the TabBar and TabBarView so that each Tab when tapped or swipe on, will be pushed to the Navigator stack, I don't find a what to do that.
On a more general case, can I react to a route change without creating a new widget but instead taking another action like scrolling to a specific item in a listView?
I've tried recreating the entire app structure every time but doing this way I don't have the nice default animation and, also, doesn't seem a good approach to me.

You can use WillPopScope widget and its onWillPop to catch the back button pressure and handle it yourself. Find more info here https://docs.flutter.io/flutter/widgets/WillPopScope-class.html
On a more general case, can I react to a route change without creating
a new widget but instead taking another action like scrolling to a
specific item in a listView?
This looks more specific rather than general. However, you do need to set a ScrollController in your ListView and let it scroll the list for you to the desired point. A simple example function returning to top:
class MyFancyClass extends StatelessWidget{
...
ScrollController _scrollController;
...
#override
Widget build(BuildContext Context){
return ....
new ListView(
...
controller: _scrollController,
...
}
void _toTop() {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.ease,
);
}
Check https://docs.flutter.io/flutter/widgets/ScrollController-class.html for more details and behaviors. In case you want the back button to bring you to the top:
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onTop,
child:
...
),
);
}
Concerning the tab behavior I suggest you to read https://docs.flutter.io/flutter/material/TabController-class.html to better understand how to implement what you have in your mind.

Related

Scroll position lost if tab changed while scrolling animation is still ongoing

I have 3 views which are accessible via the bottom navigation tab. Each view has its own ListView, which looks like this:
// primary = bottomTabNavigation.index //
ListView(
controller: primary ? null : scrollController,
key: const PageStorageKey<String>('view1'),
primary: primary,
physics: primary
? AlwaysScrollableScrollPhysics()
: NeverScrollableScrollPhysics(),
children: const [
Text("A"),
SizedBox(height: 1000),
Text("B"),
],
),
If I start a big swipe on view1, and switch to view2 via bottom tab navigator, the scroll position when I come back to view1 is still at the top. Somehow, the scroll position only saves upon the scrolling animation completing.
Is there some way to switch tabs and store the last position (without waiting for animation)?
Create a Key outside the build method
final _key = GlobalKey();
Step 1: make your widgets staefulWidget.
Step 2: now use AutomaticKeepAliveClientMixin using with keyword.
class _DealListState extends
State<DealList> with
AutomaticKeepAliveClientMixin<DealList>
{
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
// your current widget build
}}
It will keep your listview and other states when you moves from one page to another.
Note: if it's impossible to change every page to stateful widget then just make a new StatefulWidget that use AutomaticKeepAliveClientMixin and will take a child widget from outside and now you can use this widget to wrap your already present widget and can be used through the app.

Custom Event listeners in flutter

I have a widget with a list and a button with a tree dot icon in every row that shows and hides a panel in its own row. I only want one panel open in the list. When I click on a row button, I'd like to close the panels of the other rows list.  All the buttons in the list are siblings. I'd like to send an event to the other rows' code to close the panels. Which is the correct manner of flutter?  
I have tried NotificationListener but it does not work because the components to be notified are not their parents.
The question is if the correct thing to do is to use the event_listener library or to use streams. I'm new to flutter/dart and streams seem too complex to me. It's a very simple use case and in this entry
Flutter: Stream<Null> is allowed?
they say
*
Some peoples use streams as a flux of events instead of a value
changing over time, but the class isn't designed with this in mind.
They typically try to represent the following method as a stream:
So with simple events with 0 or 1 argument. event_listener or Streams?
This is the screen I'm working on. I want that when one yellow button panel opens the other one closes.
Your question is broad and it seems to be a design question, i.e. it doesn't have a right answer.
However, I don't think you should use Streams or EventListeners at all in this case, because you should not make components in the same layer communicate with each other. Components should only communicate with their parents and children, otherwise your code will increase in complexity really fast. That's even documented in flutter_bloc.
Other than that, if you don't lift state up, i.e. move the responsibility of triggering the removal of the other rows to a parent Widget, than you're fighting against Flutter instead of letting it help you.
It's easy to create a parent Widget, just wrap one Widget around it. What you want to do is hard, so why would try to communicate with sibling widgets instead of using what's Flutter designed to do?
This is a suggestion:
class _NewsSectionState extends State<NewsSection> {
Widget build(BuildContext context) {
return ListView.builder(
itemCount: newsInSection.length;
itemBuilder: (_, int index) => NewsTile(
title: Text('${newsInSection[index].title}')
onDismiss: () => onDismiss(index),
// I don't know how you set this up,
// but () => onDismiss(Index)
// should animate the dismiss of the Row with said index
),
);
}
}
class NewsRow extends StatefulWidget {
final void Function() onDismiss;
#override
State<NewsRow> _createState => _NewsRowState();
}
class _NewsRowState extends State<NewsRow> {
Widget build(BuildContext context) {
return Row(
children: [
// title
// home button
// fav button
// remove button
IconButton(
Icons.close,
onPressed: widget.onDismiss,
),
],
);
}
}

How to dynamically disable vertical swipe in PageView

I would like to find a way to update the physics param to disable the swipe action in a parent PageView from a child widget.
I am using riverpod for updating the state in a child widget when it builds to know when I should pass NeverScrollableScrollPhysics to the physics param in a parent widget. But the thing is that this approach is causing my child widget to rebuild recursively, because this is making the PageView rebuild to update the physics param. So I really don't know what to do here.
I have this parent Widget that builds the PageView:
Widget build(BuildContext context) {
final _navBar = useProvider(bottomNavigationBarProvider);
return PageView(
physics: navBar.isSwipeBlocked ? const NeverScrollableScrollPhysics() : null,
controller: pageController,
onPageChanged: onPageChanged,
children: [
Beamer(
key: const Key('feed-tab'),
routerDelegate: BeamerDelegate(locationBuilder: (state) => FeedLocation(state)),
),
]
)
}
And the child Widget that updates the state variable:
Widget build(BuildContext context) {
final _navBar = useProvider(bottomNavigationBarProvider.notifier);
useEffect(() {
Future.microtask(() => _navBar.blockSwipe());
}, []);
return Container(...);
}
So when FeedLocation loads, it updates _navBar in an attempt for disabling the scroll behavior. But as I mentioned, this causes the parent to rebuild and FeedLocation to build again and then the recursive state..
The idea was to be able to go to FeedLocation, disable the scroll, then when go back, enable it again, but I don't see a solution for that.
I think I did already what this guy suggested https://github.com/flutter/flutter/issues/37510#issuecomment-738051469 using Riverpod
And I guess I am a similar situation as this guys from the same thread https://github.com/flutter/flutter/issues/37510#issuecomment-864416592
Is anybody able to see a solution or what I am doing wrong?
You should replace null as the secondary ScrollPhysics with PageScrollPhysics() and make sure to add setState(() {}); when you update the isSwipeBlocked variable.

Flutter - What's the best practice to create a fixed AppBar during navigation

In native android development, it is common to use FragmentTransaction to create a navigation animation, where the actionBar's position stays fixed (but actionBar's content changed), and the fragment beneath actionBar performs a transition animation (like slide in or out).
To put it simple, the AppBar and the body performs different transition animation. In flutter, what is the best practice to create such animation?
Currently I can think of a solution of using a navigation in Scaffold.body and using Stream + StreamBuilder to start AppBar redraw. Something like the following code.
Scaffold(
appBar: StreamBuilder<Int>(
stream: Bloc.of(context).currentFragmentId
builder: //Some logic to decide which AppBar is appropriate
),
body: Navigator(
//Navigation between fragments
),
)
But this is really weird. So is there a best practice to do this? Please let me know!
Well, since there is currently no answer availble. I'm going to share my solution here, though it is not so elegant.
The solution is to wrap AppBar in a Hero widget. Since Scaffold only takes a PreferedSize widget as an appbar, some customization is required.
class HeroAppBar extends StatelessWidget implements PreferredSizeWidget {
//...
#override
final Size preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0));
//...
#override
Widget build(BuildContext context) {
return Hero(
tag: tag,
child: AppBar(
//...
),
);
}
}
And make sure your Navigator implemented the HeroController. (Default Navigator has already implemented it)

Flutter TextEditingController does not scroll above keyboard

In android version, Flutter TextEditingController does not scroll above keyboard like default text fields do when you start typing in field. I tried to look in sample apps provided in flutter example directory, but even there are no example of TextEditController with such behaviour.
Is there any way to implement this.
Thanks in advance.
so simple
if your textfields is between 5-10 fields
SingleChildScrollView(
reverse: true, // add this line in scroll view
child: ...
)
(August 20, 2021 Flutter 2.2.3)
I think my answer might be the cleanest solution for this problem:
#override
Widget build(BuildContext context) {
/// Get the [BuildContext] of the currently-focused
/// input field anywhere in the entire widget tree.
final focusedCtx = FocusManager.instance.primaryFocus!.context;
/// If u call [ensureVisible] while the keyboard is moving up
/// (the keyboard's display animation does not yet finish), this
/// will not work. U have to wait for the keyboard to be fully visible
Future.delayed(const Duration(milliseconds: 400))
.then((_) => Scrollable.ensureVisible(
focusedCtx!,
duration: const Duration(milliseconds: 200),
curve: Curves.easeIn,
));
/// [return] a [Column] of [TextField]s here...
}
Every time the keyboard kicks in or disappears, the Flutter framework will automatically call the build() method for u. U can try to place a breakpoint in the IDE to figure out this behavior yourself.
Future.delayed() will first immediately return a pending Future that will complete successfully after 400 milliseconds. Whenever the Dart runtime see a Future, it will enter a non-blocking I/O time (= inactive time = async time = pending time, meaning that the CPU is idle waiting for something to complete). While Dart runtime is waiting for this Future to complete, it will proceed to the lines of code below to build a column of text fields. And when the Future is complete, it will immediately jump back to the line of code of this Future and execute .then() method.
More about asynchronous programming from this simple visualization about non-blocking I/O and from the Flutter team.
Flutter does not have such thing by default.
Add your TextField in a ListView.
create ScrollController and assign it to the ListView's controller.
When you select the TextField, scroll the ListView using:
controller.jumpTo(value);
or if you wish to to have scrolling animation:
controller.animateTo(offset, duration: null, curve: null);
EDIT: Of course the duration and curve won't be null. I just copied and pasted it here.
Thank you all for the helpful answers #user2785693 pointed in the right direction.
I found complete working solution here:
here
Issue with just using scroll or focusNode.listner is, it was working only if I focus on textbox for the first time, but if I minimize the keyboard and again click on same text box which already had focus, the listner callback was not firing, so the auto scroll code was not running. Solution was to add "WidgetsBindingObserver" to state class and override "didChangeMetrics" function.
Hope this helps others to make Flutter forms more user friendly.
This is an attempt to provide a complete answer which combines information on how to detect the focus from this StackOverflow post with information on how to scroll from Arnold Parge.
I have only been using Flutter for a couple days so this might not be the best example of how to create a page or the input widget.
The link to the gist provided in the other post also looks like a more robust solution but I haven't tried it yet. The code below definitely works in my small test project.
import 'package:flutter/material.dart';
class MyPage extends StatefulWidget {
#override createState() => new MyPageState();
}
class MyPageState extends State<MyPage> {
ScrollController _scroll;
FocusNode _focus = new FocusNode();
#override void initState() {
super.initState();
_scroll = new ScrollController();
_focus.addListener(() {
_scroll.jumpTo(-1.0);
});
}
#override Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Some Page Title'),
),
body: new DropdownButtonHideUnderline(
child: new SafeArea(
top: false,
bottom: false,
child: new ListView(
controller: _scroll,
padding: const EdgeInsets.all(16.0),
children: <Widget>[
// ... several other input widgets which force the TextField lower down the page ...
new TextField(
decoration: const InputDecoration(labelText: 'The label'),
focusNode: _focus,
),
],
),
),
),
);
}
}