Cách viết code Navigation hợp lý trong Android

0

Điều hướng giữa các màn hình là một trong những khía cạnh cốt lõi trong kiến trúc ứng dụng. Cách bạn tiếp cận và xử lý việc navigate giữa các màn hình sẽ ảnh hưởng rất nhiều tới khả năng maintain ứng dụng sau này.

Nói hoa văn vậy thôi, bản thân mình mỗi lần chuẩn bị xây dựng một ứng dụng mới, đều phải đắn đo rất kỹ về vấn đề navigate giữa các screen. Việc chúng ta có một phần code base xử lý phần này sẽ giảm thiểu rất nhiều bug tiềm tàng, tránh được những cơn đau đầu kinh niên.

Bài viết hôm này, chúng ta sẽ cùng nhau trao đổi về cách điều hướng (Navigation Android) giữa các màn hình trong ứng dụng Android, kiến trúc tổng quan và kiến trúc của Navigation Component.

🤩 Có thể bạn muốn đọc thêm về lập trình android:

Chúng ta bắt đầu nhé!

Start Activity hay replace Fragment?

Có nhiều cách để bạn hiển thị một màn hình trong ứng dụng Android. Tuy nhiên, phổ biến nhất thì có hai cách sau:

  • Khởi tạo một Activity mới bằng hàm startActivity(): Mỗi màn hình tương ứng một Activity
  • Hoặc thêm một instance Fragment vào view Fragment bằng cách replace Fragment: Mỗi màn hình tương ứng một Fragment.

Chúng ta sẽ tập trung thảo luận xung quanh hai cách tiếp cận này nhé.

Trước khi đọc tiếp, chúng ta cùng nhau quy ước về cách gọi tên như hình bên dưới nhé.

 Căn bản về Android Navigation

Nếu muốn navigate tới một màn hình được hiển thị bằng Activity, chúng ta làm như sau:

Intent intent = new Intent(context, TargetActivity.class);
context.startActivity(intent);

Còn làm theo cách replace Fragment thì code sẽ phức tạp hơn một chút:

SomeFragment fragment = new SomeFragment();
fragmentManager
    .beginTransaction()
    .replace(R.id.fragment_container, fragment)
    .commit();

Tất nhiên, đoạn code chỉ là code mặc định mà thôi. Ví dụ: mặc định khi bạn tạo một Activity mới, activity cũ sẽ được tự động đưa vào backstack. Trong khi Fragment thì không. Mục đích của backstack là để người dùng bấm phím back trên điện thoại, ứng dụng của bạn sẽ trở về màn hình trước đó.

Để bật tính năng back-navigation với fragment, bạn cần câu lệnh addToBackStack():

TargetFragment fragment = new TargetFragment();
fragmentManager
    .beginTransaction()
    .addToBackStack(null)
    .replace(R.id.fragment_container, fragment)
    .commit();

Vậy thôi, tựu chung lại thì cũng không quá phức tạp phải không?!

Activity extras và Fragment arguments

Trong rất nhiều trường hợp, bạn cần truyền dữ liệu tới màn hình cần hiển thị.

Ví dụ: Ứng dụng có một danh sách các sản phẩm, khi người dùng nhấp vào một trong số đó, ứng dụng sẽ hiển thị màn hình với thông tin chi tiết về sản phẩm đó. Để làm được điều này, ít nhất bạn cần phải truyền toàn bộ thông tin dữ liệu của sản phẩm sang cho màn hình hiển thị chi tiết, hoặc ít nhất là ID của sản phẩm đó.

Để truyền dữ liệu, chúng ta sử dụng “Intent extras”.

Ví dụ, bên Activity gửi sẽ như sau:

Intent intent = new Intent(context, TargetActivity.class);
intent.putExtra(TargetActivity.INTENT_EXTRA_PRODUCT, product);
context.startActivity(intent);

Bên Activity đích, để lấy dữ liệu sẽ làm như sau:

Product product = (Product) getIntent().getExtras().getSerializable(INTENT_EXTRA_PRODUCT);

Với Acitivity thì như vậy, còn Fragment thì sao? Với Fragment, bạn có thể sử dụng “Fragment arguments”.

TargetFragment fragment = new TargetFragment();
Bundle args = new Bundle();
args.putSerializable(TargetFragment.ARG_PRODUCT, product);
fragment.setArguments(args);
fragmentManager
    .beginTransaction()
    .addToBackStack(null)
    .replace(R.id.fragment_container, fragment)
    .commit();

Bên Fragment đích sẽ lấy dữ liệu như sau:

Product product = (Product) getArguments().getSerializable(ARG_PRODUCT);
Lưu ý: Trong đoạn code minh họa ở trên, mình sử dụng Serializable để truyền dữ liệu là các Object giữa các Activity/Fragment. Ngoài ra, trong Android, bạn còn cách khác nữa là sử dụng Parcelables. Cá nhân thì mình thích sử dụng Serializable vì nó dễ dùng và dễ bảo trì. Tuy nhiên, cái này là tùy sở thích từng người thôi nhé. Với các dữ liệu kiểu nguyên thủy như: Integer, String, Double… bạn không cần phải làm như thế này,

Vấn đề nảy sinh khi Navigate giữa các màn hình?

Trong ví dụ trên, để trao đổi dữ liệu giữa các màn hình, chúng ta sử dụng các hằng số để làm KEY khi lấy dữ liệu (Ví dụ như đoạn minh họa trên: INTENT_EXTRA_PRODUCT). Đây cũng là cách làm rất phổ biến của nhiều bạn developer.

Với các ứng dụng nhỏ nhỏ thì có lẽ không có vấn đề gì cả. Nhưng với ứng dụng lớn thì sao? Các màn hình trao đổi qua lại chằng chịt, một màn hình có thể được gọi từ rất nhiều màn hình khác, danh sách hằng số làm KEY sẽ ngày càng dài, sau một thời gian, bạn cũng quên không biết KEY nào bắt buộc phải có khi hiển thị một màn hình…

Để giải quyết một phần vấn đề trên, chúng ta sử dụng factory methods.

Sử dụng Static factory methods để tái sử dụng

Nếu màn hình đích là Activity, chúng ta tạo một static method như sau:

public static void start(Context context, Product product) {
    Intent intent = new Intent(context, TargetActivity.class);
    intent.putExtra(INTENT_EXTRA_PRODUCT, product);
    context.startActivity(intent);
}

Khi muốn navigate tới một màn hình bất kỳ, ta gọi như sau:

TargetActivity.start(context, product);

Còn với trường hợp là Fragment:

public static TargetFragment newInstance(Product product) {
    TargetFragment fragment = new TargetFragment();
    Bundle args = new Bundle();
    args.putSerializable(ARG_PRODUCT, product);
    fragment.setArguments(args);
    return fragment;
}

Lúc gọi:

TargetFragment fragment = TargetFragment.newInstance(product);
fragmentManager
    .beginTransaction()
    .addToBackStack(null)
    .replace(R.id.fragment_container, fragment)
    .commit();

Lợi ích của việc viết code như thế này là gì?

  • Tất cả thông tin về map giữa hằng số KEY sẽ được gói gọn trong màn hình đích (bạn có thể đặt là private) – giải quyết được phần nào vấn đề mình nêu ở trên.
  • Tránh trùng lặp code trong trường hợp bạn navigate tới cùng một màn hình.

Giờ chúng ta thử cải tiến hơn chút nữa xem sao.

Screen navigator abstraction

Nếu bạn muốn tách phần xử lý navigation thành một phần riêng biệt, sau này dễ maintain hơn, nhìn code cũng “ngon lành” hơn, sau này lúc sử dụng cũng đơn giản hơn rất nhiều.

Mình lấy ví dụ nhé. Giả sử bạn muốn điều hướng từ màn hình A sang màn hình B. Bạn không cần quan tâm tới phía dưới xử lý  thế nào, chỉ đơn giản gọi một hàm nào đó, đại khái như: switchScreen(screenB);

OK, chúng ta sẽ tiến hành tạo một class dành riêng cho navigation, đặt tên là: ScreenNavigator.

public class ScreenNavigator {

    private final Activity mActivity;
    private final FragmentManager mFragmentManager;

    public ScreenNavigator(Activity activity, FragmentManager fragmentManager) {
        mActivity = activity;
        mFragmentManager = fragmentManager;
    }

    public void toProductDetailsScreen(Product product) {
        TargetFragment fragment = TargetFragment.newInstance(product);
        replaceFragment(fragment);
    }

    private void replaceFragment(Fragment fragment) {
        mFragmentManager
            .beginTransaction()
            .addToBackStack(null)
            .replace(R.id.fragment_container, fragment)
            .commit();
    }

    public void toProductDetailsScreen2(Product product) {
        TargetActivity.start(mActivity, product);
    }

}

Giờ đây, khi muốn chuyển màn hình, bạn chỉ cần gọi như sau:

mScreenNavigator.toProductDetailsScreen(product);

Chỉ với thay đổi nhỏ như trên, giờ đây bạn không còn quá quan tâm nhiều tới việc phải xử lý nào mỗi khi cần chuyển màn hình. Quá tiện phải không?

Back navigation

Như mình đã đề cập ở trên, mặc định mỗi khi có Activity mới được tạo, Android sẽ đặt activity cũ vào backstack để dùng khi người dùng bấm phím back.

Tuy nhiên, nếu bạn muốn chủ động xử lý phần này (trong một số trường hợp, ứng dụng cần tự động quay lại màn hình trước mà không cần đợi người dùng nhấn phím back).

Chúng ta sẽ thêm hàm navigateBack() trong ScreenNavigator như sau:

public class ScreenNavigator {

    ...

    public boolean navigateBack() {
        if(mFragNavController.isRootFragment()) {
            return false;
        } else {
            mFragNavController.popFragment();
            return true;
        }
    }

}

Sau đó, trong class Activity (nếu trường hợp ứng dụng có nhiều activity, thì đoạn code nên đặt trong base activity)

@Override
public void onBackPressed() {
    if (!mScreensNavigator.navigateBack()) {
        super.onBackPressed();
    }
}

Một ví dụ khác, với ứng dụng có Navigation Drawer, chúng ta đóng menu nếu nó đang mở và thoát ứng dụng nếu nó đang đóng.

@Override
public void onBackPressed() {
    if (mViewMvc.isDrawerVisible()) {
        mViewMvc.closeDrawer();
    } else if (!mScreensNavigator.navigateBack()) {
        super.onBackPressed();
    }
}

Nói chung có rất nhiều use case mà bạn có thể ứng dụng hàm navigateBack() này.

Bài viết về Android Navigation xin phép kết thúc ở đây, mình hi vọng nó giúp ích cho dự án của bạn.

💦 Nguồn tham khảo:

  • https://developer.android.com/guide/navigation
  • https://www.techyourchance.com/navigation-between-screens-android/
Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo