一个快速集成框架:MVP+Dagger+主流框架,有它足矣

移动开发 Android
今年的Android技术圈中MVP,Dagger2,Rxjava,Retrofit这些词汇非常火,随便打开一个技术论坛都充斥着大量的关于这些技术的文章,Github也充斥着各种以基于MVP+Retrofit+RxJava+Dagger2+MaterialDesign开发的xxxx为标题的开源项目或Demo.

前言

今年的Android技术圈中MVP,Dagger2,Rxjava,Retrofit这些词汇非常火,随便打开一个技术论坛都充斥着大量的关于这些技术的文章,Github也充斥着各种以基于MVP+Retrofit+RxJava+Dagger2+MaterialDesign开发的xxxx为标题的开源项目或Demo.

但是大家这么热心的开源此类项目,一直重复的做着同样的事教授大家使用的方式和技巧有没有想过依赖一个第三方库,就可以快速的搭建此类框架?

特性

  • 自动生成MVP,Dagger2相关类
  • 版本更新
  • 更新日志
  • 通用框架,适合所有类型的项目,支持大型项目的开发,Demo的包结构直接可以拿来用
  • 全部使用Dagger2管理(将所有模块使用Dagger连接起来,绝不是简单的使用)
  • 大量使用Rxjava
  • 修改包名(common包不要修改)后就可以直接使用,快速接入(老项目接入请按下面的步骤)
  • 全部UI自适应
  • 图片加载类ImageLoader使用策略模式和建造者模式,轻松切换图片加载框架和功能扩展
  • Model层提供Retrofit API和RxCache,是否使用缓存自行选择
  • 全局http Request(请求参数,headers) Response(服务器返回的结果,headers,耗时)信息监听,可解析json后根据状态码做相应的全局操作
  • 全局Rxjava错误处理,错误后自动重试,捕捉整个应用的所有错误

框架结构 

框架结构 

包结构 

 

 

包结构 

开发须知

  • 开发者需要具有一定的Android开发能力
  • 开发者必须有使用Dagger2,Rxjava,Retrofit的经验,没使用过也必须了解,不然很难使用

Libraries简介

  1. MvpGoogle官方出品的Mvp架构项目,含有多个不同的架构分支(此为Dagger分支).
  2. Dagger2Google根据Square的Dagger1出品的依赖注入框架,通过apt动态生成代码,性能优于用反射技术依赖注入的框架.
  3. Rxjava提供优雅的响应式Api解决异步请求.
  4. RxAndroid为Android提供响应式Api.
  5. Rxlifecycle在Android上使用rxjava都知道的一个坑,就是生命周期的解除订阅,这个框架通过绑定activity和fragment的生命周期***解决.
  6. RxbindingJakeWharton大神的View绑定框架,优雅的处理View的响应事件.
  7. RxCache是使用注解为Retrofit加入二级缓存(内存,磁盘)的缓存库
  8. RetrofitSquare出品的网络请求库,极大的减少了http请求的代码和步骤.
  9. Okhttp同样Square出品,不多介绍,做Android都应该知道.
  10. Autolayout鸿洋大神的Android全尺寸适配框架.
  11. GsonGoogle官方的Json Convert框架.
  12. ButterknifeJakeWharton大神出品的view注入框架.
  13. Androideventbus一个轻量级使用注解的Eventbus.
  14. TimberJakeWharton大神出品Log框架,内部代码极少,但是思想非常不错.
  15. Glide此库为本框架默认封装图片加载库,可参照着例子更改为其他的库,Api和Picasso差不多,缓存机制比Picasso复杂,速度快,适合处理大型图片流,支持gfit,Fresco太大了!,在5.0一下优势很大,5.0以上系统默认使用的内存管理和Fresco类似.
  16. Realm速度和跨平台性使它成为如今最火的数据库,美中不足的就是so库太大
  17. LeakCanarySquare出品的专门用来检测Android和Java的内存泄漏,通过通知栏提示内存泄漏信息
  18. RxErroHandlerRxjava错误处理库,可在出现错误后重试

1 开发准备

此框架适合自己做定制修改,所有暂时不上传至Jcenter或Maven,请自行下载或clone

1.1 导入框架

  1. compile project(':arms'

1.2 引用config.build

本框架提供一个引用大量第三方库的config.gradle文件,用于第三方库版本管理,将config.gradle复制进根目录,并在项目的***build.gradle中引用它 

 

 

 

1.2.1 使用config.build

因为在***build.gradle中引用了它,所以在整个项目的所有build.gradle中都可以使用rootProject.xxx来使用它里面的内容 

 

 

 

也可以使用它来管理一些项目的信息,这样有多个module也可以直接使用一个信息 

 

 

 

1.3 依赖Dagger2

本框架全部使用Dagger2管理,所以必须依赖Dagger2,找到app的build.gradle,加入如下代码

 [[180060]]

 

1.4 配置AndroidManifest

1.4.1 添加权限

 

1.4.2 配置Autolayout Meta

使用Autolayout 自适应框架必须配置Meta属性及设计图的宽高,详情参考Autolayout 

 

 

 

1.4.3 引用Glide自定义属性

本框架默认使用Glide加载图片,但提供一个管理器ImageLoader提供统一接口,使用策略者模式可轻松替换图片加载框架,本框架默认提供Glide的自定义缓存配置信息,使用它之前先引用它的自定义配置信息

  1. <!--glide配置--> 
  2.        <meta-data 
  3.            android:name="com.jess.arms.widget.imageloader.glide.GlideConfiguration" 
  4.            android:value="GlideModule"/>  

1.5 混淆

由于本框架依赖大量三方库,所以已经在arms Module下的proguard-rules.pro中提供了所有规则,如果想使用它,请复制它替换app Module中的proguard-rules.pro,混淆时可以根据自己的需求修改或添加规则,混淆前务必注意将Java Bean,自定义组件添加进规则

1.6 版本更新

! 一定不能修改common包的包名,旧版本需要找到和common包下同名的类并删除,然后重

新引用common包里的类

如果你获得本框架的方式是通过clone或者下载:

  1. 直接可以通过命令行git pull origin master拉取***的版本并自动合并
  2. 如果你修改了包名还得执行命令git rm --cache -r app/src/main/java/me/jessyan/mvparms,下次拉取时就不会拉取Demo的内容

如果你获得本框架的方式是通过fork到自己仓库后,clone或下载:

  1. git remote add arms https://github.com/JessYanCoding/MVPArms.git 添加远程仓库,arms是远程仓库的代号,可自定义,以后都通过这个代号对远程仓库作操作
  2. git fetch arms拉取远程仓库***的版本
  3. git merge arms/master --allow-unrelated-histories合并远程仓库到当前分支
  4. 后面如果本框架有更新就只用重复2,3步,--allow-unrelated-histories只用在***次合并时添加
  5. 如果你修改了包名还得执行命令git rm --cache -r app/src/main/java/me/jessyan/mvparms,下次拉取时就不会拉取Demo的内容

2 快速开始

2.1 继承BaseApplication

新建项目的Application继承自BaseApplication,并在AndroidManifest中声明 

 

 

 

2.1.1 AppComponent

Application生命周期是和App是一样的,所以是适合提供一些单例对象,本框架使用Dagger2管理,所以使用AppComponent来提供全局所有的单例对象

创建AppComponent接口 

 

 

 

  • 构造AppComponent对象 

 

 

 

  • ServiceModule(提供RetrofitApi)和CacheModule(提供缓存)都需自行创建,详情ServiceModule(2.1.2) ,CacheModule(2.1.3)

2.1.2 ServiceModule

ServiceModule提供RetrofitApi对应的Service,这些Service对象在AppComponent中注入ServiceManager(需继承BaseServiceManager)中统一管理

  • 自行定义Retrofit Service如下,熟练Retrofit请忽略
  1. public interface CommonService { 
  2.  
  3.    String HEADER_API_VERSION = "Accept: application/vnd.github.v3+json"
  4.  
  5.    @Headers({HEADER_API_VERSION}) 
  6.    @GET("/users"
  7.    Observable<List<User>> getUsers(@Query("since"int lastIdQueried, @Query("per_page"int perPage); 
  8.  
  • 定义ServiceModule,这里使用Retrofit对象(ClientModule提供)实例化Service接口,提供所有Service对象(可以根据不同的逻辑划分多个Service接口)
  1. @Module 
  2. public class ServiceModule { 
  3.  
  4.    @Singleton 
  5.    @Provides 
  6.    CommonService provideCommonService(Retrofit retrofit) { 
  7.        return retrofit.create(CommonService.class); 
  8.    } 
  9.  
  10.  
  • AppComponent将所有的Service注入到ServiceManager中,所有Model层都可以拿到此对象,意味着每个Model都可以请求任意Api 

 

 

 

2.1.3 CacheModule

Cache层默认使用RxCache,CacheModule提供RetrofitApi对应的Cache对象,这些Cache对象在AppComponent中注入CacheManager(需继承BaseCacheManager)中统一管理

  • 自行定义RxCache Provider如下,熟练RxCache请忽略
  1. public interface CommonCache { 
  2.  
  3.    @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES) 
  4.    Observable<Reply<List<User>>> getUsers(Observable<List<User>> oUsers, DynamicKey idLastUserQueried, EvictProvider evictProvider); 
  5.  
  6.  
  • 定义CacheModule,这里使用RxCache对象(ClientModule提供)实例化所有Cache接口,提供所有Cache对象
  1. @Module 
  2. public class CacheModule { 
  3.  
  4.    @Singleton 
  5.    @Provides 
  6.    CommonCache provideCommonService(RxCache rxCache) { 
  7.        return rxCache.using(CommonCache.class); 
  8.    } 
  9.  
  10.  
  • AppComponent将所有的Cache注入到CacheManager中,所有Model层都可以拿到所有的Cache对象 

 

 

 

2.2 继承BaseActivity

让项目的基类Activity继承BaseActivity,BaseActivity默认注入Presenter,所以如果要使用Presenter必须指定对应的范型,并且提供注入Presenter所需要的Component 

 

 

 

2.3 继承BaseFragment

让项目的基类Fragment继承BaseFragment,BaseFragment默认注入Presenter,所以如果要使用Presenter必须指定对应的范型,并且提供注入Presenter所需要的Component 

 

 

 

2.4 MVP实战

定义业务逻辑MVP,继承MVP各自的基类即可,这里可以稍微粗力度的定义MVP类,即无需每个Fragment和Activity(每个页面)都定义不同的MVP类,可以按照相同的业务逻辑使用一组MVP类

2.4.1 Contract

这里根据Google官方的MVP项目,可以在Contract中定义MVP的接口,便于管理,此框架无需定义Presenter接口,所以Contract只定义Model和View的接口 

 

 

 

2.4.2 View

一般让Activity或Fragment实现Contract中定义的View接口,供Presenter调用对应方法操作UI,BaseActivity默认注入Presenter,如想使用Presenter,必须指定Presenter的范型,和实现setupActivityComponent来提供Presenter需要的Component和Module 

 

 

 

2.4.3 Model

Model实现Contract的Model接口,并且继承BaseModel,指定范型为,上面定义的ServiceManager和CacheManager,然后通过两个Manager拿到需要的Service和Cache为Presenter提供需要的数据(是否使用缓存请自行选择) 

 

 

 

2.4.4 Presenter

Presenter在MVP中的大部分的作用为通过从Model层接口获取数据,在调用View层接口显示数据,首先实现BasePresenter,指定Model和View的范型,注意一定要指定Contract中定义的接口,Presenter需要的Model和View,都使用Dagger2注入,这样即解藕又方便测试,怎么注入? 

 

 

 

2.4.5 MVP Module

这里的Module提供当前业务逻辑对应的View和Model接口(Contract中定义的接口)的实现类,Model需要AppComponent中提供的ServiceManager和CacheManager来实现网络请求和缓存,所以需要通过Component依赖AppComponent拿到这两个Manager 

 

 

 

2.4.6 MVP Component

这里需要注意的是此Component必须依赖AppComponent,这样才能提供Model需要的ServiceManager和CacheManager,提供inject()方法就能将Module及AppComponent中提供的对象注入到对应的类中,inject()中的参数不能是接口,怎么注入?

  1. @ActivityScope 
  2. @Component(modules = UserModule.class,dependencies = AppComponent.class) 
  3. public interface UserComponent { 
  4.    void inject(UserActivity activity); 
  5.  

2.4.7 Dagger Scope

在上面的代码中ActivityScope大量出现在Module和Component中,Dagger2使用Scope限制每个Module中提供的对象的生命,Dagger2默认只提供一个@SingletonScope即单例,本框架提供@ActvityScope和@FragmentScope,如有其他需求请自行实现,Module和Component定义相同的Scope后Module中提供的对象的生命周期会和Component中一样(即在Component生命周期内,如需使用到Moudle中提供的对象,只会调用一次@Provide注解的方法得到此对象)

2.4.8 MVP总结

以后每个业务逻辑都重复构造这些类,只是换个名字而已,值得注意的是MVP刚开始用时确实会觉得平白无故多了很多类,非常繁琐麻烦,但是等页面代码逻辑越来多时,你会发现其中的好处,逻辑清晰,解耦,便于团队协作,测试容易,错误好定位,所以现在本框架提供Template自动生成代码解决这个痛点,让开发者更加愉快的使用本框架

3 功能使用

3.1 App全局配置信息(使用Dagger注入)

GlobeConfigModule使用建造者模式将App的全局配置信息封装进Module(使用Dagger注入到需要配置信息的地方),可以配置CacheFile,InterCeptor等,因为使用的是建造者模式所以如你有其他配置信息需要使用Dagger注入,直接就可以添加进Builder并且不会影响到其他地方

  1. //如需添加个Boolean字段提供给Log工具类,来判断是否打印Log 
  2.    @Module 
  3.    public class GlobeConfigModule { 
  4.        private Boolean isLog; 
  5.  
  6.        private GlobeConfigModule(Buidler buidler) { 
  7.            this.isLog = builder.isLog 
  8.        } 
  9.  
  10.        public static Buidler buidler() { 
  11.        return new Buidler(); 
  12.       } 
  13.  
  14.        public static final class Buidler { 
  15.            private Boolean isLog; 
  16.  
  17.            private Buidler() {} 
  18.  
  19.            //1.给Builder中添加个方法接受isLog字段 
  20.            public Buidler isLog(Boolean isLog) { 
  21.              this.isLog = isLog; 
  22.              return this; 
  23.           } 
  24.  
  25.            public GlobeConfigModule build() { 
  26.               return new GlobeConfigModule(this); 
  27.           } 
  28.  
  29.        } 
  30.  
  31.        //2.使用@Provides,将isLog返回出去,供Dagger注入到Log工具类 
  32.        @Singleton 
  33.        @Provides 
  34.        Boolean provideIsLog() { 
  35.            return isLog; 
  36.        } 
  37.    }  

3.2 全局捕捉Http请求和响应

通过GlobeConfigModule.globeHttpHandler()方法传入GlobeHttpHandler 

  1. @Override 
  2.    protected GlobeConfigModule getGlobeConfigModule() { 
  3.        return GlobeConfigModule 
  4.                .buidler() 
  5.                .baseurl(Api.APP_DOMAIN) 
  6.                .globeHttpHandler(new GlobeHttpHandler() {// 这里可以提供一个全局处理http响应结果的处理类, 
  7.                    // 这里可以比客户端提前一步拿到服务器返回的结果,可以做一些操作,比如token超时,重新获取 
  8.                    @Override 
  9.                    public Response onHttpResultResponse(String httpResult, Interceptor.Chain chain, Response response) { 
  10.                        //这里可以先客户端一步拿到每一次http请求的结果,可以解析成json,做一些操作,如检测到token过期后 
  11.                        //重新请求token,并重新执行请求 
  12.                        try { 
  13.                            if (!TextUtils.isEmpty(httpResult)) { 
  14.                                JSONArray array = new JSONArray(httpResult); 
  15.                                JSONObject object = (JSONObject) array.get(0); 
  16.                                String login = object.getString("login"); 
  17.                                String avatar_url = object.getString("avatar_url"); 
  18.                                Timber.tag(TAG).w("result ------>" + login + "    ||   avatar_url------>" + avatar_url); 
  19.                            } 
  20.  
  21.                        } catch (JSONException e) { 
  22.                            e.printStackTrace(); 
  23.                            return response; 
  24.                        } 
  25.  
  26.  
  27.                        //这里如果发现token过期,可以先请求***的token,然后在拿新的token放入request里去重新请求 
  28.                        //注意在这个回调之前已经调用过proceed,所以这里必须自己去建立网络请求,如使用okhttp使用新的request去请求 
  29.                        // create a new request and modify it accordingly using the new token 
  30. //                    Request newRequest = chain.request().newBuilder().header("token", newToken) 
  31. //                            .build(); 
  32.  
  33. //                    // retry the request 
  34. // 
  35. //                    response.body().close(); 
  36.                        //如果使用okhttp将新的请求,请求成功后,将返回的response  return出去即可 
  37.  
  38.                        //如果不需要返回新的结果,则直接把response参数返回出去 
  39.                        return response; 
  40.                    } 
  41.  
  42.                    // 这里可以在请求服务器之前可以拿到request,做一些操作比如给request统一添加token或者header 
  43.                    @Override 
  44.                    public Request onHttpRequestBefore(Interceptor.Chain chain, Request request) { 
  45.                        //如果需要再请求服务器之前做一些操作,则重新返回一个做过操作的的requeat如增加header,不做操作则返回request 
  46.  
  47.                        //return chain.request().newBuilder().header("token", tokenId) 
  48. //                .build(); 
  49.                        return request; 
  50.                    } 
  51.                }) 
  52.                .build(); 
  53.    }  

3.3 全局错误处理及发生错误时重新执行

如果需要使用Rxjava的全局错误处理,需通过GlobeConfigModule.responseErroListener()方法传入ResponseErroListener,并在每次使用Rxjava调用subscribe时,使用ErrorHandleSubscriber,并传入AppComponent中提供的RxErrorHandler,此Subscribe,默认已经实现OnError方法,如想自定义可以重写OnError方法 

 

 

 

  • 在Rxjava中使用 

 

 

 

3.4 切换图片请求框架

本框架默认使用Glide实现图片加载功能,使用ImagerLoader提供统一的接口,ImagerLoader使用策略模式和建造者模式,可以动态切换图片框架(比如说切换成Picasso),并且加载图片时传入的参数也可以随意扩展(loadImage方法在需要扩展参数时,也不需要改动,全部通过Builder扩展,比如你想让内部的图片加载框架,清除缓存你只需要定义个boolean字段,内部根据这个字段if|else,其他操作同理)

  • 使用ImageLoader必须传入一个实现了BaseImageLoaderStrategy接口的图片加载实现类从而实现动态切换,所以首先要实现BaseImageLoaderStrategy,实现时必须指定一个继承自ImageConfig的实现类,使用建造者模式,可以储存一些信息,比如URL, ImageView,Placeholder等,可以不断的扩展,供图片加载框架使用 

 

 

 

  • 实现ImageCofig使用建造者模式 
  1. public class PicassoImageConfig extends ImageConfig{ 
  2.  
  3.    private PicassoImageConfig(Buidler builder) { 
  4.        this.url = builder.url; 
  5.        this.imageView = builder.imageView; 
  6.        this.placeholder = builder.placeholder; 
  7.        this.errorPic = builder.errorPic; 
  8.    } 
  9.  
  10.    public static Buidler builder() { 
  11.        return new Buidler(); 
  12.    } 
  13.  
  14.  
  15.    public static final class Buidler { 
  16.        private String url; 
  17.        private ImageView imageView; 
  18.        private int placeholder; 
  19.        protected int errorPic; 
  20.  
  21.        private Buidler() { 
  22.        } 
  23.  
  24.        public Buidler url(String url) { 
  25.            this.url = url; 
  26.            return this; 
  27.        } 
  28.  
  29.        public Buidler placeholder(int placeholder) { 
  30.            this.placeholder = placeholder; 
  31.            return this; 
  32.        } 
  33.  
  34.        public Buidler errorPic(int errorPic){ 
  35.            this.errorPic = errorPic; 
  36.            return this; 
  37.        } 
  38.  
  39.        public Buidler imagerView(ImageView imageView) { 
  40.            this.imageView = imageView; 
  41.            return this; 
  42.        } 
  43.  
  44.        public PicassoImageConfig build() { 
  45.            if (url == null) throw new IllegalStateException("url is required"); 
  46.            if (imageView == null) throw new IllegalStateException("imageview is required"); 
  47.            return new PicassoImageConfig(this); 
  48.        } 
  49.    } 
  50.  
  • 在ImageLoader构造时可以传入PicassoImageLoaderStrategy(),也可以通过AppComponent拿到ImageLoader对象后,setLoadImgStrategy(new PicassoImageLoaderStrategy)替换之前的实现(默认使用Glide) 

 

 

 

3.***ndroidEventBus Tag

本框架使用AndroidEventBus实现事件总线,此框架使用注解标记目标方法,统一将Tag的常量写到EventBusTag接口中,便于管理,如果要在当前对象中使用AndroidEventBus请在需要使用的Activity,Fragment,Presenter中重写useEventBus(),返回true代表使用,默认返回true

3.6 AutoLayout组件

本框架使用AutoLayout框架,实现控件自适应,此框架要让组件自适应,必须让它的父控件,重新测量,和重写LayoutParams,而官方只默认提供了三个ViewGroup,AutoRelativeLayout,AutoLinearLayout,AutoFrameLayout实现了这些操作,为了方便开发者使用,本框架提供了一些常用的AutoLayout组件,在框架的widget包下的autolayout包中,在xml中引用即可使子控件自适应,并且还提供一个 Template(在***面)用于生成自适应所需要的的Auto系列View,如需要使ScrollView的子控件自适应,使用此Template输入ScrollView,即可生成AutoScrollView,在xml中引用即可

3.7 自定义PopupWindow

框架提供一个建造者模式的自定义PopupWindow组件CustomPopupWindow,自己实现布局后就可以直接使用这个实现PopupWindow,使用建造者模式,随意扩展自定义参数

3.8 快速实现RecycleView

本框架提供DefaultAdapter和BaseHolder基类快速实现Recycleview.

  • BaseHolder默认初始化了ButterKnife和AutoLayout,继承后不仅可以直接注入View,布局还可以自适应屏幕
  • RecycleView默认是不提供Item的点击事件的,使用DefaultAdapter调用setOnItemClickListener可以实现Item的点击事件

3.9 权限管理(适配Android6.0权限管理)

本框架使用RxPermissions用于权限管理(适配android6.0),并提供PermissionUtil工具类一行代码实现权限请求.适配Android6.0权限管理详解

  1. PermissionUtil.launchCamera(new RequestPermission() { 
  2.            @Override 
  3.            public void onRequestPermissionSuccess() { 
  4.                launchCapture();//请求权限成功后做一些操作 
  5.            } 
  6.        }, mRxPermissions, mRootView, mErrorHandler);  

3.10 Gradle配置启动DeBug模式

在主项目(app)的build.gradle中配置是否开启打印Log或则是否使用LeakCanary,等调试工具

  • 在build.gradle中配置 

 

 

 

  • 在代码中使用(比如在application中做一些初始化设置) 

 

 

 

3.11 AppManager(管理所有的Activity)

AppManager用于管理所有的Activity,内部持有一个含有所有存活的Activity(未调用onDestroy)的List,和一个当前在最前端的Activity(未调用onPause),AppManager封装有多种方法,可以很方便的对它们进行操作,也可以在未持有AppManager的情况下,通过EventBus远程遥控它的所有方法,这样我们可以在整个app的任何地方对任何Activity进行全局操作,比如在app请求网络超时时让最前端的Activity显示连接超时的交互页面(这个逻辑不用写到当前请求的Activity里,可以在一个单例类里做全局的统一操作,因为可以随时通过AppManager拿到当前的Activity)

远程遥控通过EventBuspost Message实现,通过不同的what区分不同的方法和Handler同理,可以根据自己的需求适当的在AppManager中添加对应的方法 

 

 

 

总结

如果你是构建一个全新的项目,直接将整个项目clone(或者下载)下来,直接将Demo当成主Module,再将项目包名改成自己的包名,Demo包含可以直接使用的包结构,一个主流的MVP+Dagger2+Retrofit+Rxjava框架就这样轻松的构建成功了,现在你参考Demo Mvp包下的UserActivity的格式,使用Template在对应包下自动生成MVP,Dagger2相关类,配合查阅Wiki文档慢慢掌握本框架,看再多文章不如早点在项目中使用它,在实践中学习总是最快的.

责任编辑:庞桂玉 来源: 安卓巴士Android开发者门户
相关推荐

2009-05-08 09:32:27

JavaWeb编程框架

2023-08-01 07:25:38

Expresso框架API

2022-01-06 14:59:53

Java框架magic-api

2016-03-01 13:48:36

MVVMios快速开发

2014-07-08 14:05:48

DaggerAndroid依赖

2018-12-13 10:37:13

Android开发框架

2021-07-06 11:40:28

Android框架代码

2020-11-02 08:19:18

RPC框架Java

2020-08-17 08:20:16

iOSAOP框架

2016-10-20 19:27:00

开源项目bootstrapcss框架

2009-10-01 09:19:45

PHP框架ZendFramewoCake

2021-06-08 07:32:01

框架Mock测试

2011-11-08 10:36:42

Java

2015-10-27 15:50:47

JS框架

2013-01-14 09:44:58

JavaScriptJSJS框架

2020-09-29 15:08:47

Go UI框架开发

2009-02-01 09:29:44

2019-07-19 15:51:11

框架选型分布式

2010-08-05 14:08:26

Flex框架

2012-01-04 13:55:23

Canvas
点赞
收藏

51CTO技术栈公众号