Flutter Navigation: 'RestorableRoutePushReplacement' - Sếp Creyt giải mã!
Flutter

Flutter Navigation: 'RestorableRoutePushReplacement' - Sếp Creyt giải mã!

Author

Admin System

@root

Ngày xuất bản

21 Mar, 2026

Lượt xem

4 Lượt

"RestorableRoutePushReplacement"

Chào các 'dev-er' GenZ năng động! Anh là Creyt đây. Hôm nay, chúng ta sẽ cùng 'mổ xẻ' một khái niệm nghe có vẻ hơi 'hack não' nhưng lại cực kỳ quyền năng trong Flutter: RestorableRoutePushReplacement.

1. RestorableRoutePushReplacement: Phép thuật 'Time-traveling Navigation' là gì?

Đầu tiên, hãy tưởng tượng ứng dụng của các em là một chuyến phiêu lưu, và mỗi màn hình là một điểm dừng chân. Khi các em dùng Navigator.pushReplacement, nó giống như việc các em đến một điểm dừng mới, và 'xóa sạch' dấu vết của điểm dừng trước đó khỏi bản đồ hành trình của mình. Tức là, các em không thể 'quay lại' điểm cũ bằng nút back nữa. Thẳng tiến về phía trước!

Nhưng cuộc đời đâu phải lúc nào cũng suôn sẻ, đúng không? Đôi khi, ứng dụng của chúng ta bị 'crash' hoặc bị hệ điều hành 'giết chết' để giải phóng bộ nhớ (nhất là trên mobile). Khi ứng dụng khởi động lại, các em muốn nó 'nhớ' được mình đang ở đâu và trạng thái của màn hình đó như thế nào.

Đây chính là lúc RestorableRoutePushReplacement tỏa sáng! Nó không chỉ là một cú pushReplacement thông thường. Nó là một cú pushReplacement có 'trí nhớ siêu phàm'.

Để làm gì?

Nói một cách đơn giản, khi các em dùng Navigator.restorablePushReplacement, các em đang nói với Flutter rằng: "Này, tôi vừa thay thế màn hình hiện tại bằng một màn hình mới. Nếu ứng dụng của tôi mà 'chết lâm sàng' rồi tỉnh lại, hãy đảm bảo rằng bạn biết tôi đã ở màn hình này (màn hình mới) và bạn có thể khôi phục lại trạng thái của nó một cách chính xác!".

Nó đặc biệt hữu ích khi các em muốn đảm bảo rằng trạng thái của ứng dụng (ví dụ: một giá trị trong form, vị trí cuộn, hoặc một lựa chọn nào đó) vẫn được giữ nguyên sau khi ứng dụng bị tắt và khởi động lại, ngay cả khi các em đã thay đổi luồng điều hướng bằng pushReplacement.

2. Code Ví Dụ Minh Họa: 'Hack' Trí Nhớ Ứng Dụng

Để minh họa, chúng ta sẽ tạo một ứng dụng đơn giản với hai màn hình: HomeScreenDetailScreen. HomeScreen sẽ có một bộ đếm RestorableInt.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      restorationScopeId: 'app',
      title: 'Restorable Route Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
      routes: {
        '/home': (context) => const HomeScreen(),
        '/detail': (context) => const DetailScreen(),
      },
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> with RestorationMixin {
  // Khai báo một RestorableProperty để giữ trạng thái của bộ đếm
  final RestorableInt _counter = RestorableInt(0);

  @override
  String? get restorationId => 'homeScreen';

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_counter, 'counter');
  }

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Screen')), 
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Counter: ${_counter.value}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _counter.value++;
                });
              },
              child: const Text('Increment Counter'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Dùng restorablePushReplacementNamed để thay thế màn hình
                // và đảm bảo trạng thái navigation được khôi phục
                Navigator.restorablePushReplacementNamed(
                  context,
                  '/detail',
                );
              },
              child: const Text('Go to Detail (with Restorable Push Replacement)'),
            ),
          ],
        ),
      ),
    );
  }
}

class DetailScreen extends StatefulWidget {
  const DetailScreen({super.key});

  @override
  State<DetailScreen> createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> with RestorationMixin {
  final RestorableString _message = RestorableString('Hello from Detail!');

  @override
  String? get restorationId => 'detailScreen';

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_message, 'message');
  }

  @override
  void dispose() {
    _message.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Detail Screen')), 
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _message.value,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _message.value = 'Message updated!';
                });
              },
              child: const Text('Update Message'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Quay về Home. Vì Home đã bị 'replace', nút back sẽ không hoạt động
                // Chúng ta phải pushNamed lại Home nếu muốn quay về.
                // Nhưng cái hay là, nếu app bị kill và restore, nó sẽ nhớ bạn đã ở Detail.
                Navigator.restorablePushNamed(context, '/home');
              },
              child: const Text('Go back to Home'),
            ),
          ],
        ),
      ),
    );
  }
}

Cách kiểm tra:

  1. Chạy ứng dụng.
  2. Trên HomeScreen, tăng bộ đếm lên vài lần (ví dụ: 3).
  3. Nhấn nút "Go to Detail (with Restorable Push Replacement)". Các em sẽ thấy DetailScreen.
  4. Quan trọng: Buộc ứng dụng dừng lại (ví dụ: dùng "Don't keep activities" trên Android Developer Options, hoặc kill app từ Task Manager trên iOS/Android).
  5. Mở lại ứng dụng.
  6. Các em sẽ thấy ứng dụng khởi động lại và trực tiếp hiển thị DetailScreen, không phải HomeScreen ban đầu. Điều này chứng tỏ RestorationManager đã nhớ rằng DetailScreen là màn hình cuối cùng sau khi HomeScreen bị thay thế.
  7. Nếu các em quay lại HomeScreen từ DetailScreen (bằng nút 'Go back to Home'), các em sẽ thấy bộ đếm trên HomeScreen đã được khôi phục về giá trị 3 (hoặc giá trị cuối cùng trước khi các em rời đi), chứng tỏ RestorableInt đã hoạt động đúng.
Illustration

3. Mẹo (Best Practices) để Ghi nhớ & Dùng Thực tế

  • Khi nào dùng? Khi các em có một luồng điều hướng mà việc thay thế màn hình là một phần quan trọng của logic ứng dụng (ví dụ: sau khi đăng nhập thành công, thay thế màn hình đăng nhập bằng màn hình chính), VÀ các em muốn đảm bảo rằng trạng thái của màn hình mới (hoặc các màn hình có thể truy cập được từ đó) được khôi phục chính xác nếu ứng dụng bị 'chết' và khởi động lại. Nó giúp duy trì context điều hướng một cách bền vững.
  • Đừng lạm dụng! Không phải mọi pushReplacement đều cần restorable. Chỉ dùng khi các em thực sự cần khả năng khôi phục trạng thái ứng dụng qua các lần tắt/mở lại, đặc biệt là khi các em có các RestorableProperty trên các màn hình liên quan.
  • RestorationId là chìa khóa: Nhớ đặt restorationId duy nhất cho MaterialApp, RestorationScope và từng StatefulWidget có dùng RestorationMixin để Flutter biết phải khôi phục cái gì ở đâu.
  • Kiểm tra kỹ lưỡng: Cách tốt nhất để kiểm tra là mô phỏng việc ứng dụng bị hệ điều hành 'giết' (như đã hướng dẫn ở trên) thay vì chỉ tắt và mở lại thông thường.

4. Ứng dụng/Website đã ứng dụng

Thực tế, RestorableRoutePushReplacement không phải là một tính năng 'nhìn thấy' được trực tiếp trên giao diện người dùng như một nút bấm hay hiệu ứng. Nó là một phần của kiến trúc nền tảng, giúp ứng dụng trở nên mạnh mẽ và đáng tin cậy hơn.

Bất kỳ ứng dụng di động nào có:

  • Flow đăng nhập/đăng ký phức tạp: Sau khi đăng nhập, màn hình login bị thay thế bởi màn hình Home. Nếu ứng dụng bị kill, người dùng không muốn thấy màn hình login nữa mà muốn được đưa thẳng về Home với trạng thái cuối cùng.
  • Deep linking (liên kết sâu): Khi người dùng nhấp vào một liên kết sâu đưa họ vào một màn hình cụ thể trong ứng dụng, có thể luồng điều hướng sẽ pushReplacement một phần stack. RestorableRoutePushReplacement sẽ đảm bảo rằng trạng thái sau khi deep link được khôi phục nếu ứng dụng bị ngắt.
  • Các ứng dụng có nhiều bước nhập liệu hoặc cấu hình: Ví dụ, các ứng dụng ngân hàng, ứng dụng chỉnh sửa ảnh với nhiều bước, nơi việc mất dữ liệu giữa chừng là không thể chấp nhận được.

Các ứng dụng lớn như Google Maps, Facebook, Instagram (phiên bản mobile) đều sử dụng các cơ chế khôi phục trạng thái tương tự để đảm bảo trải nghiệm người dùng liền mạch, ngay cả khi ứng dụng bị tắt bất ngờ.

5. Thử nghiệm và Nên dùng cho Case nào

Thử nghiệm đã từng: Anh đã từng gặp các dự án Flutter mà khách hàng than phiền rằng "Sao ứng dụng của tôi cứ bị reset về màn hình chính sau khi tôi chuyển sang ứng dụng khác một lúc?". Hóa ra là do họ không tận dụng RestorationManager và các phương thức restorable* của Navigator. Khi ứng dụng bị kill, toàn bộ trạng thái bị mất.

Nên dùng cho Case nào:

  • Luồng xác thực (Authentication Flow): Sau khi người dùng đăng nhập thành công, các em thường Navigator.pushReplacement màn hình đăng nhập bằng màn hình chính. Nếu ứng dụng bị kill, các em muốn người dùng quay lại màn hình chính, chứ không phải màn hình đăng nhập trống rỗng.
  • Thanh toán nhiều bước (Multi-step Checkout): Giả sử người dùng đang ở bước cuối cùng của thanh toán, ứng dụng bị kill. Khi mở lại, các em muốn họ quay lại chính bước đó, không phải bắt đầu lại từ đầu.
  • Cấu hình ứng dụng ban đầu (Onboarding/Setup): Sau khi hoàn thành quá trình onboarding, các em thay thế nó bằng màn hình chính. Đảm bảo trạng thái này được lưu để người dùng không phải onboarding lại.

Nhớ nhé, các dev-er! RestorableRoutePushReplacement không chỉ là một dòng code, nó là một lời hứa về sự bền bỉ và trải nghiệm người dùng không gián đoạn cho ứng dụng của các em. Hãy dùng nó một cách thông minh và có chiến lược để 'hack' trí nhớ của ứng dụng một cách hiệu quả nhất!

Thuộc Series: Flutter

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!