@cxm-2016
2017-07-07T10:45:54.000000Z
字数 17337
阅读 3364
0
版本:0.3
最后修改日期:2017/6/30
说明:如对架构有更改应及时更新文档
全部样例采用MVP设计模式
需求:根据俱乐部ID请求俱乐部公告列表并显示。列表每一项要能够点击跳转到详情。另外添加一个用来发布的按钮。
public interface ClubNoticeListContract {interface Presenter extends BasePresenter<SimpleListUI>, SimpleListPresenter{}interface Model extends BaseModel {}}
step1:分析接口数据创建模型类NoticeResponse
略(使用GsonFormat)
step2:根据请求方式创建Service接口
interface Service {@GET("app/new_group/get_group_announcement_list/{params}")Observable<HttpResult<List<NoticeResponse>>> getNoticeList(@Path("params") String params);}
step3:在Model接口提供访问方式
void getNoticeList(@NonNull String token,@NonNull String clubId,int page,@NonNull Action1<Throwable> onError,@NonNull Action1<HttpResult<List<NoticeResponse>>> onNext);
必须继承自BaseMvpModel,该基类提供了停止请求的方法。另外,Subscription对象必须使用register进行注册,防止内存泄漏。
public class ClubNoticeListModel extends BaseMvpModel implements ClubNoticeListContract.Model {private Service mService = RetrofitUtil.getInstance().create(Service.class);@Overridepublic void getNoticeList(@NonNull String token, @NonNull String clubId, int page, @NonNull Action1<Throwable> onError, @NonNull Action1<HttpResult<List<NoticeResponse>>> onNext) {HashMap<String, Object> params = new HashMap<>();params.put("token", token);params.put("user_group_id", clubId);params.put("page", page);Subscription subscribe = mService.getNoticeList(RetrofitUtil.buildParams(params)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(onNext, onError);register(subscribe);}}
(略)需要在运行时使用的控件ID命名采用与代码属性相同的命名方式,建议以m开头,如果需要说明功能就在中间加上功能名称,然后以控件类型结尾。如:
mContentEdit :内容输入框EditText
mRecyclerView:不需要说明功能的RecyclerView
mHeaderRecyclerView:在头部显示的RecyclerView
mNameText:显示名称的TextView
mAvatarImage:显示头像的ImageView
mDeleteBtn:点击删除功能的任意可点击View
根据需求,只需提供一个用来设置Item点击的方法,与一个加载更多的方法。
void onNoticeClick(@Nullable String id);
class NoticeHolder extends RecyclerView.ViewHolder {NoticeHolder(View itemView) {super(itemView);ButterKnife.bind(this, itemView);}}class NoticeAdapter extends CommonItemAdapter<Model.NoticeResponse, NoticeHolder> {private Presenter presenter;public NoticeAdapter(Presenter presenter) {this.presenter = presenter;}@Overrideprotected void onBindViewHolder(@NonNull NoticeHolder holder, @Nullable final Model.NoticeResponse item) {//TODO}@Overridepublic NoticeHolder onCreateViewHolder(ViewGroup parent, int viewType) {//TODO}}
public class ClubNoticeListActivity extends BaseMVPActivity implements Presenter
step1:提供静态的打开方式与Event通知
我们需要通过静态方法将打开自身的方式暴露出去。在外部使用Intent方式并携带参数的方式不不符合编程规范的,会造成代码之间的耦合增加。合格的编程方式是面向扩展开放,面向修改关闭。使用静态方法可以减少由于传参变化造成的大范围修改代码。
final private static String EXTRA_ID = "club_id";public static void start(@NonNull Activity activity, @NonNull String id) {activity.startActivity(new Intent(activity, ClubNoticeListActivity.class).putExtra(EXTRA_ID, id));}private static class OnRefreshEvent {}public static void refresh() {AppUtilKt.postEvent(new OnRefreshEvent());}
对于Kotlin:
componin object{public fun start(activity: Activity, id: String) {activity.startActivity<ClubNoticeListActivity>(EXTRA_ID to id);}private class OnRefreshEventpublic fun refresh() = postEvent(OnRefreshEvent());}
step2:声明需要使用的属性
@Nullableprivate String clubId;@NonNullprivate Model mModel = new ClubNoticeListModel();@NonNullprivate NoticeAdapter mAdapter = new NoticeAdapter(this);private int mPage = 1;private boolean mCanLoadMore = true;
如果使用的时Java代码编写,必须在应用类型的属性上声明@Nullable或者@NonNull来标记该属性的可空状态。对于标记了@NonNull的属性必须在声明时赋值或者在静态或构造代码块中赋初值,并且在代码的任何位置都不能将其赋值为null。对于标记了@Nullable的属性,不必在声明时赋初值,但是在任何使用使用场景都必须使用if进行判空。
举个例子:
1,不安全的写法:
class Test{private People mPeople;public int getAge(){return mPeople.getAge();}}
2,线程不安全的写法
class Test{@Nullableprivate People mPeople;public int getAge(){if(mPeople==null)return 0;else return mPeople.getAge();}}
3,线程安全的写法
class Test{@Nullableprivate People mPeople;public int getAge(){People people = mPeople;return people==null? 0: people.getAge();}}
Kotlin的判空是最高级的保证线程安全的机制,所以必须在先创建本地变量再判断,防止其他线程在使用时将其置空。
对于Java,如果可以保证这些属性中不涉及多线程操作,采取第二种方式即可。
step3:实现业务逻辑
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);AppUtilKt.registerEventBus(this);setTitle("群公告");clubId = getIntent().getStringExtra(EXTRA_ID);onRefresh(null);}@Overrideprotected void onRefresh(@Nullable Bundle bundle) {super.onRefresh(bundle);mPage = 1;mCanLoadMore = true;loadNotice(mPage++);}@Overridepublic void onFabClick() {}@Overridepublic void onPullToRefresh() {}@Overridepublic void onLoadMore() {if (mCanLoadMore) loadNotice(mPage++);}/*** 加载公告数据*/private void loadNotice(final int page) {String token = getMUserInfo().getToken();if (clubId != null && token != null)mModel.getNoticeList(token, clubId, page, onError, new Action1<HttpResult<List<Model.NoticeResponse>>>() {@Overridepublic void call(HttpResult<List<Model.NoticeResponse>> result) {if (result.isSuccess()) {List<Model.NoticeResponse> data = result.getData();if (data != null && !data.isEmpty()) {if (page == 1) mAdapter.setItems(data);else mAdapter.addItems(data);} else {if (page == 1) setErrorMessage("这里啥都没有,去别处看看吧");mCanLoadMore = false;}new SimpleListFragment<>().setContentView(ClubNoticeListActivity.this);} else {setErrorMessage("网络请求失败,点击刷新");}}});}/*** 公告点击事件*/@Overridepublic void onNoticeClick(@Nullable String noticeId) {if (noticeId != null && clubId != null)ClubNoticeDetailActivity.start(this, clubId, noticeId);}@Overridepublic void onUIPrepared(@NonNull SimpleListUI ui) {showTextBtn("发起");ui.setAdapter(mAdapter);}/*** 发布按钮被点击*/@Overridepublic void onBtnClick(View view) {if (clubId != null)ClubNoticeEditActivity.start(this, clubId, null, null, null);}/*** 外部通知刷新时回调*/@Subscribepublic void onRefreshEvent(OnRefreshEvent event) {onRefresh(null);}@Overrideprotected void onDestroy() {super.onDestroy();AppUtilKt.unRegisterEventBus(this);mAdapter.clear();mModel.onDestroy();}
函数说明:
setTitle("群公告") :设置标题栏标题内容
onEmptyError属性:不做任何操作的错误处理对象,另外还有一个onError属性,onError接收到错误后会将已经显示的Fragment隐藏并显示出错误信息。
protected void onRefresh(@Nullable Bundle bundle)方法用来跟错误页面进行沟通,错误页面会调用此函数通知唤醒Fragment,bundle中可以携带错误页面传来的信息,比如错误原因等等,根据信息,我们可以决定以什么样的方式重新加载这个页面。
@Overridepublic void onUIPrepared(@NonNull RepairSiteDetailContract.View ui) {if (mResponse != null) {showCollectionBtn();showShareBtn();//somethingboolean isCollection = "1".equals(mResponse.getIs_collection());setCollection(isCollection);//something}}
showCollectionBtn与showShareBtn分别用来显示标题栏上的收藏与分享按钮。
重写BaseMvpActivity的public void onCollectionClick(View view)方法。在其中实现点击收藏的事件。
@Overridepublic void onCollectionClick(View view) {//do something}
step1:重写分享回调方法,在其中显示分享页面
@Overridepublic void onShareClick(View view) {ShareUI mShareUI = new ShareUI(this);mShareUI.setOnSocialShareListener(new Function1<ShareType, Unit>() {@Overridepublic Unit invoke(ShareType shareType) {ToastsKt.toast(getApplication(), "缺少参数");// mShareServiceBuilder.setShareType(shareType).setUrl("1").setContent("2").setTitle("3").build().post(); // TODO: 2017/5/24return null;}});mShareUI.setOnFriendsShareListener(new Function0<Unit>() {@Overridepublic Unit invoke() {if (AppUtils.autoLogin(RepairSiteDetailActivity.this))FriendsListActivity.start(RepairSiteDetailActivity.this);return null;}});mShareUI.show(view);}
ShareUI是显示选择分享渠道的页面,其中的点击事件有两个回调。分别是
/*** 设置社会化分享的回调接口*/var onSocialShareListener: ((ShareType) -> Unit)? = null
和应用内好友分享
/*** 好友分享接口*/var onFriendsShareListener: (() -> Unit)? = null
stop2:在社会化分享的回调中使用ShareService对象进行发送消息。
ShareService.with(Activity).setShareType(ShareType).setUrl(String).setContent(String).setTitle(String).build().post();
step3:在应用内分享时打开好友列表,并且在onActivityResult中接收选择对象
FriendsListActivity.start(Activity);
选择结果
@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);FriendsListActivity.onResult(requestCode, resultCode, data, new Function1<String, Unit>() {@Overridepublic Unit invoke(String imId) {return null;}});}
LocationUtilKt.getLocation(this, new Function1<Location, Unit>() {@Overridepublic Unit invoke(Location location) {// todoreturn null;}});
step1:创建一个String资源数据
private static final int[] sheetItems = {R.string.baidu_map, R.string.gaode_map, R.string.google_map};
step2:创建并显示对话框
AppUtils.showActionSheet(Activity, sheetItems, ActionSheet.ActionSheetListener)
Java:
//打开百度地图AppUtilKt.startBaiBuMap(Activity, mLocation.getLatitude(), mLocation.getLongitude(), mTargetLatitude, mTargetLongitude);//打开高德地图AppUtilKt.startGaoDeMap(Activity, mTargetLatitude, mTargetLongitude);//打开谷歌地图AppUtilKt.startGoogleMap(Activity, mTargetLatitude, mTargetLongitude);
Kotlin:在Activity中
startBaiBuMap(mLatitude, mLongitude, mTargetLatitude, mTargetLongitude);startGaoDeMap(mTargetLatitude, mTargetLongitude);startGoogleMap(mTargetLatitude, mTargetLongitude);
业务逻辑:MVP
异步任务流:Rxjava
网络请求框架:Retrofit2 + OkHttp3
图片处理框架:Glide
数据库框架:GreenDao
通信框架:EventBus
编程语言:Java + Kotlin,业务逻辑中的Presenter层慎用Kotlin
业务逻辑部分包名规范如下:
com.muqi.qmotor.[模块名]
在其下再划分以下包:
adapter:用于存放适配器
contract:MVP模式的契约类
model:用于整个模块的Model类
presenter:MVP模式的Presenter类(Activity)
ui:MVP模式的View类(Fragment)
该工程项目中的Activity(无论规模大小)都必须使用MVP设计模式。
工程的Activity需要继承自BaseMVPActivity并实现一个Presenter接口。
1,启动页面
启动一个Activity的方法需要该Activity提供一个静态方法,并封装需要的参数。
示例:
private static final String EXTRA_CLUB_ID = "club_id";public static void start(@NonNull Activity activity, @NonNull String clubId) {activity.startActivity(new Intent(activity, ClubMemSortActivity.class).putExtra(EXTRA_CLUB_ID, clubId));}
2,页面通信
不同页面之间的通信使用EventBus,同第一条,接受消息的页面需要提供一个静态方法,并由接受消息的类自行处理数据并通过EventBus发送。
示例:
private static class RefreshDeleteEvent {@NonNullprivate String id;RefreshDeleteEvent(@NonNull String id) {this.id = id;}}public static void refreshDelete(@NonNull String id) {AppUtilKt.postEvent(new RefreshDeleteEvent(id));}
注意:RefreshDeleteEvent被设置为私有。
3,保存数据
在异常状态下(如屏幕旋转、内存不足等),我们需要在onSaveInstanceState方法中保存状态,并在onCreate方法中取出。这里提供一个注解:
@SaveInstanceState,仅需要将该注解声明在需要被保存的变量上,比如:
@Nullable@SaveInstanceStateprivate String hxId;@Nullable@SaveInstanceStateprivate String clubId;
需要注意的是,我们使用了该注解后,在初始化时就需要注意不要重复取值,比如需要通过getIntent获取值的时候,可以这样写:
Bundle extras = getIntent().getExtras();hxId = extras.getString(EXTRA_HX_ID, hxId);clubId = extras.getString(EXTRA_CLUB_ID, clubId);
4,onRefresh函数
在可能出现异常的页面,应当重写onRefresh函数,该函数是产生错误后点击刷新的回调函数,在这里我们可以进行重新初始化布局等操作。其参数可以带有错误信息,通过判断其中的信息,可以决定如何从错误中恢复。
Java:
必须在任何可以使用@Nullable和@NonNull的地方使用它。并且不能忽略任何可能为空的情况。目前使用上述注解的常见位置有:函数参数、全局变量、函数返回值。
Kotlin
禁止使用!!操作符,所有可空类型必须判断。
创建一个Kotlin文件。以存储用户信息为例:
class SpUserInfo {var token by SpStringvar userId by SpStringvar nickname by SpStringvar ImUserId by SpStringvar username by SpString}
SpString表示该属性与Sp文件中的String类型相关联,提供默认值null。
| 委托类型 | 影射类型 | Nullable | 默认值 |
|---|---|---|---|
| SpInt | int | false | 0 |
| SpDefaultInt(default) | int | false | default |
| SpBoolean | boolean | false | false |
| SpDefaultBoolean(default) | boolean | false | default |
| SpFloat | float | false | 0f |
| SpDefaultFloat(default) | float | false | default |
| SpLong | long | false | 0l |
| SpDefaultLong(default) | long | false | default |
| SpString | String | true | null |
| SpDefaultString(default) | String | false | default |
| SpStringSet | Set<String> |
true | null |
| SpDefaultStringSet(default) | Set<String> |
false | default |
| SpSerializable | Serializable | true | null |
| SpDefaultSerializable(default) | Serializable | false | default |
| SpAsyncSerializable* | Serializable | true | null |
说明:
SpAsyncSerializable异步类型的映射委托,其类型必须为可空,建议对于非基本数据类型或String和Set以外的对象使用此委托,能够提高系统运行效率。
使用方法:
Java:
SpSettings spSettings = SpUtil.INSTANCE.register(new SpSettings());
在OnDestroy方法中销毁
@Overrideprotected void onDestroy() {super.onDestroy();SpUtil.INSTANCE.unRegister(spSettings);}
Kotlin:
val mUserInfo = SpUtil.register(this, SpUserInfo())
在OnDestroy方法中销毁
override fun onDestroy() {super.onDestroy()if (mLoadingView != null) mLoadingView = nullSpUtil.unRegister(mUserInfo)}
1,数据临时保存功能(限Kotlin)
private var id by saveString { intent.getStringExtra(EXTRA_BOARD_ID) }
通过上述写法,id这个值将在第一次被使用时调用函数{ intent.getStringExtra(EXTRA_BOARD_ID) }获取值,如果该Activity被意外销毁,此值将会被保存在onSaveInstanceState方法的Bundle对象,并在Activity恢复时取出。
2,显示正在加载效果
showLoading(); //显示hideLoading(); //隐藏
3,获取用户信息
对于Kotlin
mUserInfo.token //获取Token
对于Java
getMUserInfo().getToken(); //获取Token
注册网络请求
注册Subscription对象以便统一关闭请求,以免造成内存泄漏
Java:
Subscription subscribe = mService.getDynamic(RetrofitUtil.buildParams(params)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(onNext, onError);register(subscribe);
Kotlin:
mService.getList(RetrofitUtil.buildParams(params)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(onNext, onError).register()
关闭全部请求
可以在Activity的onDestroy中关闭全部请求
public void onDestroy(){super.onDestroy();mModel.onDestroy();}
生成Multipart请求体
Java:
RequestBody requestToken = createRequestBody(token);MultipartBody.Part requestImgPart = createRequestBody(imageFile, "uploadfile");
Kotlin:
val requestToken = token.createRequestBody();val requestImgPart = imageFile.createRequestBody("uploadfile");
java:
private Service mService = RetrofitUtil.getInstance().create(Service.class);
kotlin:
private val mService by ServiceCreate(Service::class.java)
本应用使用Glide作为图片加载框架,图片加载类为ImageModel,该控件具有设置圆形、圆角矩形、高斯模糊等效果的功能。
使用方式如下:
1,普通图片加载方式
Java:
ImageModel.inflate(context, pic).defaultDrawable(R.drawable.default_cover).into(imageView);
2,设置圆形图片
Java:
ImageModel.inflate(context, pic).asCircleBitmap().defaultDrawable(R.drawable.default_cover).into(imageView);
3,混合设置图片效果
ImageModel.inflate(context, pic).asBlurBitmap() //设置高斯模糊.asRoundCornersBitmap(10) //设置圆角矩形.defaultDrawable(R.drawable.default_cover).into(imageView);
注意:给图片设置多种效果时,效果的绘制顺序与调用顺序相同,在上面的例子中,图片被设置完高斯模糊的效果之后才会被裁剪为圆角矩形。
在Activity任意位置调用如下代码打开相册,该方法打开仅能选择一张图片:
ImageGridActivity.start(this)
通常需要与GrideView配合选择多图,可以调用如下方法:
ImageGridActivity.start(this,9)
获取选择结果的方式是在Activity的onActivityResult中添加如下代码:
Java:
@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);ImageGridActivity.onResult(requestCode, resultCode, data, new Function1<List<ImageItem>, Unit>() {@Overridepublic Unit invoke(List<ImageItem> imageItems) {return null;}});}}
Kotlin:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)ImageGridActivity.onResult(requestCode, resultCode, data, this::onImageResult)}private fun onImageResult(imageItems: List<ImageItem> ){}
查看单张图片
ScanPictureActivity.start(activity, url);
多图查看
public void onImageClick(ArrayList<String> picList, int index) {ScanPictureActivity.start(this,picList,index);}
通用
private fun onImageClick(image: MultipartImageView.Image) {if (image is SubjectDetailContract.Model.DetailResponse.PicsBean) {ScanPictureActivity.start(this, mMultiImages, image) { (it as SubjectDetailContract.Model.DetailResponse.PicsBean).pic_original }}}
如果Pic继承自PicBean
public void onPicClick(@NonNull MultipartImageView.Image image) {if (mDynamicResponse != null)ScanPictureActivity.start(this, mDynamicResponse.getPics(), image);}
Java:
UIUtils.showDialog(Activity, "确定删除吗?", new IDialogClickListenr() {@Overridepublic void onConfirm() {// 确定事件}@Overridepublic void onCancel() {// 取消事件}});
kotlin:
dialog("确定要删除吗?") {buttonPositive("确定") {//todo}buttonNegative("取消") {//todo}}
或者
dialog("确定要删除吗?") {buttonPositive{//todo}}
buttonPositive必须有事件
DatePickerDialog.start(this, new DatePickerDialog.OnDateSetListener() {@Overridepublic void onDateSet(DatePickerDialog datePickerDialog, int year, int month, int day) {}});
如果需要设置初始值,可以传入yyyy-MM-dd格式的字符串
DatePickerDialog.start(this, "2016-11-25",new DatePickerDialog.OnDateSetListener() {@Overridepublic void onDateSet(DatePickerDialog datePickerDialog, int year, int month, int day) {}});
Java
AppUtils.showActionSheet(this, mSheetItems, new ActionSheet.ActionSheetListener() {@Overridepublic void onSheetClick(ActionSheet actionSheet, int stringId) {switch (stringId) {case R.string.share: //分享break;case R.string.edit_activity: //编辑活动break;case R.string.cancel_activity: // 取消活动break;}}});
Kotlin:
showActionSheet(intArrayOf(R.string.share,R.string.edit_activity,R.string.cancel_activity)){ id->when(id){R.string.share->{} //分享R.string.edit_activity->{} //编辑活动R.string.cancel_activity->{} //取消活动}}
通过LocationObservable.from(Context)可以创建一个定位观察对象。
LocationObservable.from(this).subscribe { location ->// 处理回调}
通过以上方式可以立即获得一次定位回调。
如果我们需要连续的获取位置,可以采用如下方式:
val subscribe = LocationObservable.from(this).interval(5) // 这里设置每5秒获得一次回调信息.subscribe { location ->// 处理回调}// 需要在合适的位置取消订阅subscribe.unSubscribe()
如果我们需要固定次数的定位回调,可以采用如下方式:
LocationObservable.from(this).interval(5) // 这里设置每5秒获得一次回调信息.take(3) // 获取3次位置信息,结束之后会自动取消订阅.subscribe { location ->// 处理回调}
在Activity的任意位置调用
BMapActivity.start(this);
然后在onActivityResult中使用如下代码接收回调
@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);BMapActivity.onResult(requestCode, resultCode, data, new Function3<String, Double, Double, Unit>() {@Overridepublic Unit invoke(String address, Double latitude, Double longitude) {return null;}});}
在Activity中使用如下代码打开选择页面
SelectRideActivity.startCrop(this);
然后在onActivityResult中处理回调:
@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);SelectRideActivity.onResult(requestCode, resultCode, data, new Function2<String, RideViewContract.Model.TripDetailResponse, Unit>() {@Overridepublic Unit invoke(String crop, RideViewContract.Model.TripDetailResponse tripDetailResponse) {return null;}});}