Tìm hiểu cách đồng bộ hóa URL web với các trang Flutter và xử lý điều hướng trên web bằng Flutter
Trong bài viết này, chúng ta sẽ tìm hiểu cách Navigation 2.0 hoạt động với Web Flutter, cách xây dựng các trang web và đồng bộ hóa URL với các dự án Flutter Web của chúng ta.
Trước đây, với các kỹ thuật điều hướng bắt buộc, chúng ta chỉ có thể push và pop các route trong stack điều hướng, nhưng điều đó không xử lý URL web và lịch sử web. Vì vậy, đội ngũ Flutter đã đưa ra kỹ thuật điều hướng khai báo mới này để xử lý các URL như một phần không thể thiếu của route management.
Hãy tìm hiểu các kỹ thuật khác nhau mà chúng ta có thể sử dụng để quản lý các Route và cách Navigation 2.0 làm cho nó tốt hơn.
1. Routing using OnGenerateRoute
Trước tiên, hãy bắt đầu với Routing bằng cách sử dụng onGenerateRoute. Nếu bạn thử điều hướng đến các trang khác nhau bằng cách sử dụngNavigator.push()
đơn giản hoặc các phương pháp khác, trang sẽ thay đổi nhưng URL thì không. Ngoài ra, sẽ không có lịch sử (history, trang trước đó) nào được lưu lại.
Chúng ta có thể làm điều này bằng cách sử dụng Generate Routes.
onGenerateRoutes
là lệnh callback của route generator được sử dụng khi ứng dụng được điều hướng đến một route đã đặt tên (named route). Khi sử dụng lệnh này, chúng ta sẽ tạo các route, điều hướng đến trang khác và đồng bộ hóa các thay đổi với URL của trình duyệt.
Chúng ta cần hai tệp route (cho code rõ ràng hơn hơn), một tệp để xác định tên route và tệp khác để xác địnhonGenerateRoute
class.
Tệproutes_name
sẽ có các route với tên route tương ứng.
class RoutesName {
// ignore: non_constant_identifier_names
static const String FIRST_PAGE = '/first_page';
// ignore: non_constant_identifier_names
static const String SECOND_PAGE = '/second_page';
}
Ở đây, chúng ta có hai trang,FIRST_PAGE
(Ví dụ: localhost: 3000 / # / first_page) vàSECOND_PAGE
(Ví dụ: localhost: 3000 / # / second_page).
Bây giờ, chúng ta sẽ tạo mộtonGenerateRoute
class có thể tái sử dụng trong tệp route thứ hai của chúng ta.
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case RoutesName.FIRST_PAGE:
return _GeneratePageRoute(
widget: FirstPage(), routeName: settings.name);
case RoutesName.SECOND_PAGE:
return _GeneratePageRoute(
widget: SecondPage(), routeName: settings.name);
default:
return _GeneratePageRoute(
widget: FirstPage(), routeName: settings.name);
}
}
Sử dụng route settings, chúng ta sẽ nhận được tên Route và sau đó điều hướng đến trang tương ứng. Ở đây, _GeneratePageRoute
là class mở rộng PageRouteBuilder
. Nó được sử dụng để thêm các hoạt ảnh chuyển đổi điều hướng.
class _GeneratePageRoute extends PageRouteBuilder {
final Widget widget;
final String routeName;
_GeneratePageRoute({this.widget, this.routeName})
: super(
settings: RouteSettings(name: routeName),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return widget;
},
transitionDuration: Duration(milliseconds: 500),
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return SlideTransition(
textDirection: TextDirection.rtl,
position: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
});
}
Việc thiết lập các route gần như đã hoàn thành. Bước cuối cùng là thêmgenerateRoute
vào tệpmain.dart
của chúng ta.
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
builder: (context, child) => HomePage(child: child),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: RoutesName.FIRST_PAGE,
);
}
}
Bên trongMaterialApp
, chúng ta sẽ thêm các route vàoonGenerateRoute
property và đặt một initial route (Trang này sẽ được tải trước và route tương ứng sẽ là initial route được hiển thị trên URL). Bây giờ, bằng cách sử dụng builder property của ứng dụng material, chúng ta sẽ chèn các widget.HomePage
là một stateless widget, tạo inserted widget.
class HomePage extends StatelessWidget {
final Widget child;
const HomePage({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
);
}
}
Chúng ta có hai trang cho hai route của chúng ta ( /first-page and /second-page). Để điều hướng từ trang này sang trang khác, chúng ta sẽ sử dụngNavigator.pushNamed(context, RouteName);
First Page:
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 400,
child: Column(
children: [
Container(
child: Center(
child: Text("FIRST PAGE"),
),
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, RoutesName.SECOND_PAGE);
},
child: Text("NAVIGATE")),
],
),
);
}
}
Second Page:
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 400,
child: Column(
children: [
Container(
child: Center(
child: Text("SECOND PAGE"),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// Navigator.pushNamed(context, RoutesName.SECOND_PAGE);
},
child: Text("NAVIGATE")),
],
),
);
}
}
Phần Router của chúng ta hiện đã hoàn thành.
Bây giờ, chúng ta hãy xem xét một phương pháp khác để thực hiện Routing trong Web Flutter bằng cách sử dụng Navigation 2.0.
2. Routing Flutter (Navigation 2.0)
Navigation 2.0 tuân theo cách tiếp cận khai báo. Sử dụng phương pháp này, chúng ta sẽ đồng bộ hóa URL web với dự án Flutter của chúng ta.
Hãy xem cách chúng ta có thể làm điều này.
Chúng ta sẽ sử dụng một constructor có tên làMaterialApp.router()
. Nó tạo ra một ứng dụng material sử dụng router thay vì Navigator và yêu cầu hai thứ,routerDelegate
vàrouterInformationParser
.
URL typed sẽ chuyển quaRouterInformationParser
và nó sẽ chuyển URL thành dữ liệu do người dùng định nghĩa (user defined data). Sau đó, nó sẽ được chuyển choRouterDelegate
, phụ trách các state của ứng dụng router.
Trước tiên, hãy tạo kiểu dữ liệu chung của chúng ta, trong đó route sẽ được chuyển đổi thông quaRouteInformationParser
.
class HomeRoutePath {
final String pathName;
final bool isUnkown;
HomeRoutePath.home()
: pathName = null,
isUnkown = false;
HomeRoutePath.otherPage(this.pathName) : isUnkown = false;
HomeRoutePath.unKown()
: pathName = null,
isUnkown = true;
bool get isHomePage => pathName == null;
bool get isOtherPage => pathName != null;
}
Trong class trên, chúng ta có hai biến,pathName
, sẽ là tham số URL (sau “/“) vàboolean
, sẽ hiển thị 'trang không xác định (unknown page)' trong trường hợp URL không hợp lệ.
Ngoài ra, chúng ta đã tạo ra thêm một số named constructors.
home()
hiển thị initial screen khi route là “/“. Trong trường hợp này, boolean không xác định sẽ là false và đường dẫn sẽ trống.otherPage()
dành cho các trangpathName
. Ví dụ. /xyz.unknown()
dành cho các đường dẫn không xác định.
Sau đó, chúng ta sẽ tạoRouteInformationParser
chuyển đổi URL thànhHomeRoutePath
của chúng ta.
class HomeRouteInformationParser extends RouteInformationParser<HomeRoutePath> {
@override
Future<HomeRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.length == 0) {
return HomeRoutePath.home();
}
if (uri.pathSegments.length == 1) {
final pathName = uri.pathSegments.elementAt(0).toString();
if (pathName == null) return HomeRoutePath.unKown();
return HomeRoutePath.otherPage(pathName);
}
return HomeRoutePath.unKown();
}
@override
RouteInformation restoreRouteInformation(HomeRoutePath homeRoutePath) {
if (homeRoutePath.isUnkown) return RouteInformation(location: '/error');
if (homeRoutePath.isHomePage) return RouteInformation(location: '/');
if (homeRoutePath.isOtherPage)
return RouteInformation(location: '/${homeRoutePath.pathName}');
return null;
}
}
Chúng ta sẽ mở rộngRouterInformationParser
với typeHomeRoutePath
và sau đó chúng ta sẽ overrideparseRouteInformation
method để chịu trách nhiệm phân tích (parsing) cú pháp đường dẫn URL thànhHomeRoutePath
.
Khi chúng ta không có phân đoạn đường dẫn (zero path segments) (pathSegements sau “/“ ví dụ/abc/xyz có 2 phân đoạn đường dẫn), chúng ta sẽ chuyển đổi thànhhome()
type. Tương tự, chúng ta sẽ chuyển đổi sang các type khác tùy theo điều kiện.
Ngoài ra, chúng ta có một method khác để override, được gọi làrestoreRouteInformation
. Method này sẽ lưu trữ lịch sử duyệt web trên trình duyệt. Ở đây, chúng ta đã chuyển đường dẫn đếnRouteInformation
theoHomeRoutePath
state.
Bây giờ chúng ta đã hoàn thànhinformationParser
.
Hãy chuyển sangRouterDelegate
.
Class này cần mở rộngRouterDelegate
với typeHomeRoutePat
. Nó có 5 override method và chúng ta không cần phải xử lý tất cả chúng.addListener
method vàremoveListener
method, chúng ta có thể sử dụngChangeNotifier
và thêm nó vào function.
Ngoài ra đối vớipopRoute
, chúng ta có thể sử dụng mixinPopNavigatorRouterDelegateMixin
.
class HomeRouterDelegate extends RouterDelegate<HomeRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<HomeRoutePath> {
String pathName;
bool isError = false;
@override
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();
@override
HomeRoutePath get currentConfiguration {
if (isError) return HomeRoutePath.unKown();
if (pathName == null) return HomeRoutePath.home();
return HomeRoutePath.otherPage(pathName);
}
void onTapped(String path) {
pathName = path;
print(pathName);
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(
onTapped: (String path) {
pathName = path;
notifyListeners();
},
),
),
if (isError)
MaterialPage(key: ValueKey('UnknownPage'), child: UnkownPage())
else if (pathName != null)
MaterialPage(
key: ValueKey(pathName),
child: PageHandle(
pathName: pathName,
))
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
pathName = null;
isError = false;
notifyListeners();
return true;
});
}
@override
Future<void> setNewRoutePath(HomeRoutePath homeRoutePath) async {
if (homeRoutePath.isUnkown) {
pathName = null;
isError = true;
return;
}
if (homeRoutePath.isOtherPage) {
if (homeRoutePath.pathName != null) {
pathName = homeRoutePath.pathName;
isError = false;
return;
} else {
isError = true;
return;
}
} else {
pathName = null;
}
}
}
Ở đây, chúng ta đã xác định hai state mà chúng ta sẽ sử dụng để điều hướng đến các trang. Một làpathName
và một là cho các trang lỗiisError
.
Hãy bắt đầu với từng override method:
currentConfiguration
: được gọi bởi [Router] khi nó phát hiện thông tin route có thể đã thay đổi do rebuild. Vì vậy, theo các điều kiện, chúng ta đang call cácHomePageRoute
constructors.setNewRoutePath
: Phương thức này xử lý việc định tuyến khi người dùng nhập URL vào trình duyệt và nhấn enter.build
: Trả về Navigator vớiHomePage
của chúng ta theo mặc định và thay đổi state thông quaHomePage
và nếupathName
không rỗng, chúng ta có thể map tên với các trang và hiển thị các trang khác nhau theo các route khác nhau (ở đây mình chỉ hiển thị một trang có tên route).
Bước cuối cùng là thêmrouterDelegate
vàrouterInformationParser
của chúng ta vàoMaterialApp.router()
bên trongmain.dart
.
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: "Flutter Navigaton 2.0",
routerDelegate: HomeRouterDelegate(),
routeInformationParser: HomeRouteInformationParser(),
);
}
}
Kết luận
Phương pháp liên quan đến Navigation 2.0 có quá nhiều code soạn sẵn để thêm vào nhưng nó xử lý các trường hợp bị bỏ sót trongonGenerateRoute
. Bạn có thể nhận thấy rằng mũi tên chuyển tiếp trong trình duyệt có thể không hoạt động đúng với cách tiếp cận đầu tiên. Tuy nhiên, cách tiếp cận đầu tiên có phần dễ thực hiện và dễ quản lý hơn so với cách thứ hai.
Bài viết được lược dịch từ Vishal Singh.
Chau Le
Cực thích đồ ngọt như sữa, kem. Thích phiêu lưu mạo hiểm nhưng vì Covid mà vô tình trở thành người chia sẻ nội dung lập trình :))
follow me :
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read