带有url更新和超链接支持的FflantWeb底部导航栏 [英] Flutter web Bottom Navigation Bar with url updating and hyperlink support

查看:12
本文介绍了带有url更新和超链接支持的FflantWeb底部导航栏的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经在网上搜索了很长时间,寻找一款支持以下功能的Flight应用程序的示例:

  1. 永久底部导航栏
  2. 对每个保持状态的底部导航条项目的导航器支持
  3. 导航器在导航时更新颤动Web中的URL
  4. 导航器支持超链接到颤动网页中的特定页面。例如,如果用户输入www.myappdomain.com/#/profile,他们会被导航到个人资料页面。或者他们输入www.myappdomain.com/#/profile?id=123,他们将被导航到用户123的个人资料页面。

我已经通过以下代码实现了第1点和第2点:

import 'package:flutter/material.dart';

void main() {
  runApp(BaseApp());
}

class BaseApp extends StatefulWidget {
  @override
  _BaseAppState createState() => _BaseAppState();
}

class _BaseAppState extends State<BaseApp> {
  // define all navigation tabs for user
  static List<NavigationTabModel> _navigationTabs = [
    NavigationTabModel(
      title: 'Home',
      icon: Icons.home,
      url: '/home',
      navigatorKey: GlobalKey<NavigatorState>(),
    ),
    NavigationTabModel(
      title: 'Search',
      icon: Icons.search,
      url: '/search',
      navigatorKey: GlobalKey<NavigatorState>(),
    ),
    NavigationTabModel(
      title: 'Profile',
      icon: Icons.person,
      url: '/profile',
      navigatorKey: GlobalKey<NavigatorState>(),
    ),
  ];

  // route generator used to build all material page routes
  RouteGenerator _routeGenerator = RouteGenerator();

  // set the current tab to the home page
  int _currentIndex = 0;

  void _select(int index) {
    if (index == _currentIndex) {
      // case 1 - if user presses on currently selected tab
      // pop to first route - i.e. ensure no routes are over laid on top of the current route
      _navigationTabs[_currentIndex]
          .navigatorKey
          .currentState
          .popUntil((route) {
        return route.isFirst;
      });
    } else {
      // case 2 - user selects any other tab
      // rebuild application state with the newly selected navigation tab
      setState(() {
        _currentIndex = index;
      });
    }
  }

  /// generate a list of navigators that will have their state persisted in an
  /// indexed stack.
  List<Widget> _getPersistantStack() {
    return _navigationTabs.map((tab) {
      return WillPopScope(
        onWillPop: () async {
          return !await tab.navigatorKey.currentState.maybePop();
        },
        child: Navigator(
          key: tab.navigatorKey,
          initialRoute: tab.url,
          onGenerateRoute: _routeGenerator.generateRoute,
        ),
      );
    }).toList();
  }

  @override
  Widget build(BuildContext context) {
    /// ********************* HOLD POINT *********************
    /// MaterialApp contains our top-level Navigator. Top level navigator is
    /// required to enable navigation via urls in flutter web. Likely that this
    /// section requires refractoring in some way to enable url updates from the
    /// nested navigators and hyperlinking from web browsers to specific pages
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: IndexedStack(
            children: _getPersistantStack(),
            index: _currentIndex,
          ),
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: _navigationTabs.map((tab) {
            return BottomNavigationBarItem(
              label: tab.title,
              icon: Icon(tab.icon),
            );
          }).toList(),
          onTap: (int index) {
            _select(index);
          },
          currentIndex: _currentIndex,
          type: BottomNavigationBarType.fixed,
          // hide titles on navigation bar
          showSelectedLabels: false,
          showUnselectedLabels: false,
        ),
      ),
    );
  }
}

class NavigationTabModel {
  final String title;
  final IconData icon;
  final String url;
  final GlobalKey<NavigatorState> navigatorKey;

  NavigationTabModel({
    this.title,
    this.icon,
    this.url,
    this.navigatorKey,
  });
}

class RouteGenerator {
  Route<dynamic> generateRoute(RouteSettings settings) {
    // Widget builder (function that returns a widget) to construct the route page
    WidgetBuilder builder;

    // build different route (page) based on the route passed to the navigator
    switch (settings.name) {
      case '/home':
        builder = (BuildContext context) {
          return SamplePage(name: 'home');
        };
        break;
      case '/search':
        builder = (BuildContext context) {
          return SamplePage(name: 'search');
        };
        break;
      case '/profile':
        builder = (BuildContext context) {
          return SamplePage(name: 'profile');
        };
        break;
      case '/':
        builder = null;
        break;
      default:
        // If there is no such named route in the switch statement
        builder = (BuildContext context) {
          return SamplePage();
        };
    }
    // prevent page being added to default '/' route
    if (builder == null) {
      return null;
    }
    return MaterialPageRoute(
      builder: builder,
      settings: settings,
    );
  }
}

class SamplePage extends StatelessWidget {
  final String name;

  SamplePage({
    this.name,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('$name'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            RaisedButton(
              child: Text('push new route'),
              onPressed: () {
                Navigator.of(context).pushNamed('/$name');
              },
            ),
            SizedBox(
              height: 16,
            ),
            Expanded(
              child: ListView.builder(
                itemCount: 100,
                itemBuilder: (context, index) {
                  return Card(
                    child: Text(
                      index.toString(),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

但是,我想不出如何更新此应用以实现第3点和第4点。有人知道如何实现这一点吗?

推荐答案

虽然Navigator 2.0一开始有点吓人,但当您(主要)针对Web应用程序时,这是非常值得的,因为您有很多从深层链接恢复状态的选择。

在@lulupoint tu已经链接到的official introduction中,有一个与您正在寻找的完全相同的示例(但在文章的最底部有一点隐藏)。

In this gist是为不同的选项卡使用不同的导航器堆栈的工作示例,为了完整起见,我在下面发布。适用于我的频道测试版,1.23.0-18.1.pre

import 'package:flutter/material.dart';

void main() {
  runApp(NestedRouterDemo());
}

class Book {
  final String title;
  final String author;

  Book(this.title, this.author);
}

class NestedRouterDemo extends StatefulWidget {
  @override
  _NestedRouterDemoState createState() => _NestedRouterDemoState();
}

class _NestedRouterDemoState extends State<NestedRouterDemo> {
  BookRouterDelegate _routerDelegate = BookRouterDelegate();
  BookRouteInformationParser _routeInformationParser =
      BookRouteInformationParser();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Books App',
      routerDelegate: _routerDelegate,
      routeInformationParser: _routeInformationParser,
    );
  }
}

class BooksAppState extends ChangeNotifier {
  int _selectedIndex;

  Book _selectedBook;

  final List<Book> books = [
    Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
    Book('Foundation', 'Isaac Asimov'),
    Book('Fahrenheit 451', 'Ray Bradbury'),
  ];

  BooksAppState() : _selectedIndex = 0;

  int get selectedIndex => _selectedIndex;

  set selectedIndex(int idx) {
    _selectedIndex = idx;
    notifyListeners();
  }

  Book get selectedBook => _selectedBook;

  set selectedBook(Book book) {
    _selectedBook = book;
    notifyListeners();
  }

  int getSelectedBookById() {
    if (!books.contains(_selectedBook)) return 0;
    return books.indexOf(_selectedBook);
  }

  void setSelectedBookById(int id) {
    if (id < 0 || id > books.length - 1) {
      return;
    }

    _selectedBook = books[id];
    notifyListeners();
  }
}

class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location);

    if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') {
      return BooksSettingsPath();
    } else {
      if (uri.pathSegments.length >= 2) {
        if (uri.pathSegments[0] == 'book') {
          return BooksDetailsPath(int.tryParse(uri.pathSegments[1]));
        }
      }
      return BooksListPath();
    }
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath configuration) {
    if (configuration is BooksListPath) {
      return RouteInformation(location: '/home');
    }
    if (configuration is BooksSettingsPath) {
      return RouteInformation(location: '/settings');
    }
    if (configuration is BooksDetailsPath) {
      return RouteInformation(location: '/book/${configuration.id}');
    }
    return null;
  }
}

class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;

  BooksAppState appState = BooksAppState();

  BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
    appState.addListener(notifyListeners);
  }

  @override
  BookRoutePath get currentConfiguration {
    if (appState.selectedIndex == 1) {
      return BooksSettingsPath();
    } else {
      if (appState.selectedBook == null) {
        return BooksListPath();
      } else {
        return BooksDetailsPath(appState.getSelectedBookById());
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage(
          child: AppShell(appState: appState),
        ),
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }

        if (appState.selectedBook != null) {
          appState.selectedBook = null;
        }
        notifyListeners();
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(BookRoutePath path) async {
    if (path is BooksListPath) {
      appState.selectedIndex = 0;
      appState.selectedBook = null;
    } else if (path is BooksSettingsPath) {
      appState.selectedIndex = 1;
    } else if (path is BooksDetailsPath) {
      appState.selectedIndex = 0;
      appState.setSelectedBookById(path.id);
    }
  }
}

// Routes
abstract class BookRoutePath {}

class BooksListPath extends BookRoutePath {}

class BooksSettingsPath extends BookRoutePath {}

class BooksDetailsPath extends BookRoutePath {
  final int id;

  BooksDetailsPath(this.id);
}

// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatefulWidget {
  final BooksAppState appState;

  AppShell({
    @required this.appState,
  });

  @override
  _AppShellState createState() => _AppShellState();
}

class _AppShellState extends State<AppShell> {
  InnerRouterDelegate _routerDelegate;
  ChildBackButtonDispatcher _backButtonDispatcher;

  void initState() {
    super.initState();
    _routerDelegate = InnerRouterDelegate(widget.appState);
  }

  @override
  void didUpdateWidget(covariant AppShell oldWidget) {
    super.didUpdateWidget(oldWidget);
    _routerDelegate.appState = widget.appState;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Defer back button dispatching to the child router
    _backButtonDispatcher = Router.of(context)
        .backButtonDispatcher
        .createChildBackButtonDispatcher();
  }

  @override
  Widget build(BuildContext context) {
    var appState = widget.appState;

    // Claim priority, If there are parallel sub router, you will need
    // to pick which one should take priority;
    _backButtonDispatcher.takePriority();

    return Scaffold(
      appBar: AppBar(),
      body: Router(
        routerDelegate: _routerDelegate,
        backButtonDispatcher: _backButtonDispatcher,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(
              icon: Icon(Icons.settings), label: 'Settings'),
        ],
        currentIndex: appState.selectedIndex,
        onTap: (newIndex) {
          appState.selectedIndex = newIndex;
        },
      ),
    );
  }
}

class InnerRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  BooksAppState get appState => _appState;
  BooksAppState _appState;
  set appState(BooksAppState value) {
    if (value == _appState) {
      return;
    }
    _appState = value;
    notifyListeners();
  }

  InnerRouterDelegate(this._appState);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        if (appState.selectedIndex == 0) ...[
          FadeAnimationPage(
            child: BooksListScreen(
              books: appState.books,
              onTapped: _handleBookTapped,
            ),
            key: ValueKey('BooksListPage'),
          ),
          if (appState.selectedBook != null)
            MaterialPage(
              key: ValueKey(appState.selectedBook),
              child: BookDetailsScreen(book: appState.selectedBook),
            ),
        ] else
          FadeAnimationPage(
            child: SettingsScreen(),
            key: ValueKey('SettingsPage'),
          ),
      ],
      onPopPage: (route, result) {
        appState.selectedBook = null;
        notifyListeners();
        return route.didPop(result);
      },
    );
  }

  @override
  Future<void> setNewRoutePath(BookRoutePath path) async {
    // This is not required for inner router delegate because it does not
    // parse route
    assert(false);
  }

  void _handleBookTapped(Book book) {
    appState.selectedBook = book;
    notifyListeners();
  }
}

class FadeAnimationPage extends Page {
  final Widget child;

  FadeAnimationPage({Key key, this.child}) : super(key: key);

  Route createRoute(BuildContext context) {
    return PageRouteBuilder(
      settings: this,
      pageBuilder: (context, animation, animation2) {
        var curveTween = CurveTween(curve: Curves.easeIn);
        return FadeTransition(
          opacity: animation.drive(curveTween),
          child: child,
        );
      },
    );
  }
}

// Screens
class BooksListScreen extends StatelessWidget {
  final List<Book> books;
  final ValueChanged<Book> onTapped;

  BooksListScreen({
    @required this.books,
    @required this.onTapped,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: [
          for (var book in books)
            ListTile(
              title: Text(book.title),
              subtitle: Text(book.author),
              onTap: () => onTapped(book),
            )
        ],
      ),
    );
  }
}

class BookDetailsScreen extends StatelessWidget {
  final Book book;

  BookDetailsScreen({
    @required this.book,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            FlatButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text('Back'),
            ),
            if (book != null) ...[
              Text(book.title, style: Theme.of(context).textTheme.headline6),
              Text(book.author, style: Theme.of(context).textTheme.subtitle1),
            ],
          ],
        ),
      ),
    );
  }
}

class SettingsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Settings screen'),
      ),
    );
  }
}

如果您希望主页导航按钮链接到主页,而不是保持状态,则必须使用以下内容替换Set seltedIndex:

  set selectedIndex(int idx) {
    _selectedIndex = idx;
    if (_selectedIndex == 1) {
      // Remove this line if you want to keep the selected book when navigating
      // between "settings" and "home" which book was selected when Settings is
      // tapped.
      selectedBook = null;
    }
    notifyListeners();
  }
在编写您自己的RouteInformationParser时,您可能希望了解如何提取查询参数: Parsing a URI to extract query parameters, with Dart

这里是您可能想要了解Navigator 2.0的另一个示例:https://github.com/flutter/flutter/pull/63424

这篇关于带有url更新和超链接支持的FflantWeb底部导航栏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆