Flutter TapBar Page white flicker on init - flutter

I am using a TabBarView in my app like this:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
TabBarView(
controller: _controller,
physics: const NeverScrollableScrollPhysics(),
children: const [
WishlistsView(),
FriendsView(),
EventsView(),
InboxView(),
ProfileView(),
],
),
Align(
alignment: Alignment.bottomCenter,
child: BottomNavBar(
initialIndex: widget.navBarOption.index,
onPageChanged: (index) => _tap(context, index),
),
),
],
),
);
}
void _tap(BuildContext context, int index) => context.go(
'/home/${BottomNavBarOption.values[index].name}',
);
Now the problem is that when I go on another page the first time there is a very short white screen before the page is actually displayed.
Here is a ScreenVideo for a better understanding. This happens on both Web and iOS.
Why is that happening? Can I avoid that? As you can see the views are not very heavy.
All they have is basically a SVGPicture.asset :
class _WishlistsViewState extends State<WishlistsView>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Stack(
children: const [
BackgroundImage(option: BackgroundImageOption.wishlists),
],
),
);
}
}

It is probably taking time to load svg on the first go and caching it for showing it later.. You can probably try OffStage Widget which should load the UI and just not display it.
class _WishlistsViewState extends State<WishlistsView>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Offstage(
offStage: false,
child: Stack(
children: const [
BackgroundImage(option: BackgroundImageOption.wishlists),
],
),
),
);
}
}

as #Kaushik Chandru correctly pointed out: The SVG are the problem. They have to load first. To solve that issue, I used this really useful function precachePicture right before I actually call runApp:
for (BackgroundImageOption imageOption in BackgroundImageOption.values) {
await precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoderBuilder,
imageOption.assetPath,
),
null,
);
}

Related

Pin widget for some seconds when widget comes into viewport in SliverStreamGrid while scrolling

I have CustomScrollView with a SliverGrid with different widget types inside. I have a widget that appears after the stream content ends and other content will load. When this widget appears, I want to shortly pin it on the screen and disable the scrolling for 3 seconds.
To simplify it, I skipped the sliver delegates and summarizes my widget tree like this:
CustomScrollView{
controller: scrollController,
slivers: [
SliverAppBar(),
SliverStreamGrid(
children: [
ProductTile(),
ProductTile(),
ProductTile(),
// End of Available Products
EndOfProductsInfoWidget(), // should be pinned on screen for 3 seconds
SomeOtherProductTile(),
SomeOtherProductTile(),
SomeOtherProductTile(),
]
)
]
}
I am using visibility_detector to detect visibility of the widget and SliverPinnedHeader from sliver_tools package.
The issue lies when our widget is visible I am using a short delay and then disabling scroll event for 3 seconds, you can use global key for this and have more precious output.
CustomScrollView's physics:NeverScrollableScrollPhysics() used to disable scroll event.
class SliverPinTHEx extends StatefulWidget {
const SliverPinTHEx({Key? key}) : super(key: key);
#override
State<SliverPinTHEx> createState() => _SliverPinTHExState();
}
class _SliverPinTHExState extends State<SliverPinTHEx> {
bool? _showPinMessage;
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: _showPinMessage == true
? const NeverScrollableScrollPhysics()
: null,
slivers: [
list(),
if (_showPinMessage != false)
SliverPinnedHeader(
child: VisibilityDetector(
onVisibilityChanged: (info) async {
if (info.visibleFraction == 1.0) {
debugPrint("disable scroll");
await Future.delayed(const Duration(seconds: 1));
setState(() {
_showPinMessage = true;
});
Future.delayed(const Duration(seconds: 3)).then((value) {
setState(() {
_showPinMessage = false;
});
});
}
},
key: const Key('my-widget-key'),
child: Container(
height: 70,
color: Colors.amber,
child: const Text("pinned widget"),
),
)),
list()
],
),
);
}
SliverList list() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return SizedBox(
height: 50,
child: Text(
index.toString(),
),
);
},
childCount: 55,
),
);
}
}

How dispose Global key from provider with flutter web?

Want to build drawer with flutter web. But got
Duplicate GlobalKey detected in widget tree.
instance is moved to the new location. The key was:
[LabeledGlobalKey#c9754]
GlobalKey reparenting is:
MainScreen(dependencies: [MediaQuery], state: _MainScreenState#dc897)
A GlobalKey can only be specified on one widget at a time in the widget tree.
import 'package:flutter/material.dart';
class MenuController with ChangeNotifier {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
GlobalKey<ScaffoldState> get scaffoldKey => _scaffoldKey;
void controlMenu() {
if (!_scaffoldKey.currentState!.isDrawerOpen) {
_scaffoldKey.currentState!.openDrawer();
}
}
// void disposeKey() {
// _scaffoldKey.currentState.();
// }
}
class _MainScreenState extends State<MainScreen> {
#override
void initState() {
print('init CALLED- GAME---');
super.initState();
}
#override
void dispose() {
print('DISPOSE CALLED- GAME---');
context.read<MenuController>().scaffoldKey.currentState!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: context.read<MenuController>().scaffoldKey,
drawer: SideMenu(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// We want this side menu only for large screen
if (Responsive.isDesktop(context))
Expanded(
// default flex = 1
// and it takes 1/6 part of the screen
child: SideMenu(),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: DashboardScreen(),
),
],
),
),
);
}}
then want to reuse key from another widget
class ProductsScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
key: context.read<MenuController>().scaffoldKey,
drawer: SideMenu(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// We want this side menu only for large screen
if (Responsive.isDesktop(context))
Expanded(
// default flex = 1
// and it takes 1/6 part of the screen
child: SideMenu(),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: ProductsListScreen(),
),
],
),
),
);
}
}
and got bug
To navigate inside SideMenu widget use
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ProductsScreen(),
),
);
You do not need to manually dispose of a GlobalKey. The main requirement is that they cannot be inserted into the widget tree twice. This is not the case with other keys (LocalKeys):
// this is allowed
Row(
children: [
SizedBox(key: Key('hello')),
Container(key: Key('hello')),
],
)
// this is not
final key = GlobalKey<ScaffoldState>();
Row(
children: [
Scaffold(key: key),
Scaffold(key: key),
],
)
A common reason why this is violated is animations. While the animation between one page and another page is playing, both pages are in the widget tree, and if they have the same GlobalKey, an error will be thrown.
Calling globalKey.currentState!.dispose() actually disposes the State of the associated widget. You should not call this yourself.
Instead, provide a new GlobalKey to the second subtree or remove the old one before navigating to the new page.

How to "merge" scrolls on a TabBarView inside a PageView?

I have an app that uses a PageView on its main page. Today, I got assigned to insert a TabBarView in one of these pages. The problem is that when I scroll the between the tabs when in the last tab, scrolling to the left won't scroll the PageView.
I need a way to make the scroll of page view scroll when at the start or end of the tabbarview.
I found a question with the inverted problem: flutter PageView inside TabBarView: scrolling to next tab at the end of page
However, the method stated there is not suitable to my issue.
I made a minimal example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
title: 'TabBarView inside PageView',
home: MyHomePage(),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageController _pageController = PageController();
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('TabBarView inside PageView'),
),
body: PageView(
controller: _pageController,
children: <Widget>[
Container(color: Colors.red),
GreenShades(),
Container(color: Colors.yellow),
],
),
);
}
class GreenShades extends StatefulWidget {
#override
_GreenShadesState createState() => _GreenShadesState();
}
class _GreenShadesState extends State<GreenShades>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
this._tabController = TabController(length: 3, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) => Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
)
],
);
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
Note that, in this MRE, it's possible to reach the 3rd page if you drag the TabBar, but not if you drag the TabBarView.
How may I achieve this behavior?
Edit:
As stated by #Fethi, there's a similar question:
Is it possible to swipe from an TabBarView content area to an adjacent PageView page?
However, the question was not answered satisfactorily, as the solution given does not really "blend" the scroll, although the behavior is similar to what was described. It doesn't scroll naturally.
This is possible by using the PageController.postion attribute's drag method, which internally drags the ScrollPosition of the screen. This way, user can intuitively drag the pages like drag halfway and then leave or continue fully.
The idea is inspired from the other post to use the OverScrollNotification but add rather more step to continue intuitive dragging.
Collect the DragstartDetail when user starts scrolling.
Listen for OverScrollNotification and start the draging and at the same time update the drag using the drag.update with the DragUpdateDetails from OverscrollNotification method.
On ScrollEndNotification cancel the the drag.
To keep the idea simple I am pasting only build method of the Tabs page.
A fully working example is available in this dart pad.
#override
Widget build(BuildContext context) {
// Local dragStartDetail.
DragStartDetails dragStartDetails;
// Current drag instance - should be instantiated on overscroll and updated alongside.
Drag drag;
return Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: NotificationListener(
onNotification: (notification) {
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is OverscrollNotification) {
drag = _pageController.position.drag(dragStartDetails, () {});
drag.update(notification.dragDetails);
}
if (notification is ScrollEndNotification) {
drag?.cancel();
}
return true;
},
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
),
),
],
);
}
Old Answer
The above might not handle some edge cases. If you need more control below code provides the same result but you can handle UserScrollNotification. I am pasting this because, it might be useful for others who would like to know which direction the use is scrolling w.r.t the Axis of the ScrollView.
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.forward &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == 0) {
_pageController.position.drag(dragStartDetails, () {});
}
// Simialrly Handle the last tab.
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.reverse &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == _tabController.length - 1) {
_pageController.position.drag(dragStartDetails, () {});
}
so you want to scroll the page view to the left when you reach the end of tabs and the same goes to scrolling to the right when on the first tab, what i have been thinking about is manually swipe the page view when in those cases as follow:
index value should the index of page that comes before the tab bar page and after it.
pageController.animateToPage(index,
duration: Duration(milliseconds: 500), curve: Curves.ease);
here is a complete code of what you are looking for, hopefully this helps!
I have a different approach using Listener Widget and TabView physics as show below:
//PageView Widget
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
children: [
Widge1()
TabBarWidget(),
Widget2()
]
)
)
}
//TabBar Widget
final _physycsNotifier = ValueNotifier<bool>(false);
....
....
#override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
//... other properties
)
Expanded(
child: Listener(
onPointerMove: (event) {
final offset = event.delta.dx;
final index = _tabController.index;
//Check if we are in the first or last page of TabView and the notifier is false
if(((offset > 0 && index == 0) || (offset < 0 && index == _categories.length - 1)) && !_physycsNotifier.value){
_physycsNotifier.value = true;
}
},
onPointerUp: (_) => _physycsNotifier.value = false;
child: ValueListenableBuilder<bool>(
valueListenable: _physycsNotifier,
builder: (_, value, __) {
return TabBarView(
controller: _tabController,
physics: value ? NeverScrollableScrollPhysics() : null,
children: List.generate(_categories.length, (index) {
return _CategoryTab(index: index);
})
);
},
),
)
)
]
)
}
this works fine if you set default physics for PageView and TabView (it means null) if you set other physisc like BouncingScrollPhsysisc there will be some bugs, but i think this is good workaround.

How to use AnimatedSwitcher and CustomScrollView together

I want switch status with animation in CustomScrollView, but it throw error.
class SliverAnimatedSwitcher extends StatefulWidget {
final state;
const SliverAnimatedSwitcher({Key key, this.state}) : super(key: key);
#override
_SliverAnimatedSwitcherState createState() => _SliverAnimatedSwitcherState();
}
class _SliverAnimatedSwitcherState extends State<SliverAnimatedSwitcher> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('SliverAnimatedSwitcher'),
),
_buildContent(),
],
),
);
}
get state => widget.state;
Widget _buildContent() {
var content;
if (state.isNotEmpty == true) {
content = SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
var item = state.items[index];
return ListTile(
title: Text(item.title),
);
},
childCount: state.items.length,
),
);
} else if (state.isError) {
content = SliverFillRemaining(
key: Key('error'),
child: Container(alignment: Alignment.center, child: Text('Error')),
);
} else if (state.isLoading) {
content = SliverFillRemaining(
key: Key('loading'),
child: Container(alignment: Alignment.center, child: Text('Loading')),
);
} else {
content = SliverFillRemaining(
key: Key('empty'),
child: Container(alignment: Alignment.center, child: Text('Empty')),
);
}
return AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: content,
);
}
}
There's a sliver_tools package on pub.dev that has a SliverAnimatedSwitcher. You'd use it like this:
SliverAnimatedSwitcher(
duration: kThemeAnimationDuration,
child: content,
)
It is because sliver widget is not capable with Stack which is using as layout builder inside AnimatedSwitcher.
I found a temporary solution. Though the result is not equal to the original one (only show the last widget in the result), but I think it is acceptable for me now with such little effort.
First create a SliverAnimatedSwitcher exact the same code as AnimatedSwitcher here
Modify the Stack part to return the currentChild only (defaultLayoutBuilder)
Change FadeTransition to SliverFadeTransition (defaultTransitionBuilder)
.
static Widget defaultLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
return currentChild;
}
.
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return SliverFadeTransition(
opacity: animation,
sliver: child,
);
}
If someone can find a way to Stack Sliver widgets above each other, the result may be more perfect.

Is possible to make Multiple flutter pages in one app?

I went to convert book to android app the book has 100 page ,
should I create page(activity) for each page ? or any other ways to do this?
You can use PageView or its builder constructor for this, here is the basic example
PageView.builder(
itemCount: 100,
itemBuilder: (_, index) => YourPage(list[index]),
)
Update:
Create two widgets like this.
// It only shows Text
class TextPage extends StatelessWidget {
final String text;
const TextPage({#required this.text});
#override
Widget build(BuildContext context) {
return Text(text, style: TextStyle(fontSize: 20));
}
}
// It only shows Image from assets
class ImagePage extends StatelessWidget {
final String image;
const ImagePage({#required this.image});
#override
Widget build(BuildContext context) {
return Image.asset(image);
}
}
Now in your main class, use them as
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PageView(
children: <Widget>[
TextPage(text: "This screen only has text"), // 1st screen only text
ImagePage(image: "assets/images/chocolate_pic.png"), // 2nd screen only image
Column( // 3rd screen will have both
children: <Widget>[
TextPage(text: "This is the text followed by an image"),
ImagePage(image: "assets/images/chocolate_pic.png"),
],
),
],
),
);
}