I'm trying to change the active page index via a pagecontroller in Flutter, using the Bloc pattern and its throwing "'_positions.isNotEmpty': ScrollController not attached to any scroll views.".
This is my code:
WelcomeWizardScreen:
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertest/blocs/wizard/bloc/bloc.dart';
import 'package:fluttertest/screens/wizardsteps/joincongregation.dart';
import 'package:fluttertest/screens/wizardsteps/welcometomapman.dart';
class WelcomeWizardScreen extends StatefulWidget {
#override
_WelcomeWizardScreenState createState() => _WelcomeWizardScreenState();
}
class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
final WizardBloc wizardBloc = WizardBloc();
#override
Widget build(BuildContext context) {
// TODO: implement build
return BlocProvider(
builder: (BuildContext context) => WizardBloc(),
child: PageView(
children: <Widget>[WelcomeToMapMan(), JoinCongregation()],
controller: wizardBloc.pageController,
),
);
}
}
WizardBloc:
import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'wizard_state.dart';
import 'wizard_event.dart';
class WizardBloc extends Bloc<WizardEvent, WizardState> {
int activeStep = 0;
final PageController pageController = PageController(initialPage: 0, keepPage: false, viewportFraction: 0.4);
#override
WizardState get initialState => WelcomeToMapManState();
#override
Stream<WizardState> mapEventToState(
WizardEvent event,
) async* {
if (event is ChangePage)
{
pageController.jumpToPage(event.pageIndex);
}
// TODO: Add Logic
}
Stream<WizardState> _mapJoinCongregationToState() async* {
}
}
One of the screens in the PageView:
class JoinCongregation extends StatelessWidget {
#override
Widget build(BuildContext context) {
final WizardBloc _wizardBloc = BlocProvider.of<WizardBloc>(context);
// TODO: implement build
return Column(
children: <Widget>[
Center(
child: Text("this is step 2"),
),
RaisedButton(
child: Text("back to step 1"),
onPressed: () => {_wizardBloc.dispatch(ChangePage(0))},
)
],
);
}
}
It seems like the PageViewController isn't "attached" to the PageView when it is called on to change pages, but it initalises correctly (on the correct page index).
How can I solve this? I'm fairly new to flutter.
You shouldn't create a PageController in a bloc because a bloc should not be coupled with the UI (theoretically you should be able to reuse your bloc between Flutter and AngularDart). Please refer to https://github.com/felangel/bloc/issues/18 for an example of how you can accomplish this.
Related
my appbar is disappeared when i go new page using navigator
actually, there was a yellow underline on the new page's text.
I found that's reason is the page didn't have Material.
so i added the Material Widget on that, so I could fixed the problem of text underline.
i think appbar disappeared problem also relates with Material,
but even i wrapped the code with Material widget, still the problem is not solved.
what can i do?
here is the navigator code.
exhibition[i] is what i wanted to transmit to new page(=DetailScreen)
InkWell(
onTap: () {Navigator.push(
context,
MaterialPageRoute(builder: (context)=>DetailScreen(exhibition: exhibitions[i])),
);
And the below is DetailScreen(new page)'s code.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'model_exhibitions.dart';
class DetailScreen extends StatefulWidget {
final Exhibition exhibition;
DetailScreen({required this.exhibition});
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
bool bookmark = false;
#override
void initState() {
super.initState();
bookmark = widget.exhibition.bookmark;
}
#override
Widget build(BuildContext context) {
return Material(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, 40, 0, 0),
child: Row( ...
Thank you for your help sincerely
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'model_exhibitions.dart';
class DetailScreen extends StatefulWidget {
final Exhibition exhibition;
DetailScreen({required this.exhibition});
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
bool bookmark = false;
#override
void initState() {
super.initState();
bookmark = widget.exhibition.bookmark;
}
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold( //add scaffold
body: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, 40, 0, 0),
child: Row( ...
It will not work without scaffold.
I think you are not getting what scaffold is and how it's behaviour
is?
Scaffold is a widget which provides your screen/route a default
behaviour similar to your android/ios screens like AppBar, Body,
Title, FloatingActionButton, Drawer etc.
So that you do not have to make yourself a new structure.
If you are not using scaffold, then your page will act like a plain
body structure in which you have to fill custom widgets as per your
requirements.
Using flutter 1.20.4
I'm trying to implement a custom digit keyboard (a simple container at the bottom), which should appear into view when tapping on one of the rows in the List.
Here a small snippet of how I do it:
Widget build(BuildContext context) {
BudgetPageState budgetPageState = Provider.of<BudgetPageState>(context, listen: true);
ButtonDial buttonDial =
budgetPageState.showButtonDial ? ButtonDial() : null;
return Scaffold(
appBar: AppBar(
title: Text("Budget Page"),
),
body: Column(children: <Widget>[
Expanded(child: CustomList()),
if (buttonDial != null) buttonDial
]));
}
}
However, when the keyboard appears, the bottom rows get obscured by the container.
I tried using Scrollable.ensureVisible, that works for the middle rows, but the last ones are still obscured. It seems like the ScrollView still has it's old size (full height) when Scrollable.ensureVisible() kicks in (I notice this by looking at the ScrollBar).
Code snippet:
Scrollable.ensureVisible(context, duration: Duration(milliseconds: 200), alignment: 0.5);
See video below.
Keyboard obscures last rows when tapped (here clicking on row 14)
However, once the keyboard is up, the SingleChildScrollView has shrunk to the new size and the Scrollable now works.
When keyboard is up, Scrollable.ensureVisible() does its job(here clicking on row 6 and 12)
I know this is similar to this question, but
I tried multiple things of this issue.
I use a "custom keyboard"
The flutter github issue here below fixed this (I think)
Read through this popular Flutter Github issue, this made me use SingleChildScrollView instead of ListView.
Tried this, this fixes the keyboard obscuring the bottom Rows by shifting them up, however now when clicking on the first Rows, they get moved out of view.
Tried KeyboardAvoider, but as this is not an onscreen Keyboard, I doesn't work.
You'll find a full minimal reproducible example here below.
main.dart
(Main + ChangeNotifierProvider for the state)
import 'package:flutter/material.dart';
import 'package:scrollTest/budgetPage.dart';
import 'package:scrollTest/budgetPageState.dart';
import 'package:provider/provider.dart';
void main() {
runApp(HomeScreen());
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ChangeNotifierProvider(
create: (_) => BudgetPageState(), child: BudgetPage()),
),
);
}
}
budgetPage.dart
(Main Page with the CustomList() and the buttonDial (custom keyboard, here just a simple container)
import 'package:flutter/material.dart';
import 'package:scrollTest/budgetPageState.dart';
import 'package:scrollTest/customList.dart';
import 'package:provider/provider.dart';
class BudgetPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
BudgetPageState budgetPageState = Provider.of<BudgetPageState>(context, listen: true);
ButtonDial buttonDial =
budgetPageState.showButtonDial ? ButtonDial() : null;
return Scaffold(
appBar: AppBar(
title: Text("Budget Page"),
),
body: Column(children: <Widget>[
Expanded(child: CustomList()),
if (buttonDial != null) buttonDial
]));
}
}
class ButtonDial extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.3,
child: Container(
color: Colors.blue,
),
);
}
}
customList.dart
(Simple List view SingleChildScrollView and a ScrollController)
import 'package:flutter/material.dart';
import 'package:scrollTest/CustomRow.dart';
class CustomList extends StatefulWidget {
#override
_CustomListState createState() => _CustomListState();
}
class _CustomListState extends State<CustomList> {
ScrollController _scrollController;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scrollbar(
isAlwaysShown: true,
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: _buildList(),
),
),
);
}
}
List<Widget> _buildList() {
List<Widget> widgetList = [];
for (int i = 0; i < 15; i++) {
widgetList.add(CustomRow(rowID: i));
}
return widgetList;
}
customRow.dart
(This is where I scroll to the selected row in handleOnTap)
import 'package:flutter/material.dart';
import 'package:scrollTest/budgetPageState.dart';
import 'package:provider/provider.dart';
class CustomRow extends StatefulWidget {
final int rowID;
CustomRow({Key key, #required this.rowID}) : super(key: key);
#override
_CustomRowState createState() => _CustomRowState();
}
class _CustomRowState extends State<CustomRow> {
BudgetPageState budgetPageState;
void handleOnTap(BuildContext context) {
if (!budgetPageState.isSelected(widget.rowID)) {
Scrollable.ensureVisible(context, duration: Duration(milliseconds: 200), alignment: 0.5);
}
budgetPageState.toggleButtonDial(widget.rowID);
budgetPageState.updateIsSelected(widget.rowID);
}
#override
void initState() {
super.initState();
budgetPageState = Provider.of<BudgetPageState>(context, listen: false);
budgetPageState.insertRowInHashMap(widget.rowID);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap:() => handleOnTap(context),
child: Container(
height: 60,
width: double.infinity,
color: budgetPageState.isSelected(widget.rowID)
? Colors.grey[200]
: Colors.white,
child: Center(
child: Text(
"Test ${widget.rowID}",
),
),
),
);
}
}
budgetPageState.dart
(The state managed using ChangeNotifier. Mainly contains logic for selecting/deselecting a row as well as logic for when to show the keyboard (using bool showButtonDial and notifyListeners())
import 'dart:collection';
import 'package:flutter/material.dart';
class BudgetPageState extends ChangeNotifier {
bool showButtonDial = false;
Map<int, bool> _isSelectedMap = HashMap();
int selectedId = -1;
bool isSelected(int rowId) {
return this._isSelectedMap[rowId];
}
Map<int, bool> get isSelectedMap => _isSelectedMap;
void updateIsSelected(int rowId) async {
///Select the row [rowId] if we tapped on a different one than the one
///that is currently highlighted ([selectedId])
///The same row was tapped, we remove the highlight i.e. we don't
///put it back to [true]
//Unselect all
_isSelectedMap.forEach((k, v) => _isSelectedMap[k] = false);
if (selectedId != rowId) {
this._isSelectedMap[rowId] = true;
selectedId = rowId;
} else {
selectedId = -1;
}
notifyListeners();
}
void toggleButtonDial(int rowId) {
if (!showButtonDial) {
showButtonDial = true;
} else if (rowId == selectedId) {
showButtonDial = false;
}
}
void insertRowInHashMap(int subcatId) {
this._isSelectedMap[subcatId] = false;
}
}
I need help calling a method of another class in a pageview. I have an app which has a pageview consisting of two classes, Page 1 and Page 2 respectively. Both of them are stateful widgets.
I am trying to call a method from Page2 from Page1, but it is difficult for me as both of them do not have a child/parent relationship so I could not use a callback function. In fact, both of them are a child of another parent class (MainParent). I tried using globalkey but i received currentState is null error.
Your help is greatly appreciated. Thanks!
class MainParent extends StatefulWidget{
#override
MainParentState createState() => MainParentSate();
}
class MainParentState extends State<MainParent> {
PageController pageController;
#override
initState(){
pageController = new PageController(
}
List<Widget> _showPageList(AppModel appModel){
List<Widget> pageList = new List();
pageList.add(Page1(/*some named parameters pass here*/));
pageList.add(Page2(/*some named parameters pass here*/));
return pageList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
physics: new NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: _onPageChanged,
children: _showPageList(),
)
);
}
}
Page 1 class
class Page1 extends StatefulWidget{
#override
Page1State createState() => Page1State();
}
class Page1State extends State<Page1> {
_callPage2Function(){
//animate to page 2 and mount it
MainParent.of(context).pageController.animateToPage(
1,
duration: const Duration(milliseconds: 700),
curve: Curves.fastLinearToSlowEaseIn);
//this doesn't work
Page2State page2state = new Page2State();
page2state.refreshList();
//creating a global key doesnt work as well, gets currentstate is null
GlobalKey<Page2State> page2Key = new GlobalKey<Page2State>();
page2Key.currentState.refreshList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: GestureDetector(
onTap: () => _callPage2Function()
)
)
);
}
}
Page 2 class
class Page2 extends StatefulWidget{
#override
Page2State createState() => Page2State();
}
class Page2State extends State<Page2> {
refreshList(){
//this function refreshes the list
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: ListView.builder(
//some child widgets here
)
)
);
}
}
Edit: Edited my code. Turns out I have to navigate to page2 tab first, because when I am in page 1, page 2 is not mounted to the screen. So i added a pageController and navigate to page2 first.
The error in your code is that you are not injecting the key in you Page2 instance when using it, thats why the currentState in null.
Usually when i want to achieve this, i use the key attribute like so
class Page2 extends StatefulWidget {
static final GlobalKey<Page2State> staticGlobalKey =
new GlobalKey<Page2State>();
Page2() : super(key: Page2.currentStateKey); // Key injection
#override
Page2State createState() => Page2State();
}
class Page2State extends State<Page2> {
refreshList() {
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: ListView.builder(
//some child widgets here
)));
}
}
then to call the refreshList() from anywhere in your app just do
Page2.staticGlobalKey.currentState.refreshList();
You could work around and use another approach instead of static key, but this should be a working solution.
In PageView, Only one page is mounted(or active) at a time.
Here when you call page2Key.currentState.refreshList(); from Page1, the Page2 is currently unmounted(or deactive). So the state itself is null.
So you cannot do setState of another page.
I'm building an app with a long form in it. So I decided to seperate it into several steps.
Each step would be a widget containing formFields.
So I would have something like this:
int _currentStep = 0;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String _firstname;
String _lastname;
String _city;
List<Widget> formGroups = [
FormGroup1(),
FormGroup2(),
FormGroup3(),
];
The form would bind to the state like this
Form(
key: _formKey,
child: formGroups[_currentStep],
),
My idea is to be able to navigate to the next widget like this
void goToNext() {
setState(() {
if (_currentStep < formGroups.length - 1) {
_currentStep += 1;
}
});
}
Firstly, is it a good practice?
How can I get the main widget to get the input from the children widgets?
For example if the FormGroup2 contains the inputfield to set the _lastname, how can I make it availaibe at the form level?
Thank you for your help.
Whenever you want parent widget to get input from the child widget you always use the NotificationListener at parent and pass Notification containing data from the child.
This technique is used by many flutter widgets like DefaultTabController receives OverscrollNotification when user swipes to the last page and is still trying to swipe.
In your use case you can pass the value notifications from the child Widgets to the Form Widget.
Here is a Demo for your reference Run on dartpad
Following is a working code demonstrating the use of this widget:
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _string = 'You haven\'t pressed a button yet' ;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notification demo'),),
body: NotificationListener<IntegerNotification>(
onNotification: (IntegerNotification notification){
print(notification.value);
setState(() {
_string = 'You have pressed button ${notification.value} times.';
});
return true;
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_string),
ChildWidget(),
],
),
),
);
}
}
class ChildWidget extends StatefulWidget {
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('Increment'),
onPressed: () {
IntegerNotification(++_counter).dispatch(context);
},
),
);
}
}
class IntegerNotification extends Notification {
final value;
IntegerNotification(this.value);
}
I hope this helps, in case of any doubt please comment.
I have the following class for holding my state:
import 'package:flutter/foundation.dart';
enum ActiveProduct {
HOME,
BURGUNDY,
VIRTUAL
}
class ActiveProductModel extends ChangeNotifier {
ActiveProduct _activeProduct = ActiveProduct.HOME;
ActiveProduct get value => _activeProduct;
void set(ActiveProduct newValue) {
_activeProduct = newValue;
notifyListeners();
}
}
Whenever the "ActiveProduct" changes, I want to change the selected tab in a TabView.
Currently I have setup the application like this:
class MainScaffold extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: TabBarView(
children: [
Text("hello! abc"),
Text("hello! sdsadsa"),
Text("hello! 231321"),
],
),
),
);
}
}
How can I change the selected tab in the TabBarView when the ActiveProduct changes?
I have tried wrapping the MainScaffold in a Consumer and set the initialIndex value of DefaultTabController. This didn't work however.
I am using the Provider package for state management.
What I would do is transforming your MainScaffold into a StatelessWidget and then, I would not use a DefaultTabController but instead I would use a TabController and pass it as an argument to the TabBarView.
Then in the initState of the MainScaffoldState, I would listen on the ActiveProductModel and use tabController.animateTo(yourpage) whenever something is fired.
I'm not sure that I was very clear in my explanation so here is an example:
class MainScaffold extends StatefulWidget {
#override
_MainScaffoldState createState() => _MainScaffoldState();
}
class _MainScaffoldState extends State<MainScaffold> with SingleTickerProviderStateMixin {
TabController tabController;
/// Just for the example
ActiveProductModel activeProductModel = ActiveProductModel();
#override
void initState() {
// Initiate the tabController
tabController = TabController(vsync: this, length: 3);
activeProductModel.addListener(() {
if (mounted)
/// Here you can do whatever you want, just going to page 2 for the example.
tabController.animateTo(2);
});
super.initState();
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: TabBarView(
controller: tabController,
children: <Widget>[
Text("hello! abc"),
Text("hello! sdsadsa"),
Text("hello! 231321"),
],
),
);
}
}