Phần ví dụ thiếu của Google Android Cho "Android Architecture Components"
Android Architecture Components Gần đây, Google đã phát hành Android Architecture Components, một tập hợp các thư viện giúp bạn phát triển các ứng dụng tốt, có thể dễ dàng sử dụng testable và maintain lại ứng dụng. Từ khi ra thư viện này ra đời, thì thực sự nó sẽ thay đổi cách các nhà phát ...
Android Architecture Components
Gần đây, Google đã phát hành Android Architecture Components, một tập hợp các thư viện giúp bạn phát triển các ứng dụng tốt, có thể dễ dàng sử dụng testable và maintain lại ứng dụng.
Từ khi ra thư viện này ra đời, thì thực sự nó sẽ thay đổi cách các nhà phát triển Android thiết kế ứng dụng của họ
Video giới thiệu Android Architecture ComponentsTuy nhiên, sau khi đọc xong hướng dẫn của Google về Android Architecture Components. Tôi thực sự thất vọng vì họ không cung cấp cho chúng ta một bản demo hoàn chỉnh có đầy đủ các case sử dụng... Tôi cũng đang chec ked qua github của Google Sample nhưng cũng chỉ đi ra trong vô vọng :-< . Vì vậy tôi quyết định tạo ra 1 ứng dụng demo có lên quan đến Guide to App Android Architecture.
Tôi chọn cách theo hướng dẫn một cách nhiều nhất có thể. Tôi chọn những thư viện nổi tiếng nhất(Cũng khuyến khích nên dùng).
- Dagger 2
- Butterknife & Glide
- Gson
Bạn có thể tìm thấy danh sách các thư viện được sử dụng trong project ở file build.gradle.
Ứng dụng này làm những việc gì???
Ứng dụng đơn giản này chỉ có một màn hình đơn. Khi màn hình này xuất hiện, các bạn có thể lấy dữ liệu thông tin (bằng Retrofit) Github của Jake Wharton và lưu trữ nó xuống cơ sở dữ liệu của ứng dụng ( bằng Room).
Tiếp theo, khi màn hình được khởi chạy lại chúng ta sẽ nhận được những thông tin tương tự, đầu tiên trong Cơ sở dữ liệu (Room) và chỉ khi cần thiết, làm mới dữ liệu từ Github Api.
1. Configuring Room
Sau khi import thư viện này vào trong project. Chúng ta tạo persistent model. Tạo User Entity. đại diện cho Github User. Và Entity này sẽ tiếp tục lưu trữ( Thông qua Room ) và nhận được từ Github Api ( Thông qua Retrofit ).
@Entity public class User { @PrimaryKey @NonNull @SerializedName("id") @Expose private String id; @SerializedName("login") @Expose private String login; @SerializedName("avatar_url") @Expose private String avatar_url; @SerializedName("name") @Expose private String name; @SerializedName("company") @Expose private String company; @SerializedName("blog") @Expose private String blog; private Date lastRefresh; // --- CONSTRUCTORS --- public User() { } public User(@NonNull String id, String login, String avatar_url, String name, String company, String blog, Date lastRefresh) { this.id = id; this.login = login; this.avatar_url = avatar_url; this.name = name; this.company = company; this.blog = blog; this.lastRefresh = lastRefresh; } // --- GETTER --- public String getId() { return id; } public String getAvatar_url() { return avatar_url; } public Date getLastRefresh() { return lastRefresh; } public String getLogin() { return login; } public String getName() { return name; } public String getCompany() { return company; } public String getBlog() { return blog; } // --- SETTER --- public void setId(String id) { this.id = id; } public void setAvatar_url(String avatar_url) { this.avatar_url = avatar_url; } public void setLastRefresh(Date lastRefresh) { this.lastRefresh = lastRefresh; } public void setLogin(String login) { this.login = login; } public void setName(String name) { this.name = name; } public void setCompany(String company) { this.company = company; } public void setBlog(String blog) { this.blog = blog; } }
Tiếp theo, chúng ta tạo DAO để duy trì người dùng vào Room.
@Dao public interface UserDao { @Insert(onConflict = REPLACE) void save(User user); @Query("SELECT * FROM user WHERE login = :userLogin") LiveData<User> load(String userLogin); @Query("SELECT * FROM user WHERE login = :userLogin AND lastRefresh > :lastRefreshMax LIMIT 1") User hasUser(String userLogin, Date lastRefreshMax); }
LiveData is an observable data holder. It lets the components in your app observe LiveData objects for changes without creating explicit and rigid dependency paths between them. LiveData also respects the lifecycle state of your app components (activities, fragments, services) and does the right thing to prevent object leaking so that your app does not consume more memory. Bởi vì Room không tồn tại kiểu dữ liệu Date, chúng ta sẽ tạo một TypeConverter
public class DateConverter { @TypeConverter public static Date toDate(Long timestamp) { return timestamp == null ? null : new Date(timestamp); } @TypeConverter public static Long toTimestamp(Date date) { return date == null ? null : date.getTime(); } }
Cuối cùng, chúng ta tạo một database object :
@Database(entities = {User.class}, version = 1) @TypeConverters(DateConverter.class) public abstract class MyDatabase extends RoomDatabase { // --- SINGLETON --- private static volatile MyDatabase INSTANCE; // --- DAO --- public abstract UserDao userDao(); }
2. Cấu hình Retrofit
Như tôi đã nói, Đối tượng User sẽ được sử dụng cả Retrofit và Room. Vì vậy, bây giờ, chúng ta chỉ phải tạo một Interface cho Retrofit :
public interface UserWebservice { @GET("/users/{user}") Call<User> getUser(@Path("user") String userId); }
3. Cấu hình cho Repository
Trong phần này, theo tôi cái quan trọng nhất trong tất cả là chúng ta sẽ chọn nguồn dữ liệu nào cần sử dụng để lấy dữ liệu của người dùng, tùy theo các tình huống được xác định trước:
- Sử dụng Webservice (thông qua Retrofit) khi người dùng lần đầu tiên khởi chạy ứng dụng.
- Sử dụng Webservice thay vì cơ sở dữ liệu khi lần tìm nạp cuối cùng từ API của dữ liệu người dùng đã hơn 3 phút trước.
- Nếu không, sử dụng cơ sở dữ liệu (thông qua Room).
Repository modules are responsible for handling data operations. They provide a clean API to the rest of the app. They know where to get the data from and what API calls to make when data is updated. You can consider them as mediators between different data sources (persistent model, web service, cache, etc.).
@Singleton public class UserRepository { private static int FRESH_TIMEOUT_IN_MINUTES = 3; private final UserWebservice webservice; private final UserDao userDao; private final Executor executor; @Inject public UserRepository(UserWebservice webservice, UserDao userDao, Executor executor) { this.webservice = webservice; this.userDao = userDao; this.executor = executor; } // --- public LiveData<User> getUser(String userLogin) { refreshUser(userLogin); // try to refresh data if possible from Github Api return userDao.load(userLogin); // return a LiveData directly from the database. } // --- private void refreshUser(final String userLogin) { executor.execute(() -> { // Check if user was fetched recently boolean userExists = (userDao.hasUser(userLogin, getMaxRefreshTime(new Date())) != null); // If user have to be updated if (!userExists) { webservice.getUser(userLogin).enqueue(new Callback<User>() { @Override public void onResponse(Call<User> call, Response<User> response) { Toast.makeText(App.context, "Data refreshed from network !", Toast.LENGTH_LONG).show(); executor.execute(() -> { User user = response.body(); user.setLastRefresh(new Date()); userDao.save(user); }); } @Override public void onFailure(Call<User> call, Throwable t) { } }); } }); } // --- private Date getMaxRefreshTime(Date currentDate){ Calendar cal = Calendar.getInstance(); cal.setTime(currentDate); cal.add(Calendar.MINUTE, -FRESH_TIMEOUT_IN_MINUTES); return cal.getTime(); } }
4. Cấu hình ViewModel
Trước khi tạo ViewModel, chúng ta nên tạo 1 Factory cho ViewModel. Nó sẽ xử lý injection of dependency một cách trong sáng nhất.
@Singleton public class FactoryViewModel implements ViewModelProvider.Factory { private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators; @Inject public FactoryViewModel(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) { this.creators = creators; } @SuppressWarnings("unchecked") @Override public <T extends ViewModel> T create(Class<T> modelClass) { Provider<? extends ViewModel> creator = creators.get(modelClass); if (creator == null) { for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) { if (modelClass.isAssignableFrom(entry.getKey())) { creator = entry.getValue(); break; } } } if (creator == null) { throw new IllegalArgumentException("unknown model class " + modelClass); } try { return (T) creator.get(); } catch (Exception e) { throw new RuntimeException(e); } } }
Bây giờ hãy tạo UserViewModel để sử dụng trong Fragment.
A ViewModel provides the data for a specific UI component, such as a fragment or activity, and handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications. The ViewModel does not know about the View and is not affected by configuration changes such as recreating an activity due to rotation.
public class UserProfileViewModel extends ViewModel { private LiveData<User> user; private UserRepository userRepo; @Inject public UserProfileViewModel(UserRepository userRepo) { this.userRepo = userRepo; } // ---- public void init(String userId) { if (this.user != null) { return; } user = userRepo.getUser(userId); } public LiveData<User> getUser() { return this.user; } }
5. Cấu hình Dependency Injection
Bởi vì Dagger 2 có thể đôi khi bạn phải chịu đau để hiểu được nó T_T. Nên tôi nhóm rất nhiều module riêng việt thành một cái chính là AppModule. Trước tiên, chúng ta hãy tạo ra tất cả các class cần thiết của Dagger để inject tất cả các dependencies :
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @MapKey public @interface ViewModelKey { Class<? extends ViewModel> value(); }
@Module public abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(UserProfileViewModel.class) abstract ViewModel bindUserProfileViewModel(UserProfileViewModel repoViewModel); @Binds abstract ViewModelProvider.Factory bindViewModelFactory(FactoryViewModel factory); }
@Module public abstract class FragmentModule { @ContributesAndroidInjector abstract UserProfileFragment contributeUserProfileFragment(); }
@Module public abstract class ActivityModule { @ContributesAndroidInjector(modules = FragmentModule.class) abstract MainActivity contributeMainActivity(); }
@Module(includes = ViewModelModule.class) public class AppModule { // --- DATABASE INJECTION --- @Provides @Singleton MyDatabase provideDatabase(Application application) { return Room.databaseBuilder(application, MyDatabase.class, "MyDatabase.db") .build(); } @Provides @Singleton UserDao provideUserDao(MyDatabase database) { return database.userDao(); } // --- REPOSITORY INJECTION --- @Provides Executor provideExecutor() { return Executors.newSingleThreadExecutor(); } @Provides @Singleton UserRepository provideUserRepository(UserWebservice webservice, UserDao userDao, Executor executor) { return new UserRepository(webservice, userDao, executor); } // --- NETWORK INJECTION --- private static String BASE_URL = "https://api.github.com/"; @Provides Gson provideGson() { return new GsonBuilder().create(); } @Provides Retrofit provideRetrofit(Gson gson) { Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(BASE_URL) .build(); return retrofit; } @Provides @Singleton UserWebservice provideApiWebservice(Retrofit restAdapter) { return restAdapter.create(UserWebservice.class); } }
App Component
@Singleton @Component(modules={ActivityModule.class, FragmentModule.class, AppModule.class}) public interface AppComponent { @Component.Builder interface Builder { @BindsInstance Builder application(Application application); AppComponent build(); } void inject(App app); }
Bây giờ chúng ta phải kích hoạt Dagger vào class Application của dự án của chúng ta:
public class App extends Application implements HasActivityInjector { @Inject DispatchingAndroidInjector<Activity> dispatchingAndroidInjector; public static Context context; @Override public void onCreate() { super.onCreate(); this.initDagger(); context = getApplicationContext(); } @Override public DispatchingAndroidInjector<Activity> activityInjector() { return dispatchingAndroidInjector; } // --- private void initDagger(){ DaggerAppComponent.builder().application(this).build().inject(this); } }
6. Cấu hình Fragment
Yeah gần kết thúc rồi !!!!. Chúng ta phải thêm ViewModel vào UserProfileFragment (bạn có thể tìm thấy layout ở đây), đăng ký LiveData Streamvà cuối cùng sex cập nhật UI khi nhận được dữ liệu.
public class UserProfileFragment extends Fragment { // FOR DATA public static final String UID_KEY = "uid"; @Inject ViewModelProvider.Factory viewModelFactory; private UserProfileViewModel viewModel; // FOR DESIGN @BindView(R.id.fragment_user_profile_image) ImageView imageView; @BindView(R.id.fragment_user_profile_username) TextView username; @BindView(R.id.fragment_user_profile_company) TextView company; @BindView(R.id.fragment_user_profile_website) TextView website; public UserProfileFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_user_profile, container, false); ButterKnife.bind(this, view); return view; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); this.configureDagger(); this