Внедрение нескольких страниц на одну страницу с помощью навигации и стека

Во 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 ...'),
    );
  }
}
Другие вопросы по тегам