Giới thiệu về Android Architecture Components (Phần II tiếp)
Dữ liệu bộ nhớ đệm Việc triển khai repository ở trên là tốt cho trừu tượng gọi đến Webservice nhưng vì nó chỉ dựa vào một nguồn dữ liệu, nó không đa chức năng cho lắm. Vấn đề với việc thực hiện UserRepository ở trên là sau khi tìm nạp dữ liệu, nó không lưu giữ dữ liệu đó ở bất cứ đâu. Nếu người ...
Dữ liệu bộ nhớ đệm
Việc triển khai repository ở trên là tốt cho trừu tượng gọi đến Webservice nhưng vì nó chỉ dựa vào một nguồn dữ liệu, nó không đa chức năng cho lắm.
Vấn đề với việc thực hiện UserRepository ở trên là sau khi tìm nạp dữ liệu, nó không lưu giữ dữ liệu đó ở bất cứ đâu. Nếu người dùng rời khỏi UserProfileFragment và quay trở lại nó, ứng dụng sẽ tìm nạp lại dữ liệu. Điều này không tốt vì hai lý do: nó làm lãng phí băng thông mạng có giá trị và buộc người dùng phải chờ đợi cho truy vấn mới để hoàn thành. Để giải quyết vấn đề này, chúng ta sẽ thêm một nguồn dữ liệu mới vào UserRepository của chúng ta để lưu trữ các đối tượng User trong bộ nhớ.
@Singleton // informs Dagger that this class should be constructed once public class UserRepository { private Webservice webservice; // simple in memory cache, details omitted for brevity private UserCache userCache; public LiveData<User> getUser(String userId) { LiveData<User> cached = userCache.get(userId); if (cached != null) { return cached; } final MutableLiveData<User> data = new MutableLiveData<>(); userCache.put(userId, data); // this is still suboptimal but better than before. // a complete implementation must also handle the error cases. webservice.getUser(userId).enqueue(new Callback<User>() { @Override public void onResponse(Call<User> call, Response<User> response) { data.setValue(response.body()); } }); return data; } }
Dữ liệu liên tục
Trong quá trình thực hiện của chúng ta, nếu người dùng xoay màn hình hoặc rời khỏi và quay lại ứng dụng, giao diện hiện tại sẽ hiển thị ngay lập tức vì kho lưu trữ dữ liệu từ bộ nhớ cache có sẵn trong bộ nhớ. Nhưng điều gì sẽ xảy ra nếu người dùng rời khỏi ứng dụng và trở lại sau vài giờ sau, sau khi hệ điều hành Android đã destroy process này?
Với việc thực hiện hiện tại, chúng ta sẽ cần phải lấy lại dữ liệu từ mạng. Đây không chỉ là trải nghiệm người dùng kém, mà còn lãng phí vì nó sẽ sử dụng dữ liệu di động để lấy lại cùng một dữ liệu. Bạn có thể chỉ cần sửa lỗi này bằng cách lưu trữ các request từ webservice, nhưng nó tạo ra những vấn đề mới. Điều gì sẽ xảy ra nếu dữ liệu người dùng tương tự xuất hiện từ một request khác (ví dụ, tìm nạp danh sách bạn bè)? Ứng dụng của bạn có thể hiển thị dữ liệu không nhất quán, đây là trải nghiệm người dùng khó hiểu nhất. Ví dụ: dữ liệu của người dùng tương tự có thể hiển thị khác nhau vì request danh sách bạn bè và request profile của người dùng có thể được thực hiện vào các thời điểm khác nhau. Ứng dụng của bạn cần hợp nhất chúng để tránh hiển thị dữ liệu không nhất quán.
Cách thích hợp để xử lý case này là sử dụng một mô hình liên tục. Đây là nơi thư viện Room xuất hiện.
- Room là một thư viện mapping đối tượng để cung cấp dữ liệu local đã tồn tại với mã tối thiểu. Tại thời gian biên dịch, nó xác nhận hợp lệ mỗi truy vấn đối với lược đồ, do đó các truy vấn SQL bị hỏng dẫn đến lỗi thời gian biên dịch thay vì lỗi thời gian chạy. Room tóm tắt một số chi tiết triển khai bên dưới làm việc với các bảng và truy vấn SQL thô. Nó cũng cho phép quan sát những thay đổi đối với dữ liệu bên trong cơ sở dữ liệu (bao gồm cả Collections và Join Queries), callback những thay đổi như vậy qua các đối tượng LiveData . Ngoài ra, nó xác định rõ ràng các ràng buộc của thread để giải quyết các vấn đề phổ biến như truy xuất dữ liệu trên Main Thread.
*Lưu ý: Nếu bạn đã quen thuộc với một giải pháp khác như ORM SQLite hoặc một cơ sở dữ liệu khác như Realm , bạn không cần phải thay thế nó bằng Room trừ khi tính năng của nó phù hợp hơn đến trường hợp sử dụng của bạn.
Để sử dụng Room, chúng ta cần phải xác định giản đồ cục bộ của chúng ta. Đầu tiên, chú thích lớp User với @Entity để đánh dấu nó như là một bảng trong cơ sở dữ liệu của bạn.
@Entity class User { @PrimaryKey private int id; private String name; private String lastName; // getters and setters for fields }
Sau đó tạo một lớp cơ sở dữ liệu bằng cách mở rộng RoomDatabase cho ứng dụng của bạn:
@Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase { }
Lưu ý rằng MyDatabase là trừu tượng. Room tự động cung cấp một thực hiện của nó. Xem tài liệu [Room](https://developer.android.com/topic/libraries/architecture/room.html) để biết chi tiết.
Bây giờ chúng ta cần một cách để chèn dữ liệu người dùng vào cơ sở dữ liệu. Đối với điều này, chúng ta sẽ tạo một đối tượng truy cập dữ liệu (DAO) .
@Dao public interface UserDao { @Insert(onConflict = REPLACE) void save(User user); @Query("SELECT * FROM user WHERE id = :userId") LiveData<User> load(String userId); }
Sau đó, tham chiếu DAO từ lớp cơ sở dữ liệu của chúng ta.
@Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase { public abstract UserDao userDao(); }
Lưu ý rằng phương thức load trả về một LiveData<User> . Room biết khi cơ sở dữ liệu được sửa đổi và nó sẽ tự động thông báo cho tất cả các observer hoạt động khi dữ liệu thay đổi. Bởi vì nó đang sử dụng LiveData , điều này sẽ hiệu quả vì nó sẽ cập nhật dữ liệu chỉ khi có ít nhất một observer đang active.
Lưu ý: Tính đến bản phát hành alpha 1, Room kiểm tra tính không hợp lệ dựa trên sửa đổi bảng, có nghĩa là nó có thể gửi thông báo sai.
Bây giờ chúng ta có thể sửa đổi UserRepository để kết hợp nguồn dữ liệu Room.
@Singleton public class UserRepository { private final Webservice webservice; private final UserDao userDao; private final Executor executor; @Inject public UserRepository(Webservice webservice, UserDao userDao, Executor executor) { this.webservice = webservice; this.userDao = userDao; this.executor = executor; } public LiveData<User> getUser(String userId) { refreshUser(userId); // return a LiveData directly from the database. return userDao.load(userId); } private void refreshUser(final String userId) { executor.execute(() -> { // running in a background thread // check if user was fetched recently boolean userExists = userDao.hasUser(FRESH_TIMEOUT); if (!userExists) { // refresh the data Response response = webservice.getUser(userId).execute(); // TODO check for error etc. // Update the database.The LiveData will automatically refresh so // we don't need to do anything else here besides updating the database userDao.save(response.body()); } }); } }
Lưu ý rằng mặc dù chúng ta thay đổi nơi dữ liệu đến từ trong UserRepository , chúng ta không cần phải thay đổi UserProfileViewModel hoặc UserProfileFragment. Đây là tính linh hoạt được cung cấp bởi sự trừu tượng. Đây cũng là cách tuyệt vời để kiểm thử vì bạn có thể mock UserRepository trong khi kiểm thử UserProfileViewModel của bạn.
Bây giờ mã của chúng ta đã hoàn tất. Nếu người dùng quay lại cùng một màn hình vài ngày sau đó, họ sẽ ngay lập tức nhìn thấy thông tin người dùng vì chúng ta đã lưu trữ nó dưới database. Trong khi đó, repository của chúng ta sẽ cập nhật dữ liệu dưới background nếu dữ liệu đã cũ. Tất nhiên, tùy vào trường hợp sử dụng của bạn, bạn có thể không muốn hiển thị dữ liệu đã tồn tại nếu quá cũ.
Trong một số trường hợp sử dụng, chẳng hạn như kéo để làm mới, điều quan trọng là UI hiển thị cho người dùng biết nếu hiện đang có request đến server. Tốt nhất là tách riêng hành động của UI khỏi dữ liệu thực tế vì nó có thể được cập nhật vì nhiều lý do khác nhau (ví dụ: nếu chúng ta load lại danh sách bạn bè, người dùng tương tự có thể được reload một lần nữa kích hoạt cập nhật LiveData<User> ).
Có 2 giải pháp phổ biến cho trường hợp sử dụng này:
- Thay đổi getUser để trả về một LiveData bao gồm tình trạng hoạt động của mạng. Ví dụ có thể xem trong phần Phụ lục: Hiện trạng mạng .
- Cung cấp một function khác trong repository có thể trả lại trạng thái làm mới của đối tượng User. Tùy chọn này sẽ tốt hơn nếu bạn chỉ muốn hiển thị trạng thái mạng trong UI của mình chỉ để phản ứng lại hành động rõ ràng của người dùng (như kéo để làm mới).
Single source of truth
Việc các endpoint API REST khác nhau trả về cùng một dữ liệu rất phổ biến. Ví dụ: nếu backend của chúng ta có endpoint khác trả về danh sách bạn bè, cùng một đối tượng người dùng có thể đến từ hai endpoint API khác nhau. Nếu UserRepository trả lại response từ request của Webservice , các giao diện của chúng ta có thể sẽ hiển thị dữ liệu không phù hợp vì dữ liệu có thể thay đổi ở phía máy chủ giữa các request này. Đây là lý do tại sao việc implement UserRepository , callback từ webservice chỉ lưu dữ liệu vào cơ sở dữ liệu. Sau đó, việc thay đổi cơ sở dữ liệu sẽ kích hoạt gọi lại đối tượng LiveData đang hoạt động. (Đoạn này dịch ra hơi khó hiểu, ý nó là chỉ nên dùng 1 datasource duy nhất để dữ liệu được đồng nhất, trong ví dụ là có thể gọi request đến 2 webservice khác nhau sau đó lưu lại dưới database và dùng database đấy là datasource duy nhất để update lên UI)
Trong mô hình này, cơ sở dữ liệu phục vụ như là một nguồn duy nhất, và các phần khác của ứng dụng truy cập nó qua Repository. Bất kể chúng ta sử dụng bộ nhớ cache trên đĩa, Google khuyên rằng Repository của chúng ta sẽ chỉ định một nguồn dữ liệu làm nguồn chân lý duy nhất cho phần còn lại của ứng dụng.
Thử nghiệm
Chúng ta đã đề cập rằng một trong những lợi ích của sự tách biệt là khả năng kiểm chứng. Cho phép xem làm thế nào chúng ta có thể kiểm tra mỗi mô-đun mã.
- Giao diện Người dùng & Tương tác : Đây sẽ là lần duy nhất bạn cần Android UI Instrumentation test. Cách tốt nhất để kiểm tra mã UI là tạo một bài kiểm tra Espresso . Bạn có thể tạo fragment và cung cấp cho nó một ViewModel giả. Vì fragment này chỉ nói đến ViewModel, nên mock nó sẽ đủ để kiểm tra giao diện người dùng này.
- ViewModel : ViewModel có thể được kiểm tra bằng cách sử dụng một bài kiểm tra JUnit . Bạn chỉ cần phải mô phỏng UserRepository để kiểm tra nó.
- UserRepository : Bạn cũng có thể kiểm tra UserRepository với một bài kiểm tra JUnit. Bạn cần phải giả lập Webservice và DAO. Bạn có thể kiểm tra rằng nó thực hiện request lên webservice đúng, lưu lại kết quả vào cơ sở dữ liệu và không thực hiện bất kỳ yêu cầu không cần thiết nếu dữ liệu được lưu trữ và cập nhật. Vì cả Webservice và UserDao đều là các giao diện, bạn có thể mô phỏng chúng hoặc tạo các hiện thực giả mạo cho các trường hợp thử nghiệm phức tạp hơn ..
- UserDao : Phương pháp được khuyến cáo để thử nghiệm các lớp DAO là instrumentation test. Vì những bài kiểm tra này không yêu cầu bất kỳ giao diện người dùng nào, chúng vẫn chạy nhanh. Đối với mỗi bài kiểm tra, bạn có thể tạo một cơ sở dữ liệu trong bộ nhớ để đảm bảo rằng bài kiểm tra không có bất kỳ phản ứng phụ nào (như thay đổi các tệp cơ sở dữ liệu trên đĩa). Room cũng cho phép xác định việc thực hiện cơ sở dữ liệu để bạn có thể kiểm tra nó bằng cách cung cấp cho nó một implement JUnit của SupportSQLiteOpenHelper . Cách tiếp cận này thường không được khuyến khích bởi vì phiên bản SQLite đang chạy trên thiết bị có thể khác với phiên bản SQLite trên máy chủ của bạn.
- Webservice : Điều quan trọng là làm các bài kiểm tra độc lập với thế giới bên ngoài, do đó các bài kiểm tra Webservice của bạn nên tránh thực hiện các request đến phần backend của bạn. Có rất nhiều thư viện giúp đỡ việc này. Ví dụ, MockWebServer là một thư viện tuyệt vời có thể giúp bạn tạo một máy chủ nội bộ giả mạo cho các bài kiểm tra của bạn.
- Kiểm tra Artifacts: Architecture Components cung cấp một artifact maven để kiểm soát các background thread của nó. Bên trong android.arch.core:core-testing , có 2 quy tắc JUnit: - InstantTaskExecutorRule : Quy tắc này có thể được sử dụng để ép buộc các thành phần Kiến trúc để ngay lập tức thực hiện bất kỳ hoạt động nền trên thread được gọi. - CountingTaskExecutorRule : Quy tắc này có thể được sử dụng trong các instrumentation test để chờ các hoạt động nền của các thành phần Kiến trúc hoặc kết nối nó với Espresso như là một nguồn không sử dụng.
Kiến trúc cuối cùng
Sơ đồ dưới đây cho thấy tất cả các mô-đun trong kiến trúc được đề xuất của Google và cách chúng tương tác với nhau như thế nào:
(Kết thúc phần 2) (Nguồn https://developer.android.com/topic/libraries/architecture/guide.html#caching_data)