Inject mọi thứ - ViewHolder và Dagger 2 (với Multibinding và AutoFactory)

Mục đích chính của Depedency Injection pattern được implement bởi Dagger 2 là tách riêng việc khởi tạo khỏi hành vi của đối tượng. Trong thực tế, điều này có nghĩa là tất cả các lời gọi toán tử new, newInstance sẽ không được gọi ở bất kỳ nơi nào khác ngoài Modules của Dagger.

Cái giá của Dagger - Inject mọi thứ

Mục đích của bài viết này là để cho thấy những gì chúng ta có thể làm, chứ không phải là những gì chúng ta nên làm. Đó là lý do tại sao điều quan trọng là phải biết những gì chúng ta sẽ mất để đánh đổi lấy sự tách biệt khỏi hành vi. Nếu bạn muốn sử dụng Dagger 2 cho hầu hết mọi thứ trong dự án của bạn, bạn sẽ nhanh chóng thấy rằng phần lớn các methods trong giới hạn 64k methods được tự động tạo ra cho injection.

Inject mọi thứ

Ví dụ Dagger 2 inject đối tượng Presenter vào Activity như sau:

public class MyActivity extends BaseActivity {

    @Inject MyActivityPresenter presenter;

    @Override
    protected void injectDependencies() {
        getAppComponent().plus(new MyActivityModule(this)).inject(this);
    }
    
    //...
}

Khi chúng ta bắt đầu phát triển mở rộng hơn cho Activity này (chẳng hạn, cho Activity hiển thị danh sách các items), đôi khi chúng ta khai báo và khởi tạo đối tượng bằng toán tử new. Có nghĩa là chúng ta không phải luôn luôn sử dụng @Inject các đối tượng mới (như đối tượng adapter trong trường hợp này):

public class MyActivity extends BaseActivity {

    @Inject MyActivityPresenter presenter;
    MyListAdapter adapter;

    @Override
    protected void injectDependencies() {
        getAppComponent().plus(new MyActivityModule(this)).inject(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        adapter = new MyListAdapter();
        recyclerView.setAdapter(adapter);
    }
}

Với cách tiếp cận trên, chúng ta không sử dụng DI và lợi ích của nó đem lại. Chúng ta khởi tạo Adapter bên trong Activity class, có nghĩa là mỗi lần cần thay đổi trong quá trình implement nó (như là thay đổi argument khởi tạo - hiển thị dạng grid thay vì list), chúng ta phải cập nhật lại code trong Activity. Đôi khi sử dụng cách tiếp cận này là được tính toán trước (khi chúng ta biết rằng code sẽ không được mở rộng nữa trong tương lai). Tuy nhiên hôm nay chúng ta sẽ cố gắng làm cho tất cả các code liên quan đến adapter theo cách tiếp cận DI.

Adapter không dùng Injection

RepositoriesListActivity

public class RepositoriesListActivity extends BaseActivity {
    @Bind(R.id.rvRepositories)
    RecyclerView rvRepositories;

    @Inject
    RepositoriesListActivityPresenter presenter;

    private RepositoriesListAdapter repositoriesListAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_repositories_list);
        ButterKnife.bind(this);
        setupRepositoriesListView();
    }

    private void setupRepositoriesListView() {
        repositoriesListAdapter = new RepositoriesListAdapter(this);
        rvRepositories.setAdapter(repositoriesListAdapter);
        rvRepositories.setLayoutManager(new LinearLayoutManager(this));
    }

    @Override
    protected void setupActivityComponent() {
        GithubClientApplication.get(this).getUserComponent()
                .plus(new RepositoriesListActivityModule(this))
                .inject(this);
    }
}

RepositoriesListAdapter

public class RepositoriesListAdapter extends RecyclerView.Adapter {

    private RepositoriesListActivity repositoriesListActivity;

    private final List<Repository> repositories = new ArrayList<>();

    public RepositoriesListAdapter(RepositoriesListActivity repositoriesListActivity) {
        this.repositoriesListActivity = repositoriesListActivity;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final RecyclerView.ViewHolder viewHolder;
        if (viewType == Repository.TYPE_NORMAL) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_normal, parent, false);
            viewHolder = new RepositoryViewHolderNormal(view);
        } else if (viewType == Repository.TYPE_BIG) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_big, parent, false);
            viewHolder = new RepositoryViewHolderBig(view);
        } else if (viewType == Repository.TYPE_FEATURED) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_featured, parent, false);
            viewHolder = new RepositoryViewHolderFeatured(view);
        } else {
            return null;
        }

        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onRepositoryItemClicked(viewHolder.getAdapterPosition());
            }
        });

        return viewHolder;
    }

    private void onRepositoryItemClicked(int adapterPosition) {
        repositoriesListActivity.onRepositoryClick(repositories.get(adapterPosition));
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ((RepositoryViewHolder) holder).bind(repositories.get(position));
    }

    @Override
    public int getItemCount() {
        return repositories.size();
    }

    @Override
    public int getItemViewType(int position) {
        Repository repository = repositories.get(position);
        if (repository.stargazers_count > 500) {
            if (repository.forks_count > 100) {
                return Repository.TYPE_FEATURED;
            }
            return Repository.TYPE_BIG;
        }
        return Repository.TYPE_NORMAL;
    }

    public void updateRepositoriesList(List<Repository> repositories) {
        this.repositories.clear();
        this.repositories.addAll(repositories);
        notifyDataSetChanged();
    }

    public static class RepositoryViewHolderNormal extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;

        public RepositoryViewHolderNormal(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
        }
    }

    public static class RepositoryViewHolderBig extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;
        @Bind(R.id.tvStars)
        TextView tvStars;
        @Bind(R.id.tvForks)
        TextView tvForks;

        public RepositoryViewHolderBig(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
            tvStars.setText("Stars: " + repository.stargazers_count);
            tvForks.setText("Forks: " + repository.forks_count);
        }
    }

    public static class RepositoryViewHolderFeatured extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;
        @Bind(R.id.tvStars)
        TextView tvStars;
        @Bind(R.id.tvForks)
        TextView tvForks;

        public RepositoryViewHolderFeatured(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
            tvStars.setText("Stars: " + repository.stargazers_count);
            tvForks.setText("Forks: " + repository.forks_count);
        }
    }
}

Adapter với injection

Chúng ta sẽ inject đối tượng adapter thay vì khởi tạo nó trong Activity. RepositoriesListActivity

public class RepositoriesListActivity extends BaseActivity {
    @Bind(R.id.rvRepositories)
    RecyclerView rvRepositories;

    @Inject
    RepositoriesListActivityPresenter presenter;
    @Inject
    RepositoriesListAdapter repositoriesListAdapter;

    //...

    private void setupRepositoriesListView() {
        rvRepositories.setAdapter(repositoriesListAdapter);
        rvRepositories.setLayoutManager(new LinearLayoutManager(this));
    }

    @Override
    protected void setupActivityComponent() {
        GithubClientApplication.get(this).getUserComponent()
                .plus(new RepositoriesListActivityModule(this))
                .inject(this);
    }
}

Để có thể inject được đối tượng RepositoriesListAdapter, chúng ta khởi tạo nó trong Module của Activity.

@Module
public class RepositoriesListActivityModule {
    private RepositoriesListActivity repositoriesListActivity;
    
    //...
    
    @Provides
    @ActivityScope
    RepositoriesListAdapter provideRepositoriesListAdapter(RepositoriesListActivity repositoriesListActivity) {
        return new RepositoriesListAdapter(repositoriesListActivity);
    }
}

Khá đơn giản. Bây giờ chúng ta refector lại class RepositoriesListAdapter. Các class Inner static bên trong cho ViewHolder phải được di chuyển ra khỏi code Adapter.

public class RepositoriesListAdapter extends RecyclerView.Adapter {

    //...

    public static class RepositoryViewHolderNormal extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;

        public RepositoryViewHolderNormal(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
        }
    }

    public static class RepositoryViewHolderBig extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;
        @Bind(R.id.tvStars)
        TextView tvStars;
        @Bind(R.id.tvForks)
        TextView tvForks;

        public RepositoryViewHolderBig(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
            tvStars.setText("Stars: " + repository.stargazers_count);
            tvForks.setText("Forks: " + repository.forks_count);
        }
    }

    public static class RepositoryViewHolderFeatured extends RepositoryViewHolder {

        @Bind(R.id.tvName)
        TextView tvName;
        @Bind(R.id.tvStars)
        TextView tvStars;
        @Bind(R.id.tvForks)
        TextView tvForks;

        public RepositoryViewHolderFeatured(View view) {
            super(view);
            ButterKnife.bind(this, itemView);
        }

        @Override
        public void bind(Repository repository) {
            tvName.setText(repository.name);
            tvStars.setText("Stars: " + repository.stargazers_count);
            tvForks.setText("Forks: " + repository.forks_count);
        }
    }
}

Assisted injection, Auto-factory

Trong bước tiếp theo của quá trình tái cấu trúc, chúng ta di chuyển quá trình khởi tạo từ phương thức onCreateViewHolder:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    final RecyclerView.ViewHolder viewHolder;
    if (viewType == Repository.TYPE_NORMAL) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_normal, parent, false);
        viewHolder = new RepositoryViewHolderNormal(view);
    } else if (viewType == Repository.TYPE_BIG) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_big, parent, false);
        viewHolder = new RepositoryViewHolderBig(view);
    } else if (viewType == Repository.TYPE_FEATURED) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_featured, parent, false);
        viewHolder = new RepositoryViewHolderFeatured(view);
    } else {
        return null;
    }

    //...

    return viewHolder;
}

Nó không đơn giản như ví dụ trước. Như bạn thấy, contructor có tham số khởi tạo bắt buộc là đối tượng view (class RecyclerView.ViewHolder chỉ có một hàm khởi tạo public ViewHolder(View itemView)). Có nghĩa là mỗi khi chúng ta muốn tạo ra một đối tượng mới, chúng ta cần phải cung cấp view (được tạo ra trong thời gian chạy vì vậy chúng ta không thể thiết lập trước trong các class Module). Giải pháp cuối cùng là inject đối tượng Factory. Google cung cấp cho chúng ta một giải pháp để tự động tạo ra các Fatory đó. AutoFactory tạo ra các Factory có thể được sử dụng riêng hoặc với annotation đơn giản theo JSR-330. Để sử dụng chúng trong dự án, thêm vào build.gradle:

compile 'com.google.auto.factory:auto-factory:1.0-beta3'

Bây giờ điều chúng ta phải làm là annotation các class mà chúng ta muốn tạo ra các Factory:

@AutoFactory(implementing = RepositoriesListViewHolderFactory.class)
public class RepositoryViewHolderNormal extends RepositoryViewHolder {

    @Bind(R.id.tvName)
    TextView tvName;

    public RepositoryViewHolderNormal(ViewGroup parent) {
        super(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_normal, parent, false));
        ButterKnife.bind(this, itemView);
    }

    @Override
    public void bind(Repository repository) {
        tvName.setText(repository.name);
    }
}

Argument của annotation @AutoFactory có thể làm cho class Factory extend class hoặc implement interface. Trong trường hợp này, nó sẽ là:

public interface RepositoriesListViewHolderFactory {
    RecyclerView.ViewHolder createViewHolder(ViewGroup parent);
}

Sau đó, method onCreateViewHolder của Adapter sẽ được cập nhật như sau:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    final RecyclerView.ViewHolder viewHolder;
    if (viewType == Repository.TYPE_NORMAL) {
        viewHolder = new RepositoryViewHolderNormalFactory().createViewHolder(parent);
    } else if (viewType == Repository.TYPE_BIG) {
        viewHolder = new RepositoryViewHolderBigFactory().createViewHolder(parent);
    } else if (viewType == Repository.TYPE_FEATURED) {
        viewHolder = new RepositoryViewHolderFeaturedFactory().createViewHolder(parent);
    } else {
        return null;
    }

    //...
    return viewHolder;
}

Bây giờ, code của chúng ta đã rõ ràng hơn, nhưng vẫn còn hàm khởi tạo trong đó.

Multibinding

Bước cuối cùng trong quá trình refactor là khởi tạo các đối tượng Factory của chúng ta trong Module để loại bỏ các lời gọi contructor trong Adapter. Chúng ta có thể đơn giản inject chúng như param contructor của RepositoriesListAdapter. Nhưng nó có nghĩa mỗi khi chúng ta muốn thêm/bỏ loại mới của ViewHolder chúng ta vẫn sẽ cần cập nhật code Adapter bằng tay. Thay vào đó, chúng ta có thể sử dụng Multibinding. Nhờ tính năng này, Dagger cho phép chúng ta bind nhiều đối tượng vào một collection (ngay cả khi đối tượng được bind trong các Module khác nhau). Các Factory ViewHolder của chúng ta có một điểm chung: interface RepositoriesListViewHolderFactory. Có nghĩa là chúng ta có thể inject đối tượng Map của Interger(kiểu của đối tượng Repository được khai báo là static final int) và RepositoriesListViewHolderFactory.

public class RepositoriesListAdapter extends RecyclerView.Adapter {

    private RepositoriesListActivity repositoriesListActivity;
    private Map<Integer, RepositoriesListViewHolderFactory> viewHolderFactories;

    public RepositoriesListAdapter(RepositoriesListActivity repositoriesListActivity,
                                   Map<Integer, RepositoriesListViewHolderFactory> viewHolderFactories) {
        this.repositoriesListActivity = repositoriesListActivity;
        this.viewHolderFactories = viewHolderFactories;
    }

    //...
}

Và bây giờ, bạn có thể thấy được đối tượng Map<Integer, RepositoriesListViewHolderFactory> của chúng ta được tạo ra như thế nào:

@Module
public class RepositoriesListActivityModule {
    
    //...
    
    @Provides
    @IntoMap
    @IntKey(Repository.TYPE_NORMAL)
    RepositoriesListViewHolderFactory provideViewHolderNormal() {
        return new RepositoryViewHolderNormalFactory();
    }

    @Provides
    @IntoMap
    @IntKey(Repository.TYPE_BIG)
    RepositoriesListViewHolderFactory provideViewHolderBig() {
        return new RepositoryViewHolderBigFactory();
    }

    @Provides
    @IntoMap
    @IntKey(Repository.TYPE_FEATURED)
    RepositoriesListViewHolderFactory provideViewHolderFeatured() {
        return new RepositoryViewHolderFeaturedFactory();
    }
}

Cuối cùng, sau tất cả những cải tiến, method onCreateViewHolder sẽ đơn giản như sau:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    final RecyclerView.ViewHolder viewHolder = viewHolderFactories.get(viewType).createViewHolder(parent);
    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            onRepositoryItemClicked(viewHolder.getAdapterPosition());
        }
    });
    return viewHolder;
}

Không còn các contructor, không còn câu lệnh if-else. Tất cả mọi thứ được thiết lập bởi Dagger.

Kết luận

Và đó là tất cả - Adapter và ViewHolder của nó bây giờ là một phần của đồ thị Dagger 2. Chúng ta có thể inject mọi thứ trong code. Với AutoFactoryMultibinding nó sẽ đơn giản hơn rất nhiều. Tuy nhiên, đó là lý thuyết những gì chúng ta có thể làm được, nhưng trong thực tế, nếu làm vậy dự án của bạn sẽ nhanh chóng đạt tới giới hạn 64k methods do Dagger tự sinh ra. Vì vậy, hãy cân nhắc những gì chúng ta thực sự cần Inject.