@cxm-2016
2017-07-07T18:45:54.000000Z
字数 17337
阅读 2451
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);
@Override
public 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;
}
@Override
protected void onBindViewHolder(@NonNull NoticeHolder holder, @Nullable final Model.NoticeResponse item) {
//TODO
}
@Override
public 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 OnRefreshEvent
public fun refresh() = postEvent(OnRefreshEvent());
}
step2:声明需要使用的属性
@Nullable
private String clubId;
@NonNull
private Model mModel = new ClubNoticeListModel();
@NonNull
private 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{
@Nullable
private People mPeople;
public int getAge(){
if(mPeople==null)
return 0;
else return mPeople.getAge();
}
}
3,线程安全的写法
class Test{
@Nullable
private People mPeople;
public int getAge(){
People people = mPeople;
return people==null? 0: people.getAge();
}
}
Kotlin的判空是最高级的保证线程安全的机制,所以必须在先创建本地变量再判断,防止其他线程在使用时将其置空。
对于Java,如果可以保证这些属性中不涉及多线程操作,采取第二种方式即可。
step3:实现业务逻辑
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppUtilKt.registerEventBus(this);
setTitle("群公告");
clubId = getIntent().getStringExtra(EXTRA_ID);
onRefresh(null);
}
@Override
protected void onRefresh(@Nullable Bundle bundle) {
super.onRefresh(bundle);
mPage = 1;
mCanLoadMore = true;
loadNotice(mPage++);
}
@Override
public void onFabClick() {
}
@Override
public void onPullToRefresh() {
}
@Override
public 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>>>() {
@Override
public 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("网络请求失败,点击刷新");
}
}
});
}
/**
* 公告点击事件
*/
@Override
public void onNoticeClick(@Nullable String noticeId) {
if (noticeId != null && clubId != null)
ClubNoticeDetailActivity.start(this, clubId, noticeId);
}
@Override
public void onUIPrepared(@NonNull SimpleListUI ui) {
showTextBtn("发起");
ui.setAdapter(mAdapter);
}
/**
* 发布按钮被点击
*/
@Override
public void onBtnClick(View view) {
if (clubId != null)
ClubNoticeEditActivity.start(this, clubId, null, null, null);
}
/**
* 外部通知刷新时回调
*/
@Subscribe
public void onRefreshEvent(OnRefreshEvent event) {
onRefresh(null);
}
@Override
protected void onDestroy() {
super.onDestroy();
AppUtilKt.unRegisterEventBus(this);
mAdapter.clear();
mModel.onDestroy();
}
函数说明:
setTitle("群公告") :设置标题栏标题内容
onEmptyError属性:不做任何操作的错误处理对象,另外还有一个onError属性,onError接收到错误后会将已经显示的Fragment隐藏并显示出错误信息。
protected void onRefresh(@Nullable Bundle bundle)方法用来跟错误页面进行沟通,错误页面会调用此函数通知唤醒Fragment,bundle中可以携带错误页面传来的信息,比如错误原因等等,根据信息,我们可以决定以什么样的方式重新加载这个页面。
@Override
public void onUIPrepared(@NonNull RepairSiteDetailContract.View ui) {
if (mResponse != null) {
showCollectionBtn();
showShareBtn();
//something
boolean isCollection = "1".equals(mResponse.getIs_collection());
setCollection(isCollection);
//something
}
}
showCollectionBtn
与showShareBtn
分别用来显示标题栏上的收藏与分享按钮。
重写BaseMvpActivity的public void onCollectionClick(View view)
方法。在其中实现点击收藏的事件。
@Override
public void onCollectionClick(View view) {
//do something
}
step1:重写分享回调方法,在其中显示分享页面
@Override
public void onShareClick(View view) {
ShareUI mShareUI = new ShareUI(this);
mShareUI.setOnSocialShareListener(new Function1<ShareType, Unit>() {
@Override
public Unit invoke(ShareType shareType) {
ToastsKt.toast(getApplication(), "缺少参数");
// mShareServiceBuilder.setShareType(shareType).setUrl("1").setContent("2").setTitle("3").build().post(); // TODO: 2017/5/24
return null;
}
});
mShareUI.setOnFriendsShareListener(new Function0<Unit>() {
@Override
public 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);
选择结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
FriendsListActivity.onResult(requestCode, resultCode, data, new Function1<String, Unit>() {
@Override
public Unit invoke(String imId) {
return null;
}
});
}
LocationUtilKt.getLocation(this, new Function1<Location, Unit>() {
@Override
public Unit invoke(Location location) {
// todo
return 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 {
@NonNull
private 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
@SaveInstanceState
private String hxId;
@Nullable
@SaveInstanceState
private 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 SpString
var userId by SpString
var nickname by SpString
var ImUserId by SpString
var 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方法中销毁
@Override
protected 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 = null
SpUtil.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:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
ImageGridActivity.onResult(requestCode, resultCode, data, new Function1<List<ImageItem>, Unit>() {
@Override
public 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() {
@Override
public void onConfirm() {
// 确定事件
}
@Override
public void onCancel() {
// 取消事件
}
});
kotlin:
dialog("确定要删除吗?") {
buttonPositive("确定") {
//todo
}
buttonNegative("取消") {
//todo
}
}
或者
dialog("确定要删除吗?") {
buttonPositive{
//todo
}
}
buttonPositive
必须有事件
DatePickerDialog.start(this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePickerDialog datePickerDialog, int year, int month, int day) {
}
});
如果需要设置初始值,可以传入yyyy-MM-dd
格式的字符串
DatePickerDialog.start(this, "2016-11-25",new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePickerDialog datePickerDialog, int year, int month, int day) {
}
});
Java
AppUtils.showActionSheet(this, mSheetItems, new ActionSheet.ActionSheetListener() {
@Override
public 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
中使用如下代码接收回调
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
BMapActivity.onResult(requestCode, resultCode, data, new Function3<String, Double, Double, Unit>() {
@Override
public Unit invoke(String address, Double latitude, Double longitude) {
return null;
}
});
}
在Activity中使用如下代码打开选择页面
SelectRideActivity.startCrop(this);
然后在onActivityResult
中处理回调:
@Override
protected 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>() {
@Override
public Unit invoke(String crop, RideViewContract.Model.TripDetailResponse tripDetailResponse) {
return null;
}
});
}