When building a mobile application, creating a rewards screen is an essential component of keeping users engaged. A rewards screen shows users their progress towards earning rewards and motivates them to keep using the app. In this context, Flutter provides a powerful framework for building a rewards screen(Reward Screen UI in Flutter) that is both aesthetically pleasing and highly functional.
In this project, we will be exploring how to build a user interface for a rewards screen that is similar to the Google Pay Rewards screen using Flutter widgets. By the end of this project, you will learn how to create an adaptive UI that includes a custom SliverAppBar, and a button widget with text and an icon. This tutorial assumes that you have a basic understanding of Flutter widgets and the Dart programming language.
Understanding the Rewards UI of Google Pay
Let’s break it down. When you open Google Pay and go to the rewards screen, you will see a grid view of cards. Some cards may be scratched off, and you can also see your past rewards.
If you click on any card, it will take you to a new screen where you can see the card details. This screen has a transparent background and a draggable sheet at the bottom. As you drag the sheet up, an animated container gets smaller. At the top, there are menu and cross buttons on the right and left, respectively.
So, we need to create two screens. The first screen will show the grid view of rewards, and the second screen will show the details of a selected reward.
but in this article, we will learn the flutter implementation of the first screen to keep this article short and simple you can check the next article for the reward detail screen
Implementing the Rewards Screen UI in Flutter
To create a custom app bar similar to the one in the Google Pay reward screen, we first need to use the silver app bar. We can do this by creating a stateful class and using the scaffold body to provide a nested scroll view.
Essentially, we want to create an app bar that is similar to the one in Google Pay rewards. To do this, we’ll use a silver app bar. This requires creating a stateful class, which allows us to create a bar that can change over time.
Then, we’ll use a scaffold body to create a nested scroll view. This will allow us to create a scrolling bar that works well with the rest of the page.
NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
// add other properties here
flexibleSpace: FlexibleSpaceBar(
// add flexibleSpace content here
),
),
];
},
body: CustomScrollView(
// add body content here
),
)
In this code, we are creating a NestedScrollView
widget, which is used to create a scrollable view with multiple layers of widgets. The NestedScrollView
takes a headerSliverBuilder
argument, which is a callback function that returns a list of slivers that should be placed above the main scrollable area.
Creating the SilverAppBar
SliverAppBar(
elevation: 0,
backgroundColor:_isShrink? Colors.white:Colors.transparent,
pinned: true,
expandedHeight: height,
leading: _isShrink
? const BackButton(
color: Colors.black, // <-- SEE HERE
)
: null,
title: _isShrink
? const Text(
"₹62 total rewards",
style: TextStyle(color: Colors.black,fontWeight: FontWeight.normal,fontSize: 18),
)
: null,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.more_vert,
color: Colors.black,
),
),
],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: SafeArea(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
alignment: Alignment.topCenter,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text(
"₹62",style:
TextStyle(color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 24),
),
const Text(
"Total rewards",style: TextStyle(color: Colors.black),
),
],
),
),
),
],
),
),
),
),
To create a collapsible app bar in Flutter like Google Pay, you can use the SliverAppBar
widget. Set the elevation
property to 0 for no shadow effect and set the backgroundColor
to transparent when fully expanded and white when collapsed. Use pinned
to keep the app bar visible as the user scrolls and set the expandedHeight
to determine the size when fully expanded. Use the leading
property to specify a back button widget, title
to specify a text widget that appears in the center, and actions
to specify an IconButton
widget with the Icons.more_vert
icon to open a menu. Finally, use the flexibleSpace
property to create the background of the app bar, which can display user information or other content.

Solving the Overlapping by Adding Scroll Controller
As you can see, a potential problem is that when the app bar is expanded, the text in the app bar can overlap or appear merged together, which can make it difficult to read. However, the text appears properly when the app bar is collapsed
To fix the issue of overlapping or merged text in the SliverAppBar
widget when it’s expanded, you can add a _scrollController
to your code. This controller will keep track of the user’s scrolling behavior and can be used to make the app bar disappear as the user scrolls up, which frees up space on the screen and prevents the text from overlapping. This approach can help ensure that the app bar remains useful and readable, even when it’s expanded.
First, we define a _scrollController
variable and set it to null. We also define a lastStatus
boolean variable and set it to true
.
ScrollController? _scrollController;
bool lastStatus = true;
Next, we define a _scrollListener
function that will be called whenever the user scrolls on the screen. This function checks to see if the _isShrink
boolean has changed since the last time it was called. If _isShrink
has changed, we update the lastStatus
variable to reflect this change.
void _scrollListener() {
if (_isShrink != lastStatus) {
setState(() {
lastStatus = _isShrink;
});
}
}
The _isShrink
boolean is a getter function that determines whether the user has scrolled past a certain threshold on the screen. In this case, the threshold is defined as the difference between the height
of the app bar and the kToolbarHeight
constant, which represents the standard height of a toolbar.
bool get _isShrink {
return _scrollController != null &&
_scrollController!.hasClients &&
_scrollController!.offset > (height - kToolbarHeight);
}
an initState
function that is called when the widget is first created. This function initializes the _scrollController
and adds a listener to it that will call the _scrollListener
function whenever the user scrolls on the screen.
We also define a dispose
function that is called when the widget is destroyed. This function removes the _scrollListener
from the _scrollController
and disposes of the _scrollController
.
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_scrollListener);
}
@override
void dispose() {
_scrollController?.removeListener(_scrollListener);
_scrollController?.dispose();
super.dispose();
}
.now using the _isShrink boolean in our code to hide the title and lead back button
//back button will hide when we will scroll up
leading: _isShrink // <-- SEE HERE
? const BackButton(
color: Colors.black,
)
: null,
//title will hide when we will scroll up
title: _isShrink // <-- SEE HERE
? const Text(
"₹62 total rewards",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 18),
)
: null,
now after adding the image our app bar is done
SliverAppBar(
elevation: 0,
backgroundColor: _isShrink ? Colors.white : Colors.transparent,
pinned: true,
expandedHeight: height,
//back button will hide when we will scroll up
leading: _isShrink // <-- SEE HERE
? const BackButton(
color: Colors.black,
)
: null,
//title will hide when we will scroll up
title: _isShrink // <-- SEE HERE
? const Text(
"₹62 total rewards",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 18),
)
: null,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.more_vert,
color: Colors.black,
),
),
],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: SafeArea(
child: Stack(
children: [
Container(
width: double.infinity,
child: SingleChildScrollView(scrollDirection: Axis.horizontal,child: Image.asset("assets/background_image.jpg")),
//in future we will add image
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
alignment: Alignment.topCenter,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Text(
"₹62",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 24),
),
Text(
"Total rewards",
style: TextStyle(color: Colors.black),
),
],
),
),
),
],
),
),
),
),

Coming towards our body part
In the Google Pay rewards screen, there’s a long-width rectangular button that says “Upcoming rewards on your path” When you tap on it, it takes you to a new screen that shows rewards that you can earn in the future.
Back on the main rewards screen, there’s another section with the heading “My rewards.” This section displays all the rewards that you’ve earned so far in a grid view. The grid view has two rewards displayed side by side. As you scroll down, more rewards are loaded and displayed using an infinite scrolling feature, which means that you can keep scrolling and more rewards will keep appearing.
in the body part
body: CustomScrollView(slivers: [buildRewardsbody()]),
This is creating a CustomScrollView
widget with one child sliver called buildRewardsbody()
. A CustomScrollView
is a widget that allows you to create a scrollable view with a flexible header and footer, along with any number of sliver elements in between. Sliver elements are widgets that can be scrolled and laid out in a scrolling list.
In this case, the buildRewardsbody()
function returns a widget that creates a sliver element for displaying the content of the reward. The slivers
property of CustomScrollView
takes an array of sliver widgets, and we’re passing an array with just one element (the sliver widget returned by buildRewardsbody()
).
By using a CustomScrollView
with a sliver element, we can create a scrolling view with a flexible header that can expand or contract as the user scrolls. This allows for a more dynamic and interactive user experience, especially when combined with other scrolling widgets like ListView
or GridView
.
Widget Function buildRewardsbody
Widget buildRewardsbody() => SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: UpcomingRewardsButton(
onTap: () {
// Add your navigation code here
},
),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("My Rewards",style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 20),),
),
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 0.0,
mainAxisSpacing: 0.0,
),
primary: false,
shrinkWrap: true,
itemCount: 20,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: buildRewardsContainer(),
)
),
],
),
);
function buildRewardsbody()
that returns a sliver element for displaying the content of the reward. The sliver element contains a column with three child widgets:
- The
UpcomingRewardsButton
widget: This is a custom button widget that shows the text “Upcoming rewards on your path” and a “>” icon on the right. When the user taps on this button, theonTap
function is called, and you can add your navigation code to this function. - The
Text
widget: This widget shows the heading “My Rewards” in black color with a normal font weight and a font size of 20. - The
GridView.builder
widget: This widget creates a grid view with two columns and an infinite number of rows. It uses theSliverGridDelegateWithFixedCrossAxisCount
delegate to specify the number of columns and the spacing between them. TheitemCount
parameter specifies the total number of items in the grid view.
For each item in the grid view, the itemBuilder
callback function is called, which creates a padding widget with the buildRewardsContainer()
function as its child. The buildRewardsContainer()
function returns a container widget with a reward image, a reward title, and a reward description.
Stateless Class UpcomingRewardsButton
class UpcomingRewardsButton extends StatelessWidget {
final VoidCallback onTap;
const UpcomingRewardsButton({Key? key, required this.onTap}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.06),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Upcoming rewards on your path',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
Spacer(),
Icon(Icons.arrow_forward),
SizedBox(width: 16),
],
),
),
);
}
}
Widget Function buildRewardsContainer

Widget buildRewardsContainer() => Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey)
),
child: SizedBox(
height: 100,
width: 100,
child: Stack(
children:[
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Flat ₹5000",style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),),
Expanded(child: Text('''Bonus cash on1st deposit on My11Circle''',maxLines: 2,)),
]
),
),
const CircleAvatar(
radius: 100*0.11,
backgroundColor: Colors.teal,
child: CircleAvatar(
// backgroundImage: AssetImage('assets/appdev.png'),
backgroundColor: Colors.white,
radius: 100*0.1,
),
),
],
),
),
],
),
),
);
Output
Full Source Code
import 'package:flutter/material.dart';
class GPayRewardScreen extends StatefulWidget {
@override
State<GPayRewardScreen> createState() => _GPayRewardScreenState();
}
class _GPayRewardScreenState extends State<GPayRewardScreen> {
ScrollController? _scrollController;
bool lastStatus = true;
double height = 200;
void _scrollListener() {
if (_isShrink != lastStatus) {
setState(() {
lastStatus = _isShrink;
});
}
}
bool get _isShrink {
return _scrollController != null &&
_scrollController!.hasClients &&
_scrollController!.offset > (height - kToolbarHeight);
}
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_scrollListener);
}
@override
void dispose() {
_scrollController?.removeListener(_scrollListener);
_scrollController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
elevation: 0,
backgroundColor: _isShrink ? Colors.white : Colors.transparent,
pinned: true,
expandedHeight: height,
leading:const BackButton(
color: Colors.black,
),
//title will hide when we will scroll up
title: _isShrink // <-- SEE HERE
? const Text(
"₹62 total rewards",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 18),
)
: null,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.more_vert,
color: Colors.black,
),
),
],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: SafeArea(
child: Stack(
children: [
Container(
width: double.infinity,
child: SingleChildScrollView(scrollDirection: Axis.horizontal,child: Image.asset("assets/background_image.jpg")),
//in future we will add image
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
alignment: Alignment.topCenter,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Text(
"₹62",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 24),
),
Text(
"Total rewards",
style: TextStyle(color: Colors.black),
),
],
),
),
),
],
),
),
),
),
];
},
body: CustomScrollView(slivers: [buildRewardsbody()]),
),
);
}
Widget buildRewardsbody() => SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: UpcomingRewardsButton(
onTap: () {
// Add your navigation code here
},
),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("My Rewards",style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 20),),
),
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 0.0,
mainAxisSpacing: 0.0,
),
primary: false,
shrinkWrap: true,
itemCount: 20,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: buildRewardsContainer(),
)
),
],
),
);
class UpcomingRewardsButton extends StatelessWidget {
final VoidCallback onTap;
const UpcomingRewardsButton({Key? key, required this.onTap}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.06),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Upcoming rewards on your path',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
Spacer(),
Icon(Icons.arrow_forward),
SizedBox(width: 16),
],
),
),
);
}
}
Widget buildRewardsContainer() => Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey)
),
child: SizedBox(
height: 100,
width: 100,
child: Stack(
children:[
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Flat ₹5000",style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),),
Expanded(child: Text('''Bonus cash on1st deposit on My11Circle''',maxLines: 2,)),
]
),
),
const CircleAvatar(
radius: 100*0.11,
backgroundColor: Colors.teal,
child: CircleAvatar(
// backgroundImage: AssetImage('assets/appdev.png'),
backgroundColor: Colors.white,
radius: 100*0.1,
),
),
],
),
),
],
),
),
);
Conclusion
we have learned how to create a user interface similar to the Google Pay Rewards screen using Flutter. We started by creating a custom SliverAppBar with a flexible space bar and parallax effect. Then, we added a button widget with text and an icon using a Container and InkWell widget. After that, we created a GridView.builder widget with a fixed cross-axis count and infinite scrolling functionality. Finally, we wrapped all these widgets inside a CustomScrollView and a SliverToBoxAdapter to create a responsive layout.
With these widgets and techniques, we can build a UI with a similar look and feel as the Google Pay Rewards screen. This UI is not only aesthetically pleasing but also highly functional and can be used in a variety of applications. By understanding these concepts, developers can create a wide range of visually appealing interfaces in their Flutter applications.
How to Implement the 60-30-10 Rule in Flutter UI Design: Complete Guide