owned this note
owned this note
Published
Linked with GitHub
# JessYanCoding - MVPArms
![](https://i.imgur.com/AYeQb5C.png)
基於 MVP 開發的 Android App 通用架構,集成了許多開源項目(如Dagger2、RxJava、Retrofit ...),使您的開發更快更容易。
## 官方 MVP 架構圖
![](https://i.imgur.com/Iqb02EQ.png)
## UML MVP 架構圖
### Activity
![](https://i.imgur.com/2tojiF3.png)
```
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.InflateException;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.jess.arms.base.delegate.IActivity;
import com.jess.arms.integration.cache.Cache;
import com.jess.arms.integration.cache.CacheType;
import com.jess.arms.integration.lifecycle.ActivityLifecycleable;
import com.jess.arms.mvp.IPresenter;
import com.jess.arms.utils.ArmsUtils;
import com.trello.rxlifecycle2.android.ActivityEvent;
import javax.inject.Inject;
import butterknife.ButterKnife;
import butterknife.Unbinder;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
import static com.jess.arms.utils.ThirdViewUtil.convertAutoView;
public abstract class BaseActivity<P extends IPresenter> extends AppCompatActivity implements IActivity, ActivityLifecycleable {
protected final String TAG = this.getClass().getSimpleName();
private final BehaviorSubject<ActivityEvent> mLifecycleSubject = BehaviorSubject.create();
@Inject
@Nullable
protected P mPresenter;
private Cache<String, Object> mCache;
private Unbinder mUnbinder;
@NonNull
@Override
public synchronized Cache<String, Object> provideCache() {
if (mCache == null) {
//noinspection unchecked
mCache = ArmsUtils.obtainAppComponentFromContext(this).cacheFactory().build(CacheType.ACTIVITY_CACHE);
}
return mCache;
}
@NonNull
@Override
public final Subject<ActivityEvent> provideLifecycleSubject() {
return mLifecycleSubject;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = convertAutoView(name, context, attrs);
return view == null ? super.onCreateView(name, context, attrs) : view;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
int layoutResID = initView(savedInstanceState);
if (layoutResID != 0) {
setContentView(layoutResID);
mUnbinder = ButterKnife.bind(this);
}
} catch (Exception e) {
if (e instanceof InflateException) {
throw e;
}
e.printStackTrace();
}
initData(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mUnbinder != null && mUnbinder != Unbinder.EMPTY) {
mUnbinder.unbind();
}
this.mUnbinder = null;
if (mPresenter != null) {
mPresenter.onDestroy();
}
this.mPresenter = null;
}
@Override
public boolean useEventBus() {
return true;
}
@Override
public boolean useFragment() {
return true;
}
}
```
### Frgment
![](https://i.imgur.com/oi3aHZG.png)
```
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.jess.arms.base.delegate.IFragment;
import com.jess.arms.integration.cache.Cache;
import com.jess.arms.integration.cache.CacheType;
import com.jess.arms.integration.lifecycle.FragmentLifecycleable;
import com.jess.arms.mvp.IPresenter;
import com.jess.arms.utils.ArmsUtils;
import com.trello.rxlifecycle2.android.FragmentEvent;
import javax.inject.Inject;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
public abstract class BaseFragment<P extends IPresenter> extends Fragment implements IFragment, FragmentLifecycleable {
protected final String TAG = this.getClass().getSimpleName();
private final BehaviorSubject<FragmentEvent> mLifecycleSubject = BehaviorSubject.create();
protected Context mContext;
@Inject
@Nullable
protected P mPresenter;
private Cache<String, Object> mCache;
@NonNull
@Override
public synchronized Cache<String, Object> provideCache() {
if (mCache == null) {
mCache = ArmsUtils.obtainAppComponentFromContext(getActivity()).cacheFactory().build(CacheType.FRAGMENT_CACHE);
}
return mCache;
}
@NonNull
@Override
public final Subject<FragmentEvent> provideLifecycleSubject() {
return mLifecycleSubject;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mContext = context;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return initView(inflater, container, savedInstanceState);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mPresenter != null) {
mPresenter.onDestroy();
}
this.mPresenter = null;
}
@Override
public void onDetach() {
super.onDetach();
mContext = null;
}
@Override
public boolean useEventBus() {
return true;
}
}
```
### Presenter
![](https://i.imgur.com/Xj6xmqd.png)
```
import android.app.Service;
import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
public class BasePresenter<M extends IModel, V extends IView> implements IPresenter, LifecycleObserver {
protected final String TAG = this.getClass().getSimpleName();
protected M mModel;
protected V mRootView;
public BasePresenter(M model, V rootView) {
this.mModel = model;
this.mRootView = rootView;
onStart();
}
public BasePresenter(V rootView) {
this.mRootView = rootView;
onStart();
}
public BasePresenter() {
onStart();
}
@Override
public void onStart() {
if (mRootView != null && mRootView instanceof LifecycleOwner) {
((LifecycleOwner) mRootView).getLifecycle().addObserver(this);
if (mModel != null && mModel instanceof LifecycleObserver) {
((LifecycleOwner) mRootView).getLifecycle().addObserver((LifecycleObserver) mModel);
}
}
}
@Override
public void onDestroy() {
if (mModel != null) {
mModel.onDestroy();
}
this.mModel = null;
this.mRootView = null;
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void onDestroy(LifecycleOwner owner) {
owner.getLifecycle().removeObserver(this);
}
}
```
## Demo
### Contract
![](https://i.imgur.com/gRALmuX.png)
```
/*
* Copyright 2017 JessYan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yi.androidarchitecturemvp.jessyancoding_mvparms.demo.contract;
import android.app.Activity;
import com.tbruyelle.rxpermissions2.RxPermissions;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.base.presenter.IModel;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.base.presenter.IView;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.demo.model.entity.User;
import java.util.List;
import io.reactivex.Observable;
import me.jessyan.mvparms.demo.mvp.model.entity.User;
public interface UserContract {
interface View extends IView {
void startLoadMore();
void endLoadMore();
Activity getActivity();
//申请权限
RxPermissions getRxPermissions();
}
interface Model extends IModel {
Observable<List<User>> getUsers(int lastIdQueried, boolean update);
}
}
```
### Model
![](https://i.imgur.com/XSnZVLF.png)
```
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.OnLifecycleEvent;
import com.jess.arms.di.scope.ActivityScope;
import com.jess.arms.integration.IRepositoryManager;
import com.jess.arms.mvp.BaseModel;
import java.util.List;
import javax.inject.Inject;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.functions.Function;
import io.rx_cache2.DynamicKey;
import io.rx_cache2.EvictDynamicKey;
import me.jessyan.mvparms.demo.mvp.contract.UserContract;
import me.jessyan.mvparms.demo.mvp.model.api.cache.CommonCache;
import me.jessyan.mvparms.demo.mvp.model.api.service.UserService;
import me.jessyan.mvparms.demo.mvp.model.entity.User;
import timber.log.Timber;
@ActivityScope
public class UserModel extends BaseModel implements UserContract.Model {
public static final int USERS_PER_PAGE = 10;
@Inject
public UserModel(IRepositoryManager repositoryManager) {
super(repositoryManager);
}
@Override
public Observable<List<User>> getUsers(int lastIdQueried, boolean update) {
return Observable.just(mRepositoryManager
.obtainRetrofitService(UserService.class)
.getUsers(lastIdQueried, USERS_PER_PAGE))
.flatMap((Function<Observable<List<User>>, ObservableSource<List<User>>>) listObservable -> mRepositoryManager.obtainCacheService(CommonCache.class)
.getUsers(listObservable
, new DynamicKey(lastIdQueried)
, new EvictDynamicKey(update))
.map(listReply -> listReply.getData()));
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void onPause() {
Timber.d("Release Resource");
}
}
```
### Presenter
![](https://i.imgur.com/JhwCoKE.png)
```
import android.app.Application;
import androidx.core.app.ComponentActivity;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.recyclerview.widget.RecyclerView;
import com.jess.arms.di.scope.ActivityScope;
import com.jess.arms.integration.AppManager;
import com.jess.arms.mvp.BasePresenter;
import com.jess.arms.utils.PermissionUtil;
import com.jess.arms.utils.RxLifecycleUtils;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.base.presenter.BasePresenter;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.demo.contract.UserContract;
import java.util.List;
import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import me.jessyan.mvparms.demo.mvp.contract.UserContract;
import me.jessyan.mvparms.demo.mvp.model.entity.User;
import me.jessyan.rxerrorhandler.core.RxErrorHandler;
import me.jessyan.rxerrorhandler.handler.ErrorHandleSubscriber;
import me.jessyan.rxerrorhandler.handler.RetryWithDelay;
@ActivityScope
public class UserPresenter extends BasePresenter<UserContract.Model, UserContract.View> {
@Inject
RxErrorHandler mErrorHandler;
@Inject
AppManager mAppManager;
@Inject
Application mApplication;
@Inject
List<User> mUsers;
@Inject
RecyclerView.Adapter mAdapter;
private int lastUserId = 1;
private boolean isFirst = true;
private int preEndIndex;
@Inject
public UserPresenter(UserContract.Model model, UserContract.View rootView) {
super(model, rootView);
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
void onCreate() {
requestUsers(true);
}
public void requestUsers(final boolean pullToRefresh) {
PermissionUtil.externalStorage(new PermissionUtil.RequestPermission() {
@Override
public void onRequestPermissionSuccess() {
//request permission success, do something.
requestFromModel(pullToRefresh);
}
@Override
public void onRequestPermissionFailure(List<String> permissions) {
mRootView.showMessage("Request permissions failure");
mRootView.hideLoading();
}
@Override
public void onRequestPermissionFailureWithAskNeverAgain(List<String> permissions) {
mRootView.showMessage("Need to go to the settings");
mRootView.hideLoading();
}
}, mRootView.getRxPermissions(), mErrorHandler);
}
private void requestFromModel(boolean pullToRefresh) {
if (pullToRefresh) {
lastUserId = 1;
}
boolean isEvictCache = pullToRefresh;
if (pullToRefresh && isFirst) {
isFirst = false;
isEvictCache = false;
}
mModel.getUsers(lastUserId, isEvictCache)
.subscribeOn(Schedulers.io())
.retryWhen(new RetryWithDelay(3, 2))
.doOnSubscribe(disposable -> {
if (pullToRefresh) {
mRootView.showLoading();
} else {
mRootView.startLoadMore();
}
}).subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> {
if (pullToRefresh) {
mRootView.hideLoading();
} else {
mRootView.endLoadMore();
}
})
.compose(RxLifecycleUtils.bindToLifecycle(mRootView))
.subscribe(new ErrorHandleSubscriber<List<User>>(mErrorHandler) {
@Override
public void onNext(List<User> users) {
lastUserId = users.get(users.size() - 1).getId();
if (pullToRefresh) {
mUsers.clear();
}
preEndIndex = mUsers.size();
mUsers.addAll(users);
if (pullToRefresh) {
mAdapter.notifyDataSetChanged();
} else {
mAdapter.notifyItemRangeInserted(preEndIndex, users.size());
}
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
this.mAdapter = null;
this.mUsers = null;
this.mErrorHandler = null;
this.mAppManager = null;
this.mApplication = null;
}
}
```
### Activity
![](https://i.imgur.com/3zqheWD.png)
```
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.jess.arms.base.BaseActivity;
import com.jess.arms.base.DefaultAdapter;
import com.jess.arms.di.component.AppComponent;
import com.jess.arms.utils.ArmsUtils;
import com.paginate.Paginate;
import com.tbruyelle.rxpermissions2.RxPermissions;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.base.activity.BaseActivity;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.demo.contract.UserContract;
import com.yi.androidarchitecturemvp.jessyancoding_mvparms.demo.presenter.UserPresenter;
import javax.inject.Inject;
import butterknife.BindView;
import me.jessyan.mvparms.demo.R;
import me.jessyan.mvparms.demo.di.component.DaggerUserComponent;
import me.jessyan.mvparms.demo.mvp.contract.UserContract;
import me.jessyan.mvparms.demo.mvp.presenter.UserPresenter;
import timber.log.Timber;
import static com.jess.arms.utils.Preconditions.checkNotNull;
public class UserActivity extends BaseActivity<UserPresenter> implements UserContract.View, SwipeRefreshLayout.OnRefreshListener {
@BindView(R.id.recyclerView)
RecyclerView mRecyclerView;
@BindView(R.id.swipeRefreshLayout)
SwipeRefreshLayout mSwipeRefreshLayout;
@Inject
RxPermissions mRxPermissions;
@Inject
RecyclerView.LayoutManager mLayoutManager;
@Inject
RecyclerView.Adapter mAdapter;
private Paginate mPaginate;
private boolean isLoadingMore;
@Override
public void setupActivityComponent(@NonNull AppComponent appComponent) {
DaggerUserComponent
.builder()
.appComponent(appComponent)
.view(this)
.build()
.inject(this);
}
@Override
public int initView(@Nullable Bundle savedInstanceState) {
return R.layout.activity_user;
}
@Override
public void initData(@Nullable Bundle savedInstanceState) {
initRecyclerView();
mRecyclerView.setAdapter(mAdapter);
initPaginate();
}
@Override
public void onRefresh() {
mPresenter.requestUsers(true);
}
private void initRecyclerView() {
mSwipeRefreshLayout.setOnRefreshListener(this);
ArmsUtils.configRecyclerView(mRecyclerView, mLayoutManager);
}
@Override
public void showLoading() {
Timber.tag(TAG).w("showLoading");
mSwipeRefreshLayout.setRefreshing(true);
}
@Override
public void hideLoading() {
Timber.tag(TAG).w("hideLoading");
mSwipeRefreshLayout.setRefreshing(false);
}
@Override
public void showMessage(@NonNull String message) {
checkNotNull(message);
ArmsUtils.snackbarText(message);
}
@Override
public void launchActivity(@NonNull Intent intent) {
checkNotNull(intent);
ArmsUtils.startActivity(intent);
}
@Override
public void killMyself() {
finish();
}
@Override
public void startLoadMore() {
isLoadingMore = true;
}
@Override
public void endLoadMore() {
isLoadingMore = false;
}
@Override
public Activity getActivity() {
return this;
}
@Override
public RxPermissions getRxPermissions() {
return mRxPermissions;
}
private void initPaginate() {
if (mPaginate == null) {
Paginate.Callbacks callbacks = new Paginate.Callbacks() {
@Override
public void onLoadMore() {
mPresenter.requestUsers(false);
}
@Override
public boolean isLoading() {
return isLoadingMore;
}
@Override
public boolean hasLoadedAllItems() {
return false;
}
};
mPaginate = Paginate.with(mRecyclerView, callbacks)
.setLoadingTriggerThreshold(0)
.build();
mPaginate.setHasMoreDataToLoad(false);
}
}
@Override
protected void onDestroy() {
DefaultAdapter.releaseAllHolder(mRecyclerView);
super.onDestroy();
this.mRxPermissions = null;
this.mPaginate = null;
}
}
```
## 總結
![](https://i.imgur.com/kXPo0qo.png)
分 3 大部分
* View
* UserActivity 繼承 BaseActivity
而 BaseActivity(P extends IPresenter)
* UserContract
* View 繼承 IView
* Model 繼承 IModel
* Presenter
* UserPresenter 繼承 BasePresenter
BasePresenter<M extends IModel, V extends IView> implements IPresenter
* Model
UserModel 繼承 BaseModel 實作 UserContract.Model
值得注意的是,該架構的 Model 是由 Dagger 框架來做依賴注入(DI)。
## 沒玩過的 Libariy
* [~~com.zhy:autolayout~~](https://github.com/hongyangAndroid/AndroidAutoLayout)
Android屏幕尺寸,設計圖上的尺寸直接完成重新設計。
沒在維護了,推薦使用[AndroidAutoSize](https://github.com/JessYanCoding/AndroidAutoSize)
* [JessYanCoding - AndroidAutoSize](https://github.com/JessYanCoding/AndroidAutoSize)
一個極低成本的Android 屏幕適配方案。
* [Bigkoo - Android-PickerView](https://github.com/Bigkoo/Android-PickerView)
這是一款仿iOS的PickerView控件,有時間選擇器和選項選擇器。
* [Baseflow - PhotoView](https://github.com/Baseflow/PhotoView)
PhotoView 旨在幫助生成一個易於使用的縮放 Android ImageView 實現。
* [daimajia - NumberProgressBar](https://github.com/daimajia/NumberProgressBar)
![image](https://camo.githubusercontent.com/fca6171915076f73e5429c964321dac4e5b17b3ed55f7d44fb86d07e910f887a/687474703a2f2f7777332e73696e61696d672e636e2f6d773639302f36313064633033346a77316566797264386e376937673230637a30326d7135662e676966)
* [~~JakeWharton - NineOldAndroids~~](https://github.com/JakeWharton/NineOldAndroids)
棄用的動畫庫。
* [MarkoMilos - Paginate](https://github.com/MarkoMilos/Paginate)
RecyclerView用於在或 AbsListView上創建簡單分頁功能(又名無限滾動)的 Android 庫。
* [~~alibaba - vlayout~~](https://github.com/alibaba/vlayout)
RecyclerView 庫
* [ReactiveX - RxAndroid](https://github.com/ReactiveX/RxAndroid)
* [RXJava](https://github.com/ReactiveX/RxJava)
* [trello - RxLifecycle](https://github.com/trello/RxLifecycle)
* [VictorAlbertos - RxCache](https://github.com/VictorAlbertos/RxCache)
緩存您的數據模型。
* [VictorAlbertos - Jolyglot](https://github.com/VictorAlbertos/Jolyglot)
Jolyglot 允許在不依賴任何具體實現的情況下將對象與 Json 相互轉換。
* [JakeWharton - RxBinding](https://github.com/JakeWharton/RxBinding)
來自平台和支持庫的 Android UI 小部件的 RxJava 綁定 API。
* [tbruyelle - RxPermissions](https://github.com/tbruyelle/RxPermissions)
該庫允許將 RxJava 與新的 Android M 權限模型一起使用。
* [JessYanCoding - RxErrorHandler](https://github.com/JessYanCoding/RxErrorHandler)
Error Handle Of Rxjava
* [~~hehonghui - AndroidEventBus~~](https://github.com/hehonghui/AndroidEventBus)
這是一個適用於 Android 的 EventBus 庫。它簡化了Activity、Fragment、Threads、Services等之間的通信,並在很大程度上降低了它們之間的耦合,從而使代碼更簡單,耦合更低,代碼質量提高。
* [greenrobot - EventBus](https://github.com/greenrobot/EventBus)
EventBus is a publish/subscribe event bus for Android and Java.
![](https://i.imgur.com/dxNyP67.png)
* [~~square - otto~~](https://github.com/square/otto)
Otto is an event bus designed to decouple different parts of your application while still allowing them to communicate efficiently.
已棄用,改用 RxJava and RxAndroid
* [alibaba - ARouter](https://github.com/alibaba/ARouter)
* [JessYanCoding - ProgressManager](https://github.com/JessYanCoding/ProgressManager)
![](https://i.imgur.com/oAiuuFm.png)
* [JessYanCoding - RetrofitUrlManager](https://github.com/JessYanCoding/RetrofitUrlManager)
讓 Retrofit 支持多個 baseUrl,並且可以在運行時更改 baseUrl。
* [JessYanCoding - LifecycleModel](https://github.com/JessYanCoding/LifecycleModel)
Android 架構組件中的ViewModel。
* [JakeWharton - timber](https://github.com/JakeWharton/timber)
* [umeng - analytics](https://www.umeng.com/analytics)
## Libariy
### network
com.squareup.retrofit2:retrofit
com.squareup.retrofit2:converter-gson
com.squareup.retrofit2:adapter-rxjava
com.squareup.retrofit2:adapter-rxjava2
com.squareup.okhttp3:okhttp
com.squareup.okhttp3:okhttp
com.squareup.okhttp:okhttp-urlconnection
com.github.bumptech.glide:glide
com.github.bumptech.glide:compiler
com.github.bumptech.glide:okhttp3-integration
com.squareup.picasso:picasso
### view
com.zhy:autolayout
com.jakewharton:butterknife
com.jakewharton:butterknife-compiler
com.contrarywind:Android-PickerView
com.github.chrisbanes.photoview:library
com.daimajia.numberprogressbar:library
com.nineoldandroids:library
com.github.markomilos:paginate
com.alibaba.android:vlayout
me.jessyan:autosize
### rx1
io.reactivex:rxandroid
io.reactivex:rxjava
com.trello:rxlifecycle
com.trello:rxlifecycle-components
com.github.VictorAlbertos.RxCache:runtime
com.github.VictorAlbertos.Jolyglot:gson
com.jakewharton.rxbinding:rxbinding-recyclerview-v7
com.tbruyelle.rxpermissions:rxpermissions
me.jessyan:rxerrorhandler
### rx2
io.reactivex.rxjava2:rxandroid
io.reactivex.rxjava2:rxjava
com.trello.rxlifecycle2:rxlifecycle
com.trello.rxlifecycle2:rxlifecycle-android
com.trello.rxlifecycle2:rxlifecycle-components
com.github.VictorAlbertos.RxCache:runtime
com.github.tbruyelle:rxpermissions
me.jessyan:rxerrorhandler
### tools
com.google.dagger:dagger
com.google.dagger:dagger-android
com.google.dagger:dagger-android-support
com.google.dagger:dagger-compiler
com.google.dagger:dagger-android-processor
org.simple:androideventbus
org.greenrobot:eventbus
com.squareup:otto
com.google.code.gson:gson
com.android.support:multidex
javax.annotation:jsr250-api
com.alibaba:arouter-api
com.alibaba:arouter-compiler
me.jessyan:progressmanager
me.jessyan:retrofit-url-manager
me.jessyan:lifecyclemodel
### test
junit:junit
androidx.test.runner.AndroidJUnitRunner
com.android.support.test:runner
com.android.support.test.espresso:espresso-core
com.android.support.test.espresso:espresso-contrib
com.android.support.test.espresso:espresso-intents
org.mockito:mockito-core
com.jakewharton.timber:timber
com.orhanobut:logger
com.squareup.leakcanary:leakcanary-android
com.squareup.leakcanary:leakcanary-android-no-op
com.umeng.analytics:analytics
###### tags: `Android Architecture` `MVP`