I have a cupertinoTabBar (wrapped in CupertinoTabScaffold) which contains 3 tabs.
CupertinoTabScaffold(
tabBar: CupertinoTabBar(
currentIndex: selectedIndex,
items: getBarItems(),
),
tabBuilder: (BuildContext context, int index) {
return BaseHomeState.widgetOptions[index];
},
),
(BaseHomeState is an abstract state I use to share code with both android & iOS platform)
widgetOptions :
static final List<Widget> widgetOptions = <Widget>[
const FormPage(),
const DraftsPage(),
const SettingsPage(),
];
Those 'Page' are StatelessWidgets which contain BlocProviders with Platform Dedicated StatefulWidgets as child.
I need my tab's content (widgetOptions) to relaunch build method every time I switch tab which is not the case now. The problem is, sometimes the DraftsPage content must be updated due to changes on FormPage (user can save form drafts and see those on DraftPage). But those two pages aren't linked anywhere :
I just want my DraftsPage to fetch database (thanks to a rebuild) everytime I select its tab and not only the first time.
Do you have an idea? :)
Related
I have a list of some entries I want edit on focus out. I createe FocusNode for each entry, CupertinoTextField for each entry too.
var textField = (UserMotivator um) {
var controller;
var focusNode = new FocusNode();
focusNode.addListener(() {
if (!focusNode.hasFocus) {
post(um);
}
});
var controller = TextEditingController(text: um.text);
return CupertinoTextField(
focusNode: focusNode,
controller: controller,
onChanged: (String value) {
um.text = value;
}
);
};
For some weird reason, in simulator (not tested on real device), when I click on many of these TextFields, I get this:
How do I bound a focus out even to a TextField without using FocusNode/ without having all of these cursors blinking?
So I resolved the issue I think. The reason it was buggy was that I was on v1.1.8, after updating to v1.5.4 it somehow got fixed, was not perfect but was better. After I moved the FocusNodes creation code form build to initState method it got even better but the cursor was still blinking at the start of the TextField. This was because I had called setState in the onChange handler, which somehow caused the TextField to redraw and act so weirdly.
I am using a normal bottom navigation bar with a local state containing the index of the currently selected page. When tapping on one of the nav items the tapped page is displayed:
class _HomePageState extends State<HomePage> {
int _selectedIndex = 1;
final _widgetOptions = [
Text('Index 0'),
Overview(),
Details(),
Text('Index 3'),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
...
currentIndex: _selectedIndex,
onTap: onTap: (index) => _selectedIndex = index,
),
);
}
}
How to proceed if you want to navigate from inside one page to another and update the selected index of the bottom navigation bar?
Example: Page 2 is an overview list which contains among other things the ID of the list items. If you tap an item in the list, it should be navigated to the details page (3 in bottom nav bar) displaying details for the selected item and therefore the ID of the selected item should be passed.
Navigator.of(context)... can't be used, because the individual pages are items of the nav bar and therefore not routes.
You will need a better state management way. I advise you to use BLoC pattern to manage navigation changes in this widget. I will put a simplified example here about how to do this with some comments and external reference to improvements.
// An enum to identify navigation index
enum Navigation { TEXT, OVERVIEW, DETAILS, OTHER_PAGE}
class NavigationBloc {
//BehaviorSubject is from rxdart package
final BehaviorSubject<Navigation> _navigationController
= BehaviorSubject.seeded(Navigation.TEXT);
// seeded with inital page value. I'am assuming PAGE_ONE value as initial page.
//exposing stream that notify us when navigation index has changed
Observable<Navigation> get currentNavigationIndex => _navigationController.stream;
// method to change your navigation index
// when we call this method it sends data to stream and his listener
// will be notified about it.
void changeNavigationIndex(final Navigation option) => _navigationController.sink.add(option);
void dispose() => _navigationController?.close();
}
This bloc class expose a stream output currentNavigationIndex. HomeScreen will be the listener of this output which provides info about what widget must be created and displayed on Scaffold widget body. Note that the stream starts with an initial value that is Navigation.TEXT.
Some changes are required in your HomePage. Now we're using StreamBuilder widget to create and provide a widget to body property. In some words StreamBuilder is listening a stream output from bloc and when some data is received which will be a Navigation enum value we decide what widget should be showed on body.
class _HomePageState extends State<HomePage> {
final NavigationBloc bloc = new NavigationBloc();
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<Navigation>(
stream: bloc.currentNavigationIndex,
builder: (context, snapshot){
_selectedIndex = snapshot.data.index;
switch(snapshot.data){
case Navigation.TEXT:
return Text('Index 0');
case Navigation.OVERVIEW:
// Here a thing... as you wanna change the page in another widget
// you pass the bloc to this new widget for it's capable to change
// navigation values as you desire.
return Overview(bloc: bloc);
//... other options bellow
case Navigation.DETAILS:
return Details(/* ... */);
}
},
),
bottomNavigationBar: BottomNavigationBar(
//...
currentIndex: _selectedIndex,
onTap: (index) => bloc.changeNavigationIndex(Navigation.values[index]),
),
);
}
#override
void dispose(){
bloc.dispose();// don't forgot this.
super.dispoe();
}
}
Since you wanna change the body widget of HomePage when you click in a specific items that are in other widget, Overview by example, then you need pass the bloc to this new widget and when you click in item you put new data into Stream that the body will be refreshed. Note that this way of send a BLoC instance to another widget is not a better way. I advise you take a look at InheritedWidget pattern. I do here in a simple way in order to not write a bigger answer which already is...
class Overview extends StatelessWidget {
final NavigationBloc bloc;
Overview({this.bloc});
#override
Widget build(BuildContext context) {
return YourWidgetsTree(
//...
// assuming that when you tap on an specific item yout go to Details page.
InSomeWidget(
onTap: () => bloc.changeNavigationIndex(Navigation.DETAILS);
),
);
}
}
I know so much code it's a complex way to do this but it's the way. setState calls will not solve all your needs. Take a look at this article It's one of the best and the bigger ones that talks about everything that I wrote here with minimum details.
Good Luck!
You can use Provider to manage the index of the BottomNavigationBar.
Make a class to handle the indexPage and consume it in your widgets using the Consumer. Don't forget to register the ChangeNotifierProvider in the
ChangeNotifierProvider<NavigationModel> (create: (context) => NavigationModel(),),
class NavigationModel extends ChangeNotifier {
int _pageIndex = 0;
int get page => _pageIndex;
changePage(int i) {
_pageIndex = i;
notifyListeners();
}
}
make some change like..
int _currentIndex = 0;
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
body: _widgetOptions[_currentIndex],
tab click..
onTap: onTabTapped,
I have 2 pages PAGE A and PAGE B. I navigate form PAGE A -> PAGE B and do edit some data, or toggle a setting. Now I want to navigate form PAGE B -> PAGE A and also what that a parameter would be send on navigator pop method. Now my question:
How I can access to these parameter in PAGE A?
Navigator.pop(context, this.selectedEquipmentId);
In fact you got to return something when you ends PageA. I put you an exemple with a popup to select an adress i made recently, this work exactly the same if this is not a popup.
Future<PlacesDetailsResponse> showAdressPicker({#required BuildContext context}) async {
assert(context != null);
return await showDialog<PlacesDetailsResponse>(
context: context,
builder: (BuildContext context) => AdressPickerComponent(),
);
}
You can send a result from Navigator.pop(...) and get it from PageA
Navigator.pop(context, result)
Just put anything you want in result, (here i created a class named PlacesDetailsResponse, use yours or just Int, String...).
Now In pageA when you call this
showAdressPicker(context: context).then((PlacesDetailsResponse value) {
//do whatever you want here
// this fires when PageB call previous to PageA
});
My question is about navigation used with the BLoC pattern.
In my LoginScreen widget I have a button that adds an event into the EventSink of the bloc. The bloc calls the API and authenticates the user.
Where in the LoginScreen Widget do I have to listen to the stream, and how do I navigate to another screen after it returns a success status?
Use BlockListener.
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
The navigator is not working in blocBuilder, because in blocBuilder, you can only return a widget
But BlocListener solved it for me.
Add this code:
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
First of all: if there isn't any business logic, then there isn't any need to go to YourBloc class.
But from time to time some user's activity is required to perform some logic in Bloc class and then the Bloc class has to decide what to do next: just rebuild widgets or show dialog or even navigate to next route. In such a case, you have to send some State to UI to finish the action.
Then another problem appears: what shall I do with widgets when Bloc sends State to show a toast?
And this is the main issue with all of this story.
A lot of answers and articles recommend to use flutter_block. This library has BlocBuilder and BlocListener. With those classes you can solve some issues, but not 100% of them.
In my case I used BlocConsumer which manages BlocBuilder and BlocListener and provides a brilliant way to manage states.
From the documentation:
BlocConsumer<BlocA, BlocAState>(
listenWhen: (previous, current) {
// Return true/false to determine whether or not
// to invoke listener with state
},
listener: (context, state) {
// Do stuff here based on BlocA's state
},
buildWhen: (previous, current) {
// Return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// Return widget here based on BlocA's state
}
)
As you can see with BlocConsumer, you can filter states: you can easily define states to rebuild widgets and states to show some popups or navigate to the next screen.
Something like this:
if (state is PhoneLoginCodeSent) {
// Dispatch here to reset PhoneLoginFormState
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return VerifyCodeForm(phoneLoginBloc: _phoneLoginBloc);
},
),
);
return;
});
}
In Flutter project, I need to listen to the input text in TextFormField and do certain actions, especially when user put some character (eg. space) in this filed or when he request focus. When this kind of event happen, I need to modify filed's value.
I know there is a property called controller but I don't know how to use it in this case.
Thank you in advance.
You can specify a controller and focus node, then add listeners to them to monitor for changes.
Ex:
Define controllers and focus nodes
TextEditingController _controller = new TextEditingController();
FocusNode _textFocus = new FocusNode();
Define listener function
void onChange(){
String text = _controller.text;
bool hasFocus = _textFocus.hasFocus;
//do your text transforming
_controller.text = newText;
_controller.selection = new TextSelection(
baseOffset: newText.length,
extentOffset: newText.length
);
}
Add listner to controller and focusnode at initState
// you can have different listner functions if you wish
_controller.addListener(onChange);
_textFocus.addListener(onChange);
Then you can use it as
new TextFormField(
controller: _controller,
focusNode: _textFocus,
)
Hope that helps!
If you are just trying to transform the input to other form in TextFormField, you better use "TextInputFormatter". Using a listener with TextController cause a lot of troubles. Take a look at my sample code see if that helps you. btw, The last line of code is just trying to move the cursor to the end of text.
TextFormField(inputFormatters: [QuantityInputFormatter()])
class QuantityInputFormatter extends TextInputFormatter {
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final intStr = (int.tryParse(newValue.text) ?? 0).toString();
return TextEditingValue(
text: intStr, selection: TextSelection.collapsed(offset: intStr.length),);
}
}