I am using a NestedScrollView with a SliverAppBar and a TabBar. The scrolling works well when scrolling down scrollable content like ListView or SingleChildScrollView, but whenever I try to scroll up and get the SliverAppBar to overgrow, nothing happens.
In the SliverPersistentHeaderDelegate of the SliverAppBar I am overwriting the following method like so:
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration();
This is the class of the screen I am talking about:
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({
super.key,
required this.userId
});
final String userId;
#override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> with
TickerProviderStateMixin {
late final TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(length: 3, vsync: this);
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final AsyncValue<List<BasicEventModel>> eventsAsync =
ref.watch(profileEventsProvider(widget.userId));
return AnnotatedRegion<SystemUiOverlayStyle>(
value: Themes.systemUiOverlayStyle(context),
child: Scaffold(
backgroundColor: Themes.color(context, light: Palette.white, dark:
Palette.gray900),
body: ExtendedNestedScrollView(
pinnedHeaderSliverHeightBuilder: () => topPadding + kToolbarHeight,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
ProfileAppBar()
],
body: Column(
children: [
ProfileTabBar(controller: tabController),
Expanded(
child: TabBarView(
controller: tabController,
children: [
eventsAsync.when(
data: (List<BasicEventModel> events) =>
ProfileEventsList(events: events),
error: (Object e, StackTrace s) =>
_eventsText('profile_events_error'.tr()),
loading: () => const ProfileEventsShimmer()
),
// other Widgets for the other tabs
]
)
)
]
)
)
)
);
}
}
Related
I have a widget which is built recursively and i need to find its height for another widget in the same row containing the recursively called widget.
I tried getting the height of the parent using LayoutBuilder but it doesnt work because of the expanded widget.
I have tried to use this method to find the height using key before the widget is build but it doesnt work for the recursive case.
CODE
class MyWidget extends StatefulWidget {
const MyWidget({
Key key,
}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
TextEditingController _textEditingController;
GlobalKey _keyRed = GlobalKey();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_textEditingController = TextEditingController();
super.initState();
}
_afterLayout(_) {
_getSizes();
}
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
_getSizes() {
final RenderBox renderBoxRed = _keyRed.currentContext.findRenderObject();
final sizeRed = renderBoxRed.size;
print("SIZE of Red: $sizeRed");
return sizeRed.height;
}
#override
Widget build(BuildContext context) {
// condition for recursion to stop avoided here for brevity
return Container(
child: Row(
children: [
getLeadingWidget(),
Expanded(
key: _keyRed,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
getTextField(note),
Column(
children:
getChildren()
)
],
),
),
],
),
);
}
getLeadingWidget() {
return LayoutBuilder(
builder: (context, constraints) => Container(
height: constraints.constrainHeight(),
child: Container(...)
));
}
getTextField(NotesModel note) {
return TextFormField(
keyboardType: TextInputType.multiline,
maxLines: null,
controller: _textEditingController..text = 'TEST',
);
}
getChildrenNotes(int noteId) {
List<Widget> childrenNotes = [];
for (...) {
// widget called recursively
childrenNotes.add(MyWidget());
}
return childrenNotes;
}
}
Diagram with relevant widgets:
In my flutter app, I use a simple tab-bar. I used the code from the flutter website and updated to make sure that I can keep the state of each tab using AutomaticKeepAliveClientMixin.
I have 3 tabs and each tab is fetching a list of data (why I need to use AutomaticKeepAliveClientMixin) from my backend API.
The problem is that when I switch between first and 3rd tabs (Page1 and Page3), the middle tab keeps rebuilding over and over again until I switch to that tab (Page2) and only at that point it doesn't get rebuilt anymore.
Every rebuild results in fetching data from API and that's not desirable.
Below, i have included a simplified code to reproduce this issue.
You can see in the debug console once switching between 1st and 3rd tab (without switching to 2nd tab) that it keeps printing "p2" (in my real app, it keeps fetching data for the 2nd tab).
Is there a way to switch between tabs without other tabs in between being built/rebuilt?
This is my code.
import 'package:flutter/material.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Page1(),
Page2(),
Page3(),
],
),
),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1>
with AutomaticKeepAliveClientMixin<Page1> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p1');
return Container(
child: Center(
child: Icon(Icons.directions_car),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2>
with AutomaticKeepAliveClientMixin<Page2> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p2');
return Container(
child: Center(
child: Icon(Icons.directions_transit),
),
);
}
}
class Page3 extends StatefulWidget {
#override
_Page3State createState() => _Page3State();
}
class _Page3State extends State<Page3>
with AutomaticKeepAliveClientMixin<Page3> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p3');
return Container(
child: Center(
child: Icon(Icons.directions_bike),
),
);
}
}
I believe this isn't a bug with flutter, but ultimately comes down to your implementation.
Please take a look at the code I wrote for you.
import 'package:flutter/material.dart';
import 'dart:async';
class FakeApi {
Future<List<int>> call() async {
print('calling api');
await Future.delayed(const Duration(seconds: 3));
return <int>[for (var i = 0; i < 100; ++i) i];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
static const _icons = [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
];
#override
Widget build(BuildContext context) => DefaultTabController(
length: _icons.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [for (final icon in _icons) Tab(icon: icon)],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Center(child: _icons[0]),
StaggeredWidget(_icons[1]),
Center(child: _icons[2]),
],
),
),
);
}
class StaggeredWidget extends StatefulWidget {
const StaggeredWidget(this.icon)
: super(key: const ValueKey('StaggeredWidget'));
final Icon icon;
#override
_StaggeredWidgetState createState() => _StaggeredWidgetState();
}
class _StaggeredWidgetState extends State<StaggeredWidget> {
Widget _child;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => _child = MyApiWidget(widget.icon));
}
});
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) => _child ?? widget.icon;
}
class MyApiWidget extends StatefulWidget {
const MyApiWidget(this.icon, [Key key]) : super(key: key);
final Icon icon;
#override
_MyApiWidgetState createState() => _MyApiWidgetState();
}
class _MyApiWidgetState extends State<MyApiWidget>
with AutomaticKeepAliveClientMixin {
final _api = FakeApi();
#override
Widget build(BuildContext context) {
print('building `MyApiWidget`');
super.build(context);
return FutureBuilder<List<int>>(
future: _api(),
builder: (context, snapshot) => !snapshot.hasData
? const Center(child: CircularProgressIndicator())
: snapshot.hasError
? const Center(child: Icon(Icons.error))
: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
),
),
);
}
#override
bool get wantKeepAlive => true;
}
i am trying to create material native tabs so when we scroll through the page, appbar collapses but the tabbar should be visible always and I implemented this using NestedScrollView in flutter
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool isBoxScrolled) {
return [
SliverAppBar(
title: Text("Scroller title"),
forceElevated: isBoxScrolled,
pinned: true,
floating: true,
bottom: TabBar(
tabs: [Tab(text: "tab1"), Tab(text: "tab2")],
controller: _tabController))
];
},
body: TabBarView(
children: [Page1(), Page1()],
controller: _tabController,
));
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
ScrollController _controller;
void _scrollListener(){
if(_controller.offset >= _controller.position.maxScrollExtent && !_controller.position.outOfRange){
print("reached the bottom");
}
}
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _controller,
children: <Widget>[Text("data"), SizedBox(height: 2000.0)],
);
}
}
but when I tried to use scrollController inside one of my tabBarview widget it disconnects contact with appbar and scroll individually.
the solution is the PrimaryScrollController
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool isBoxScrolled) {
return [
SliverAppBar(
title: Text("Scroller title"),
forceElevated: isBoxScrolled,
pinned: true,
floating: true,
bottom: TabBar(
tabs: [Tab(text: "tab1"), Tab(text: "tab2")],
controller: _tabController))
];
},
body: Builder(
builder: (BuildContext context) {
final innerScrollController = PrimaryScrollController.of(context);
// Use the innerScrollController to listen to the scrolling.
// This would be your controller for list. You can listen to this controller to know whether the list has reached maxScrollExtent and fetch data from API.
return TabBarView(
children: [
Page1(innerScrollController),
Page1(innerScrollController)
],
controller: _tabController,
);
},
),
);
}
}
class Page1 extends StatefulWidget {
final ScrollController _PrimaryScrollController;
Page1(this._PrimaryScrollController);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
void _scrollListener(){
if(this.widget._PrimaryScrollController.offset >= this.widget._PrimaryScrollController.position.maxScrollExtent && !this.widget._PrimaryScrollController.position.outOfRange){
print("reached the bottom");
}
}
#override
void initState() {
super.initState();
this.widget._PrimaryScrollController.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[Text("data"), SizedBox(height: 2000.0)],
);
}
}
I have 3 tabs and each tab has a PageView inside.
At the end of the PageView, I want to be able to scroll to the next tab.
Is there a way I can do TabBar scroll instead of PageView scroll if there's no more page to the direction? (only left or right scroll)
Here's the sample code.
When I scroll to right at the last page of the 1st tab, I want to see the first page of the 2nd tab.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: ScrollableTabsDemo());
}
}
class _Page {
const _Page({ this.icon, this.text });
final IconData icon;
final String text;
}
const List<_Page> _allPages = <_Page>[
_Page(icon: Icons.grade, text: 'TRIUMPH'),
_Page(icon: Icons.playlist_add, text: 'NOTE'),
_Page(icon: Icons.check_circle, text: 'SUCCESS'),
];
class ScrollableTabsDemo extends StatefulWidget {
static const String routeName = '/material/scrollable-tabs';
#override
ScrollableTabsDemoState createState() => ScrollableTabsDemoState();
}
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> with SingleTickerProviderStateMixin {
TabController _controller;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: _allPages.length);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final Color iconColor = Theme.of(context).accentColor;
return Scaffold(
appBar: AppBar(
title: const Text('Scrollable tabs'),
bottom: TabBar(
controller: _controller,
isScrollable: true,
tabs: _allPages.map<Tab>((_Page page) {
return Tab(text: page.text, icon: Icon(page.icon));
}).toList(),
),
),
body: TabBarView(
controller: _controller,
children: _allPages.map<Widget>((_Page page) {
return SafeArea(
top: false,
bottom: false,
child: PageView.builder(
itemBuilder: (context, position)
{
return Container(child: Center(child: Text(position.toString())));
},
itemCount: 5,
),
);
}).toList(),
),
);
}
}
Add to the pageBuilder the onPageChange param. Then check if its the last page, if so, animate the tabController to the nextPage.
onPageChanged: (page) {
if (page == _allPages.length &&
(_controller.index + 1) < _controller.length) {
_controller.animateTo(_controller.index + 1);
}
},
itemCount: _allPages.length + 1,
I have two tabs, the left tab having a list of tiles and the right tab having nothing. The user can drag the screen from right-to-left or left-to-right to get from one tab to the other.
The left tab has a list of dismissible tiles that only have "direction: DismissDirection.startToEnd" (from left-to-right) enabled so that the user can still theoretically drag (from right-to-left) to go to the right tab.
However, I believe the Dismissible widget still receives the right-to-left drag information which is disabling the TabView drag to change tabs.
In essence, how do I allow the right-to-left drag to be detected by only the TabView and not the Dismissible item?
If an explicit solution/example with code snippets can be given, I would very very much appreciate the help!
Here's a paste for your main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
void main() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(vsync: this, length: 2, initialIndex: 1);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
TabWithSomething(),
TabWithNothing(),
],
),
),
],
),
),
),
);
}
}
class TabWithNothing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text("Swipe from left-to-right!"),
),
);
}
}
class TabWithSomethingItem implements Comparable<TabWithSomethingItem> {
TabWithSomethingItem({this.index, this.name, this.subject, this.body});
TabWithSomethingItem.from(TabWithSomethingItem item)
: index = item.index,
name = item.name,
subject = item.subject,
body = item.body;
final int index;
final String name;
final String subject;
final String body;
#override
int compareTo(TabWithSomethingItem other) => index.compareTo(other.index);
}
class TabWithSomething extends StatefulWidget {
const TabWithSomething({Key key}) : super(key: key);
static const String routeName = '/material/leave-behind';
#override
TabWithSomethingState createState() => TabWithSomethingState();
}
class TabWithSomethingState extends State<TabWithSomething> {
List<TabWithSomethingItem> TabWithSomethingItems;
void initListItems() {
TabWithSomethingItems =
List<TabWithSomethingItem>.generate(10, (int index) {
return TabWithSomethingItem(
index: index,
name: 'Item $index',
subject: 'Swipe from left-to-right to delete',
body: "Swipe from right-to-left to go back to old tab");
});
}
#override
void initState() {
super.initState();
initListItems();
}
void _handleDelete(TabWithSomethingItem item) {
setState(() {
TabWithSomethingItems.remove(item);
});
}
#override
Widget build(BuildContext context) {
Widget body;
body = ListView(
children:
TabWithSomethingItems.map<Widget>((TabWithSomethingItem item) {
return _TabWithSomethingListItem(
item: item,
onDelete: _handleDelete,
dismissDirection: DismissDirection.startToEnd,
);
}).toList());
return body;
}
}
class _TabWithSomethingListItem extends StatelessWidget {
const _TabWithSomethingListItem({
Key key,
#required this.item,
#required this.onDelete,
#required this.dismissDirection,
}) : super(key: key);
final TabWithSomethingItem item;
final DismissDirection dismissDirection;
final void Function(TabWithSomethingItem) onDelete;
void _handleDelete() {
onDelete(item);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Semantics(
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
},
child: Dismissible(
key: ObjectKey(item),
direction: dismissDirection,
onDismissed: (DismissDirection direction) => _handleDelete(),
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(bottom: BorderSide(color: theme.dividerColor))),
child: ListTile(
title: Text(item.name),
subtitle: Text('${item.subject}\n${item.body}'),
isThreeLine: true),
),
),
);
}
}
UPDATE:
I'm thinking we could change the "dismissible.dart" file to change the "TabControlller", but i'm not sure how I might do that.
In the "dismissible.dart" file:
...
void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _moveController.isAnimating)
return;
final double delta = details.primaryDelta;
if (delta < 0) print(delta); // thinking of doing something here
...