how to use GetX Flutter package to create a PageView

Nastaran Mohammadi
8 min readJan 2, 2021

--

Greetings, I am a Flutter developer and in this tutorial, I will demonstrate how to utilize the GetX package to build a PageView in Flutter.

To start, let’s create a new Flutter project by running the following command in the terminal:

flutter create pageView

Next, we will run our project on an emulator and observe the result:

Initially, I will construct the PageView in Flutter using a StatefulWidget to demonstrate the process. Afterward, I will convert it to a StatelessWidget and manage the state with the Flutter GetX package.

Now let’s go! First, let’s define two variables :

int page = 0;
PageController controller = PageController();

Next, we create a view using a Stack widget with two children: a Body widget and a BottomNavBar widget. The code is as follows:

@override
Widget build(BuildContext context) {

var width = MediaQuery.of(context).size.width;

return new Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: Stack(
children: [
PageView(
children: <Widget>[
Home(),
Search(),
Liked(),
News()
],
controller: controller,
pageSnapping: false,
physics: NeverScrollableScrollPhysics(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: bottomNav(width),
),
],
),
));
}

I created a Home screen with the following code :

import 'package:flutter/material.dart';

class Home extends StatelessWidget {

@override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
return new Container(
height: height,
alignment: Alignment.center,
child: Text('Home'),
);
}

}

It’s the same story for the other pages. Now, what is the bottomNav function? It's the following code:

bottomNav(width){
return Container(
height: 60,
width: width,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
boxShadow: [boxShadow],borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
item(0,Icons.home,width),
item(1,Icons.search,width),
item(2,Icons.favorite_outline,width),
item(3,Icons.fiber_new,width)
],
));
}

And the item function, which is responsible for the view of each item, is:

item(index,name,width) {
return Material(
color: Colors.white.withOpacity(.5),
borderRadius: BorderRadius.all(Radius.circular(9)),
child: InkWell(
splashColor: Colors.white.withOpacity(.1),
borderRadius: BorderRadius.all(Radius.circular(500)),
enableFeedback: true,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
),
width: width*.18,
height: 50,
child: Icon(name, color: index == page ? Colors.purple : Colors.grey,
size: index == page ? 30 : 25)
),
onTap: (){
navigateToPage(index);
},
),
);
}

Now, if we take a look at the emulator, we will see the following screen that we have created:

To change the page by tapping on each item, I need to call this function:

navigateToPage(int input) {
page = input;
controller.animateToPage(input, duration: Duration(milliseconds: 300), curve: Curves.ease);
setState(() {});
}

Let’s add complexity to the question. How can we change the page of a PageView from another page and scroll to the bottom? First, I will create another PageView inside one of the screens. The outcome will be:

What is the code for this? The Home page is:

class Home extends StatefulWidget {

@override
HomeState createState() => HomeState();
}

class HomeState extends State<Home> {

var page = 0;
var controller = PageController();
var fromController = ScrollController(initialScrollOffset: 0.0);

@override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
return new Container(
height: height,
alignment: Alignment.center,
child: profileUi(width,height),
);
}

profileUi(width, height) {
return Container(
width: width,
height: height,
color: Colors.white,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
expandedHeight: 430,
floating: true,
pinned: true,
bottom: PreferredSize(
child: Material(
color: Colors.white,
child: tabBar(width),
),
preferredSize: Size.fromHeight(50),
),
flexibleSpace: FlexibleSpaceBar(
background: header(width,height)),
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
elevation: 0,
),
];
},
controller: fromController,
body: tabBarView(width, height)));
}

header(width,height) {
return Container(
height: 450,
width: width,
color: Colors.purple,
);
}

tabBar(width) {
return Container(
width: width,
color: Colors.amber,
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
animateTo(0);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Text('Page 1',
style: TextStyle(color: page == 0 ? Colors.white : Colors.grey)),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(1);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Text('Page2',
style: TextStyle(color: page == 1 ? Colors.white : Colors.grey)),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(2);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Text('Page 3',
style: TextStyle(color: page == 2 ? Colors.white : Colors.grey)),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(3);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Text('Page 4',
style: TextStyle(color: page == 3 ? Colors.white : Colors.grey)),
width: width * .23),
),
],
),
],
),
);
}

tabBarView(width, height) {
return Container(
height: height - 140,
padding: EdgeInsets.only(bottom: 65),
child: PageView(
children: <Widget>[
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 1'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 2'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 3'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 4'),
),
],
controller: controller,
onPageChanged: (value){
page = value;
setState(() {});
},
pageSnapping: true,
),
);
}

animateTo(int input) {
page = input;
controller.animateToPage(input, duration: Duration(milliseconds: 300), curve: Curves.ease);
setState(() {});
}
}

Now, I want to call the pageController of the Home page from other pages, such as a New page. To do this, I added some buttons to the New page, and the result is:

What about the code? The code for the News page is:

import 'package:flutter/material.dart';

typedef IntCallback = Function(int num);

class News extends StatelessWidget {

final IntCallback page;
News({this.page});

@override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
return new Container(
height: height,
alignment: Alignment.center,
child: SingleChildScrollView(
child: Column(
children: [
OutlineButton(
onPressed:()=>page(0),
child: Text('go Home and Page 1'),
),
OutlineButton(
onPressed:()=>page(1),
child: Text('go Home and Page 2'),
),
OutlineButton(
onPressed:()=>page(2),
child: Text('go Home and Page 3'),
)
],
),
),
);
}

}

On the bottom page, two parameters are defined:

int homePage = 0;
double scroll = 0.0;

And the PageView widget is changed to:

PageView(
children: <Widget>[
Home(page: homePage,scroll: scroll),
Search(),
Liked(),
News(page: (val){
setState(() {
homePage = val;
scroll = 390;
});
navigateToPage(0);
},)
],
controller: controller,
pageSnapping: false,
physics: NeverScrollableScrollPhysics(),
),

Inside the home page, two values are defined:

final int page;
final double scroll;
Home({this.page,this.scroll});

Inside the home page, in the initState method:

@override
void initState() {
page = widget.page ?? 0;
controller = PageController(initialPage: widget.page ?? 0);
fromController = ScrollController(initialScrollOffset: widget.scroll ?? 0.0);
super.initState();
}

Now the result is:

Let’s make the switch to StatelessWidgets even more thrilling! To do this, we need to install the amazing Flutter ‘get’ package. Get ready to take your app to the next level with this fantastic tool. Head over to the link below to grab your copy now!

Next, add the following line to your pubspec.yaml file:

get: ^3.24.0

With the get package installed, we can now proceed to creating a 'controller' folder. Within this folder, create two Dart files: homeController and bottomController. In the bottomController file, add the following code:

import 'package:get/get.dart';
import 'package:flutter/material.dart';

class BottomController extends GetxController{

var page = 0.obs;
var controller = PageController().obs;

onPageChanged(input) {
page.value = input;
}

animateTo(int page) {
if (controller.value.hasClients)
controller.value.animateToPage(page,
duration: Duration(milliseconds: 300), curve: Curves.easeIn);
}
}

And inside the homeController file, add the following code:

import 'package:get/get.dart';
import 'package:flutter/material.dart';

class OpenController extends GetxController{

var page = 0.obs;
var controller = PageController().obs;
var fromController = ScrollController(initialScrollOffset: 0.0).obs;

onPageChanged(input) {
page.value = input;
}

animateTo(int page) {
if (controller.value.hasClients)
controller.value.animateToPage(page,
duration: Duration(milliseconds: 300), curve: Curves.easeIn);
}

resetController(int page) {
controller.value = PageController(initialPage: page);
}

}

In the bottom page, convert it to a StatelessWidget and define the controllers. Remove the parameters and update the code to something like this: Remember, if the PageView view will be changed by the controller, it must be wrapped inside an Obx. The updated bottom page should now look like this:

class Bottom extends StatelessWidget {

@override
Widget build(BuildContext context) {

var width = MediaQuery.of(context).size.width;
final BottomController boController = Get.put(BottomController());
final OpenController drawer = Get.put(OpenController());

return new Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: Stack(
children: [
PageView(
children: <Widget>[
Home(),
Search(),
Liked(),
News(page: (val){
animateTo(val,drawer);
boController.animateTo(0);
boController.onPageChanged(0);
drawer.fromController.value = ScrollController(initialScrollOffset: 390);
},)
],
controller: boController.controller.value,
pageSnapping: false,
physics: NeverScrollableScrollPhysics(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: bottomNav(width,boController,drawer),
),
],
),
));
}

bottomNav(width,boController,drawer){
return Container(
height: 60,
width: width,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
boxShadow: [boxShadow],borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
item(0,Icons.home,width,boController,drawer),
item(1,Icons.search,width,boController,drawer),
item(2,Icons.favorite_outline,width,boController,drawer),
item(3,Icons.fiber_new,width,boController,drawer)
],
));
}

navigateToPage(int input,BottomController boController,OpenController drawer) {
boController.animateTo(input);
boController.onPageChanged(input);
if(input == 0){
drawer.onPageChanged(0);
drawer.resetController(input);
}
}

item(index,name,width,boController,drawer) {
return Material(
color: Colors.white.withOpacity(.5),
borderRadius: BorderRadius.all(Radius.circular(9)),
child: InkWell(
splashColor: Colors.white.withOpacity(.1),
borderRadius: BorderRadius.all(Radius.circular(500)),
enableFeedback: true,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
),
width: width*.18,
height: 50,
child: Obx(()=>Icon(name, color: index == boController.page.value ? Colors.purple : Colors.grey,
size: index == boController.page.value ? 30 : 25))
),
onTap: (){
navigateToPage(index,boController,drawer);
},
),
);
}

animateTo(int page,OpenController drawer) {
drawer.onPageChanged(page);
drawer.resetController(page);
}
}

Similarly, in the home page:

class Home extends StatelessWidget {

@override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;

final OpenController drawer = Get.find();


return new Container(
height: height,
alignment: Alignment.center,
child: profileUi(width,height,drawer),
);
}

profileUi(width, height,drawer) {
return Container(
width: width,
height: height,
color: Colors.white,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
expandedHeight: 430,
floating: true,
pinned: true,
bottom: PreferredSize(
child: Material(
color: Colors.white,
child: tabBar(width,drawer),
),
preferredSize: Size.fromHeight(50),
),
flexibleSpace: FlexibleSpaceBar(
background: header(width,height,drawer)),
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
elevation: 0,
),
];
},
controller: drawer.fromController.value,
body: tabBarView(width, height,drawer)));
}

header(width,height,drawer) {
return Container(
height: 450,
width: width,
color: Colors.purple,
);
}

tabBar(width,drawer) {
return Container(
width: width,
color: Colors.amber,
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
animateTo(0,drawer);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Obx(()=>Text('Page 1',
style: TextStyle(color: drawer.page.value == 0 ? Colors.white : Colors.grey))),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(1,drawer);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Obx(()=>Text('Page2',
style: TextStyle(color: drawer.page.value == 1 ? Colors.white : Colors.grey))),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(2,drawer);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Obx(()=>Text('Page 3',
style: TextStyle(color: drawer.page.value == 2 ? Colors.white : Colors.grey))),
width: width * .23),
),
InkWell(
onTap: () {
animateTo(3,drawer);
},
child: Container(
alignment: Alignment.center,
height: 50,
child: Obx(()=>Text('Page 4',
style: TextStyle(color: drawer.page.value == 3 ? Colors.white : Colors.grey))),
width: width * .23),
),
],
),
],
),
);
}

tabBarView(width, height,drawer) {
return Container(
height: height - 140,
padding: EdgeInsets.only(bottom: 65),
child: PageView(
children: <Widget>[
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 1'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 2'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 3'),
),
Container(
height: height,
alignment: Alignment.center,
child: Text('Page 4'),
),
],
controller: drawer.controller.value,
onPageChanged: (value){
drawer.onPageChanged(value);
},
pageSnapping: true,
),
);
}

animateTo(int input,drawer) {
drawer.onPageChanged(input);
drawer.animateTo(input);
}

}

By converting to StatelessWidgets and following the Model-View-Controller (MVC) architecture, we have successfully achieved the same result without the use of StatefulWidgets. Congratulations on this step towards becoming a knowledgeable and respectful developer! The full code is available at the following link:

--

--

Nastaran Mohammadi
Nastaran Mohammadi

Written by Nastaran Mohammadi

Experienced Mobile Developer in React Native and Flutter, known for crafting pixel-perfect UI and maintaining clean, maintainable code.