تزریق وابستگی یا Dependency Injection بسته به اندازهی پروژههای اندرویدی شما میتواند مشکل ساز باشد و برای همین Dagger مدتی هست که معرفی شده است و برنامه نویسان اندروید با فریم ورک Dagger کار کردهاند یا حداقل چیزی دربارهاش شنیدهاند. در مقاله آموزش dagger در اندروید ما با ویژگیها و کاراییهای آن آشنا میشویم. همراه ما باشید تا با آموزش Dagger در اندروید، اطلاعاتی دربارهی آن کسب کنید و بتوانید با آن کار کنید و از ویژگیهای آن بهره ببرید.
اصطلاح dependency injection context معمولاً برای توصیف مجموعه اشیاء قابل تزریق استفاده میشود.
در Dagger 2، کلاس هایی که با Module@ حاشیه نویسی شده اند، مسئولیت تهیهی اشیاء قابل تزریق را دارند. چنین کلاس هایی میتوانند متد هایی را که با Provides@ حاشیه نویسی شده است، تعریف کنند. اشیاء برگشت یافته از این متدها برای تزریق وابستگی در دسترس هستند.
با Provides@ متدهای حاشیه نویسی میتوانند وابستگی را از طریق پارامترهای متد بیان کنند. این وابستگیها در صورت امکان توسط Dagger 2 برآورده میشوند.
dagger در اندروید چیست
به طور خلاصه ابزاری برای تولید کدی جهت مدیریت وابستگی بین اشیاء مختلف است. Dagger یک Dependency Injector برای جاوا و اندروید میباشد. Dagger یک فریم ورک (Framework) برای تزریق وابستگی (Dependency Injection) میباشد که در زمان کامپایل (Compile) اجرا میشود. Dagger از کدها به هیچ عنوان در زمان اجرا (runtime) استفاده نمیکند، تمام تحلیلهای خود را در زمان کامپایل انجام میدهد و کد منبع جاوا را تولید میکند. همانطور که میدانید Dagger با استفاده از حاشیه نویسی (annotations) کار میکند، که به آن کمک میکند تا تمام کد مورد نیاز برای کار را بخواند و تولید کند. همهی حاشیه نویسیهای مختلفی که در Dagger استفاده میشود، راهی برای گفتن آن چیز هایی هست که Dagger نمیداند و برای رسیدن به هدف خود و ایجاد کدی که در غیر این صورت باید خودتان بنویسید از Annotations استفاده میشود.تزریق وابستگی به شما مزایای زیر را ارائه میدهد:
-
قابلیت استفاده مجدد از کدهای نوشته شده
-
سهولت در اصلاح کد
- سهولت در تست نویسی
تفاوتهای Dagger 1 با Dagger 2
به طور خلاصه Dagger 2 نسخهی بهبود یافتهی Dagger 1 میباشد و مشکلاتی را که درون Dagger 1 بود تا حدودی حل کرده است. مشکلات dagger 1:- Ugly generated code
- Runtime graph composition
- Inefficient graph creation
- Partial traceability
- Map-like API
- Compile-time validation of the entire graph
- Easy debugging; entirely concrete call stack for provision and creation
- Fully traceable
- POJO API
- Performance
- Module@ و Provides@ : کلاسها و متد هایی را برای وابستگی به ما تحویل میدهند.
- Inject@ : درخواست dependencies میباشد که قابل استفاده در field ،constructor یا method است.
- Component@ : ماژولهای انتخاب شده را فعال کرده و برای انجام تزریق وابستگی استفاده میشود.
تعریف ارائه دهندگان وابستگی ((Defining dependency providers(object providers)
تعریف وابستگی ((Defining dependencies (object consumers)
شما از حاشیه نویسی Inject@ برای تعریف یک وابستگی استفاده میکنید. اگر یک Constructor را با Inject@ حاشیه نویسی کنید، Dagger 2 میتواند نمونه ای از این شی را برای تحقق وابستگیها استفاده کند. این کار برای جلوگیری از ساخت بیشتر متدهای Provides برای این اشیاء انجام شده است.
ارتباط Consumers و ارائه دهندگان
در یک Interface استفاده میشود. چنین رابطی توسط Dagger 2 برای تولید کد استفاده میشود. الگوی پایه برای کلاس تولید شده این است که، Dagger به عنوان پیشوند و پس از آن با نام رابط استفاده میشود. این یک کلاس تولید میکند که یک متد ساخته و امکان پیکربندی اشیاء را بر اساس پیکربندی داده شده فراهم میکند. متدهای تعریف شده در Interface برای دسترسی به اشیاء تولید شده در دسترس هستند.
یک Interface @Component ارتباط بین ارائه دهندهی اشیاء (ماژول ها) و اشیاء را بیان میکند که وابستگی را بیان میکند.
Scope annotations
می توانید از حاشیه نویسی Singleton@ استفاده کنید تا نشان دهید فقط باید یک نمونه از شی وجود داشته باشد.
فیلدها در Dagger
Dagger 2 به صورت خودکار فیلدها را تزریق نمیکند. همچنین نمیتواند فیلدهای خصوصی را تزریق کند. اگر میخواهید از تزریق فیلد استفاده کنید، باید روشی را در رابط Component@ خود تعریف کنید که نمونه ای از آن را به عنوان پارامتر تزریق میکند.
استفاده از Dagger در اندروید استودیو
برای فعال کردن Dagger 2 وابستگیهای زیر را به پروندهی build.gradle خود اضافه کنید.
implementation 'com.google.dagger:dagger:2.15'
annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
بسیاری از مؤلفههای اندروید (Android Components)، به عنوان مثال Activities، توسط چارچوب Android انجام میشود و در کد ما نیست. این امر باعث میشود تامین وابستگی از طریق Constructorها به مؤلفههای Android دشوار شود.
اگر میخواهید از کلاسهای پکیج dagger.android مانند کلاس DaggerActivity استفاده کنید، وابستگیهای زیر را نیز به پروندهی build.gradle خود اضافه کنید و اگر میخواهید Activities components مانند Activities یا Fragments را تزریق کنید، این مورد نیز لازم است.
implementation 'com.google.dagger:dagger-android:2.15'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.10'
اگر میخواهید از Support Library با Dagger 2 استفاده کنید، باید مورد زیر را نیز به build.gradle اضافه کنید.
implementation 'com.google.dagger:dagger-android-support:2.10'
اگر خطای مشابه را دریافت میکنید: (Conflict with dependency 'com.google.code.findbugs:jsr305' in project ':app')
میتوانید مورد زیر را به پروندهی app / build.gradle خود اضافه کنید.
android {
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
}
}
پیکربندی پروژه
def supportVersion = '27.1.1'
def retrofitVersion = '2.3.0'
def rxJavaVersion = '2.0.1'
def butterKnifeVersion = '8.8.1'
def daggerVersion = '2.15'
dependencies {
...
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxJavaVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "com.jakewharton:butterknife:$butterKnifeVersion"
implementation 'com.android.support:support-v4:27.1.1'
annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion"
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-android-processor:$daggerVersion"
...
}
ما به صورت Package by Feature کار خواهیم کرد. این باعث میشود کد شما modular و قابل کنترلتر شود.
راه اندازی Retrofit
public interface RepoService {
@GET("orgs/Google/repos")
Single<List<Repo>> getRepositories();
@GET("repos/{owner}/{name}")
Single<Repo> getRepo(@Path("owner") String owner, @Path("name") String name);
}
Base Fragment ،Base Activity ،Application Class را تنظیم میکنیم.
BaseApplication.java
public class BaseApplication extends DaggerApplication {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
ApplicationComponent component = DaggerApplicationComponent.builder().application(this).build();
component.inject(this);
return component;
}
}
BaseActivity.java
public abstract class BaseActivity extends DaggerAppCompatActivity {
@LayoutRes
protected abstract int layoutRes();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(layoutRes());
ButterKnife.bind(this);
}
}
ما از abstract LayoutRes() function برای دادن Id طرح (Resource Layout) استفاده میکنیم.
BaseFragment.java
public abstract class BaseFragment extends DaggerFragment {
private Unbinder unbinder;
private AppCompatActivity activity;
@LayoutRes
protected abstract int layoutRes();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(layoutRes(), container, false);
unbinder = ButterKnife.bind(this, view);
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
activity = (AppCompatActivity) context;
}
@Override
public void onDetach() {
super.onDetach();
activity = null;
}
public AppCompatActivity getBaseActivity() {
return activity;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(unbinder != null) {
unbinder.unbind();
unbinder = null;
}
}
}
Dagger 2 Component & Modules را تنظیم میکنیم.
ApplicationComponent.java
@Singleton
@Component(modules = {ContextModule.class, ApplicationModule.class, AndroidSupportInjectionModule.class, ActivityBindingModule.class})
public interface ApplicationComponent extends AndroidInjector<DaggerApplication> {
void inject(BaseApplication application);
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
ApplicationComponent build();
}
}
همانطور که مشاهده میکنید، ما فقط AndroidSupportInjectionModule ، ActivityBindingModule و ViewModelModule را در پارامتر ماژول نوشتیم. ما ماژولهای مورد نیاز دیگر را که Activity یا Fragment نیاز خواهد داشت، خواهیم نوشت.
ActivityBindingModule.java
@Module
public abstract class ActivityBindingModule {
@ContributesAndroidInjector(modules = {MainFragmentBindingModule.class})
abstract MainActivity bindMainActivity();
}
MainFragmentBindingModule.java
@Module
public abstract class MainFragmentBindingModule {
@ContributesAndroidInjector
abstract ListFragment provideListFragment();
@ContributesAndroidInjector
abstract DetailsFragment provideDetailsFragment();
}
ApplicationModule.java
@Singleton
@Module(includes = ViewModelModule.class)
public class ApplicationModule {
private static final String BASE_URL = "https://api.github.com/";
@Singleton
@Provides
static Retrofit provideRetrofit() {
return new Retrofit.Builder().baseUrl(BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
@Singleton
@Provides
static RepoService provideRetrofitService(Retrofit retrofit) {
return retrofit.create(RepoService.class);
}
}
ContextModule.java
@Module
public abstract class ContextModule {
@Binds
abstract Context provideContext(Application application);
}
ViewModelModule.java
@Singleton
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(ListViewModel.class)
abstract ViewModel bindListViewModel(ListViewModel listViewModel);
@Binds
@IntoMap
@ViewModelKey(DetailsViewModel.class)
abstract ViewModel bindDetailsViewModel(DetailsViewModel detailsViewModel);
@Binds
abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);
}
ساخت Custom ViewModel Factory
ViewModelFactory کارخانهای است که ViewModelProvider.Factive را به منظور ارائهی نمونههای ViewModel به کلاسهای Fragment مصرف کننده گسترش میدهد. ما آن کلاس را با Inject ،ViewModelModule کردهایم.
ViewModelFactory.java
@Singleton
public class ViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
@Inject
public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@NonNull
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(@NonNull 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);
}
}
}
ViewModel را راه اندازی میکنیم.
ListViewModel.java
public class ListViewModel extends ViewModel {
private final RepoRepository repoRepository;
private CompositeDisposable disposable;
private final MutableLiveData<List<Repo>> repos = new MutableLiveData<>();
private final MutableLiveData<Boolean> repoLoadError = new MutableLiveData<>();
private final MutableLiveData<Boolean> loading = new MutableLiveData<>();
@Inject
public ListViewModel(RepoRepository repoRepository) {
this.repoRepository = repoRepository;
disposable = new CompositeDisposable();
fetchRepos();
}
LiveData<List<Repo>> getRepos() {
return repos;
}
LiveData<Boolean> getError() {
return repoLoadError;
}
LiveData<Boolean> getLoading() {
return loading;
}
private void fetchRepos() {
loading.setValue(true);
disposable.add(repoRepository.getRepositories().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribeWith(new DisposableSingleObserver<List<Repo>>() {
@Override
public void onSuccess(List<Repo> value) {
repoLoadError.setValue(false);
repos.setValue(value);
loading.setValue(false);
}
@Override
public void onError(Throwable e) {
repoLoadError.setValue(true);
loading.setValue(false);
}
}));
}
@Override
protected void onCleared() {
super.onCleared();
if (disposable != null) {
disposable.clear();
disposable = null;
}
}
}
Fragment را ایجاد میکنیم.
ListFragment.java
public class ListFragment extends BaseFragment implements RepoSelectedListener {
@BindView(R.id.recyclerView) RecyclerView listView;
@BindView(R.id.tv_error) TextView errorTextView;
@BindView(R.id.loading_view) View loadingView;
@Inject ViewModelFactory viewModelFactory;
private ListViewModel viewModel;
@Override
protected int layoutRes() {
return R.layout.screen_list;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(this, viewModelFactory).get(ListViewModel.class);
listView.addItemDecoration(new DividerItemDecoration(getBaseActivity(), DividerItemDecoration.VERTICAL));
listView.setAdapter(new RepoListAdapter(viewModel, this, this));
listView.setLayoutManager(new LinearLayoutManager(getContext()));
observableViewModel();
}
@Override
public void onRepoSelected(Repo repo) {
DetailsViewModel detailsViewModel = ViewModelProviders.of(getBaseActivity(), viewModelFactory).get(DetailsViewModel.class);
detailsViewModel.setSelectedRepo(repo);
getBaseActivity().getSupportFragmentManager().beginTransaction().replace(R.id.screenContainer, new DetailsFragment())
.addToBackStack(null).commit();
}
private void observableViewModel() {
viewModel.getRepos().observe(this, repos -> {
if(repos != null) listView.setVisibility(View.VISIBLE);
});
viewModel.getError().observe(this, isError -> {
if (isError != null) if(isError) {
errorTextView.setVisibility(View.VISIBLE);
listView.setVisibility(View.GONE);
errorTextView.setText("An Error Occurred While Loading Data!");
}else {
errorTextView.setVisibility(View.GONE);
errorTextView.setText(null);
}
});
viewModel.getLoading().observe(this, isLoading -> {
if (isLoading != null) {
loadingView.setVisibility(isLoading ? View.VISIBLE : View.GONE);
if (isLoading) {
errorTextView.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
}
}
});
}
}
RecyclerView Adapter را ایجاد میکنیم.
RepoListAdapter.java
public class RepoListAdapter extends RecyclerView.Adapter<RepoListAdapter.RepoViewHolder>{
private RepoSelectedListener repoSelectedListener;
private final List<Repo> data = new ArrayList<>();
RepoListAdapter(ListViewModel viewModel, LifecycleOwner lifecycleOwner, RepoSelectedListener repoSelectedListener) {
this.repoSelectedListener = repoSelectedListener;
viewModel.getRepos().observe(lifecycleOwner, repos -> {
data.clear();
if (repos != null) {
data.addAll(repos);
notifyDataSetChanged();
}
});
setHasStableIds(true);
}
@NonNull
@Override
public RepoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_repo_list_item, parent, false);
return new RepoViewHolder(view, repoSelectedListener);
}
@Override
public void onBindViewHolder(@NonNull RepoViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
@Override
public long getItemId(int position) {
return data.get(position).id;
}
static final class RepoViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_repo_name) TextView repoNameTextView;
@BindView(R.id.tv_repo_description) TextView repoDescriptionTextView;
@BindView(R.id.tv_forks) TextView forksTextView;
@BindView(R.id.tv_stars) TextView starsTextView;
private Repo repo;
RepoViewHolder(View itemView, RepoSelectedListener repoSelectedListener) {
super(itemView);
ButterKnife.bind(this, itemView);
itemView.setOnClickListener(v -> {
if(repo != null) {
repoSelectedListener.onRepoSelected(repo);
}
});
}
void bind(Repo repo) {
this.repo = repo;
repoNameTextView.setText(repo.name);
repoDescriptionTextView.setText(repo.description);
forksTextView.setText(String.valueOf(repo.forks));
starsTextView.setText(String.valueOf(repo.stars));
}
}
}
جمع بندی
در مقاله آموزش Dagger در اندروید ، با Dagger و کارایی آن آشنا شدیم و متوجه شدیم که استفاده از Dagger چه مزایایی برای توسعه اپلیکیشنهای اندرویدی دارد. اگر دربارهی این مقاله سوال و نظری دارید خوشحال میشویم که با ما و کاربران سون لرن به اشتراک بگذارید.
اگر به یادگیری بیشتر در زمینهی برنامه نویسی اندروید علاقه داری، با شرکت در دورهی آموزش برنامه نویسی اندروید در کمتر از یکسال به یک توسعهدهنده اندروید همه فن حریف تبدیل میشوی که آمادهی استخدام، دریافت پروژه و حتی پیادهسازی اپلیکیشن خودت هستی.