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 testablemaintain 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 Components

Tuy 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ả RetrofitRoom. 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.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