Внедрение нескольких страниц на одну страницу с помощью навигации и стека
Во Flutter я хочу делать экраны как с Fragment
в android, в этом моем коде я пытаюсь заменить каждый экран на текущий экран, например, с Fragment.replecae
в android я использовал Hook
а также Provider
и мой код работает нормально, когда я нажимаю кнопки для переключения между ними, но я не могу реализовать задний стек, что означает, когда я нажимаю на Back
кнопку на телефоне, мой код должен отображать последний экран, который я сохранил в _backStack
переменная, каждый переключатель между этими экранами я сохранял текущий индекс экрана в этой переменной.
как я могу решить эту проблему из этого стека в моем примере кода?
// Switch Between screens:
DashboardPage(), UserProfilePage(), SearchPage()
-------------> -------------> ------------->
// When back from stack:
DashboardPage(), UserProfilePage(), SearchPage()
Exit from application <-------------- <---------------- <-----------
я использовал Hook
и я хочу реализовать это действие с помощью функций этой библиотеки
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MultiProvider(providers: [
Provider.value(value: StreamBackStackSupport()),
StreamProvider<homePages>(
create: (context) =>
Provider.of<StreamBackStackSupport>(context, listen: false)
.selectedPage,
)
], child: StartupApplication()));
}
class StartupApplication extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final List<Widget> _fragments = [
DashboardPage(),
UserProfilePage(),
SearchPage()
];
List<int> _backStack = [0];
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: WillPopScope(
// ignore: missing_return
onWillPop: () {
customPop(context);
},
child: Container(
child: Column(
children: <Widget>[
Consumer<homePages>(
builder: (context, selectedPage, child) {
_currentIndex = selectedPage != null ? selectedPage.index : 0;
_backStack.add(_currentIndex);
return Expanded(child: _fragments[_currentIndex]);
},
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenDashboard),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenProfile),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenSearch),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
void navigateBack(int index) {
useState(() => _currentIndex = index);
}
void customPop(BuildContext context) {
if (_backStack.length - 1 > 0) {
navigateBack(_backStack[_backStack.length - 1]);
} else {
_backStack.removeAt(_backStack.length - 1);
Provider.of<StreamBackStackSupport>(context, listen: false)
.switchBetweenPages(homePages.values[_backStack.length - 1]);
Navigator.pop(context);
}
}
}
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}
enum homePages { screenDashboard, screenProfile, screenSearch }
class StreamBackStackSupport {
final StreamController<homePages> _homePages = StreamController<homePages>();
Stream<homePages> get selectedPage => _homePages.stream;
void switchBetweenPages(homePages selectedPage) {
_homePages.add(homePages.values[selectedPage.index]);
}
void close() {
_homePages.close();
}
}
1 ответ
TL;DR
Полный код находится в конце.
Использовать Navigator
вместо
Вам следует подойти к этой проблеме иначе. Я мог бы представить вам решение, которое будет работать с вашим подходом, однако я думаю, что вместо этого вам следует решить эту проблему, реализовав собственный Navigator
поскольку это встроенное решение во Flutter.
Когда вы используете Navigator
, вам не нужно никакого управления на основе потоков, т.е. вы можете удалить StreamBackStackSupport
целиком.
Теперь вы вставляете Navigator
виджет, где у вас был ваш Consumer
перед:
children: <Widget>[
Expanded(
child: Navigator(
...
),
),
Container(...), // Your bottom bar..
]
Навигатор управляет своими маршрутами с помощью строк, а это значит, что нам понадобится способ конвертировать ваши enum
(который я переименовал в Page
) к String
с. Мы можем использовать describeEnum
для этого и поместите это в extension
:
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
Теперь вы можете получить строковое представление страницы, используя, например, Page.screenDashboard.route
.
Кроме того, вы хотите сопоставить свои фактические страницы с виджетами фрагментов, что вы можете сделать следующим образом:
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
...
Чтобы получить доступ к Navigator
, нам нужно иметь GlobalKey
. Обычно у нас был StatefulWidget
и управлять GlobalKey
как это. Поскольку вы хотите использоватьflutter_hooks
, Я решил использовать GlobalObjectKey
вместо:
@override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
...
Теперь вы можете использовать navigatorKey.currentState
в любом месте вашего виджета, чтобы получить доступ к этому пользовательскому навигатору. ПолныйNavigator
настройка выглядит так:
Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere((element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings, builder: (context) => _fragments[page]);
},
)
Как видите, мы передаем navigatorKey
созданный ранее и определить initialRoute
, используя route
расширение, которое мы создали. ВonGenerateRoute
, мы находим Page
запись перечисления, соответствующая имени маршрута (String
), а затем вернуть MaterialPageRoute
с соответствующими _fragments
вход.
Чтобы протолкнуть новый маршрут, вы просто используете navigatorKey
а также pushNamed
:
onPressed: () => navigatorKey.currentState.pushNamed(Page.screenDashboard.route),
Кнопка назад
Еще нам нужно индивидуально позвонить pop
в нашем пользовательском навигаторе. Для этого WillPopScope
необходим:
WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: ..,
)
Доступ к настраиваемому навигатору внутри вложенных страниц
На любой странице, переданной в onGenerateRoute
, т.е. в любом из ваших "фрагментов" вы можете просто позвонить Navigator.of(context)
вместо использования глобального ключа. Это возможно, потому что эти маршруты являются дочерними по отношению к настраиваемому навигатору и, следовательно,BuildContext
содержит этот настраиваемый навигатор.
Например:
// In SearchPage
Navigator.of(context).pushNamed(Page.screenProfile.route);
Навигатор по умолчанию
Вам может быть интересно, как получить доступ к MaterialApp
теперь корневой навигатор, например, чтобы запустить новый полноэкранный маршрут. Вы можете использовать findRootAncestorStateOfType
для этого:
context.findRootAncestorStateOfType<NavigatorState>().push(..);
или просто
Navigator.of(context, rootNavigator: true).push(..);
Вот полный код:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(StartupApplication());
}
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
class StartupApplication extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
@override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
return WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: Container(
child: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere(
(element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings,
builder: (context) => _fragments[page]);
},
),
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenDashboard.route),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenProfile.route),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenSearch.route),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
}
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}