Trong Flutter, việc điều hướng giữa các màn hình thường được thực hiện thông qua Navigator toàn cục. Tuy nhiên, có những trường hợp sử dụng đặc biệt yêu cầu quản lý điều hướng riêng lẻ cho một phần giao diện, ví dụ như biểu mẫu nhiều bước, hoặc khi bạn muốn mỗi tab trong thanh điều hướng dưới cùng có một ngăn xếp điều hướng độc lập.
Navigator là gì?
Navigator là một widget chịu trách nhiệm quản lý các routes (tuyến đường) của ứng dụng. Thông thường, chúng ta sử dụng Navigator.of(context).push(...) để chuyển sang màn hình mới. Điều này hoạt động bởi vì MaterialApp (widget gốc của hầu hết các ứng dụng Flutter) bao gồm WidgetsApp, và WidgetsApp tích hợp sẵn một Navigator.
Cấu trúc cơ bản của Navigator
Khi bạn cần một Navigator tùy chỉnh, bạn có thể định nghĩa nó như sau:
Navigator(
initialRoute: '/', // Tuyến đường ban đầu khi Navigator được khởi tạo
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'home':
builder = (context) => ScreenA();
break;
case 'user_profile':
builder = (context) => ScreenB();
break;
default:
// Xử lý trường hợp route không xác định
throw Exception('Unknown route: ${settings.name}');
}
// Tạo MaterialPageRoute để hiển thị màn hình
return MaterialPageRoute(builder: builder, settings: settings);
},
)
Trong đó:
initialRoute: Chỉ định route mặc định sẽ được hiển thị khiNavigatornày bắt đầu.onGenerateRoute: Một hàm callback được gọi mỗi khi có yêu cầu điều hướng đến một route mới. Nó nhậnRouteSettingsvà trả về mộtRoute(thường làMaterialPageRoute).
Trường hợp sử dụng: Điều hướng trong một cửa sổ popup
Hãy xem xét ví dụ về tính năng báo cáo trong một ứng dụng tin tức. Khi người dùng nhấn vào biểu tượng "X" trên một bài báo, một cửa sổ nhỏ sẽ hiện ra. Nếu người dùng chọn "Báo cáo", một giao diện báo cáo sẽ hiển thị ngay bên trong cửa sổ đó, thay vì chuyển toàn màn hình.
Đây là một tình huống lý tưởng để sử dụng Navigator cục bộ. Giao diện chính của popup có thể được định nghĩa như sau:
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 350,
width: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Navigator(
initialRoute: 'report_options', // Route ban đầu cho popup
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'report_options':
builder = (context) => ReportOptionsScreen(
onReportSelected: () {
// Khi người dùng chọn báo cáo, điều hướng đến màn hình báo cáo
Navigator.of(context).pushNamed('report_form');
},
);
break;
case 'report_form':
builder = (context) => ReportFormScreen();
break;
default:
throw Exception('Unknown route: ${settings.name}');
}
// Sử dụng MaterialPageRoute để có hiệu ứng chuyển trang
return MaterialPageRoute(builder: builder, settings: settings);
},
),
),
);
}
Màn hình tùy chọn báo cáo (ReportOptionsScreen)
Màn hình này hiển thị các lựa chọn như "Không quan tâm", "Báo cáo", v.v.
class ReportOptionsScreen extends StatelessWidget {
final VoidCallback onReportSelected;
ReportOptionsScreen({this.onReportSelected});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildOptionItem(Icons.clear, 'Không quan tâm', 'Giảm nội dung tương tự'),
Divider(),
_buildOptionItem(Icons.access_alarm, 'Báo cáo', 'Tiêu đề sai, nội dung kém', isReport: true),
Divider(),
_buildOptionItem(Icons.perm_identity, 'Chặn tác giả', 'Chặn bài viết từ tác giả này'),
Divider(),
_buildOptionItem(Icons.account_circle, 'Ẩn bài viết', 'Ẩn bài viết về chủ đề này'),
],
),
);
}
Widget _buildOptionItem(IconData icon, String title, String subtitle, {bool isReport = false}) {
return InkWell(
onTap: isReport ? onReportSelected : null, // Chỉ kích hoạt khi là tùy chọn báo cáo
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
children: <Widget>[
Icon(icon, size: 24),
SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
if (subtitle.isNotEmpty)
Text(subtitle, style: TextStyle(color: Colors.grey[600], fontSize: 13)),
],
),
),
if (isReport) Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
),
),
);
}
}
Màn hình nhập liệu báo cáo (ReportFormScreen)
Màn hình này sẽ hiển thị khi người dùng chọn "Báo cáo".
class ReportFormScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
color: Colors.grey[200],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(); // Quay lại màn hình trước
},
),
Text('Trở về', style: TextStyle(fontSize: 16)),
Spacer(),
Text('Báo cáo nội dung', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Spacer(),
],
),
SizedBox(height: 20),
Text('Lý do báo cáo:', style: TextStyle(fontSize: 14, color: Colors.grey[700])),
SizedBox(height: 8),
// Thêm các widget nhập liệu cho báo cáo tại đây
TextField(
decoration: InputDecoration(
hintText: 'Nhập lý do chi tiết...',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
maxLines: 4,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Xử lý gửi báo cáo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Đã gửi báo cáo!'))
);
Navigator.of(context).pop(); // Đóng màn hình báo cáo
},
child: Text('Gửi báo cáo'),
),
],
),
);
}
}
Kết quả là, chỉ phần nội dung của popup thay đổi, trong khi phần còn lại của ứng dụng không bị ảnh hưởng.
Trường hợp sử dụng: Điều hướng trong mỗi Tab
Một ứng dụng phổ biến khác cho Navigator cục bộ là khi bạn có thanh điều hướng dưới cùng (bottom navigation bar), và mỗi tab cần có một ngăn xếp điều hướng riêng biệt. Điều này có nghĩa là khi bạn chuyển đổi giữa các tab, trạng thái điều hướng của mỗi tab sẽ được bảo toàn.
Cấu trúc chính của phần này bao gồm:
- Một widget quản lý trạng thái cho thanh điều hướng dưới cùng (
TabMain). - Một widget
IndexedStackđể hiển thị nội dung của tab được chọn. - Một widget
Navigatorriêng biệt cho mỗi tab (TabNavigator).
Widget quản lý Tab (TabMain)
class TabMain extends StatefulWidget {
@override
_TabMainState createState() => _TabMainState();
}
class _TabMainState extends State<TabMain> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
// Mỗi child là một Navigator riêng cho từng tab
children: <Widget>[
TabNavigator(tabIndex: 0), // Tab 0: Trang chủ
TabNavigator(tabIndex: 1), // Tab 1: Sách
TabNavigator(tabIndex: 2), // Tab 2: Cá nhân
],
),
bottomNavigationBar: BottomNavigationBar(
onTap: (int index) {
setState(() {
_currentIndex = index;
});
},
currentIndex: _currentIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Trang chủ'),
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'Sách'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Cá nhân'),
],
),
);
}
}
Widget Navigator cho từng Tab (TabNavigator)
Widget này chứa một Navigator riêng, được cấu hình để quản lý các route cho tab cụ thể của nó.
class TabNavigator extends StatelessWidget {
final int tabIndex;
TabNavigator({this.tabIndex});
@override
Widget build(BuildContext context) {
// Mỗi TabNavigator có một Navigator riêng
return Navigator(
// Sử dụng key để phân biệt các Navigator nếu cần
// key: ObjectKey(tabIndex), // Ví dụ: sử dụng key dựa trên index
initialRoute: '/', // Route ban đầu cho tab này
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
// Trang danh sách ban đầu cho mỗi tab
builder = (context) => TabListPage(index: tabIndex);
break;
case '/detail':
// Route cho trang chi tiết
builder = (context) => DetailScreen();
break;
default:
throw Exception('Unknown route: ${settings.name}');
}
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
Trang danh sách (TabListPage)
Đây là trang hiển thị nội dung ban đầu của mỗi tab. Nó có một nút để điều hướng đến trang chi tiết.
class TabListPage extends StatelessWidget {
final int index;
TabListPage({this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tab ${index + 1}')),
body: Center(
child: ElevatedButton(
child: Text('Đi tới Chi tiết (Tab ${index + 1})'),
onPressed: () {
// Điều hướng trong Navigator của tab hiện tại
Navigator.of(context).pushNamed('/detail');
},
),
),
);
}
}
Trang chi tiết (DetailScreen)
Trang này đơn giản chỉ hiển thị nội dung chi tiết.
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Trang Chi tiết')),
body: Center(
child: Text('Nội dung chi tiết', style: TextStyle(fontSize: 20)),
),
);
}
}
Với cách tiếp cận này, mỗi tab có bộ nhớ riêng về lịch sử điều hướng. Bạn có thể quay lại tab trước đó, và nội dung của nó vẫn được giữ nguyên.
Mặc dù Navigator cục bộ không được sử dụng thường xuyên như Navigator.of(context) toàn cục, nhưng nó cung cấp một giải pháp mạnh mẽ và linh hoạt cho các tình huống điều hướng phức tạp và có phạm vi hẹp.