听说你还不会用Dagger2?Dagger2 For Android最佳实践教程

news2024/11/26 12:41:52

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG,chef.cook());
}
}

可以看到,在使用Dagger2的时候,使用者的代码会变得非常简洁。但是,Dagger 2还需要一些列的辅助代码来实现依赖注入的。如果用过Dagger2就知道要实现依赖注入的话,需要写十分多模版代码。那么我们可不可以用更简单的方式使用Dagger2呢?今天笔者就来介绍一下在Android上使用Dagger2的更简洁的方案。

我们先来看看在DaggerMainActivity上实现依赖注入还需要哪些代码。

CookModules

@Module
public class CookModules {

@Singleton
@Provides
public Map<String, Boolean> providerMenus(){
Map<String, Boolean> menus = new LinkedHashMap<>();
menus.put(“酸菜鱼”, true);
menus.put(“土豆丝”, true);
menus.put(“铁板牛肉”, true);
return menus;
}
}

ActivityModules

@Module
abstract class ActivityModules {

@ContributesAndroidInjector
abstract MainActivity contributeMainActivity();
}

CookAppComponent

@Singleton
@Component(modules = {
AndroidSupportInjectionModule.class,
ActivityModules.class,
CookModules.class})
public interface CookAppComponent extends AndroidInjector {

@Component.Builder
abstract class Builder extends AndroidInjector.Builder{}

}

MyApplication

public class MyApplication extends DaggerApplication{

@Override
public void onCreate() {
super.onCreate();
}

@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
return DaggerCookAppComponent.builder().create(this);
}
}

Dagger2 For Android 使用要点分析

  1. CookModules CookModule很简单,它的目的就是通过@Providers注解提供Menu对象需要的数据。因为Menu是需要依赖一个Map对象的,所以我们通过CookModules给它构造一个Map对象,并自动把它注入到Menu实例里面。
  2. ActivityModules ActivityModules的主要作用就是通过@ContributesAndroidInjector来标记哪个类需要使用依赖注入功能,这里标记的是ManActivity,所以MainActivity能通过@Inject注解来注入Chef对象。
  3. CookAppComponent CookAppComponent相当于一个注射器,我们前面定义的Modules就是被注射的类,使用@Inject注入对象的地方就是接收者类。
  4. MyApplication MyAppliction的特点是继承了DaggerAppliction类,并且在applicationInjector方法中构建了一个DaggerCookAppComponent注射器。

这就是Dagger 2在Android中的使用方案了,在这里我们可以看到,接收这类(MainActivity)中的代码非常简单,实现依赖注入只使用了:

@Inject
Chef chef;

在接收类里面完全没有多余的代码,如果我们要拓展可以SecondsActivity的话,在SecondsActivity我们要用到Menu类。

那么我们只需要在ActivityModules中增加:

@ContributesAndroidInjector
abstract SecondsActivity contributeSecondsActivity();

然后在SecondsActivity注入Menu:

@Inject
Menu menu;

可以看到,对于整个工程来说,实现使用Dagger2 For Android实现依赖注入要写的模版代码其实非常少,非常简洁。只需要进行一次配置就可以,不需要频繁写一堆模版代码。总的来说,Dagger2造成模版代码增加这个问题已经解决了。

Demo地址:目录下的Dagger2Simple就是Demo地址,上面的例子为Dagger2Simple中的simple Modules

Dagger2的优势

在这里我们总结下使用Dagger2带来的优点。

  1. 减少代码量,提高工作效率 例如上面的例子中,我们构建一个Chef对象的话,不使用Dagger2的情况下,需要在初始化Chef对象之前进行一堆前置对象(Menu、Map)的初始化,并且需要手工注入到对应的实例中。你想像下,如果我们再加一个Restaurant( 餐馆 )对象,并且需要把Chef注入到Restaurant中的话,那么初始化Restaurant对象时,需要的前置步骤就更繁琐了。 可能有人会觉得,这也没什么啊,我不介意手工初始化。但是如果你的系统中有N处需要初始化Restaurant对象的地方呢?使用Dagger2 的话,只需要用注解注入就可以了。
  2. 自动处理依赖关系 使用Dagger2的时候,我们不需要指定对象的依赖关系,Dagger2会自动帮我们处理依赖关系(例如Chef需要依赖Menu,Menu需要依赖Map,Dagger自动处理了这个依赖关系)。
  3. 采用静态编译,不影响运行效率 因为Dagger2是在编译期处理依赖注入的,所以不会影响运行效率在一定的程度上还能提高系统的运行效率(例如采用Dagger2实现单例,不用加锁效率更高)。
  4. 提高多人编程效率 在多人协作的时候,一个人用Dagger2边写完代码后,其它所有组员都能通过@Inject注解直接注入常用的对象。加快编程效率,并且能大大增加代码的复用性。

上面我们介绍完了Dagger2 For Android的基本用法了。可能有些读者意犹未尽,觉得这个例子太简单了。那么我们来尝试下构建一个更加复杂的系统,深度体验下Dagger2 For Android的优势。现在我们在上面这个例子的基础上拓展下,尝试开发一个简单的点餐Demo来深度体验下。

Dagger2应用实战

现在我们来看下如何使用Dagger2来开发一个简单的Demo,这里笔者开发的Demo是一个简单的点餐Demo。这个Demo的功能非常简单,提供了菜单展示、菜单添加/编辑/删除和下单功能。而下单功能只是简单地把菜品名用Snackbar显示到屏幕上。

Demo展

操作展示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Demo地址:目录下的Dagger2Simple就是Demo地址,上面的例子为Dagger2Simple中的order Modules

代码目录

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个Demo采用经典的MVP架构,我们先来简单分析下Demo的细节实现。

  1. 使用SharedPreferences提供简单的缓存功能(存储菜单)。
  2. 使用Gson把列表序列化成Json格式数据,然后以String的形式保存在SharedPreferences中。
  3. 使用Dagger2实现依赖注入功能。

这样基本就实现了一个简单的点菜Demo了。

Dagger在Demo中的应用解释

当我们使用SharedPreferences和Gson实现缓存功能的时候我们会发现,项目中很多地方都会需要这个SharedPreferences和Gson对象。所以我们可以得出两个结论:

  1. 项目中多个模块会用到一些公共实例。
  2. 这些公共实例应该是单例对象。

我们看看是如何通过使用Dagger2提供全局的Modules来实现这类型对象的依赖注入。

CookAppModules

@Module
public abstract class CookAppModules {

public static final String KEY_MENU = “menu”;
private static final String SP_COOK = “cook”;

@Singleton
@Provides
public static Set providerMenus(SharedPreferences sp, Gson gson){
Set menus;
String menuJson = sp.getString(KEY_MENU, null);
if (menuJson == null){
return new LinkedHashSet<>();
}
menus = gson.fromJson(menuJson, new TypeToken<Set>(){}.getType());
return menus;
}

@Singleton
@Provides
public static SharedPreferences providerSharedPreferences(Context context){
return context.getSharedPreferences(SP_COOK, Context.MODE_PRIVATE);
}

@Singleton
@Provides
public static Gson providerGson(){
return new Gson();
}

@Singleton
@Binds
public abstract Context context(OrderApp application);

}

在这里以dishes模块为例子,dishes中DishesPresenter是负责数据的处理的,所以我们会在DishesPresenter注入这些实例。

DishesPresenter

public class DishesPresenter implements DishesContract.Presenter{

private DishesContract.View mView;

@Inject
Set dishes;

@Inject
Gson gson;

@Inject
SharedPreferences sp;

@Inject
public DishesPresenter(){

}

@Override
public void loadDishes() {
mView.showDishes(new ArrayList<>(dishes));
}

@Override
public String order(Map<Dish, Boolean> selectMap) {
if (selectMap == null || selectMap.size() == 0) return “”;
StringBuilder sb = new StringBuilder();

for (Dish dish : dishes){
if (selectMap.get(dish)){
sb.append(dish.getName()).append(“、”);
}
}
if (TextUtils.isEmpty(sb.toString())) return “”;

return "烹饪: " + sb.toString();
}

@Override
public boolean deleteDish(String id) {
for (Dish dish : dishes){
if (dish.getId().equals(id)){
dishes.remove(dish);
sp.edit().putString(CookAppModules.KEY_MENU, gson.toJson(dishes)).apply();
return true;
}
}
return false;
}

@Override
public void takeView(DishesContract.View view) {
mView = view;
loadDishes();
}

@Override
public void dropView() {
mView = null;
}
}

上面的代码能很好地体验Dagger2的好处,假如我们项目中有比较复杂的对象在很多地方都会用到的话,我们可以通过这种方式来简化我们的代码。

Dishes模块的UI是由Activity加Fragment实现的,Fragment实现了主要的功能,而Activity只是简单作为Fragment的外层。它们分别是:DishesActivity和DishesFragment

DishesActivity依赖了DishesFragment对象,而在DishesFragment则依赖了DishesAdapter、RecyclerView.LayoutManager、DishesContract.Presenter对象。

我们先来分别看看DishesActivity与DishesFragment的关键代码。

DishesActivity

public class DishesActivity extends DaggerAppCompatActivity {

@Inject
DishesFragment mDishesFragment;


}

DishesFragment

public class DishesFragment extends DaggerFragment implements DishesContract.View{

RecyclerView rvDishes;

@Inject
DishesAdapter dishesAdapter;

@Inject
RecyclerView.LayoutManager layoutManager;

@Inject
DishesContract.Presenter mPresenter;

@Inject
public DishesFragment(){

}

}

DishesFragment通过Dagger2注入了DishesAdapter、RecyclerView.LayoutManager、DishesContract.Presenter,而这些实例是由DishesModules提供的。

DishesModules

@Module
public abstract class DishesModules {

@ContributesAndroidInjector
abstract public DishesFragment dishesFragment();

@Provides
static DishesAdapter providerDishesAdapter(){
return new DishesAdapter();
}

@Binds
abstract DishesContract.View dishesView(DishesFragment dishesFragment);

@Binds
abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);

}

这里我们先说明下这几个注解的作用。

  • @ContributesAndroidInjector 你可以把它看成Dagger2是否要自动把需要的用到的Modules注入到DishesFragment中。这个注解是Dagger2 For Android简化代码的关键,下面的小节会通过一个具体例子来说明。

  • @Module 被这个注解标记的类可以看作为依赖对象的提供者,可以通过这个被标记的类结合其它注解来实现依赖关系的关联。

  • @Provides 主要作用就是用来提供一些第三方类库的对象或提供一些构建非常复杂的对象在Dagger2中类似工厂类的一个角色。

  • @Binds 主要作用就是确定接口与具体的具体实现类,这样说得比较抽象,我们还是看看例子吧。 在DishesFragment中有这么一句代码:

@Inject
DishesContract.Presenter mPresenter;

我们知道DishesContract.Presenter是一个接口而这个接口可能有很多不同的实现类,而@Binds的作用就是用来确定这个具体实现类的。以看看PresenterModules的代码:

@Module
public abstract class PresenterModules {
@Binds
abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);


}

从这句代码可以看出,使用@Inject注入的DishesContract.Presenter对象的具体实现类是DishesPre​
senter。

Dagger2 For Android是如何注入依赖的?

我们在用Dagger2的时候是通过一些模版代码来实现依赖注入的( DaggerXXXComponent.builder().inject(xxx) 这种模版代码),但是在Demo中的DishesFragment根本没看到类似的代码啊,那么这些对象是什么时候注入到DishesFragment重的呢?

答案就是**@ContributesAndroidInjector**注解

我们先来看看Dagger2是通过什么方式来实现自动把依赖注入到DishesActivity中的。

ActivityModules

@Module
public abstract class ActivityModules {

@ContributesAndroidInjector(modules = DishesModules.class)
abstract public DishesActivity contributesDishActivity();

@ContributesAndroidInjector(modules = AddEditModules.class)
abstract public AddEditDishActivity contributesAddEditDishActivity();

}

没错,就是@ContributesAndroidInjector这个注解,modules就代表这个DishesActivity需要依赖哪个Modules。这篇教程我们不解释它的具体实现原理,你只需要知道@ContributesAndroidInjector的作用就可以了。

我们以前使用Dagger2的时候,需要些很多Component来辅助我们实现依赖注入,而现在我们整个App中只需要写一个Component就可以了。@ContributesAndroidInjector注解会帮助我们生成其它需要的Component,并且自动处理Component之间的关系,自动帮我们使用生成的Component来注入依赖。

我们先看看我们现在整个模块中唯一存在的Component是怎么使用的。

OrderAppComponent

@Singleton
@Component(modules = {
AndroidSupportInjectionModule.class,
LayoutManagerModules.class,
CookAppModules.class,
PresenterModules.class,
ActivityModules.class})
public interface OrderAppComponent extends AndroidInjector{

@Component.Builder
abstract class Builder extends AndroidInjector.Builder{
}

}

OrderApp

public class OrderApp extends DaggerApplication {

@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
return DaggerOrderAppComponent.builder().create(this);
}
}

为了加深大家对@ContributesAndroidInjecto注解r的理解,我们稍微修改下DishesModules

@Module
public abstract class DishesModules {

//@ContributesAndroidInjector
//abstract public DishesFragment dishesFragment();

@Provides
static DishesAdapter providerDishesAdapter(){
return new DishesAdapter();
}

@Binds
abstract DishesContract.View dishesView(DishesFragment dishesFragment);

@Binds
abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);

}

DishesActivity

public class DishesActivity extends DaggerAppCompatActivity {

//@Inject
DishesFragment mDishesFragment;

Toolbar toolbar;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dishes);

DishesFragment dishesFragment
= (DishesFragment) getSupportFragmentManager().findFragmentById(R.id.content_fragment);

if (dishesFragment == null){
mDishesFragment = new DishesFragment();//新增代码
dishesFragment = mDishesFragment;
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), dishesFragment, R.id.content_fragment);
}
initView();

}

}

//DaggerFragment改为Fragment
public class DishesFragment extends Fragment implements DishesContract.View{
}

这个时候,我们运行的时候会发现,DishesFragment中的依赖注入失败了,运行时会抛出空指针异常,没注入需要的数据。导致这个原因是因为我们在这里使用new来创建DishesFragment实例的,为什么使用new的时候会Dagger2没有帮我们注入实例呢?

当我们使用@Inject来注入DishesFragment的时候,Dagger2会自动帮我们判断DishesFragment所依赖的对象(@Inject注解标记),如果能直接注入的对象则直接注入到Fragment中,否则则从DishesModules中寻找是否有需要的对象,有的话则注入到DishesFragment中。而我们使用new来创建DishesFragment时Dagger2无法通过DishesModules来查找对象,因为我们没有声明DishesFragment与DishesModules的联系,DishesFragment也没有自动注入注解的标记( 没有实现HasSupportFragmentInjector )。所以Dagger2无法判断它们依赖关系也没办法自动帮DishesFragment自动注入依赖。

如果我们坚持要使用new的方式来依赖DishesFragment的话,则可以通过@ContributesAndroidInjecto注解来实现它们之间的关联。具体实现方式如下:

DishesModules

@Module(includes = PresenterModules.class)
public abstract class DishesModules {

@ContributesAndroidInjector
abstract public DishesFragment dishesFragment(); //增加这个抽象方法

@Provides
static DishesAdapter providerDishesAdapter(){
return new DishesAdapter();
}

@Binds
abstract DishesContract.View dishesView(DishesFragment dishesFragment);

@Binds
abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);

}

DishesFragment继承于DaggerFragment

public class DishesFragment extends DaggerFragment implements DishesContract.View{

}

改成这样,我们通过new方法来创建DishesFragment的时候也能实现通过注解进行依赖注入了,为什么会这样呢?因为@ContributesAndroidInjector的作用时帮我们生成需要的Subcomponent,然后在DaggerFragment通过 DispatchingAndroidInjector 对象来实现依赖注入( 底层原理和我们使用DaggerXXXComponent手动实现依赖注入差不多 )。我们可以看看DishesModules中被@ContributesAndroidInjector注解的方法生成的代码。

@Module(subcomponents = DishesModules_DishesFragment.DishesFragmentSubcomponent.class)
public abstract class DishesModules_DishesFragment {
private DishesModules_DishesFragment() {}

@Binds
@IntoMap
@FragmentKey(DishesFragment.class)
abstract AndroidInjector.Factory<? extends Fragment> bindAndroidInjectorFactory(
DishesFragmentSubcomponent.Builder builder);

@Subcomponent
public interface DishesFragmentSubcomponent extends AndroidInjector {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder {}
}
}

可以看出,编生成的代码符合我们上面的结论。

Dagger2 For Android使用要点

我们现在来总结下,简化版的Dagger实现依赖注入的几个必要条件:

  1. 第三方库通过Modules的@provides注解来提供依赖
  2. 提供一个全局唯一的Component,并且Modules中需要添加AndroidSupportInjectionModule类,它的作用时关联需求与依赖之间的关系
  3. Application需要继承DaggerApplication类,并且在applicationInjector构建并返回全剧唯一的Component实例
  4. 其它需要使用依赖注入的组建都需要继承Dagger组件名字类,并且需要在相应的Modules中通过@ContributesAndroidInjector注解标记需要注入依赖的组建。

上面四个步骤就是使用Dagger2实现依赖注入的要点了,总的来说,复杂度比之前的方法简单了非常多,要写的模版代码也减少了非常多。

一般来说,上面的知识点已经足够让我们在项目中正常使用Dagger2了,但是在使用中还会遇到一些其它的问题,Dagger2也提供了解决方法。如果希望进一步了解的话,可以继续阅读下文。

Dagger2拓展

@Scope

Scope字面的意思是作用域,在我们使用Dagger2的时候经常会用到@Singleton这个注解,这个注解的意思的作用是提供单例对象。而我们在使用@Singleton这个注解的时候,会同时@Provides和@Component,为什么要这样做呢?因为@Scope的作用范围其实就是单例的作用范围,这个范围主要是通过Component来确定的。

所以@Scope的作用就是以指定Component的范围为边界,提供局部的单例对象。我们可以以上面的例子为例验证这个论点论点。

我们在DishesActivity中增加一句代码,作用时注入DishesPresneter对象。

@Inject
DishesContract.Presenter mPresenter;

从上面的代码中,我们知道DishesFragment中也用同样的方式来注入过DishesPresneter对象,那么它们有什么区别的,我们通过调试功能来看下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看出,DishesActivity和DishesFragment中的DishesPresenter不是同一个实例,它们的内存地址是不一样的。如果我们在PresenterModules的dishesPresenter方法中加上@Singleton

@Singleton
@Binds
abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);

可以预见,DishesActivity和DishesFragment中的DishesPresenter会变成同一个实例,在这个例子中@Singleton的作用是提供全局的单例( 因为OrderAppComponent这个全局唯一的Component也被标注成@Singleton )。这种用法比较简单,这里不再深入。而比较难理解的就是自定义Scope了,下面我们通过一个例子来加深大家对自定义Scope的理解。

@DishesScoped

@Documented
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface DishesScoped {
}

为了使测试效果更明显,我们稍微修改下Order这个Demo。

DishesModules

@Module
public abstract class DishesModules {

@DishesScoped // 添加注解
@Binds
abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);

}

ActivityModules

@Module
public abstract class ActivityModules {

@DishesScoped // 添加注解
@ContributesAndroidInjector(modules = DishesModules.class)
abstract public DishesActivity contributesDishActivity();
}

然后现在我们来运行Demo,看下DishesActivity和DishesFragment中的DishesContract.Presenter的对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看出,它们是同一个对象,这验证了我们上面的结论。这里又个小问题就是,我们之前说@Scope是通过Component来确定作用边界的,但是上面这个例子中,并没有对任何Component类使用@Dishes注解啊?那么这里是如何确认边界的呢?

我们可以看看Dagger生成的类ActivityModules_ContributesDishActivity,这个类是根据ActivityModules中的contributesDishActivity方法生成的。

@Module(subcomponents = ActivityModules_ContributesDishActivity.DishesActivitySubcomponent.class)
public abstract class ActivityModules_ContributesDishActivity {
private ActivityModules_ContributesDishActivity() {}

@Binds
@IntoMap
@ActivityKey(DishesActivity.class)
abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
DishesActivitySubcomponent.Builder builder);

@Subcomponent(modules = DishesModules.class)
@DishesScoped //看这里
public interface DishesActivitySubcomponent extends AndroidInjector {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder {}
}
}

谜底揭晓,当我们为contributesDishActivity添加上@DishesScoped注解后,自动生成的DishesActivitySubcomponent类被@DishesScoped注解了。所以@DishesScoped是通过DishesActivitySubcomponent来确认作用范围的,这也符合上面的结论。

@Scope的实现原理

@Scope实现单例的原理其实很简单,我们可以看下加了@DishesScoped后Dagger为我们生成的注入辅助代码。在这里我们只看关键方法:

private void initialize(final DishesActivitySubcomponentBuilder builder) {
this.dishesFragmentSubcomponentBuilderProvider =
new Provider<DishesModules_DishesFragment.DishesFragmentSubcomponent.Builder>() {
@Override
public DishesModules_DishesFragment.DishesFragmentSubcomponent.Builder get() {
return new DishesFragmentSubcomponentBuilder();
}
};
this.dishesPresenterProvider =
DishesPresenter_Factory.create(
DaggerOrderAppComponent.this.providerMenusProvider,
DaggerOrderAppComponent.this.providerGsonProvider,
DaggerOrderAppComponent.this.providerSharedPreferencesProvider);
this.dishesPresenterProvider2 = DoubleCheck.provider((Provider) dishesPresenterProvider); //这句代码是实现单例的关键。
}

可以看到,我们的dishesPresenterProvider2这个对象的初始化是通过双锁校验的方式来实现单例的,所以这个对象是一个单例对象。而其它没有使用@Spoce注解的类则没有使用双锁校验的方式实现初始化,Dagger通过@Scope实现单例的原理其实非常简单。关于@Spoce的介绍就到这里了,如果需要深入的话,可以进一步查看Dagger2生成的辅助代码。

@Qualifier和@Named注解

除了作用域的问题之外我们还会经常会遇到一个问题,总所周知,Dagger2是自动判断依赖关系的,如果我们的代码中需要使用同一个类生成两个或多个不同的对象呢?例如我们的LinearManager,我们现在想用Dagger提供一个横向的Manager,如果直接写在项目中是会报错的,因为Dagger无法判断需要注入/依赖的对象是哪个。如下面的代码:

LayoutManagerModules

@Module
public class LayoutManagerModules {

@Provides
public LinearLayoutManager providesLinearLayoutManager(Context context){
return new LinearLayoutManager(context);
}

@Provides
public LinearLayoutManager providesHorizonalLinearLayoutManager(Context context){
return new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
}

}

这段代码肯定是会报错的,如果我们想实现这个功能的话,这个时候我们就需要用到@Qualifier或者@Named注解了。

我们先用@Named来实现上面这个需求。

LayoutManagerModules

@Module
public class LayoutManagerModules {

@Named(“vertical”)
@Provides
public LinearLayoutManager providesLinearLayoutManager(Context context){
return new LinearLayoutManager(context);
}

@Named(“horizontal”)
@Provides
public LinearLayoutManager providesHorizonalLinearLayoutManager(Context context){
return new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
}

}

DishesModules

public class DishesFragment extends DaggerFragment implements DishesContract.View{

RecyclerView rvDishes;

@Inject
DishesAdapter dishesAdapter;

@Named(“horizontal”)

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

资料⬅专栏获取
ew LinearLayoutManager(context);
}

@Named(“horizontal”)
@Provides
public LinearLayoutManager providesHorizonalLinearLayoutManager(Context context){
return new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
}

}

DishesModules

public class DishesFragment extends DaggerFragment implements DishesContract.View{

RecyclerView rvDishes;

@Inject
DishesAdapter dishesAdapter;

@Named(“horizontal”)

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

[外链图片转存中…(img-8zcdc7z5-1719100004969)]一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

资料⬅专栏获取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1852202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows10中端口被占用处理方法

前言 在Windows 10中&#xff0c;查看端口被占用情况的方法主要依赖于命令行工具netstat。以下是详细步骤&#xff0c;以及必要的解释和归纳&#xff1a; 打开命令提示符 方法1&#xff1a;使用快捷键Win R&#xff0c;打开“运行”对话框&#xff0c;输入cmd&#xff0c;然…

2024-06-23 编译原理实验3——语义分析

文章目录 一、实验要求二、实验设计三、实验结果四、附完整代码 补录与分享本科实验&#xff0c;以示纪念。 一、实验要求 基于前面的实验&#xff0c;编写一个程序对使用 C—语言书写的源代码进行语义分析&#xff0c;输出语义分析中发现的错误&#xff08;涉及 17 种错误类…

异地局域网纯软件组网如何设置?

在现代社会中&#xff0c;随着企业的不断扩张和分布&#xff0c;异地办公成为一种常见的工作模式。随之而来的是&#xff0c;如何实现异地局域网的组网设置成为了一个挑战。在这种情况下&#xff0c;采用纯软件组网方案是一种有效的解决方案。本文将介绍异地局域网纯软件组网设…

Redis数据库的删除和安装

Redis数据库的删除和安装 1、删除Redis数据库2、下载Redis数据库 1、删除Redis数据库 没有下载过的&#xff0c;可以直接跳到下面的安装过程↓ 我们电脑中如果有下载过Redis数据库&#xff0c;要更换版本的话&#xff0c;其实Redis数据库的删除是比较简单的&#xff0c;打开我…

字节大神强推千页PDF学习笔记,弱化学历问题,已拿意向书字节提前批移动端!

主要问java&#xff0c;以及虚拟机&#xff0c;问了一点android 1.实习项目有关的介绍以及问题回答 2.反射与代理的区别&#xff0c;动态代理&#xff0c;静态代理&#xff0c;二者的区别&#xff0c;以及代理模式的UML图 3.字节码技术 4.虚拟机的双亲委派&#xff0c;以及好…

1Panel应用推荐:Bitwarden开源密码管理器

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…

贪心算法—

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法。这种算法并不总是能找到全局最优解&#xff0c;但在某些问题上能提供足够好的解决方案。贪心算法的关键特性包括&#…

判断题无答案22届期末复习

判断: 1-3.结构体变量不能进行整体输入输出。 1-4.不同类型的结构变量之间也可以直接赋值。 1-5假设结构指针p已定义并正确赋值,其指向的结构变量有一个成员是int型的num,则语句 (*p).num = 100; 等价于p->num=1…

1panel OpenResty 设置网站重定向

当我们部署网站时需要&#xff0c;输入"cheshi.com"域名回车&#xff0c;希望他自动跳转https://cheshi.com/indx/&#xff0c;而不是直接跳转https://cheshi.com时可以利用重定向来实现&#xff0c; 这里演示的是 1panel 如何设置&#xff08;nginx 貌似也是这样配…

【大疆pocket3】到手后5个必改初始设置关键点(下)

【大疆pocket3】到手后5个必改初始设置关键点&#xff08;下&#xff09; 一&#xff0c;简介二&#xff0c;必改关键点2.1 数字变焦2.2 慢动作拍摄2.3 神奇的小摇杆2.4 云台模式使用方法&#xff08;默认增稳模式和俯仰角锁定的差异化以及使用场景&#xff09;2.5 云台转向速度…

[HBM] HBM 国产进程, 国产HBM首次研发成功 (202406)

依公知及经验整理&#xff0c;原创保护&#xff0c;禁止转载。 专栏 《深入理解DDR》 AI 的火热浪潮带火了高带宽内存的需求&#xff0c;HBM已是存储市场耀眼的明星。目前市场上还没有国产HBM, 什么时候可以看到国产希望呢&#xff1f; 或许现在可以看到曙光了。 1. 设计端 1…

Linux查看公网IP的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

使用Android Studio导入源码

2-1 基础准备工作 首先你得安装配置了Android Studio&#xff0c;具体不明白的参考《Android Studio入门到精通 》。 接着你得下载好了源码Code&#xff0c;至于如何下载这里不再说明&#xff0c;比较简单&#xff0c;上官网查看就行了。 其次你需要保证源码已经被编译生成了…

压力测试

1.什么是压力测试 压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内&#xff0c;做到心中有数 使用压力测试&#xff0c;我们有希望找到很多种用其他测试方法更难发现的错误&#…

写一个坏越的个人天地(一)

好久没写什么大点的项目了,今天想着写一个个人博客好了。I did it! 做个人天地。肯定得有个主题色吧。整个下拉界面,先准备三个色系吧 <el-header class="title"><el-dropdown @command="handleCommand"><span class="el-dropdown-…

DEV-C++与EasyX图形库

&#x1f3ae;&#x1f50a;本文代码适合编译环境&#xff1a;DEV-C&#x1f4bb; ✨&#x1f9e8;温馨提示&#xff1a;此文乃作者心血&#xff0c;如要转载请标注版权&#xff0c;否则视为抄袭&#xff01;&#x1f389;&#x1f3a0; 今天就算是我们Easyx教程的第一篇博文…

使用python下载图片且批量将图片插入word文档

最近有一个小的功能实现&#xff0c;从小某书上下载指定帖子的图片们&#xff0c;然后批量插入到word文档中&#xff0c;便于打印。于是有了以上需求。 一、下载图片 1、首先获取图片们的链接img_urls 首先&#xff0c;获取到的指定帖子的所有信息可以存入一个json文件中&am…

Python 深入学习局部函数和闭包函数

目录 局部函数与闭包函数的关联 变量捕获与状态保留 应用场景的交集与差异 闭包的本质 局部函数示例 闭包函数示例 局部函数和闭包函数之间存在着密切的联系&#xff0c;同时也有一些本质的区别。 局部函数与闭包函数的关联 局部函数&#xff08;Nested Function&#…

git 拉下来的项目,出现“加载失败”的解决方法

现象&#xff1a; 1、对加载失败的项目&#xff0c;尝试重新加载 解决思路&#xff1a;根据上面的提示&#xff0c;打开F盘对应的 .vcxproj文件&#xff0c;查看里面关于opencv454.props的内容 先删了&#xff0c;后面再补 2、当前的工作重点是消除加载失败的情况&#xff0c;…

VBA技术资料MF165:关闭当前打开的所有工作簿

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…