Navigation/routing package which is:
- Compile safe
- Web compatible yet does not required url handling on mobile
- Comes with a powerful nested navigator pattern
- No codegen
- Index
- Getting started
- Web/Deep linking
- Create tabbed page stack
- Handle pop
- Handle back button
- Custom transitions
- React to visibility change
class HomePageStack extends PageStack {
@override
Widget build(BuildContext context) {
return HomeScreen();
}
}
class SettingsPageStack extends PageStack {
@override
Widget build(BuildContext context) {
return SettingsScreen();
}
@override
PageStackBase? get pageStackBellow => HomePageStack();
}
💡 Tip: use pstack
in your IDE to easily create a PageStack
MaterialApp(
home: Theater(
initialPageStack: HomePageStack(),
),
);
// Navigate to Home
context.to(HomePageStack());
// Navigate to Seettings
context.to(SettingsPageStack());
If you need deep-linking, or are on the web, use translators to translate you PageStack
s to url and back
Theater(
initialPageStack: HomePageStack(),
translatorsBuilder: (_) => [
PathTranslator<HomePageStack>(
path: '/',
pageStack: HomePageStack(),
),
PathTranslator<SettingsPageStack>(
path: '/settings',
pageStack: SettingsPageStack(),
),
],
)
💡 Tip: use ptrans
in your IDE to easily create a PathTranslator
// DO
PathTranslator<HomePageStack>(...)
// DON'T
PathTranslator(...)
You also need to add ensureInitialized
before runApp
void main() {
Theater.ensureInitialized();
runApp(...)
}
Use the .parse
constructor to add and extract arguments from the url:
PathTranslator<HomePageStack>.parse(
path: '/:userId',
matchToPageStack: (match) => HomePageStack(userId: match.pathParams['userId']!),
pageStackToWebEntry: (pageStack) => WebEntry(path: '/${pageStack.userId}'),
),
💡 Tip: use ptransparse
in your IDE to easily create a PathTranslator.parse
RedirectorTranslator(path: '*', pageStack: HomePageStack())
The translators can be added/removed dynamically:
Theater(
initialPageStack: HomePageStack(),
translatorsBuilder: (_) => [
isLoggedIn
? PathTranslator<HomePageStack>(
path: '/',
pageStack: HomePageStack(),
),
: PathTranslator<HomePageStack>(
path: '/login',
pageStack: LoginPageStack(),
),
],
)
Page stacks of the tabs:
// Use [Tab1In] on [PageStack]s of the 1st tab
class HomePageStack extends PageStack with Tab1In<MyScaffoldPageStack> {
@override
Widget build(BuildContext context) {
return HomeScreen();
}
}
// Use [Tab2In] on [PageStack]s of the 2nd tab
class ProfilePageStack extends PageStack with Tab2In<MyScaffoldPageStack> {
@override
Widget build(BuildContext context) {
return ProfileScreen();
}
}
class SettingsPageStack extends PageStack with Tab2In<MyScaffoldPageStack> {
@override
Widget build(BuildContext context) {
return SettingsScreen();
}
@override
Tab2In<MyScaffoldPageStack>? get pageStackBellow => ProfilePageStack();
}
Page stack of the scaffold in which the tabs are:
class MyScaffoldPageStack extends Multi2TabsPageStack {
// See how to use this constructor in the next section
const MyScaffoldPageStack(StateBuilder<Multi2TabsState> stateBuilder)
: super(stateBuilder);
@override
Widget build(BuildContext context, MultiTabPageState<Multi2TabsState> state) {
return MyScaffold(children: state.tabs, currentIndex: state.currentIndex);
}
@override
Multi2TabsState get initialState => Multi2TabsState(
currentIndex: 0,
tab1PageStack: HomePageStack(),
tab2PageStack: ProfilePageStack(),
);
}
💡 Tip: use m2tabspagestack
in your IDE to easily create a Multi2TabsPageStack
// To the Profile tab
context.theater.to(
MyScaffoldPageStack((state) => state.withCurrentStack(ProfilePageStack())),
)
// Switch between tabs
context.theater.to(
MyScaffoldPageStack((state) => state.withCurrentIndex(0)),
)
Multi2TabsTranslator<MyScaffoldPageStack>(
pageStack: MyScaffoldPageStack.new,
tab1Translators: [
PathTranslator<HomePageStack>(
path: '/home',
pageStack: HomePageStack(),
),
],
tab2Translators: [
PathTranslator<ProfilePageStack>(
path: '/profile',
pageStack: ProfilePageStack(),
),
],
)
💡 Tip: use m2tabspagetrans
in your IDE to easily create a Multi2TabsTranslator
Use the WillPopScope
provided by flutter:
WillPopScope(
onWillPop: () async {
// Prevent any pop
return false;
},
child: child,
);
Use the BackButtonListener
provided by flutter:
BackButtonListener(
onBackButtonPressed: () async {
// Prevent the back button from popping the stack.
return true;
},
child: child,
);
Transitions are provided by the [Page] object created by each of your page stack. You can use a custom page to create a custom transition.
Theater provides an helpful page for this: [CustomTransitionPage].
// Create a FadeTransition as the default transition
Theater(
defaultPageBuilder: (context, pageStack, child) {
return CustomTransitionPage(
key: pageStack.key, // DO use pageStack.key
child: child,
transitionsBuilder: (context, animation, _, child) =>
FadeTransition(opacity: animation, child: child),
);
},
...
),
class HomePageStack extends PageStack with {
@override
Page buildPage(BuildContext context, PageState state, Widget child) {
return CustomTransitionPage(
child: child,
// Create a slide transition from left to right
transitionsBuilder: (context, animation, _, child) => SlideTransition(
position: animation.drive(
Tween<Offset>(begin: const Offset(-1, 0.0), end: Offset.zero),
),
child: child,
),
);
}
@override
Widget build(BuildContext context) => HomeScreen();
}
Keys are used by Flutter navigator to know if two pages are the same. Transitions only play, when pages are siblings, when their type or keys are different.
For example, with the following PageStack
:
class UserPageStack extends PageStack {
final String userId;
UserPageStack({required this.userId});
@override
Widget build(BuildContext context) {
return UserScreen(userId: userId);
}
}
If you are in the UserScreen
and use:
context.to(UserPageStack(userId: 'anotherUserId'))
You won't see any transition.
To see a transition, specify a key in you PageStack
:
class UserPageStack extends PageStack {
final String userId;
UserPageStack({required this.userId});
// Use userId, which differentiate the UserPageStacks
LocalKey get key => ValueKey(userId);
@override
Widget build(BuildContext context) {
return UserScreen(userId: userId);
}
}
If you need to know if you widget is currently visible, use PageState.of(context).isCurrent
:
class MyScreen extends StatefulWidget {
@override
State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late bool _isCurrent;
@override
void didChangeDependencies() {
// Use [PageState.of] in [didChangeDependencies] or directly in your [build] method
_isCurrent = PageState.of(context).isCurrent;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return child;
}
}
This will cause your widget to rebuild when isCurrent
changes.