I'm having trouble using the Hero widget with SliverAppBar.
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Center(
child: Row(
children: [Colors.red, Colors.blue, Colors.yellow]
.map((e) => GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScrollPage(color: e),
));
},
child: Hero(
tag: e,
child: Container(
width: 100,
height: 100,
color: e,
),
),
))
.toList(),
),
),
);
}
}
import 'package:flutter/material.dart';
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
// final GlobalKey key = GlobalKey<SliverState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
Hero(
tag: widget.color,
child: SliverAppBar.large(
key: UniqueKey(),
expandedHeight: 200,
backgroundColor: widget.color,
title: const Text(
'This is a title blblaa',
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
);
}
}
I'm getting error like:
════════ Exception caught by widgets library ═══════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 6405 pos 12: 'renderObject.child == child': is not true.
and
════════ Exception caught by widgets library ═══════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 6369 pos 12: 'child == _child': is not true.
framework.dart:6369
The relevant error-causing widget was
MaterialApp
and sometimes:
════════ Exception caught by widgets library ═══════════════════════════════════
Duplicate GlobalKey detected in widget tree.
════════════════════════════════════════════════════════════════════════════════
I did similar not using Appbar and I achieved it. Demo video here. But, I really want to use SliverAppbar in this case.
Full minimal reproducible project: https://github.com/iqfareez/flutter_hero_sliver
How do I make the SliverAppBar work with Hero?
The issue is here Hero is a general widget rather than a sliver-widget. That's the issue occurs while wrapping the SliverAppBar with Hero widget.
You can do
SliverToBoxAdapter(
child: Hero(
tag: widget.color,
child: // customAppBar but it might loss the scroll-effect,
Also, you can wrap the Scaffold with Hero widget, but it will show a little different animation.
You can create SliverPersistentHeaderDelegate.
Check the pr and commit difference.
import 'package:flutter/material.dart';
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: MySliverPersistentHeaderDelegate(widget.color)),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
);
}
}
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final tag;
MySliverPersistentHeaderDelegate(this.tag);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Hero(
tag: tag,
child: Material(
color: tag,
child: Stack(
children: [
Align(
child: const Text(
'This is a title blblaa',
),
),
Align(
alignment: Alignment.topLeft,
child: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(Icons.arrow_back)),
),
],
),
),
);
}
#override
double get maxExtent => 200;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
You can try moving the Hero widget outside of the SliverAppBar, wrapping it around the entire CustomScrollView. Here's how you can modify your code to achieve that:
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'row',
child: Center(
child: Row(
children: [Colors.red, Colors.blue, Colors.yellow]
.map((e) => GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScrollPage(color: e),
));
},
child: Container(
width: 100,
height: 100,
color: e,
),
))
.toList(),
),
),
),
);
}
}
class ScrollPage extends StatefulWidget {
const ScrollPage({super.key, required this.color});
final Color color;
#override
State<ScrollPage> createState() => _ScrollPageState();
}
class _ScrollPageState extends State<ScrollPage> {
// final GlobalKey key = GlobalKey<SliverState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'row',
child: CustomScrollView(
slivers: [
SliverAppBar(
key: UniqueKey(),
expandedHeight: 200,
backgroundColor: widget.color,
title: const Text(
'This is a title blblaa',
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item $index'),
),
childCount: 100,
),
),
],
),
),
);
}
}
Related
There are lots of widgets like Visibility or AnimatedOpacity, but I want a widget to appear and grow to its full size with a smooth animation, moving the other Column's children around it apart.
AnimatedContainer would be cool, but I don't want to set the child's constraints (e.g. height) from the parent (then I would have to test and set the correct size on every UI change).
Use Hero Animation check this link to learn:
https://docs.flutter.dev/development/ui/animations/hero-animations
Example :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/first',
routes: {
'/first': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
);
}
}
class FirstScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: Padding(
padding: EdgeInsets.all(15),
child: Column(
children: [
Hero(
tag: "HeroOne",
child: Icon(
Icons.image,
size: 50.0,
),
),
ElevatedButton(
child: Text('Go to second screen'),
onPressed: () {
Navigator.push(context, CustomPageRoute(SecondScreen()));
},
),
],
),
),
);
}
}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Hero(
tag: "HeroOne",
child: Icon(
Icons.image,
size: 150.0,
),
),
ElevatedButton(
child: Text('Back to first screen!'),
onPressed: () {
Navigator.pop(context);
},
),
],
)
),
);
}
}
class CustomPageRoute<T> extends PageRoute<T> {
final Widget child;
CustomPageRoute(this.child);
#override
Color get barrierColor => Colors.black;
#override
String get barrierLabel => '';
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(seconds: 2);
#override
Widget buildPage({1}
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation
) {
return FadeTransition(
opacity: animation,
child: child,
);
}
}
For More Check :
https://www.woolha.com/tutorials/flutter-creating-hero-transition-examples
While researching more for my own question, I found the AnimatedSize widget which does exactly what I need:
To dynamically show and hide a widget with a size animation, just wrap it in AnimatedSize() and give it a duration.
https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html
Expected behaviour:
The nested scroll view scrolls normaly if the custom scroll view gets scrolled and thus scrolls the appbar with it.
Actually happening:
Only the custom scrollview scrolls, but not the appbar (=nested scrollview).
Wanted Behaviour can be achieved by either not using a customscrollview or not using a navigator (both no option in my case). This implementation worked before null safety if it helps in any case.
Code example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TestScreen(),
);
}
}
class TestScreen extends StatefulWidget {
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
GlobalKey<NavigatorState> get navigatorKey =>
GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
))
],
// body: DemoPage(),
body: Navigator(
key: navigatorKey,
onGenerateRoute: (settings) =>
MaterialPageRoute(builder: (context) => DemoPage())),
));
}
}
class DemoPage extends StatelessWidget {
Color generateRandomColor1() {
// Define all colors you want here
const predefinedColors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.black,
Colors.white
];
Random random = Random();
return predefinedColors[random.nextInt(predefinedColors.length)];
}
#override
Widget build(BuildContext context) {
// return Container(
// color: Colors.red,
// height: 1000,
// width: 500,
// child: Center(
// child: RaisedButton(
// child: Text("Press"),
// onPressed: () {
// Navigator.of(context)
// .push(MaterialPageRoute(builder: (context) => DemoPage()));
// },
// ),
// ),
// );
return CustomScrollView(slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => DemoPage()));
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
),
)
]);
}
}
Probably, your NestedScrollView acts as if there is an individual scroll view when DemoPage builds the CustomScrollView below the Navigator widget.
You can set the physics of the CustomScrollView to NeverScrollableScrollPhysics, so that the NestedScrollView can control it.
...
return CustomScrollView(
// Add the following line:
physics: NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DemoPage(),
),
);
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
),
),
],
);
...
PageView
If making CustomScrollView never scrollable doesn't give the desired result, you can try PageView instead of Navigator. Since you want to keep SliverAppBar stationary across all pages, this will provide navigation only inside PageView.
All you need to do is to put the list of your pages in PageView and pass its controller to your pages. Then, you can navigate to other pages via a button or swipe gesture.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TestScreen(),
);
}
}
class TestScreen extends StatefulWidget {
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
// Page controller
final PageController controller = PageController(initialPage: 0);
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
),
)
],
// Page view here
body: PageView(
// Scroll horizontally
scrollDirection: Axis.horizontal,
controller: controller,
// Your pages
children: <Widget>[
// Pass the controller to your page
DemoPage(pageController: controller),
DemoPage(pageController: controller),
DemoPage(pageController: controller),
],
),
),
);
}
}
class DemoPage extends StatelessWidget {
// Page Controller
final PageController pageController;
DemoPage({required this.pageController});
Color generateRandomColor1() {
// Define all colors you want here
const predefinedColors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.black,
Colors.white
];
Random random = Random();
return predefinedColors[random.nextInt(predefinedColors.length)];
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
color: generateRandomColor1(),
height: 1000,
width: 500,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RaisedButton(
child: Text("Press"),
onPressed: () {
// Go to next page
pageController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.linear,
);
},
),
RaisedButton(
child: Text("pop"),
onPressed: () {
// Go back to previous page
pageController.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.linear,
);
},
),
],
),
),
),
)
],
);
}
}
It should work I guess.
Also have a look at this post ref: nested_reference
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
pinned: true,
floating: true,
forceElevated: innerBoxIsScrolled,
.....
You can find the answer here:
Answer from github
There are 2 solutions:
First, get innerController of NestedScrollView and pass it by the PrimaryScrollController widget below Navigator
Second, access the state of NestedScrollView from your widget where you need a controller of NestedScrollView, get innerController of NestedScrollViewState and assign that innerController to your scrollable widget
P.s.
You can pass controller by InheritedWidget
First-way example:
...
GlobalKey<NavigatorState> get navigatorKey =>
GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text("title"),
pinned: true,
floating: false,
snap: false,
expandedHeight: 200,
))
],
// body: DemoPage(),
body: Builder(builder: (context) {
var scrollController = PrimaryScrollController.of(context);
return Navigator(
key: navigatorKey,
onGenerateRoute: (settings) =>
MaterialPageRoute(builder: (context) => PrimaryScrollController(controller: scrollController!,
child: Scaffold(body:ListView(
children:[
/// children
],
),
),
))),
);
}
);
}
}
The second way:
You can innerController of NestedScrollView
context.findAncestorStateOfType<NestedScrollViewState>()?.innerController
then pass it to your scrollable widget
CustomScrollView(
controller: context.findAncestorStateOfType<NestedScrollViewState>()?.innerController,
...
)
I think that Navigator does not pass innerController of NestedView
Try removing pinned property of SliverAppBar.
Flutter documentation has interactive example of what it does.
https://api.flutter.dev/flutter/material/SliverAppBar-class.html
I have a Hero animation that should animate an image from a ListView to the background of a FlexibleSpaceBar inside of a SliverAppBar.
This works all fine, except that the hero animation takes place on top of the SliverAppBar and when the animation is finished, the SliverAppBarsuddenly pops in on top of the background, which doesn't look so nice.
How can I either a) include the SliverAppBar in the hero transition so that it fades in on top of the image as the transition progresses or b) make the hero transition take place under the SliverAppBar without covering it?
Sample Code:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: AppBar().preferredSize,
child: Hero(
tag: 'appbar',
child: AppBar(
title: Text('title'),
actions: <Widget>[
PopupMenuButton<String>(
itemBuilder: (BuildContext context) {
return [
PopupMenuItem<String>(
child: Text('Action'),
)
];
},
),
],
),
),
),
body: ListView.builder(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
itemCount: 10,
itemBuilder: (context, index) => buildListItem(context, index),
),
);
}
Widget buildListItem(BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: () async {
await Navigator.of(context).pushNamed(
'/details',
arguments: index,
);
},
child: Row(
children: <Widget>[
Hero(
tag: index,
child:
SizedBox(width: 80, height: 80, child: widgetForIndex(index)),
),
],
),
),
);
}
}
Widget widgetForIndex(int index) {
return Container(color: Color.fromARGB(255, max(0, 255 - 20 * index), 0, 0));
}
class DetailsPage extends StatefulWidget {
#override
_DetailsPageState createState() => _DetailsPageState();
}
class _DetailsPageState extends State<DetailsPage> {
#override
Widget build(BuildContext context) {
final int index = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text('Details'),
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: Hero(
tag: index,
child: widgetForIndex(index),
),
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
color: Colors.green,
width: 200,
height: 200,
),
],
),
),
],
),
);
}
}
I'm trying to display tabs for each main tab (Nested Tab Bar) in SliverAppBar(). It's look like this:
See the image
See the GIF
The content of the exam tab it's in Container() widget (That the error in the image came from).
Now, with the Container() widget the SliverAppBar() will collapse when the user scroll the exam tab content (white screen in the image), everything is fine for now.
So, After I replaced the Container() with ListView.builder() to make the tab content scrollable, now I can't collapse SliverAppBar() from the tab content (white screen in the image). but I can from the SliverAppBar().
See this GIF after I added ListView.builder()
So, How I can make the SliverAppBar scrollable (collapsing ) with Listview?
Can anyone help me? please :(
This example (demo):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'SliverAppBar App Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverSafeArea(
top: false,
sliver: SliverAppBar(
pinned: true,
title: Text(widget.title),
expandedHeight: 500,
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(tabs: [Tab(text: 'Tab A'), Tab(text: 'Tab B')]),
Colors.blue,
),
pinned: false,
),
];
},
body: TabBarView(
children: <Widget>[
NestedTabs('A'),
NestedTabs('B'),
],
),
),
),
),
);
}
}
// This class is to handle the main tabs (Tab A & Tab B)
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
TabBar _tabBar;
final Color _color;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
alignment: Alignment.center,
child: _tabBar,
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
class NestedTabs extends StatelessWidget {
final String mainTabName;
NestedTabs(this.mainTabName);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
color: Colors.blue,
alignment: Alignment.bottomCenter,
child: TabBar(
tabs: [
Tab(text: 'Tab $mainTabName-1'),
Tab(text: 'Tab $mainTabName-2')
],
),
),
),
body: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 500,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
width: 200,
color: Colors.black45,
child: Center(child: Text('Index ${index}')));
},
),
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 500,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
width: 200,
color: Colors.black45,
child: Center(child: Text('Index ${index}')));
},
)
],
),
);
}
}
Thank you :)
Use SliverList() instead of SliverFillRemaining for ListView
In the code below, I have a method myMenu on a card. How do I navigate to another page when the card is tapped? There are going to be several of these cards which will link to its own page content. Each time I add a function to for an example it gives an error. How do I do it properly?
import 'package:flutter/material.dart';
import 'package:tarjous_app/gridview_demo.dart';
void main(List<String> args) {
runApp(
new MaterialApp(home: TarjousAle(), debugShowCheckedModeBanner: false));
}
class TarjousAle extends StatefulWidget {
#override
_TarjousAleState createState() => _TarjousAleState();
}
class _TarjousAleState extends State<TarjousAle> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: Text("Study Plan"),
backgroundColor: Colors.amber,
),
body: Container(
child: GridView.count(
crossAxisCount: 3,
children: <Widget>[
MyMenu(
title: "Records",
icon: Icons.account_balance_wallet,
shape: Colors.brown,
),
MyMenu(
title: "Academy",
icon: Icons.account_balance,
shape: Colors.grey,
),
],
),
),
);
}
}
class MyMenu extends StatelessWidget {
MyMenu({this.title, this.icon, this.shape});
final String title;
final IconData icon;
final MaterialColor shape;
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(9.0),
child: InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => GridViewDemo()),
),
splashColor: Colors.amberAccent,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
icon,
size: 80.0,
color: shape,
),
Text(title, style: new TextStyle(fontSize: 18.0))
],
),
),
),
);
}
}
In the inkwell widget, I add a function that works for all the cards. But what I really want it for each card to navigate to its own page. E.g Records should navigate to its own records page, the same thing for Academy to academy page
You could receive the page in the constructor and then go to that page, like this:
class MyMenu extends StatelessWidget {
MyMenu({this.title, this.icon, this.shape, this.page});
final Widget page;
...
}
Then, in onTap:
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => page),
)
So now you can do this:
MyMenu(
...
page: GridViewDemo1(),
),
MyMenu(
...
page: GridViewDemo2(),
)
Note that to navigate to some page, your context must contain a Navigator instance of parent. So if you try to navigate directly from MaterialApp, you might run into issues. I will not belabour the point here since it was explained very well in this thread, but it is something to keep in mind in case you happen to run into it.
Edited to address comments:
I'd do something like this for your case. Named routes make it easy to specify which route you'd like the card to take you to, which you kind of need to do if you want the same widget to take you to different routes.
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(
new MaterialApp(
home: TarjousAle(),
debugShowCheckedModeBanner: false,
routes: {
GridViewDemo.route: (context) => GridViewDemo(),
AnotherDemo.route: (context) => AnotherDemo(),
},
),
);
}
class TarjousAle extends StatefulWidget {
#override
_TarjousAleState createState() => _TarjousAleState();
}
class _TarjousAleState extends State<TarjousAle> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: Text("Study Plan"),
backgroundColor: Colors.amber,
),
body: Container(
child: GridView.count(
crossAxisCount: 3,
children: <Widget>[
MyMenu(
title: "Records",
icon: Icons.account_balance_wallet,
shape: Colors.brown,
route: GridViewDemo.route
),
MyMenu(
title: "Academy",
icon: Icons.account_balance,
shape: Colors.grey,
route: AnotherDemo.route
),
],
),
),
);
}
}
class MyMenu extends StatelessWidget {
MyMenu({this.title, this.icon, this.shape, this.route});
final String title;
final IconData icon;
final MaterialColor shape;
final String route;
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(9.0),
child: InkWell(
onTap: () => Navigator.pushNamed(context, route),
splashColor: Colors.amberAccent,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
icon,
size: 80.0,
color: shape,
),
Text(title, style: new TextStyle(fontSize: 18.0))
],
),
),
),
);
}
}
class GridViewDemo extends StatelessWidget {
static String route = '/demo';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.brown,
appBar: AppBar(title: Text('Grid view demo')),
body: Center(
child: Text('Grid view demo'),
),
);
}
}
class AnotherDemo extends StatelessWidget {
static String route = '/another';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(title: Text('Another demo')),
body: Center(
child: Text('Another demo'),
),
);
}
}
You can read more about the basics of navigation in official docs, and also another docs page if you fancy the named routes.
Wrap the card with GestureDetector and you can use opnTap property.
for more details Official Documentation
Try wrapping your Card in a GestureDetector like below:
GestureDetector (
child: Card(),
onTap: () {},
),
wrap the card with InkWell widget and define your navigator.push in the onTap method.
class CardWidget extends StatelessWidget {
final Function onTapCard;
const CardWidget({Key key, #required this.onTapCard}) : super(key: key);
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(9.0),
child: InkWell(
onTap: onTapCard,
splashColor: Colors.amberAccent,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
icon,
size: 80.0,
color: shape,
),
Text(title, style: new TextStyle(fontSize: 18.0))
],
),
),
),
);
}
}
then we have our list here
class CardList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
CardWidget(
onTapCard: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => YourSecondPage()),
),
),
CardWidget(
onTapCard: Navigator.push(
context,
MaterialPageRoute(builder: (context) => YourThirdPage()),
),
),
],
);
}
}