I am getting started with the BLoC pattern but I have a question:
1) Should you use the BloC pattern to determine if the routes should change?
Example: authentication object changes to unauthenticated so the listeners should handle the route changes.
2) Should the BLoC pattern only be used for UI state and handle the route changes on UI changes?
Example: User clicks on login and navigates to the home screen.
I ask this question because I'm facing a problem where I don't have a central navigation management solution.
This code is in my BLoC now:
loggedIn.listen((AuthResponse user) {
currentUserSubject.add(user);
Navigator.pushReplacement(
_context,
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) {
return HomePage();
},
transitionsBuilder: (context, animation, _, child) {
return new SlideTransition(
child: child,
position: new Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
);
},
transitionDuration: Duration(milliseconds: 400),
),
);
}, onError: (error) {
Scaffold.of(_context).showSnackBar(new SnackBar(
content: new Text(error.message),
));
});
What I'm currently doing is:
Register a NavigatorBloc that receive a NavigatorState in the constructor and receive actions to navigate to different pages in your app Ex: GoToHomePageAction, GoToRegisterPageAction, NavigatorActionPop.
In your App widget you register the NavigatorBloc and provide the navigator key attached to the MaterialApp widget.
#override
Widget build(BuildContext context) {
return BlocProvider<NavigatorBloc>(
bloc: NavigatorBloc(navigatorKey: widget.navigatorKey),
child: MaterialApp(
navigatorKey: widget.navigatorKey,
title: 'Medicine Tracker',
theme: ThemeData(
primarySwatch: Colors.red,
scaffoldBackgroundColor: Colors.white
),
home: HomePage(),
),
);
}
Then in side your NavigatorBloc you just check the the action and navigate to the desired page:
class NavigatorBloc extends Bloc<NavigatorAction, dynamic>{
final GlobalKey<NavigatorState> navigatorKey;
NavigatorBloc({this.navigatorKey});
#override
dynamic get initialState => 0;
#override
Stream<dynamic> mapEventToState(NavigatorAction event) async* {
if(event is NavigatorActionPop){
yield navigatorKey.currentState.pop();
}
}
}
Hope it help.
I see there are 2 Yes/No questions there (in contrast to W/H questions), and my answers are yes to both of them. The reason being that with BloC and navigation, you can actually recover the current screen if the app crashes (auto-save state and state recover must be in place, but it's another W/H question), and other nice features of BLoC/event-based state management (history snapshot, time machine, event replay, separate of concerns, testability, etc.)
I ask this question because I'm facing a problem where I don't have a central navigation management solution.
Is there anything I can help with regarding your problem?
Related
The context:
I stumbled upon a minor crash while testing a ListView of Dismissibles in Flutter. When swiping a dismissible, a Dialog is shown using the confirmDismiss option, for confirmation. This all works well, however the UI crashes when testing an unlikely use case. On the page are several options to navigate to other (named) routes. When a dismissible is swiped, and during the animation an option to navigate to a new route is tapped, the crash happens.
How to replicate the crash:
Dismiss the Dismissible
During the animation that follows (the translation of the position of the dismissible), tap on an action that brings you to a
new route. The timeframe to do this is minimal, I've extended it in the example.
The new route loads and the UI freezes
For reference, this is the error message:
AnimationController.reverse() called after AnimationController.dispose()
The culprit is the animation that tries to reverse when it was already disposed:
package:flutter/…/widgets/dismissible.dart:449
Things I've tried:
Initially, I tried checking this.mounted inside the showDialog builder but quickly realised the problem is not situated there.
Another idea was to circumvent the problem by using CancelableOperation.fromFuture and then cancelling it in the dispose() method of the encompassing widget, but that was to no avail.
What can I do solve or at least circumvent this issue?
The code (can also be found and cloned here):
// (...)
class _DimissibleListState extends State<DimissibleList> {
int childSize = 3;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: childSize,
itemBuilder: (context, index) {
if (index == 0) {
return _buildNextPageAction(context);
}
return _buildDismissible();
},
),
);
}
Widget _buildNextPageAction(context) {
return FlatButton(
child: Text("Go to a new page"),
onPressed: () => Navigator.of(context).pushNamed('/other'),
);
}
Dismissible _buildDismissible() {
GlobalKey key = GlobalKey();
return Dismissible(
key: key,
child: ListTile(
title: Container(
padding: const EdgeInsets.all(8.0),
color: Colors.red,
child: Text("A dismissible. Nice."),
),
),
confirmDismiss: (direction) async {
await Future.delayed(const Duration(milliseconds: 100), () {});
return showDialog(
context: context,
builder: (context) {
return Dialog(
child: FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text("Confirm dismiss?"),
),
);
},
);
},
resizeDuration: null,
onDismissed: (direction) => setState(() => childSize--),
);
}
}
I had almost same problem with confirmDismiss ,in my case I was using (await Navigator.push() ) inside of confirmDismiss to navigate to another screen but in return I faced this error :
AnimationController.reverse() called after
AnimationController.dispose()
so to solve my problem inside of confirmDismiss I call a future function out side of confirmDismiss (without await ) and then add return true or false after that function call to finish animation of confirmDismiss.
What I'm trying to do is allow users to share a post they see on my app. I am using the Flutter Share plugin, but I'm having some difficulty with it and have been looking all over, including the docs which aren't very good.
I have the share button itself working - so the panel will rise up and be displayed to the user. However, it doesn't provide them with any useful option and I can't find how to add those options, such as Facebook, Twitter, Email, Text, etc..
Here is my code, and a picture to show what is happening:
// build method from another class
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Firestore.instance.collection('stories').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildEventCards(context, snapshot.data.documents[index]),
);
},
);
}
class ShareButton extends StatelessWidget {
const ShareButton({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text(
'Share',
style: TextStyle(
color: Color.fromRGBO(245, 93, 62, 1.0)),
),
color: Colors.grey[100],
onPressed: () {
final RenderBox box = context.findRenderObject();
Share.share('Hello this is a test',
sharePositionOrigin:
box.localToGlobal(Offset.zero) &
box.size);
},
);
}
}
Sorry the picture is so big, but I would like to add sharing options such as Facebook, Twitter, Email, Messages, etc.
Any help would be much appreciated!
I feel kind of dumb now because I now realize how this Share thing works, but this is definitely something that could be confusing for beginners or people using simulators on their computers wondering what's happening..
Everything works fine the reason I couldn't get anything to show up in the Share panel is because I was testing with simulator app on my computer which doesn't have the mail app, isn't associated with my phone number, isn't logged into Facebook / Twitter, and doesn't have any other apps that could be shared through.
All you have to do is test your app on your actual device and then what ever apps / accounts you're logged into will show up here and you'll be able to share stuff.
I have a problem with the structure of Flutter project.
At the moment structure looks like this:
Homepage with bottomNavigationBar with multiple tabs, each tab is StatefulWidget and contains some heavy processing (remote API calls and data display).
In case I call Navigator.pushNamed from inside of any tab, following happens:
All tabs are being rebuild in the background (making API calls, etc).
New page opens normally.
When I press back button, page closes and all tabs are rebuild again.
So in total everything (each tab) is rebuilt 2 times just to open external navigator page.
Is this some sort of bug? Completely not understandable why it's fully rebuilding bottomNavigationBar just before pushing new route.
How it should work:
When I call Navigator.pushNamed from inside the tab, new page should be open and all bottomNavigationBar tabs should not be rebuild and stay in unchanged state.
When I press back, page should close and user return to the same state of bottomNavigationBar and it's tabs, no rebuilding at all.
Is this possible to achieve?
Here is the code:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
int index = 0;
final _tab1 = new tab1(); //StatefulWidget, api calls, heavy data processing
final _tab2 = new tab2(); //StatefulWidget, api calls, heavy data processing
#override
Widget build(BuildContext context) {
debugPrint('homepage loaded:'+index.toString());
return new Scaffold(
body: new Stack(
children: <Widget>[
new Offstage(
offstage: index != 0,
child: new TickerMode(
enabled: index == 0,
child: _tab1,
),
),
new Offstage(
offstage: index != 1,
child: new TickerMode(
enabled: index == 1,
child: _tab2,
),
),
],
),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: index,
type: BottomNavigationBarType.fixed,
onTap: (int index) { setState((){ this.index = index; }); },
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
icon: new Icon(Icons.live_help),
title: new Text("Tab1"),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.favorite_border),
title: new Text("Tab 2"),
),
],
),
);
}
}
Here is single tab code:
class tab1 extends StatefulWidget {
#override
tab1State createState() => new tab1State();
}
class tab1State extends State<tab1> {
#override
Widget build(BuildContext cntx) {
debugPrint('tab loaded'); //this gets called when Navigator.pushNamed called and when back button pressed
//some heave processing with http.get here...
//...
return new Center(
child: new RaisedButton(
onPressed: () {
Navigator.pushNamed(context, '/some_other_page');
},
child: new Text('Open new page'),
));
}
}
In build you should simply be building the Widget tree, not doing any heavy processing. Your tab1 is a StatefulWidget, so its state should be holding onto the current state (including results of your heavy processing). Its build should simply be rendering the current version of that state.
In tab1state, override initState to set initial values, and possibly start some async functions to start doing the fetching - calling setState once the results are available. In build, render whatever the current state is, bearing in mind that it may only be partially available as the heavy work continues in the background. So, for example, test for values being null and maybe replace them with progress indicators or empty Containers.
You can get more sophisticated by using StreamBuilder and FutureBuilder which make the fetch/setState/(possibly partial)render more elegant.
Since you are building BottomNavigationBar inside build function, it will be rebuilt every time state changes.
To avoid this you can build BottomNavigationBar inside initState() method as given below,
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
BottomNavigationBar _bottomNavigationBar;
#override
void initState() {
super.initState();
_bottomNavigationBar = _buildBottomNavigationBar();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Center(child: new Text('Hello', style: new TextStyle(decoration: TextDecoration.underline),),),
bottomNavigationBar: _bottomNavigationBar, // Use already created BottomNavigationBar rather than creating a new one
);
}
// Create BottomNavigationBar
BottomNavigationBar _buildBottomNavigationBar() {
return new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.add),
title: new Text("trends")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.location_on),
title: new Text("feed")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.people),
title: new Text("community")
)
],
onTap: (index) {},
);
}
}
You can use any one of these to save the states and precvent rebuilding:
IndexedStack https://api.flutter.dev/flutter/widgets/IndexedStack-class.html
PageStorageKey https://api.flutter.dev/flutter/widgets/PageStorageKey-class.html
AutomaticKeepAliveClientMixin https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html
I'm currently developing a reader and using PageView to slide the page of images. How do I make the next page preload so that the user can slide to next page without waiting for the page to load? I don't want to download all the pages first because it will load the server and freezes my app. I just want to download just next one or two pages when the user browsing on current page.
Here is the excerpt of my code.
PageController _controller;
ZoomableImage nextPage;
Widget _loadImage(int index) {
ImageProvider image = new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}");
ZoomableImage zoomed = new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
return zoomed;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: book.numPages,
itemBuilder: (BuildContext context, int index) {
return index == 0 || index == 1 ? _loadImage(index) : nextPage;
},
onPageChanged: (int index) {
nextPage = _loadImage(index+1);
},
),
),
);
}
Thank you!
Simple! Just set allowImplicitScrolling: true, // in PageView.builder
I ended up using FutureBuilder and CachedNetworkImageProvider from the package cached_network_image to prefetch all the images. Here is my solution:
PageController _controller;
ZoomableImage currPage, nextPage;
Future<List<CachedNetworkImageProvider>> _loadAllImages(Book book) async {
List<CachedNetworkImageProvider> cachedImages = [];
for(int i=0;i<book.numPages;i++) {
var configuration = createLocalImageConfiguration(context);
cachedImages.add(new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}")..resolve(configuration));
}
return cachedImages;
}
FutureBuilder<List<CachedNetworkImageProvider>> _futurePages(Book book) {
return new FutureBuilder(
future: _loadAllImages(book),
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasData) {
return new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
ImageProvider image = snapshot.data[index];
return new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
},
onPageChanged: (int index) {},
),
);
} else if(!snapshot.hasData) return new Center(child: CupertinoActivityIndicator());
},
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: _futurePages(widget.book),
);
}
As people mentioned before the cached_network_image library is a solution, but not perfect for my situation. There are a full page PageView(fit width and height) in my project, when I try previous code my PageView will show a blank page first, then show the image.
I start read PageView source code, finally I find a way to fit my personal requirement. The basic idea is change PageView source code's cacheExtent
This is description about how cacheExtent works:
The viewport has an area before and after the visible area to cache items that are about to become visible when the user scrolls.
Items that fall in this cache area are laid out even though they are not (yet) visible on screen. The cacheExtent describes how many pixels the cache area extends before the leading edge and after the trailing edge of the viewport.
Change flutter's source code directly is a bad idea so I create a new PrelodPageView widget and use it at specific place when I need preload function.
Edit:
I add one more parameter preloadPagesCount for preload multiple pages automatically.
https://pub.dartlang.org/packages/preload_page_view
I've been searching around for a good navigation/router example for Flutter but I have not managed to find one.
What I want to achieve is very simple:
Persistent bottom navigation bar that highlights the current top level route
Named routes so I can navigate to any route from anywhere inside the app
Navigator.pop should always take me to the previous view I was in
The official Flutter demo for BottomNavigationBar achieves 1 but back button and routing dont't work. Same problem with PageView and TabView. There are many other tutorials that achieve 2 and 3 by implementing MaterialApp routes but none of them seem to have a persistent navigation bar.
Are there any examples of a navigation system that would satisfy all these requirements?
All of your 3 requirements can be achieved by using a custom Navigator.
The Flutter team did a video on this, and the article they followed is here: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386
Basically, you will need to wrap the body of your Scaffold in a custom Navigator:
class _MainScreenState extends State<MainScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/page1':
builder = (BuildContext context) => Page1();
break;
case '/page2':
builder = (BuildContext context) => Page2();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
// You can also return a PageRouteBuilder and
// define custom transitions between pages
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
}
Within your bottom navigation bar, to navigate to a new screen in the new custom Navigator, you just have to call this:
_navigatorKey.currentState.pushNamed('/yourRouteName');
To achieve the 3rd requirement, which is Navigator.pop taking you to the previous view, you will need to wrap the custom Navigator with a WillPopScope:
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Navigator(
// ...
),
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
And that should be it! No need to manually handle pop or manage a custom history list.
CupertinoTabBar behave exactly same as you described, but in iOS style. It can be used in MaterialApps however.
Sample Code
What you are asking for would violate the material design specification.
On Android, the Back button does not navigate between bottom
navigation bar views.
A navigation drawer would give you 2 and 3, but not 1. It depends on what's more important to you.
You could try using LocalHistoryRoute. This achieves the effect you want:
class MainPage extends StatefulWidget {
#override
State createState() {
return new MainPageState();
}
}
class MainPageState extends State<MainPage> {
int _currentIndex = 0;
List<int> _history = [0];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Bottom Nav Back'),
),
body: new Center(
child: new Text('Page $_currentIndex'),
),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _currentIndex,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
icon: new Icon(Icons.touch_app),
title: new Text('keypad'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.assessment),
title: new Text('chart'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.cloud),
title: new Text('weather'),
),
],
onTap: (int index) {
_history.add(index);
setState(() => _currentIndex = index);
Navigator.push(context, new BottomNavigationRoute()).then((x) {
_history.removeLast();
setState(() => _currentIndex = _history.last);
});
},
),
);
}
}
class BottomNavigationRoute extends LocalHistoryRoute<void> {}