I have three screens. A, B and C. I push A, B, and C by using the following code. A is the first screen of the app.
class FadeInSlideOutRoute<T> extends MaterialPageRoute<T> {
FadeInSlideOutRoute({WidgetBuilder builder, RouteSettings settings})
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return new FadeTransition(opacity: animation, child: child);
}
}
Screen declare in build method.
#override
Widget build(BuildContext context) {
return new MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/B':
return new FadeInSlideOutRoute(
builder: (_) => new LoginScreen(),
settings: settings,
);
case '/C':
return new FadeInSlideOutRoute(
builder: (_) => new ForgotPasswordScreen(),
settings: settings,
);
}
assert(false);
},
home: _LandingScreen(),
);
}
Now, I'm calling the following method to push new screens.
Navigator.pushNamed(context, '/B');
After that
Navigator.pushNamed(context, '/C');
issue is
when I'm pressing back of Android device from screen C it's come to screen A directly. But it should on screen B. Please help guys.
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> {}
I might very well be missing something as I'm so new to flutter, but I'm finding ThemeData's options very limited (at least with my understanding of how to implement it).
If you look at this random design below from MaterialUp, I'd want to model something roughly like:
Themedata.cyclingColor = Color.pink;
ThemeData.runningColor = Color.green;
That way everywhere in my app I can reference cycling, running, swimming, gym colors (Or whatever colors make sense in the context of my app/design) and keep things consistent.
Is there a recommended way to achieve this currently in Flutter? What are my options?
I recommend this approach, which is simple, works with hot reload and can be easily extended to support switching between dark and light themes.
First create your own analog to ThemeData, let's call it AppThemeData:
class AppThemeData {
final BorderRadius borderRadius = BorderRadius.circular(8);
final Color colorYellow = Color(0xffffff00);
final Color colorPrimary = Color(0xffabcdef);
ThemeData get materialTheme {
return ThemeData(
primaryColor: colorPrimary
);
}
}
The materialTheme can be used whenever the standard ThemeData is needed.
Then create a widget called AppTheme, which provides an instance of AppThemeData using the provider package.
class AppTheme extends StatelessWidget {
final Widget child;
AppTheme({this.child});
#override
Widget build(BuildContext context) {
final themeData = AppThemeData(context);
return Provider.value(value: themeData, child: child);
}
}
Finally, wrap the whole app with AppTheme. To access the theme you can call context.watch<AppThemeData>(). Or create this extension...
extension BuildContextExtension on BuildContext {
AppThemeData get appTheme {
return watch<AppThemeData>();
}
}
... and use context.appTheme. I usually put final theme = context.appTheme; on the first line of the widget build method.
Updated for null-safety
I've extended standard ThemeData class so that at any time one could access own theme fields like that:
Theme.of(context).own().errorShade
Or like that:
ownTheme(context).errorShade
A theme can be defined and extended with new fields as follows(via addOwn() called on a certain ThemeData instance):
final ThemeData lightTheme = ThemeData.light().copyWith(
accentColor: Colors.grey.withAlpha(128),
backgroundColor: Color.fromARGB(255, 255, 255, 255),
textTheme: TextTheme(
caption: TextStyle(
fontSize: 17.0, fontFamily: 'Montserrat', color: Colors.black),
))
..addOwn(OwnThemeFields(
errorShade: Color.fromARGB(240, 255, 200, 200),
textBaloon: Color.fromARGB(240, 255, 200, 200)));
final ThemeData darkTheme = ThemeData.dark().copyWith( ...
...
Themes can be applied to MaterialApp widget in a conventional way:
MaterialApp(
...
theme: lightTheme,
darkTheme: darkTheme,
)
The idea is to put all custom fields required for theming in a separate class OwnThemeFields.
Then extend ThemeData class with 2 methods:
addOwn() that connects a certain instance of ThemedData to OwnThemeFields instance
own() that allows to lookup for own fields associated with the given theme data
Also ownTheme helper method can be created to shorten the extraction of own fields.
class OwnThemeFields {
final Color? errorShade;
final Color? textBaloon;
const OwnThemeFields({Color? errorShade, Color? textBaloon})
: this.errorShade = errorShade,
this.textBaloon = textBaloon;
factory OwnThemeFields.empty() {
return OwnThemeFields(errorShade: Colors.black, textBaloon: Colors.black);
}
}
extension ThemeDataExtensions on ThemeData {
static Map<InputDecorationTheme, OwnThemeFields> _own = {};
void addOwn(OwnThemeFields own) {
_own[this.inputDecorationTheme] = own;
}
static OwnThemeFields? empty = null;
OwnThemeFields own() {
var o = _own[this.inputDecorationTheme];
if (o == null) {
if (empty == null) empty = OwnThemeFields.empty();
o = empty;
}
return o!;
}
}
OwnThemeFields ownTheme(BuildContext context) => Theme.of(context).own();
Complete source: https://github.com/maxim-saplin/dikt/blob/master/lib/ui/themes.dart
2022: Use ThemeExtensions introduced in flutter 3
Here's a link! to the medium article I wrote.
Create your ThemeExtension class
import 'package:flutter/material.dart';
#immutable
class MyCardTheme extends ThemeExtension<MyCardTheme> {
const MyCardTheme({
this.background = Colors.white,
this.shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
});
final Color background;
final ShapeBorder shape;
#override
MyCardTheme copyWith({
Color? background,
ShapeBorder? shape,
}) {
return MyCardTheme(
background: background ?? this.background,
shape: shape ?? this.shape,
);
}
#override
MyCardTheme lerp(ThemeExtension<MyCardTheme>? other, double t) {
if (other is! MyCardTheme) {
return this;
}
return MyCardTheme(
background: Color.lerp(background, other.background, t) ?? Colors.white,
shape: ShapeBorder.lerp(shape, other.shape, t) ??
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
);
}
#override
String toString() => 'MyCardTheme('
'background: $background, radius: $shape'
')';
}
Create dark and light themes as per requirements
MyCardTheme lightCardTheme = MyCardTheme(
background: Colors.blueGrey[200]!,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
);
MyCardTheme darkCardTheme = MyCardTheme(
background: Colors.blueGrey[800]!,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
);
Add extensions to your ThemeData for both, light and dark themes.
theme: ThemeData(
primarySwatch: Colors.green,
cardTheme: const CardTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
color: Colors.green,
),
extensions: <ThemeExtension<dynamic>>[
lightCardTheme,
],
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.green,
cardTheme: const CardTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
color: Colors.green,
),
extensions: <ThemeExtension<dynamic>>[
darkCardTheme,
],
),
Use them in your build methods
final MyCardTheme customCardTheme =
Theme.of(context).extension<MyCardTheme>()!;
Card(
shape: customCardTheme.shape,
color: customCardTheme.background,
child: Container(
padding: const EdgeInsets.all(16),
child: const Text('Card styled from custom theme')),
),
),
You can't extend ThemeData because then material components won't find it anymore.
You can just create and provide MyThemeData in addition to the ThemeData included in Flutter the same way.
Create a widget CustomThemeWidget that extends InheritedWidget and provide your custom theme there.
When you want to get a value from the current theme use
myTheme = CustomThemeWidget.of(context).myTheme;
To change the current theme change the MyThemeData in CustomThemeWidget.myTheme
Update
Like shown in https://github.com/flutter/flutter/pull/14793/files, it should be possible to extend ThemeData and provide it as ThemeData by overriding runtimeType
See also the comment in https://github.com/flutter/flutter/issues/16487#event-1573761656
Dart 2.7 later, extension support
you can add extension for system class
only add instance property is easy, but if you would get a dynamic color
you need think about it. for example, Use a constant to get the colors in light and dark modes
Determine if it is dark mode
two ways
MediaQuery.of(context).platformBrightnes == Brightness.dark;
Theme.of(context).brightness == Brightness.dark;
As you can see, you need the context, the context
Add Extension for BuildContext
Here is the code
extension MYContext on BuildContext {
Color dynamicColor({int light, int dark}) {
return (Theme.of(this).brightness == Brightness.light)
? Color(light)
: Color(dark);
}
Color dynamicColour({Color light, Color dark}) {
return (Theme.of(this).brightness == Brightness.light)
? light
: dark;
}
/// the white background
Color get bgWhite => dynamicColor(light: 0xFFFFFFFF, dark: 0xFF000000);
}
How to use
import 'package:flutter/material.dart';
import 'buildcontext_extension.dart';
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: context.bgWhite,
);
}
}
Also
This color may require multiple files, so you can create a public.dart file to manage it all
Like This
public.dart
library public;
// Export some common header files
// extensions
export 'buildcontext_extension.dart';
DarkMode images support
Put the light images in the same category as the dark ones
some code
static String getImgPath(String name, {
String folder = '',
String format = 'png',
bool isDark = false,
bool needDark = true
}) {
String finalImagePath;
if (needDark) {
final folderName = isDark ? '${folder}_dark' : folder;
finalImagePath = 'assets/images/$folderName/$name.$format';
} else {
finalImagePath = 'assets/images/$folder/$name.$format';
}
String isDarkPath = isDark ? "🌙 DarkMode" : "🌞 LightMode";
print('$isDarkPath imagePath 🖼 $finalImagePath');
return finalImagePath;
}
Instead of extending, you can use the new feature ThemeExtension in flutter.
We can add custom styling and even use class type theme configuration in css.
example:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
#immutable
class MyColors extends ThemeExtension<MyColors> {
const MyColors({
required this.brandColor,
required this.danger,
});
final Color? brandColor;
final Color? danger;
#override
MyColors copyWith({Color? brandColor, Color? danger}) {
return MyColors(
brandColor: brandColor ?? this.brandColor,
danger: danger ?? this.danger,
);
}
#override
MyColors lerp(ThemeExtension<MyColors>? other, double t) {
if (other is! MyColors) {
return this;
}
return MyColors(
brandColor: Color.lerp(brandColor, other.brandColor, t),
danger: Color.lerp(danger, other.danger, t),
);
}
// Optional
#override
String toString() => 'MyColors(brandColor: $brandColor, danger: $danger)';
}
void main() {
// Slow down time to see lerping.
timeDilation = 5.0;
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isLightTheme = true;
void toggleTheme() {
setState(() => isLightTheme = !isLightTheme);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: MyApp._title,
theme: ThemeData.light().copyWith(
extensions: <ThemeExtension<dynamic>>[
const MyColors(
brandColor: Color(0xFF1E88E5),
danger: Color(0xFFE53935),
),
],
),
darkTheme: ThemeData.dark().copyWith(
extensions: <ThemeExtension<dynamic>>[
const MyColors(
brandColor: Color(0xFF90CAF9),
danger: Color(0xFFEF9A9A),
),
],
),
themeMode: isLightTheme ? ThemeMode.light : ThemeMode.dark,
home: Home(
isLightTheme: isLightTheme,
toggleTheme: toggleTheme,
),
);
}
}
class Home extends StatelessWidget {
const Home({
Key? key,
required this.isLightTheme,
required this.toggleTheme,
}) : super(key: key);
final bool isLightTheme;
final void Function() toggleTheme;
#override
Widget build(BuildContext context) {
final MyColors myColors = Theme.of(context).extension<MyColors>()!;
return Material(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(width: 100, height: 100, color: myColors.brandColor),
const SizedBox(width: 10),
Container(width: 100, height: 100, color: myColors.danger),
const SizedBox(width: 50),
IconButton(
icon: Icon(isLightTheme ? Icons.nightlight : Icons.wb_sunny),
onPressed: toggleTheme,
),
],
)),
);
}
}
from Flutter API Documentation
I have also found that the ThemeData is restricting. What I have done, and will be doing for all of my apps in the future is creating my own ThemeData.
I have created a file named color_themes.dart and created a class named ColorThemes with constructors with the name of the colors that I desire. such as cyclingColor;
class ColorThemes {
static const cyclingColor = const Color(0xffb74093);
}
You can then call these colors by importing the file and calling ColorThemes.cyclingColor You can assign these values within your ThemeData to have these colors default to your ColorThemes. One of the benefits with using this method is that you do not need to use/reference context like so ThemeData.of(context) making it a lot easier to use your code in extracted widgets.
I solved this problem also for multiple themes by creating a CustomThemeData class like this:
class CustomThemeData {
final double imageSize;
CustomThemeData({
this.imageSize = 100,
});
}
Then, creating instances for each Theme:
final _customTheme = CustomThemeData(imageSize: 150);
final _customDarkTheme = CustomThemeData();
And writing an extension on ThemeData:
extension CustomTheme on ThemeData {
CustomThemeData get custom => brightness == Brightness.dark ? _customDarkTheme : _customTheme;
}
Finally, the value can be accessed like this:
Theme.of(context).custom.imageSize
For more information see: https://bettercoding.dev/flutter/tutorial-themes-riverpod/#custom_attributes_extending_themedata
use this lib adaptive_theme for theme switch.
And create extension of ColorSheme
extension MenuColorScheme on ColorScheme {
Color get menuBackground => brightness == Brightness.light
? InlLightColors.White
: InlDarkColors.Black;
}
In widget use that
Container(
color: Theme.of(context).colorScheme.menuBackground,
...
)
This way is very simple and elegance. Nice to codding.
A simple workaround if you are not using all the textTheme headlines you can set some colors of some of them and use them like you normally use other colors.
set the headline1 color:
ThemeData(textTheme: TextTheme(headline1: TextStyle(color: Colors.red),),),
Use it:
RawMaterialButton(fillColor: Theme.of(context).textTheme.headline1.color,onPressed: onPressed,)
I created an implementation analog to the implementation of ThemeData:
#override
Widget build(BuildContext context) {
final Brightness platformBrightness = Theme.of(context).brightness;
final bool darkTheme = platformBrightness == Brightness.dark;
return CustomAppTheme(
customAppTheme:
darkTheme ? CustomAppThemeData.dark : CustomAppThemeData.light,
child: Icon(Icons.add, color: CustomAppTheme.of(context).addColor,),
);
}
import 'package:calendarflutter/style/custom_app_theme_data.dart';
import 'package:flutter/material.dart';
class CustomAppTheme extends InheritedWidget {
CustomAppTheme({
Key key,
#required Widget child,
this.customAppTheme,
}) : super(key: key, child: child);
final CustomAppThemeData customAppTheme;
static CustomAppThemeData of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CustomAppTheme>()
.customAppTheme;
}
#override
bool updateShouldNotify(CustomAppTheme oldWidget) =>
customAppTheme != oldWidget.customAppTheme;
}
import 'package:flutter/material.dart';
class CustomAppThemeData {
final Color plusColor;
const CustomAppThemeData({
#required this.plusColor,
});
static CustomAppThemeData get dark {
return CustomAppThemeData(
plusColor: Colors.red,
);
}
static CustomAppThemeData get light {
return CustomAppThemeData(
plusColor: Colors.green,
);
}
}
To extend (pun not intended) the answer of Maxim Saplin:
You may encounter a problem, where theme stays on the last one initialized in your code. This is happening because InputDecorationTheme is always the same for all of yours themes.
What solved it for me, was changing key (InputDecorationTheme) in _own to something unique, like themeID (you'll have to implement it somehow).