Phần ví dụ thiếu của Google Android Cho "Android Architecture Components"
Bài đăng này đã không được cập nhật trong 6 năm
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 Stream
và 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.configureViewModel();
}
// -----------------
// CONFIGURATION
// -----------------
private void configureDagger(){
AndroidSupportInjection.inject(this);
}
private void configureViewModel(){
String userLogin = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this, viewModelFactory).get(UserProfileViewModel.class);
viewModel.init(userLogin);
viewModel.getUser().observe(this, user -> updateUI(user));
}
// -----------------
// UPDATE UI
// -----------------
private void updateUI(@Nullable User user){
if (user != null){
Glide.with(this).load(user.getAvatar_url()).apply(RequestOptions.circleCropTransform()).into(imageView);
this.username.setText(user.getName());
this.company.setText(user.getCompany());
this.website.setText(user.getBlog());
}
}
}
7. Cấu hình Main Activity
Cuối cùng, chúng ta tạo Activity (bạn có thể tìm thấy file layout ở đây) chứa Fragment UserProfileFragment.
public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
private static String USER_LOGIN = "JakeWharton";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.configureDagger();
this.showFragment(savedInstanceState);
}
@Override
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidInjector;
}
// ---
private void showFragment(Bundle savedInstanceState){
if (savedInstanceState == null) {
UserProfileFragment fragment = new UserProfileFragment();
Bundle bundle = new Bundle();
bundle.putString(UserProfileFragment.UID_KEY, USER_LOGIN);
fragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, fragment, null)
.commit();
}
}
private void configureDagger(){
AndroidInjection.inject(this);
}
}
Giờ chạy ứng dụng thôi các bạn .
Đó là tất cả ứng dụng mà tôi viết để triển khai Android Architecture Component
. Và tôi biết có nhiều class rất đặc biệt (Cái này là do Dagger). Nhưng tôi cũng cố hoàn thành theo Separation of concerns . nó hiện giờ đang đứng top của Android Architecture Components.
Các bạn có thể tìm link source code tại ĐÂY.
Nguồn bài viết tại : https://medium.com/@Phil_Boisney/the-missing-google-sample-of-android-architecture-components-guide-c7d6e7306b8f
All rights reserved