Flutter: Tìm hiểu hệ thống điều hướng và định tuyến mới của Flutter

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Mục lục bài viết

Bài viết này giải thích cách hoạt động của API mới của Flutter là Navigator và Router. Nếu bạn theo dõi các tài liệu thiết kế mở của Flutter, bạn có thể đã thấy các tính năng mới này được gọi là Navigator 2.0 và Bộ định tuyến. Bài viết này sẽ khám phá cách các API này cho phép kiểm soát tốt hơn các màn hình trong ứng dụng của bạn và cách bạn có thể sử dụng nó để phân tích cú pháp các định tuyến.

Các API mới này không phá vỡ các thay đổi, chúng chỉ thêm một API khai báo mới. Trước Navigator 2.0, rất khó để đẩy hoặc bật nhiều trang hoặc xóa một trang bên dưới trang hiện tại.

Các Router cung cấp khả năng xử lý các định tuyến từ nền tảng cơ bản và hiển thị các trang thích hợp. Trong bài viết này, Router được định cấu hình để phân tích cú pháp URL của trình duyệt để hiển thị trang thích hợp.

Bài viết này giúp bạn chọn mẫu Navigator hoạt động tốt nhất cho ứng dụng của mình và giải thích cách sử dụng Navigator 2.0 để phân tích cú pháp URL của trình duyệt và có toàn quyền kiểm soát chồng trang đang hoạt động. Bài tập trong bài viết này chỉ ra cách tạo một ứng dụng xử lý các định tuyến đến từ nền tảng và quản lý các trang trong ứng dụng của bạn. Ảnh GIF sau đây cho thấy ứng dụng mẫu đang hoạt động:

Hình ảnh cho bài đăng

Navigator 1.0

Nếu bạn đang sử dụng Flutter, có thể bạn đã đang sử dụng Navigator và quen thuộc với các khái niệm sau:

  • Navigator - một widget quản lý một chồng các đối tượng Route.
  • Route - một đối tượng được quản lý bởi một Navigator đại diện cho một màn hình, thường được thực hiện bởi các lớp như MaterialPageRoute.

Trước Navigator 2.0, Routes đã được đẩy và đưa vào ngăn xếp Navigator của các định tuyến được đặt tên hoặc các định tuyến ẩn danh. Các phần tiếp theo là một bản tóm tắt ngắn gọn về hai cách tiếp cận này.

Các định tuyến ẩn danh

Hầu hết các ứng dụng dành cho thiết bị di động đều hiển thị các màn hình chồng lên nhau, giống như một ngăn xếp. Trong Flutter, điều này dễ dàng đạt được bằng cách sử dụng Navigator.

MaterialApp và CupertinoApp đã sử dụng Navigator. Bạn có thể truy cập trình điều hướng bằng cách sử dụng Navigator.of() hoặc hiển thị màn hình mới bằng cách sử dụng Navigator.push() và quay lại màn hình trước đó bằng Navigator.pop():

File main.dart:

import 'package:flutter/material.dart';

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

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) {
                return DetailScreen();
              }),
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop!'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

Khi push() được gọi, widget DetailScreen được đặt trên đầu widget HomeScreen như sau:

Hình ảnh cho bài đăng

Màn hình trước (HomeScreen) vẫn là một phần của cây widget, vì vậy bất kỳ đối tượng State nào được liên kết với nó vẫn ở xung quanh trong khi DetailScreen hiển thị.

Các định tuyến được đặt tên

Flutter cũng hỗ trợ các định tuyến được đặt tên, được xác định trong tham số routes trên MaterialApp hoặc CupertinoApp:

import 'package:flutter/material.dart';

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

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context) => DetailScreen(),
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details',
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop!'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

Các định tuyến này phải được xác định trước. Mặc dù bạn có thể chuyển các đối số cho một định tuyến đã đặt tên, nhưng bạn không thể phân tích cú pháp các đối số từ chính định tuyến đó. Ví dụ: nếu ứng dụng được chạy trên web, bạn không thể phân tích cú pháp ID từ một định tuyến như /details/:id.

Các định tuyến được đặt tên nâng cao với onGenerateRoute

Một cách linh hoạt hơn để xử lý các định tuyến được đặt tên là sử dụng onGenerateRoute. API này cung cấp cho bạn khả năng xử lý tất cả các đường dẫn:

onGenerateRoute: (settings) {
  // Handle '/'
  if (settings.name == '/') {
    return MaterialPageRoute(builder: (context) => HomeScreen());
  }

  // Handle '/details/:id'
  var uri = Uri.parse(settings.name);
  if (uri.pathSegments.length == 2 &&
  uri.pathSegments.first == 'details') {
    var id = uri.pathSegments[1];
    return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
  }

  return MaterialPageRoute(builder: (context) => UnknownScreen());
},

Đây là ví dụ hoàn chỉnh:

import 'package:flutter/material.dart';

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

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: (settings) {
        // Handle '/'
        if (settings.name == '/') {
          return MaterialPageRoute(builder: (context) => HomeScreen());
        }

        // Handle '/details/:id'
        var uri = Uri.parse(settings.name);
        if (uri.pathSegments.length == 2 &&
            uri.pathSegments.first == 'details') {
          var id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
        }

        return MaterialPageRoute(builder: (context) => UnknownScreen());
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details/1',
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  String id;

  DetailScreen({
    this.id,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Viewing details for item $id'),
            FlatButton(
              child: Text('Pop!'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class UnknownScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('404!'),
      ),
    );
  }
}

Đây, settingslà một ví dụ của RouteSettings. Trường tên và đối số là các giá trị được cung cấp khi Navigator.pushNamedđược gọi hoặc những gì initialRouteđược đặt thành.

Navigator 2.0

API Navigator 2.0 thêm các lớp mới vào framework để làm cho màn hình của ứng dụng trở thành một chức năng của trạng thái ứng dụng và cung cấp khả năng phân tích cú pháp các định tuyến từ nền tảng bên dưới (như URL web). Dưới đây là tổng quan về những tính năng mới:

  • Page - một đối tượng bất biến được sử dụng để thiết lập ngăn xếp lịch sử của trình điều hướng.
  • Router - cấu hình danh sách các trang sẽ được hiển thị bởi Bộ điều hướng. Thông thường danh sách các trang này thay đổi dựa trên nền tảng cơ bản hoặc trạng thái của ứng dụng thay đổi.
  • RouteInformationParser, lấy RouteInformation từ RouteInformationProvider và phân tích cú pháp nó thành một kiểu dữ liệu do người dùng xác định.
  • RouterDelegate - xác định hành vi dành riêng cho ứng dụng về cách thức Router tìm hiểu về những thay đổi trong trạng thái ứng dụng và cách ứng dụng phản ứng với chúng. Công việc của nó là lắng nghe RouteInformationParser và trạng thái ứng dụng và xây dựng Navigator với danh sách các Pages hiện tại.
  • BackButtonDispatcher - báo cáo các lần nhấn nút quay lại cho Router.

Sơ đồ dưới đây cho thấy cách RouterDelegate tương tác với RouterRouteInformationParser và trạng thái của ứng dụng:

Hình ảnh cho bài đăng

Dưới đây là một ví dụ về cách các phần này tương tác:

  1. Khi nền tảng tạo ra một định tuyến mới (ví dụ: “books/2”), thì RouteInformationParser sẽ chuyển đổi nó thành một kiểu dữ liệu trừu tượng T mà bạn xác định trong ứng dụng của mình (ví dụ: một lớp được gọi là BooksRoutePath).
  2. Phương thức setNewRoutePath của RouterDelegate được gọi với kiểu dữ liệu này, và phải cập nhật trạng thái ứng dụng để phản ánh sự thay đổi (ví dụ, bằng cách thiết lập selectedBookId) và lời gọi notifyListeners.
  3. Khi notifyListeners được gọi, nó yêu cầu Router xây dựng lại RouterDelegate (sử dụng phương thức build() của nó)
  4. RouterDelegate.build() trả về một trang mới Navigator, có các trang hiện phản ánh sự thay đổi đối với trạng thái ứng dụng (ví dụ selectedBookId:).

Bài tập Navigator 2.0

Phần này hướng dẫn bạn qua một bài tập sử dụng API Navigator 2.0. Chúng ta sẽ tạo một ứng dụng có thể đồng bộ hóa với thanh URL và xử lý các lần nhấn nút quay lại từ ứng dụng và trình duyệt, như được hiển thị trong GIF sau:

Hình ảnh cho bài đăng

Để làm theo, hãy chuyển sang kênh chínhtạo một dự án Flutter mới với hỗ trợ web và thay thế nội dung của lib/main.dart bằng nội dung sau:

import 'package:flutter/material.dart';

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

class Book {
  final String title;
  final String author;

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

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Books App',
      home: Navigator(
        pages: [
          MaterialPage(
            key: ValueKey('BooksListPage'),
            child: Scaffold(),
          )
        ],
        onPopPage: (route, result) => route.didPop(result),
      ),
    );
  }
}

pages

Bộ điều hướng có một đối số pages mới trong hàm tạo của nó. Nếu danh sách các đối tượng Page thay đổi thì Navigator sẽ cập nhật ngăn xếp các định tuyến để phù hợp. Để xem cách này hoạt động như thế nào, chúng ta sẽ xây dựng một ứng dụng hiển thị danh sách sách.

Trong _BooksAppState, giữ hai phần trạng thái: danh sách sách và sách đã chọn:

class _BooksAppState extends State<BooksApp> {
  // New:
  Book _selectedBook;
  bool show404 = false;
  List<Book> books = [
    Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
    Book('Foundation', 'Isaac Asimov'),
    Book('Fahrenheit 451', 'Ray Bradbury'),
  ];

// ...

Sau đó trong _BooksAppState sẽ trả về một Navigator với một danh sách các đối tượng Page:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Books App',
    home: Navigator(
      pages: [
        MaterialPage(
          key: ValueKey('BooksListPage'),
          child: BooksListScreen(
            books: books,
            onTapped: _handleBookTapped,
          ),
        ),
      ],
    ),
  );
}
void _handleBookTapped(Book book) {
  setState(() {
    _selectedBook = book;
  });
}
// ...
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(
      appBar: AppBar(),
      body: ListView(
        children: [
          for (var book in books)
            ListTile(
              title: Text(book.title),
              subtitle: Text(book.author),
              onTap: () => onTapped(book),
            )
        ],
      ),
    );
  }
}

Vì ứng dụng này có hai màn hình, danh sách sách và màn hình hiển thị chi tiết, hãy thêm trang (chi tiết) thứ hai nếu sách được chọn (sử dụng collection if):

pages: [
  MaterialPage(
    key: ValueKey('BooksListPage'),
    child: BooksListScreen(
      books: books,
      onTapped: _handleBookTapped,
    ),
  ),
  // New:
  if (show404)
   MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
  else if (_selectedBook != null)
    MaterialPage(
      key: ValueKey(_selectedBook),
      child: BookDetailsScreen(book: _selectedBook)
    )
],

Lưu ý rằng key cho trang được xác định bởi giá trị của đối tượng book. Điều này nói với Navigator rằng đối tượng MaterialPage này khác với đối tượng khác khi đối tượng Book là khác nhau. Nếu không có khóa duy nhất thì framework không thể xác định thời điểm hiển thị hoạt ảnh chuyển tiếp giữa các Pages.

Lưu ý : Nếu muốn, bạn cũng có thể mở rộng Page để tùy chỉnh hành vi. Ví dụ: trang này thêm một hoạt ảnh chuyển tiếp tùy chỉnh:

class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return PageRouteBuilder(
      settings: this,
      pageBuilder: (context, animation, animation2) {
        final tween = Tween(begin: Offset(0.0, 1.0), end: Offset.zero);
        final curveTween = CurveTween(curve: Curves.easeInOut);
        return SlideTransition(
          position: animation.drive(curveTween).drive(tween),
          child: BookDetailsScreen(
            key: ValueKey(book),
            book: book,
          ),
        );
      },
    );
  }
}

Cuối cùng, sẽ có lỗi khi cung cấp đối số pages mà không cung cấp lệnh callback onPopPage. Hàm này được gọi bất cứ khi nào Navigator.pop() được gọi. Nó nên được sử dụng để cập nhật trạng thái (xác định danh sách các trang) và nó phải gọi didPop trên định tuyến để xác định xem cửa sổ bật lên có thành công hay không:

onPopPage: (route, result) {
  if (!route.didPop(result)) {
    return false;
  }
  
  // Update the list of pages by setting _selectedBook to null
  setState(() {
    _selectedBook = null;
  });
  
  return true;
},

Điều quan trọng là phải kiểm tra xem didPop có lỗi không trước khi cập nhật trạng thái ứng dụng.

Sử dụng setState thông báo khung để gọi phương thức build(), phương thức này sẽ trả về một danh sách với một trang duy nhất khi _selectedBook rỗng.

Đây là ví dụ đầy đủ:

import 'package:flutter/material.dart';

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

class Book {
  final String title;
  final String author;

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

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  Book _selectedBook;

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Books App',
      home: Navigator(
        pages: [
          MaterialPage(
            key: ValueKey('BooksListPage'),
            child: BooksListScreen(
              books: books,
              onTapped: _handleBookTapped,
            ),
          ),
          if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
        ],
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }

          // Update the list of pages by setting _selectedBook to null
          setState(() {
            _selectedBook = null;
          });

          return true;
        },
      ),
    );
  }

  void _handleBookTapped(Book book) {
    setState(() {
      _selectedBook = book;
    });
  }
}

class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        return BookDetailsScreen(book: book);
      },
    );
  }
}

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(
      appBar: AppBar(),
      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(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (book != null) ...[
              Text(book.title, style: Theme.of(context).textTheme.headline6),
              Text(book.author, style: Theme.of(context).textTheme.subtitle1),
            ],
          ],
        ),
      ),
    );
  }
}
Như hiện tại, ứng dụng này chỉ cho phép chúng ta xác định chồng trang theo cách khai báo. Chúng ta không thể xử lý nút quay lại của nền tảng và URL của trình duyệt không thay đổi khi chúng ta điều hướng.

Router (Bộ định tuyến)

Cho đến nay, ứng dụng có thể hiển thị các trang khác nhau, nhưng nó không thể xử lý các định tuyến từ nền tảng bên dưới, chẳng hạn như nếu người dùng cập nhật URL trong trình duyệt.

Phần này cho thấy cách để thực hiện RouteInformationParserRouterDelegate và cập nhật trạng thái ứng dụng. Sau khi thiết lập, ứng dụng vẫn đồng bộ với URL của trình duyệt.

Loại dữ liệu

RouteInformationParser sẽ phân tích cú pháp thông tin định tuyến thành kiểu dữ liệu do người dùng xác định, vì vậy trước tiên chúng ta sẽ xác định điều đó:

class BookRoutePath {
  final int id;
  final bool isUnknown;

  BookRoutePath.home()
      : id = null,
        isUnknown = false;

  BookRoutePath.details(this.id) : isUnknown = false;

  BookRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool get isDetailsPage => id != null;
}

Trong ứng dụng này, tất cả các định tuyến trong ứng dụng có thể được biểu diễn bằng một lớp duy nhất. Thay vào đó, bạn có thể chọn sử dụng các lớp khác nhau triển khai lớp cha hoặc quản lý thông tin định tuyến theo một cách khác.

RouterDelegate

Tiếp theo, ta thêm một lớp mở rộng RouterDelegate:

class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
  @override
  Widget build(BuildContext context) {
    // TODO
    throw UnimplementedError();
  }

  @override
  // TODO
  GlobalKey<NavigatorState> get navigatorKey => throw UnimplementedError();

  @override
  Future<void> setNewRoutePath(BookRoutePath configuration) {
    // TODO
    throw UnimplementedError();
  }
}

Loại chung được xác định RouterDelegate là BookRoutePath, chứa tất cả trạng thái cần thiết để quyết định trang nào sẽ hiển thị.

Chúng ta sẽ cần chuyển một số logic từ _BooksAppState sang BookRouterDelegate và tạo một GlobalKey. Trong ví dụ này, trạng thái ứng dụng được lưu trữ trực tiếp trên RouterDelegate, nhưng cũng có thể được tách thành một lớp khác.

Để hiển thị đường dẫn chính xác trong URL, chúng ta cần trả về một đường dẫn BookRoutePath dựa trên trạng thái hiện tại của ứng dụng:

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

  Book _selectedBook;
  bool show404 = false;

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

  BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
// ...

Tiếp theo, phương thức build trong RouterDelegate cần trả về Navigator:

@override
Widget build(BuildContext context) {
  return Navigator(
    key: navigatorKey,
    pages: [
      MaterialPage(
        key: ValueKey('BooksListPage'),
        child: BooksListScreen(
          books: books,
          onTapped: _handleBookTapped,
        ),
      ),
      if (show404)
        MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
      else if (_selectedBook != null)
        BookDetailsPage(book: _selectedBook)
    ],
    onPopPage: (route, result) {
      if (!route.didPop(result)) {
        return false;
      }

      // Update the list of pages by setting _selectedBook to null
      _selectedBook = null;
      show404 = false;
      notifyListeners();

      return true;
    },
  );
}

Callback onPopPage bây giờ sử dụng notifyListeners thay vì setState, vì lớp này bây giờ là một ChangeNotifier, không phải là một widget. Khi RouterDelegate thông báo cho các listener của nó, thì widget Router cũng được thông báo rằng currentConfiguration của RouterDelegate đã thay đổi và phương thức build của nó cần được gọi lại để xây dựng một Navigator mới.

Phương thức _handleBookTapped cũng cần phải sử dụng notifyListeners thay vì setState:

void _handleBookTapped(Book book) {
  _selectedBook = book;
  notifyListeners();
}

Khi một định tuyến mới đã được đẩy đến ứng dụng, thì Router sẽ gọi setNewRoutePath, điều này cho phép ứng dụng của chúng ta có cơ hội cập nhật trạng thái ứng dụng dựa trên những thay đổi đối với định tuyến:

@override
Future<void> setNewRoutePath(BookRoutePath path) async {
  if (path.isUnknown) {
    _selectedBook = null;
    show404 = true;
    return;
  }

  if (path.isDetailsPage) {
    if (path.id < 0 || path.id > books.length - 1) {
      show404 = true;
      return;
    }

    _selectedBook = books[path.id];
  } else {
    _selectedBook = null;
  }

  show404 = false;
}

RouteInformationParser

RouteInformationParser cung cấp một cái móc (hook) để phân tích các định tuyến đến (RouteInformation) và chuyển đổi nó thành kiểu do người dùng định nghĩa (BookRoutePath). Sử dụng lớp Uri phân tích cú pháp:

class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return BookRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return BookRoutePath.unknown();
      return BookRoutePath.details(id);
    }

    // Handle unknown routes
    return BookRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath path) {
    if (path.isUnknown) {
      return RouteInformation(location: '/404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/book/${path.id}');
    }
    return null;
  }
}

Việc triển khai này dành riêng cho ứng dụng này, nó không phải là giải pháp phân tích cú pháp định tuyến nói chung. Ta sẽ nói thêm về điều này sau.

Để sử dụng các lớp mới này, chúng ta sử dụng hàm tạo mới MaterialApp.router và truyền vào các triển khai tùy chỉnh của chúng ta:

return MaterialApp.router(
  title: 'Books App',
  routerDelegate: _routerDelegate,
  routeInformationParser: _routeInformationParser,
);

Đây là ví dụ hoàn chỉnh:

import 'package:flutter/material.dart';

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

class Book {
  final String title;
  final String author;

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

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  BookRouterDelegate _routerDelegate = BookRouterDelegate();
  BookRouteInformationParser _routeInformationParser =
  BookRouteInformationParser();

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

class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return BookRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return BookRoutePath.unknown();
      return BookRoutePath.details(id);
    }

    // Handle unknown routes
    return BookRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath path) {
    if (path.isUnknown) {
      return RouteInformation(location: '/404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/book/${path.id}');
    }
    return null;
  }
}

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

  Book _selectedBook;
  bool show404 = false;

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

  BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  BookRoutePath get currentConfiguration {
    if (show404) {
      return BookRoutePath.unknown();
    }
    return _selectedBook == null
        ? BookRoutePath.home()
        : BookRoutePath.details(books.indexOf(_selectedBook));
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage(
          key: ValueKey('BooksListPage'),
          child: BooksListScreen(
            books: books,
            onTapped: _handleBookTapped,
          ),
        ),
        if (show404)
          MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
        else if (_selectedBook != null)
          BookDetailsPage(book: _selectedBook)
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }

        // Update the list of pages by setting _selectedBook to null
        _selectedBook = null;
        show404 = false;
        notifyListeners();

        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(BookRoutePath path) async {
    if (path.isUnknown) {
      _selectedBook = null;
      show404 = true;
      return;
    }

    if (path.isDetailsPage) {
      if (path.id < 0 || path.id > books.length - 1) {
        show404 = true;
        return;
      }

      _selectedBook = books[path.id];
    } else {
      _selectedBook = null;
    }

    show404 = false;
  }

  void _handleBookTapped(Book book) {
    _selectedBook = book;
    notifyListeners();
  }
}

class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        return BookDetailsScreen(book: book);
      },
    );
  }
}

class BookRoutePath {
  final int id;
  final bool isUnknown;

  BookRoutePath.home()
      : id = null,
        isUnknown = false;

  BookRoutePath.details(this.id) : isUnknown = false;

  BookRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool get isDetailsPage => id != null;
}

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(
      appBar: AppBar(),
      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(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (book != null) ...[
              Text(book.title, style: Theme.of(context).textTheme.headline6),
              Text(book.author, style: Theme.of(context).textTheme.subtitle1),
            ],
          ],
        ),
      ),
    );
  }
}

class UnknownScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('404!'),
      ),
    );
  }
}
Chạy ví dụ trên trong Chrome để hiển thị các định tuyến khi chúng đang được điều hướng và điều hướng đến đúng trang khi URL được chỉnh sửa thủ công.

TransitionDelegate

Bạn có thể cung cấp triển khai tùy chỉnh của TransitionDelegate để tùy chỉnh cách các định tuyến xuất hiện trên (hoặc bị xóa khỏi) màn hình khi danh sách các trang thay đổi. Nếu bạn cần tùy chỉnh điều này, hãy đọc tiếp, nhưng nếu bạn hài lòng với hành vi mặc định, bạn có thể bỏ qua phần này.

Cung cấp một tùy chỉnh TransitionDelegate để một Navigator định nghĩa hành vi mong muốn:

// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();

child: Navigator(
  key: navigatorKey,
  // New:
  transitionDelegate: transitionDelegate,

Ví dụ: triển khai sau đây sẽ tắt tất cả các hoạt ảnh chuyển tiếp:

class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
  @override
  Iterable<RouteTransitionRecord> resolve({
    List<RouteTransitionRecord> newPageRouteHistory,
    Map<RouteTransitionRecord, RouteTransitionRecord>
    locationToExitingPageRoute,
    Map<RouteTransitionRecord, List<RouteTransitionRecord>>
    pageRouteToPagelessRoutes,
  }) {
    final results = <RouteTransitionRecord>[];

    for (final pageRoute in newPageRouteHistory) {
      if (pageRoute.isWaitingForEnteringDecision) {
        pageRoute.markForAdd();
      }
      results.add(pageRoute);
    }

    for (final exitingPageRoute in locationToExitingPageRoute.values) {
      if (exitingPageRoute.isWaitingForExitingDecision) {
        exitingPageRoute.markForRemove();
        final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
        if (pagelessRoutes != null) {
          for (final pagelessRoute in pagelessRoutes) {
            pagelessRoute.markForRemove();
          }
        }
      }

      results.add(exitingPageRoute);
    }
    return results;
  }
}

Việc thực thi tùy chỉnh này sẽ ghi đè resolve(), nó sẽ chịu trách nhiệm đánh dấu các định tuyến khác nhau là được đẩy, bật lên, thêm, hoàn thành hoặc xóa:

  • markForPush - hiển thị các định tuyến với một chuyển đổi động
  • markForAdd - hiển thị định tuyến mà không có chuyển đổi động
  • markForPop - loại bỏ định tuyến với một chuyển đổi động và hoàn thành nó với một kết quả. "Đang hoàn thành" trong ngữ cảnh này có nghĩa là đối tượng result được truyền tới callback onPopPage trên AppRouterDelegate.
  • markForComplete - loại bỏ định tuyến mà không cần chuyển tiếp và hoàn thành nó bằng một result
  • markForRemove - Loại bỏ các định tuyến mà không có chuyển đổi động và không hoàn thành.

Lớp này chỉ ảnh hưởng đến API khai báo, đó là lý do tại sao nút back vẫn hiển thị hoạt ảnh chuyển tiếp.

Cách hoạt động của ví dụ này: Ví dụ này xem xét cả các định tuyến mới và các định tuyến đang thoát khỏi màn hình. Nó đi qua tất cả các đối tượng trong newPageRouteHistory và đánh dấu chúng sẽ được thêm vào mà không sử dụng hoạt ảnh chuyển tiếp markForAdd. Tiếp theo, nó lặp lại các giá trị của ánh xạ locationToExitingPageRoute. Nếu nó tìm thấy một định tuyến được đánh dấu là isWaitingForExitingDecision, thì nó sẽ gọi markForRemove để chỉ ra rằng định tuyến đó nên được loại bỏ mà không cần chuyển tiếp và không hoàn thành.

Đây là toàn bộ code mẫu:

import 'package:flutter/material.dart';

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

class Book {
  final String title;
  final String author;

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

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  BookRouterDelegate _routerDelegate = BookRouterDelegate();
  BookRouteInformationParser _routeInformationParser =
  BookRouteInformationParser();

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

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

    if (uri.pathSegments.length >= 2) {
      var remaining = uri.pathSegments[1];
      return BookRoutePath.details(int.tryParse(remaining));
    } else {
      return BookRoutePath.home();
    }
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath path) {
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/book/${path.id}');
    }
    return null;
  }
}

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

  Book _selectedBook;

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

  BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  BookRoutePath get currentConfiguration => _selectedBook == null
      ? BookRoutePath.home()
      : BookRoutePath.details(books.indexOf(_selectedBook));

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      transitionDelegate: NoAnimationTransitionDelegate(),
      pages: [
        MaterialPage(
          key: ValueKey('BooksListPage'),
          child: BooksListScreen(
            books: books,
            onTapped: _handleBookTapped,
          ),
        ),
        if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }

        // Update the list of pages by setting _selectedBook to null
        _selectedBook = null;
        notifyListeners();

        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(BookRoutePath path) async {
    if (path.isDetailsPage) {
      _selectedBook = books[path.id];
    }
  }

  void _handleBookTapped(Book book) {
    _selectedBook = book;
    notifyListeners();
  }
}

class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        return BookDetailsScreen(book: book);
      },
    );
  }
}

class BookRoutePath {
  final int id;

  BookRoutePath.home() : id = null;

  BookRoutePath.details(this.id);

  bool get isHomePage => id == null;

  bool get isDetailsPage => id != null;
}

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(
      appBar: AppBar(),
      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(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (book != null) ...[
              Text(book.title, style: Theme.of(context).textTheme.headline6),
              Text(book.author, style: Theme.of(context).textTheme.subtitle1),
            ],
          ],
        ),
      ),
    );
  }
}

class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
  @override
  Iterable<RouteTransitionRecord> resolve({
    List<RouteTransitionRecord> newPageRouteHistory,
    Map<RouteTransitionRecord, RouteTransitionRecord>
    locationToExitingPageRoute,
    Map<RouteTransitionRecord, List<RouteTransitionRecord>>
    pageRouteToPagelessRoutes,
  }) {
    final results = <RouteTransitionRecord>[];

    for (final pageRoute in newPageRouteHistory) {
      if (pageRoute.isWaitingForEnteringDecision) {
        pageRoute.markForAdd();
      }
      results.add(pageRoute);
    }

    for (final exitingPageRoute in locationToExitingPageRoute.values) {
      if (exitingPageRoute.isWaitingForExitingDecision) {
        exitingPageRoute.markForRemove();
        final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
        if (pagelessRoutes != null) {
          for (final pagelessRoute in pagelessRoutes) {
            pagelessRoute.markForRemove();
          }
        }
      }

      results.add(exitingPageRoute);
    }
    return results;
  }
}

Bộ định tuyến lồng nhau

Bản demo lớn hơn này cho thấy cách thêm một Router trong một Router khác. Nhiều ứng dụng yêu cầu các định tuyến cho các điểm đến trong một BottomAppBar và các định tuyến cho một chồng các chế độ xem phía trên nó, điều này yêu cầu hai Bộ điều hướng. Để làm điều này, ứng dụng sử dụng đối tượng trạng thái ứng dụng để lưu trữ trạng thái điều hướng dành riêng cho ứng dụng (chỉ mục menu đã chọn và đối tượng Book đã chọn). Ví dụ này cũng cho thấy cách định cấu hình với việc Router nào xử lý nút back.

Mẫu code định tuyến lồng nhau:

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;
    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();
  }

  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);
  }

  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.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'),
      ),
    );
  }
}

Tiếp theo là gì

Bài viết này khám phá cách sử dụng các API này cho một ứng dụng cụ thể, nhưng cũng có thể được sử dụng để xây dựng gói API cấp cao hơn. Hy vọng rằng bạn sẽ có thể khám phá những gì mà một API cấp cao hơn được xây dựng trên các tính năng này có thể làm cho người dùng.

Nguồn: flutter.dev
» Tiếp: Liên kết sâu
« Trước: Thêm Asset và ảnh
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!