====Exeption caught by widgets library ==== Class 'String' has no instance method 'toStringAsFixed' - dart

I am very new to flutter and following a tutorial video on how to create a cart for a mobile application, this has a cart Screen with a widget inside it for calculating a cart total, this mentioned cart total widget relies on another file i named cart controller to calculate a subtotal for each item added to cart and a grand total for all cart items within the cart.
however when i build the app i keep getting an error stating Class 'String' has no instance method 'toStringAsFixed'
I need help on how i can be able to show the total of all items available in the cart
heres my code for the cart controller:
import 'package:get/get.dart';
import 'package:shop_app/models/things.dart';
class CartController extends GetxController{
//Add dictionary to store the products in the cart
var _things = {}.obs;
//void AddProduct
void addProduct(Things things) {
if (_things.containsKey(things)){
_things[things] += 1;
} else {_things[things] = 1;}
Get.snackbar("Product added!","You have added ${things.title} to the cart",
snackPosition: SnackPosition.TOP,
duration: Duration(seconds: 2),
);
}
//void RemoveProduct
void removeProduct(Things things){
if (_things.containsKey(things) && _things[things] == 1) {
_things.removeWhere((key, value) => key == things);
}
else {
_things[things] -= 1;
}
Get.snackbar("Product removed!","You have removed ${things.title} from the cart",
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
}
//get Products;
get products => _things;
//get ProductsSubtotal
get productsSubtotal => _things.entries
.map((things) => things.key.price * things.value)
.toList();
//get Total
get total => _things.entries
.map((things) => things.key.price * things.value)
.toList()
.reduce((value, element) => value + element)
.toStringAsFixed(2);
}
heres my code for the cart total widget that's supposed to display this grand total:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shop_app/controllers/cart_controller.dart';
class CartTotal extends StatelessWidget {
final CartController controller = Get.find();
CartTotal({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 50),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Obx( () =>
Text(
//
'${controller.total}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}
this is what i'm going for

Related

TextController on TextField doesn't listen to changes && OnChanged after being edited once doesn't work anymore

I want to be able to edit the TextField and after all of them are filled, a value is calculated. Every time the user change a value, the value must be recalculated.
I've tried with both the Streams and Controller. The stream was doing fine, even tho the update of the value was done with data's that were 1 change old.
i.e. height, weight and age all must be != null, when we have at first {height: 2, weight:3, age:2}, it doesn't get calculated, after one of them is changed then the result is calculated but with the above data.
The controller instead doesn't seem to listen on the variable change at all.
Here I initialize the controller in another class
class Bsmi extends StatelessWidget {
StreamController<void> valueBoxStream = StreamController<void>.broadcast();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new DefaultAppBar(context: context, title: Text(" ")),
body: new ListView(
children: <Widget>[
BsmiResult(valueBoxStream, {}),
new Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
),
child: ListView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
padding: EdgeInsets.all(20.0),
itemExtent: 80.0,
children: <Widget>[
_BsmiResultState().indices("height", valueBoxStream),
_BsmiResultState().indices("weight", valueBoxStream),
_BsmiResultState().indices("age", valueBoxStream),
],
),
),
],
),
);
}
}
This is the important code
class BsmiResult extends StatefulWidget {
final StreamController<void> valueBoxStream;
const BsmiResult(this.valueBoxStream, this.data);
final Map data;
#override
_BsmiResultState createState() =>
_BsmiResultState(valueBoxStream: this.valueBoxStream, data: this.data);
}
class _BsmiResultState extends State<BsmiResult> {
_BsmiResultState({this.valueBoxStream, this.data});
StreamController<void> valueBoxStream;
final Map data;
final textFieldController = new TextEditingController();
int weight;
int age;
int height;
String _result = "0.0";
_printLatestValue() {
print("Second text field: ${textFieldController.text}");
}
void setData(data) {
if (data['type'] == 'height') {
print("I'm inside height\n");
height = int.parse(data['value']);
} else if (data['type'] == 'weight') {
print("I'm inside weight\n");
weight = int.parse(data['value']);
} else {
print("I'm inside age\n");
age = int.parse(data['value']);
}
}
#override
void initState() {
super.initState();
textFieldController.addListener(_printLatestValue);
valueBoxStream.stream.listen((_){
_calculateResult();
});
}
#override
void dispose() {
// Clean up the controller when the Widget is removed from the Widget tree
textFieldController.dispose();
super.dispose();
}
void _calculateResult() {
if ((height != null) && (height != 0) && (weight != null) && (weight != 0) && (age != null) && (age != 0)) {
print("height: " +
height.toString() +
"" +
" weight: " +
weight.toString() +
" age: " +
age.toString());
_result = ((height + weight) / age).toString();
print(_result + "\n");
} else {
_result = "0.0";
}
}
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Text("Risultato"),
new Container(
decoration: new BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
), //For the controller i don't use the StreamBuilder but a normal Text("$_result"),
child: StreamBuilder(
stream: valueBoxStream.stream,
builder: (context, snapshot) {
if (snapshot.hasData || _result != "0.0") {
print(snapshot.data);
setData(snapshot.data);
return new Text(
"$_result",
textAlign: TextAlign.end,
);
} else {
return new Text(
"0.0",
textAlign: TextAlign.end,
);
}
},
),
),
],
);
}
Row indices(String text, StreamController<void> valueBoxStream) {
return new Row(
children: <Widget>[
leftSection(text),
rightSection(text, valueBoxStream),
],
);
}
Widget leftSection(text) {
return new Expanded(
child: new Container(
child: new Text(
text,
style: TextStyle(fontSize: 18),
),
),
);
}
Container rightSection(text, valueBoxStream) {
return new Container(
child: new Flexible(
flex: 1,
child: new TextField(
controller: textFieldController,
keyboardType: TextInputType.number,
textAlign: TextAlign.end,
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter(new RegExp("[0-9.]")),
LengthLimitingTextInputFormatter(4),
],
decoration: new InputDecoration(
enabledBorder: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(15),
borderSide: new BorderSide(width: 1.2),
),
border: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(15),
borderSide: new BorderSide(color: Colors.lightBlueAccent),
),
hintText: '0.0',
),
onChanged: (val) {
valueBoxStream.add({'type': text, 'value': val});
},
),
),
);
}
}
I should be able at this point to get Result: x.y, but I get it only once after I change the textField only once and in the same 'session'
Thanks in advance who can really explain to me why this is not working and what errors I'm making.
I'm really sorry.. but there have been so many things wrong with that code, I'm not even sure where to begin. It looks like you read all versions on how to handle state in flutter and managed to combine it all in one simple use case.. Here is one working example fix: https://gitlab.com/hpoul/clinimetric/commit/413d32d4041f2e6adb7e30af1126af4b05e72271
I'm not sure how to answer this "question" so that others could possibly benefit from it. but..
If you create a stream controller or a text view controller.. You have state.. hence, create a stateful widget (otherwise you can't for example close streams and subscriptions)
Get a handle on which widget is responsible for which state. And pass down only useful values to child views. One possible architecture is to use a ViewController and use Sinks as inputs for change events, and Streams for handling those events. So if you create a stream controller, only use a streamController.sink if you want to notify of events, and use a streamController.stream if a widget needs to listen to events
the build method is not the right place to handle events. I refactored your code that the handling of events and calculation of events is done in the listener of the stream. If you want to use a StreamBuilder you could have a StreamController and your producers directly push the Result into the sink.. so the builder method of the StreamBuilder would only need to do Text('${snapshot.data.value}')
There are probably quite a few things i'm missing in that list. but that should get you started. look at my change set. It is not perfect, but it works. You should try to understand what you are doing and reduce complexity instead of stacking everything you find on top of each other.

How to fix setState after disposeError on a screen with a tabBar

So in my app, I have a screen with a tabBar. When the screen loads, it sorts out the items and moves them into different tabs. But when I run this, The Items keep duplicating, and I am shown an error in the debug console that says I called setState() after dispose()
Here's the code for the screen:
import 'package:flutter/material.dart';
import './uiComponents/customWidgets.dart';
import './ticketsComponents/ticketsList.dart';
import './tabs.dart';
class Tickets extends StatefulWidget {
Tickets({ this.tickets, this.user });
final List tickets;
final Map user;
#override
_TicketsState createState() => new _TicketsState();
}
class _TicketsState extends State<Tickets> with SingleTickerProviderStateMixin {
TabController controller; // Tab controller for the screen
List _tickets;
// Variables to Store the sorted Tickets
List _availableTickets = [];
List _usedTickets = [];
List _expiredTickets = [];
#override
void initState(){
controller = new TabController(
vsync: this,
length: 4,
initialIndex: 1
);
WidgetsBinding.instance.addPersistentFrameCallback((_) async {
// Get the tickets and sort them
_tickets = widget.tickets;
if(_tickets != null){
_sortTickets();
}
});
super.initState();
}
#override
void dispose(){
controller.dispose();
super.dispose();
}
// DELETE A TICKET (FROM ID)
void deleteTicket(int id){
setState(() {
_tickets.removeWhere((item)=> item["id"] == id);
_availableTickets = [];
_usedTickets = [];
_expiredTickets = [];
_sortTickets();
});
}
// SORT THE TICKETS INTO AVAILABLE / UNUSED, USED AND EXPIRED
void _sortTickets(){
for (int i = 0; i < _tickets.length; i++){
Map ticket = _tickets[i];
if(ticket["isUsed"]){
setState(() {
_usedTickets.add(ticket);
});
}
else if(ticket["expired"]){
setState(() {
_expiredTickets.add(ticket);
});
}
else{
setState(() {
_availableTickets.add(ticket);
});
}
}
}
// NAVIGATE TO MAIN TAB AND CLEAR PREVIOUS ROUTES
void _navProfile(){
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => Tabs(
user: widget.user
)
),
(route)=> false
);
}
// TabBar for Filtering Tickets
TabBar _buildTabBar(){
return new TabBar(
controller: controller,
indicatorWeight:2.2,
labelStyle: TextStyle(
fontSize:14.0,
fontWeight: FontWeight.bold
),
unselectedLabelStyle: TextStyle(
fontWeight:FontWeight.normal
),
labelColor: blue,
indicatorColor: blue,
unselectedLabelColor: Colors.black,
tabs: [
Tab(text: "All"),
Tab(text: "Available"),
Tab(text: "Used"),
Tab(text: "Expired")
],
);
}
// THE AppBar with the sub menu under it
AppBar _buildAppBar(){
Function onBackButtonPressed = _navProfile;
return new AppBar(
title: customTextBold("My Tickets"),
iconTheme: new IconThemeData(
color: blue
),
leading: GestureDetector(
child: Icon(Icons.arrow_back, color: blue),
onTap: onBackButtonPressed,
),
elevation: 1.0,
backgroundColor: Colors.white,
bottom: _buildTabBar()
);
}
// BUILD MAIN SCREEN
Container _buildTicketsPage(){
return new Container(
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: backgroundBlue
),
child: new TabBarView(
controller: controller,
children: [
TicketsList(
tickets: _tickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _availableTickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _usedTickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _expiredTickets,
deleteTicket: deleteTicket,
)
],
)
);
}
// UI
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: _buildAppBar(),
body: (_tickets == null)
? buildLoadingScreen("Fetching Tickets")
: _buildTicketsPage()
);
}
}
Running this will render the correct screen, but the ticket items will start duplicating, and this error is displayed on the debug console:
.
E/flutter (31673): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: setState() called after dispose(): _TicketsState#2cafe(lifecycle state: defunct, not mounted, ticker inactive)
E/flutter (31673): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the t
Please how do I fix this ?
add before each setState a condition
if(!mounted) return;
for example:
if(!mounted) return;
setState(() {
_expiredTickets.add(ticket);
});

Flutter BLoC being recreated

I'm discovering Flutter and the bloc pattern and to practice I'm making an app about pizzas.
I am using a BlocProvider to access the blocks. It is from the generic_bloc_provider package. It is a basic implementation using an InheritedWidget combined with a StatelessWidget.
I have a page with two editable textfields, for the name and price of the pizza I want to create. It is backed by a bloc.
Here's the code :
AddPizzaPage.dart :
class AddPizzaPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("Building AddPizzaPage");
return Scaffold(
appBar: AppBar(
title: Text("Adding Pizza"),
),
body: BlocProvider(
bloc: AddPizzaBloc(),
child: ModifyPizzaWidget(),
),
);
}
}
ListPage.dart:
class ModifyPizzaWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final addPizzaBloc = BlocProvider.of<AddPizzaBloc>(context);
return Container(
margin: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(hintText: "Nom de la pizza"),
onChanged: (name) {
addPizzaBloc.pizzaNameSink.add(name);
},
),
TextField(
decoration: InputDecoration(hintText: "Prix de la pizza"),
keyboardType: TextInputType.number,
onChanged: (price) {
addPizzaBloc.pizzaPriceSink.add(price);
},
),
IconButton(
icon: Icon(Icons.check),
iconSize: 40,
onPressed: () {
addPizzaBloc.evenSink.add(AddPizzaEvent.VALIDATE);
Navigator.of(context).pop();
},
)
],
),
);
}
}
AddPizzaBloc.dart :
enum AddPizzaEvent {
VALIDATE
}
class AddPizzaBloc extends Bloc {
final _pizza = Pizza.empty();
final _pizzaSubject = BehaviorSubject<Pizza>();
final _repository = PizzaRepository();
Sink<String> get pizzaNameSink => _pizzaNameController.sink;
final _pizzaNameController = StreamController<String>();
Sink<String> get pizzaPriceSink => _pizzaPriceController.sink;
final _pizzaPriceController = StreamController<String>();
Sink<AddPizzaEvent> get evenSink => _eventSink.sink;
final _eventSink = StreamController<AddPizzaEvent>();
AddPizzaBloc() {
print("Created");
_pizzaNameController.stream.listen(_addPizzaName);
_pizzaPriceController.stream.listen(_addPizzaPrice);
_eventSink.stream.listen(_onEventReceived);
}
dispose() {
print("Disposed");
_pizzaSubject.close();
_pizzaNameController.close();
_pizzaPriceController.close();
_eventSink.close();
}
void _addPizzaName(String pizzaName) {
_pizza.name = pizzaName;
print(_pizza);
}
void _addPizzaPrice(String price) {
var pizzaPrice = double.tryParse(price) ?? 0.0;
_pizza.price = pizzaPrice;
print(_pizza);
}
void _onEventReceived(AddPizzaEvent event) {
print("Received $event");
if (event == AddPizzaEvent.VALIDATE) {
print(_pizza);
_repository.addPizza(_pizza);
}
}
}
My issue is that I store the Pizza being built inside the block but the widget is rebuilt, and so the bloc is rebuilt and I lose the state.
The full code is available on gitlab
I don't know how to use the bloc to power the addPizza form.
This happens because you're creating the instance of your BLoC within the build method:
BlocProvider(
bloc: Bloc(),
child: ...
)
The consequence is that any rebuild would not reuse the previous instance (with some awful memory leaks too).
The solution would be to make a StatefulWidget and create that BLoC instance within initState, followed by a dispose override to clean things.
But since you're using a package already, you can use provider instead. It is a popular alternative that does everything listed above.
As such your BlocProvider usage becomes:
StatefulProvider(
valueBuilder: (_) => AddPizzaBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: // ...
),
then obtained as such:
Provider.of<AddPizzaBloc>(context);

How to Conditionally Route Using One Button? (Flutter)

I have a scene (collections.dart) that takes an index of several other scenes/files in a PageView.builder. You can swipe between scenes from the collections.dart file. Also in collections.dart is a button.
I want it to be the case that if you click on the button, and the current scene being shown through collections.dart is, for example, FirstScreen, then I can route to a table I have built specifically for first.dart, with the same being true for all other scenes in the index.
I have tried to accomplish this by a conditional statement in the onPressed argument, but no success yet. There is no error, it just takes no action. Here is the code in its entirety for collections.dart (including the unsuccessful conditional statement for onPressed):
import 'package:flutter/material.dart';
import 'package:circle_indicator/circle_indicator.dart';
import 'first.dart';
import 'second.dart';
import 'third.dart';
import 'fourth.dart';
import 'fifth.dart';
import 'sixth.dart';
import 'seventh.dart';
import 'eighth.dart';
import 'ninth.dart';
import 'tenth.dart';
class CollectionsScreen extends StatelessWidget {
#override
Widget build(BuildContext context){
return Collections();
}
}
class Collections extends StatefulWidget {
#override
CollectionsState createState() => CollectionsState();
}
class CollectionsState extends State<Collections> {
FirstScreen one;
SecondScreen two;
ThirdScreen three;
FourthScreen four;
FifthScreen five;
SixthScreen six;
SeventhScreen seven;
EighthScreen eight;
NinthScreen nine;
TenthScreen ten;
List<Widget> pages;
#override
void initState() {
one = FirstScreen();
two = SecondScreen();
three = ThirdScreen();
four = FourthScreen();
five = FifthScreen();
six = SixthScreen();
seven = SeventhScreen();
eight = EighthScreen();
nine = NinthScreen();
ten = TenthScreen();
pages = [one, two, three, four, five, six, seven, eight, nine, ten];
super.initState();
}
final PageController controller = new PageController();
#override
Widget build(BuildContext context){
return new Stack(
children: <Widget>[
new Scaffold(
body: new Container(
child: new PageView.builder( //Swipe Between Pages
controller: controller,
itemCount: 10,
itemBuilder: (context, index){
return pages[index];
}
),
),
),
new Container( //CircleIndicator
child: new CircleIndicator(controller, 10, 8.0, Colors.white70, Colors.white,),
alignment: Alignment(0.0, 0.9),
),
new Container( //Button
alignment: Alignment(0.0, 0.65),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new Container(
child: new RaisedButton(
elevation: 4.0,
child: new Text(
'SHOW ME',
style: new TextStyle(
fontWeight: FontWeight.w900,
fontSize: 22.0,
),
),
color: Color(0xFF70E0EF),
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(7.5)
),
//This is the conditional statement I'm talking about
onPressed: () {
new PageView.builder(
controller: controller,
itemBuilder: (context, index) {
if (pages[index] == one){
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new FirstTable()),
);
}
else if (pages[index] == two){
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new SecondTable()),
);
}
else {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new ThirdTable()),
);
}
}
);
},
),
width: 150.0,
height: 60.0,
),
],
),
),
],
);
}
}
The "Table" classes I'm referring to in the conditional statement are in the files for first.dart, second.dart, etc. Here is the file for first.dart. For the moment, the code is identical between all these files (first.dart, second.dart, etc.):
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new First();
}
}
class First extends StatefulWidget {
#override
FirstState createState() => FirstState();
}
class FirstState extends State<First>{
#override
Widget build(BuildContext context) {
double fontSize = MediaQuery.of(context).size.height;
double fontSizeFractional = fontSize * 0.07;
return Scaffold(
body: new Stack(
fit: StackFit.passthrough,
children: [
new Container( //Background
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('assets/FirstBG.png'),
fit: BoxFit.cover
),
),
),
new Container( //Title
margin: EdgeInsets.all(40.0),
alignment: new Alignment(0.0, -0.70),
child: new Text(
'FIRST',
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: fontSizeFractional,
color: Colors.white,
fontFamily: 'baron neue',
),
),
),
],
),
);
}
}
class FirstTable extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Table();
}
}
class Table extends StatefulWidget {
#override
TableState createState() => TableState();
}
class TableState extends State<Table>{
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: new Text(
'Go Back',
),
),
),
);
}
}
I have a theory that the reason it isn't working is that collections.dart isn't actually caching any data as to what page of the index it is on (that could be totally wrong, though). Curious to hear your ideas!
Your problem is that you should directly use controller.page inside the onPressed of your button. instead of instantiating a widget.
Although ultimately you should hide an abstract layer between your gallery class and the list of items.
To do that you can create a custom class which will hols all informations about a gallery item :
#immutable
class GalleryItem {
final Widget content;
final Widget details;
GalleryItem({#required this.content, this.details}) : assert(content != null);
}
Your gallery will then take a list of such class as parameter. And do it's job with these.
Ideally you want to use your gallery like this :
Gallery(
items: [
GalleryItem(
content: Container(
color: Colors.red,
),
details: Text("red"),
),
GalleryItem(
content: Container(
color: Colors.blue,
),
details: Text("blue"),
),
],
),
The code of such gallery would be :
class Gallery extends StatefulWidget {
final List<GalleryItem> items;
Gallery({#required this.items, Key key})
: assert(items != null),
super(key: key);
#override
_GalleryState createState() => _GalleryState();
}
class _GalleryState extends State<Gallery> {
final PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: PageView(
children: widget.items.map((item) => item.content).toList(),
controller: pageController,
),
),
RaisedButton(
onPressed: showContentDetails,
child: Text("More info"),
)
],
);
}
void showContentDetails() {
final index = pageController.page.round();
if (widget.items[index]?.details != null) {
showDialog(
context: context,
builder: (_) =>
GalleryItemDetails(details: widget.items[index].details),
);
}
}
}
class GalleryItemDetails extends StatelessWidget {
final Widget details;
GalleryItemDetails({#required this.details, Key key})
: assert(details != null),
super(key: key);
#override
Widget build(BuildContext context) {
return Dialog(
child: details,
);
}
}

Nesting PageViews in flutter

I want to nest PageViews in flutter, with a PageView inside a Scaffold inside a PageView. In the outer PageView I will have the logo and contact informations, as well as secundary infos. As a child, I will have a scaffold with the inner PageView and a BottomNavigationBar as the main user interaction screen. Here is the code I have so far:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp>{
int index = 0;
final PageController pageController = PageController();
final Curve _curve = Curves.ease;
final Duration _duration = Duration(milliseconds: 300);
_navigateToPage(value){
pageController.animateToPage(
value,
duration: _duration,
curve: _curve
);
setState((){
index = value;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageViewCeption',
home: PageView(
children: <Widget>[
Container(
color: Colors.blue,
),
Scaffold(
body: PageView(
controller: pageController,
onPageChanged: (page){
setState(() {
index = page;
});
},
children: <Widget>[
Container(
child: Center(
child: Text('1', style: TextStyle(color: Colors.white))
)
),
Container(
child: Center(
child: Text('2', style: TextStyle(color: Colors.white))
)
),
Container(
child: Center(
child: Text('3', style: TextStyle(color: Colors.white))
)
),
],
),
backgroundColor: Colors.green,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (value) =>_navigateToPage(value),
currentIndex: index,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('1')
),
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('2')
),
BottomNavigationBarItem(
icon: Icon(Icons.cake),
title: Text('3')
)
],
),
),
Container(
color: Colors.blue
)
],
),
);
}
}
Here is the result:
Problem is: When I am in the inner PageView, I can't get away from it to the outer one scrolling left on the first page, or scrolling right on the last page of the inner PageView. The only way to go back to the outer PageView in scrolling (swiping) on the BottomNavigationBar.
In the docs of the Scroll Physics Class we find this in the description:
For example, determines how the Scrollable will behave when the user reaches the maximum scroll extent or when the user stops scrolling.
But I haven't been able to come up with a solution yet. Any thoughts?
Update 1
I had progress working with a CustomScrollPhysics class:
class CustomScrollPhysics extends ScrollPhysics{
final PageController _controller;
const CustomScrollPhysics(this._controller, {ScrollPhysics parent }) : super(parent: parent);
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(_controller, parent: buildParent(ancestor));
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw new FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
);
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent){ // underscroll
_controller.jumpTo(position.viewportDimension + value);
return 0.0;
}
if (position.maxScrollExtent <= position.pixels && position.pixels < value) {// overscroll
_controller.jumpTo(position.viewportDimension + (value - position.viewportDimension*2));
return 0.0;
}
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
}
Which is a modification of the ClampingScrollPhysics applyBoundaryConditions. It kinda works but because of the pageSnapping it is really buggy. It happens, because according to the docs:
Any active animation is canceled. If the user is currently scrolling, that
action is canceled.
When the action is canceled, the PageView starts to snap back to the Scafold page, if the user stop draggin the screen, and this messes things up. Any ideas on how to avoid the page snapping in this case, or for better implementation for that matter?
I was able to replicate the issue on the nested PageView. It seems that the inner PageView overrides the detected gestures. This explains why we're unable to navigate to other pages of the outer PageView, but the BottomNavigationBar can. More details of this behavior is explained in this thread.
As a workaround, you can use a single PageView and just hide the BottomNavigationBar on the outer pages. I've modified your code a bit.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
var index = 0;
final PageController pageController = PageController();
final Curve _curve = Curves.ease;
final Duration _duration = Duration(milliseconds: 300);
var isBottomBarVisible = false;
_navigateToPage(value) {
// When BottomNavigationBar button is clicked, navigate to assigned page
switch (value) {
case 0:
value = 1;
break;
case 1:
value = 2;
break;
case 2:
value = 3;
break;
}
pageController.animateToPage(value, duration: _duration, curve: _curve);
setState(() {
index = value;
});
}
// Set BottomNavigationBar indicator only on pages allowed
_getNavBarIndex(index) {
if (index <= 1)
return 0;
else if (index == 2)
return 1;
else if (index >= 3)
return 2;
else
return 0;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageViewCeption',
home: Scaffold(
body: Container(
child: PageView(
controller: pageController,
onPageChanged: (page) {
setState(() {
// BottomNavigationBar only appears on page 1 to 3
isBottomBarVisible = page > 0 && page < 4;
print('page: $page bottom bar: $isBottomBarVisible');
index = page;
});
},
children: <Widget>[
Container(
color: Colors.red,
),
Container(
color: Colors.orange,
),
Container(
color: Colors.yellow,
),
Container(
color: Colors.green,
),
Container(color: Colors.lightBlue)
],
),
),
bottomNavigationBar: isBottomBarVisible // if true, generate BottomNavigationBar
? new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (value) => _navigateToPage(value),
currentIndex: _getNavBarIndex(index),
items: [
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '1'),
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '2'),
BottomNavigationBarItem(icon: Icon(Icons.cake), label: '3')
],
)
//else, create an empty container to hide the BottomNavigationBar
: Container(
height: 0,
),
),
);
}
}

Resources