Android开发库- VenusAndroid
项目地址
https://gitee.com/premeditate/VenusAndroid
在下文笔记中笔者觉得有些数据时根据后台数据结构来的,忽然出现在客户端有些突兀,所以在这里使用之前先说明一下,与后台相关的数据,后台是笔者使用Java Spring Boot开发的,使用了统一的数据结构往客户端返回,这个数据结构是笔者自己定义的,所以客户端就用这样的数据结构接收,如果你访问的是别人的后台接口,别人返回的数据结构可能与笔者不同,那么就需要按照别人返回的数据结构定义接收对象。这里主要记录此Android开发库内容的使用,关于后台项目在这里就不深入讲解,有时间关于后台项目开发的再单独写。 在这里贴一下后台的项目地址: VenusJava
后台项目地址
https://gitee.com/premeditate/VenusJava
使用方式
- 在项目级build.gradle文件中allprojects节点下的repositories节点下添加:maven { url 'https://jitpack.io' }
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
...
}
- 在App级build.gradle中dependencies节点下添加:api 'com.gitee.premeditate:VenusAndroid:V1.0.19',笔者使用的是Android studio3.5.2。项目构建的版本号[V1.0.19]是笔者写笔记时最新的,后面有时间可能会更新,如果你能用到的话,可以先查看版本。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
api 'com.gitee.premeditate:VenusAndroid:V1.0.19'
...
}
- 如果是离线环境使用,可以现在互联网环境中通过项目地址将项目下载到本地,再拷贝到离线环境中使用,项目中常用的都是jar包,下载项目可以带jar包一起下载。butterknife和calendarview等采用的是在线依赖,如果离线使用的话,请自行找到对应的引用文件,如果不使用的话可以将本项目对应的内容删除掉,或者要使用什么内容直接参考那里的代码。
Retrofit网络访问
对象介绍
项目中依赖了[retrofit-2.3.0,okio-1.7.0,,okhttp-3.3.1,converter-gson-2.3.0]用来做网络访问。
定义了HttpRetrofit类用来快速构建Retrofit对象,使用静态函数HttpRetrofit.getInstance(String baseUrl)拿到Retrofit对象。
定义了RetrofitCallback<T>对象用来做Retrofit.enqueue()函数的回调,其中success(Call<Result<T>> call, T data)是访问成功回调,errorNet(Call<Result<T>> call, Throwable throwable)是网络错误回调,errorServer(Call<Result<T>> call, Response<Result<T>> response)是服务器错误回调,errorData(Call<Result<T>> call, Result body)是数据错误回调,back()是网络请求结束的回调,不管结果。
其中errorServer(Call<Result<T>> call, Response<Result<T>> response)是根据后台返回数据的来的,后台是自己使用Spring Boot开发的,后天开发的数据结构和异常都是自己定义的,所以在这里可以直接使用,如果接入其他后台,可能就不会有这个错误码,不会执行这个回调,即使有这个错误码,那含义也不一样,所以使用过程中可以根据业务进行修改。
定义了统一的返回结果Result<T>类,这个也是因为后台是自己开发的所以可以这样写,后台接口使用了统一返回数据类型,都使用Result<T>类接收,具体数据内容使用泛型。
定义了分页对象Page<T>类,这个Page<T>类是根据Spring Boot中mybatis-plus中的分页结构写的,内容一模一样,因为后台返回那样的数据结构,所以客户端也要这样写而已,作为使用者在分页接口中使用这个对象接收分页数据就行,具体数据内容使用泛型。
使用方式
- 定义Retrofit的Api接口:这里是定义用户登录接口,传入一个SysUser 对象,返回的也是SysUser 对象,上级上返回的是Result对象,不过这是一个统一的返回体,指定泛型是SysUser,所以可以理解是返回的SysUser对象。笔者使用的接口都是这种类型,可以看到所有的接口返回的都是Result对象。
public interface UserApi {
@POST("sysUser/login-pad")
Call<Result<SysUser>> loginPad(@Body SysUser user);
}
- 定义一个Api类,这个类中用来获取上一步中定义的接口对象,然后通过这个接口对象进行网络请求。因为业务会变得更多,所以像上一步中的接口对象也会变得更多(不同业务定义自己的接口对象),所以这个类相当于用来集中管理这些接口对象的。在这里创建接口对象的同事,使用HttpRetrofit类构建了Retrofit对象,所以后面在做网络请求时都是通过这个类对象获取对应业务的接口对象调用对应的业务函数。这里的setNull()函数是将baseUrl 和Retrofit对象置空的,因为Retrofit使用了单例模式,创建后就不会改变了,场景是如果修改了服务器地址,就会出现没有反应的现象,是因为Retrofit对象已经根据之前的地址创建了,修改地址并不会再创建新的对象。因此一般在修改服务器地址等操作后将baseUrl 和Retrofit置空,这样后面再使用时就会重新创建。
public class Api {
public static UserApi getUserApi() {
return HttpRetrofit.getInstance(getBaseUrl()).create(UserApi.class);
}
private static String baseUrl = null;
private static String getBaseUrl() {
if (baseUrl == null) {
synchronized (Api.class) {
if (baseUrl == null) {
baseUrl = "http://"
+ SharedPreferencesUtil.getString(LibraryApplication.getContext(), Constant.SP_SERVER_IP, Constant.SP_SERVER_IP_DEFAULT)
+ ":"
+ SharedPreferencesUtil.getInt(LibraryApplication.getContext(), Constant.SP_SERVER_PORT, Constant.SP_SERVER_PORT_DEFAULT)
+ "/mp/";
}
}
}
return baseUrl;
}
public static void setNull() {
baseUrl = null;
HttpRetrofit.setNull();
}
}
- 在业务模块中调用接口,在登录界面中点击登录按钮后拿到账号和密码,调用登录函数进行登录。使用上文中的Api类,获取到登录接口对象getUserApi(),调用登录函数loginPad(),传入该函数SysUser ,得到Retrofit的Call对象,执行enqueu()函数,这个enqueu()函数本来需要Callback对象参数,Callback有2个回调函数,还需要在回调中处理多种接口返回结果,因此使用上文中RetrofitCallback类,RetrofitCallback继承Callback,在RetrofitCallback对Callback的回调函数做了一些通用的结果处理 ,调用者尽可能的只面向业务。
@Override
public void login(String username, String password) {
SysUser body = new SysUser();
body.setUsername(username);
body.setPassword(password);
DialogManager.showTipLoadingDialog(this, "登录中...");
Api.getUserApi().loginPad(body).enqueue(new RetrofitCallback<SysUser>() {
@Override
public void success(Call<Result<SysUser>> call, SysUser data) {
ToastUtil.show(LoginActivity.this, "登录成功");
MainActivity.join(LoginActivity.this);
finish();
}
@Override
public void errorNet(Call<Result<SysUser>> call, Throwable throwable) {
super.errorNet(call, throwable);
}
@Override
public void errorServer(Call<Result<SysUser>> call, Response<Result<SysUser>> response) {
super.errorServer(call, response);
}
@Override
public void errorData(Call<Result<SysUser>> call, Result body) {
super.errorData(call, body);
}
@Override
public void back() {
//super.back();
DialogManager.closeTipLoadingDialog();
}
});
}
StatusControllerLayout状态控制布局
状态控制布局是结合网络请求数据,然后加载列表时自定义的一个控制布局,因为这个布局中会有好几种状态,这几种状态根据不同的结果或者操作是要进行切换的。比如需要在界面中加载一些数据然后做显示这个过程就会产生如下几种:
- 加载中
- 显示数据
- 空数据
- 数据错误
- 网络异常
- 其他异常
这些不同的状态如果每次都根据操作进行切换的话,会比较容易出错。因此笔者写了这个状态控制布局。虽然也是通过切换不同的控件,但是将过程进行了统一处理,在业务编码时只需要做简单的函数调用即可。
- 在界面布局中引用StatusControllerLayout布局
<org.venus.library.view.StatusControllerLayout
android:id="@+id/controller"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 在界面中通过id找到布局中的StatusControllerLayout对象:private StatusControllerLayout mController;
- 根据界面操作切换控制布局显示的状态:界面开始请求网络数据时切换状态为STATUS_LOADING,请求网络错误时切换状态为STATUS_ERROR_NET,请求服务器错误时切换状态为STATUS_ERROR_SERVER,请求数据错误时切换状态为STATUS_ERROR_DATA,请求成功时就显示数据,当数据空时切换状态为STATUS_EMPTY_DATA,切换不同状态布局中就会加载对应的控件进行对应的提示,在切换状态时可以传入提示文本替换默认的提示。当需要显示数据时,就需要添加一个自定义布局了,因为状态控制布局并不知道使用者需要显示什么数据,所以调用StatusControllerLayout的addCustomView()函数,传入一个控件,控制器中就显示这个控件。在下文的代码中传入的是一个列表。
private void getSubscribedSystem() {
Map<String, Object> map = new HashMap<>();
map.put("page", page);
map.put("limit", limit);
mController.changeStatus(StatusControllerLayout.STATUS_LOADING);
Api.getSubSystemApi().getSubscribedSystem(map).enqueue(new RetrofitCallback<List<SubSystem>>() {
@Override
public void success(Call<Result<List<SubSystem>>> call, List<SubSystem> data) {
showSubscribedSystem(data);
}
@Override
public void errorNet(Call<Result<List<SubSystem>>> call, Throwable throwable) {
//super.errorNet(call, throwable);
mController.changeStatus(StatusControllerLayout.STATUS_ERROR_NET);
}
@Override
public void errorServer(Call<Result<List<SubSystem>>> call, Response<Result<List<SubSystem>>> response) {
//super.errorServer(call, response);
mController.changeStatus(StatusControllerLayout.STATUS_ERROR_SERVER, response.code());
}
@Override
public void errorData(Call<Result<List<SubSystem>>> call, Result body) {
//super.errorData(call, body);
mController.changeStatus(StatusControllerLayout.STATUS_ERROR_DATA, body.msg);
}
@Override
public void back() {
//super.back();
}
});
}
private void showSubscribedSystem(List<SubSystem> records) {
if (records == null || records.size() < 1) {
mController.changeStatus(StatusControllerLayout.STATUS_EMPTY_DATA, "没有已订阅的系统");
return;
}
mAdapterSubscribed.setData(records);
mListSubscribed.setAdapter(mAdapterSubscribed);
mController.addCustomView(mListSubscribed);
}
LoadMoreLayout下拉刷新上拉加载更多布局
在界面中显示列表时,需要使用分页时,移动端一般不会像web一样做分页控件,而是使用滑动来对数据分页,所以在列表中需要对下拉和上拉时间进行处理。下拉就刷新整个列表,上拉就在当前数据基础上再加一页数据。
在LoadMoreLayout布局中有一个SwipeRefreshLayout,SwipeRefreshLayout是Google官方提供的下拉刷新控件,笔者这里的下拉刷新就是直接使用SwipeRefreshLayout的下拉事件,主要实现的是上拉刷新。有一个RecyclerView用来显示数据和响应上拉事件。有一个上拉加载中的布局,在响应用户上拉加载更多时会显示,上拉加载完成后隐藏。这里将代码都贴出来,本来想描述一下实现的,代码一贴出来看着就太烦了,想想算了,哈哈。直接用就行。
package org.venus.library.view.loadmore;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.venus.library.R;
import org.venus.library.utils.TVUtil;
/**
* Author Jxx _让世界看到我
* On 2021/9/27
* Note recyclerView 下拉刷新/上拉加载
*/
public class LoadMoreLayout extends RelativeLayout {
private SwipeRefreshLayout mSwipeRefreshLayout;
private RecyclerView mRecyclerView;
private RelativeLayout mCLoadShow;
private TextView mTvLoadMore;
private RelativeLayout mCNothing;
private TextView mTvNothing;
private boolean loadingMore = false;
private LinearLayoutManager mLinearLayoutManager;
public LoadMoreLayout(Context context) {
//super(context);
this(context, null);
}
public LoadMoreLayout(Context context, AttributeSet attrs) {
//super(context, attrs);
this(context, attrs, 0);
}
public LoadMoreLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.load_more_layout, this);
initView();
initEvent();
}
private void initEvent() {
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
onRefreshStart();
}
});
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
if (loadingMore == true) {
return;
}
int itemCount = mLinearLayoutManager.getItemCount();
int firstPosition = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition();
int lastPosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
if (firstPosition == 0) {
return;
}
if (lastPosition == itemCount - 1) {
onLoadMoreStart();
}
break;
case RecyclerView.SCROLL_STATE_DRAGGING:
break;
case RecyclerView.SCROLL_STATE_SETTLING:
break;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
}
private void initView() {
mSwipeRefreshLayout = findViewById(R.id.swipe);
mRecyclerView = findViewById(R.id.list);
mCLoadShow = findViewById(R.id.c_load_show);
mTvLoadMore = findViewById(R.id.tv_load_more);
mCNothing = findViewById(R.id.c_nothing);
mTvNothing = findViewById(R.id.tv_nothing);
loadingMore = false;
mCLoadShow.setVisibility(GONE);
mCNothing.setVisibility(GONE);
mLinearLayoutManager = new LinearLayoutManager(getContext());
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mSwipeRefreshLayout.setColorSchemeColors(getResources().getColor(R.color.app_color_theme_9), getResources().getColor(R.color.app_color_theme_3), getResources().getColor(R.color.app_color_theme_6));
}
public void setRecyclerViewAdapter(RecyclerView.Adapter adapter) {
mRecyclerView.setAdapter(adapter);
}
public void onRefreshStart() {
if (mOnEventListener != null) {
mOnEventListener.onRefresh();
}
}
public void onRefreshStop() {
mSwipeRefreshLayout.setRefreshing(false);
}
public void onLoadMoreStart() {
loadingMore = true;
mCLoadShow.setVisibility(VISIBLE);
if (mOnEventListener != null) {
mOnEventListener.onLoadMore();
}
scrollToLast();
}
public void onLoadMoreStop() {
loadingMore = false;
mCLoadShow.setVisibility(GONE);
}
public void setLoadText(String text) {
TVUtil.setText(mTvLoadMore, text);
}
private void setNothingText(String text) {
TVUtil.setText(mTvLoadMore, text);
}
public void scrollToLast() {
mLinearLayoutManager.scrollToPosition(mLinearLayoutManager.getItemCount() - 1);
}
public RecyclerView getRecyclerView(){
return mRecyclerView;
}
public LinearLayoutManager getLinearLayoutManager(){
return mLinearLayoutManager;
}
private void showNothing(boolean show) {
mCNothing.setVisibility(show ? VISIBLE : GONE);
}
public interface OnEventListener {
void onRefresh();
void onLoadMore();
}
private OnEventListener mOnEventListener;
public void setOnEventListener(OnEventListener onEventListener) {
mOnEventListener = onEventListener;
}
}
- 在界面布局中引用LoadMoreLayout布局
<org.venus.library.view.loadmore.LoadMoreLayout
android:id="@+id/c"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
- 找到LoadMoreLayout对象:private LoadMoreLayout mCLoadMoreLayout;,mCLoadMoreLayout设置监听。
mCLoadMoreLayout.setOnEventListener(mOnEventListener);
LoadMoreLayout.OnEventListener mOnEventListener = new LoadMoreLayout.OnEventListener() {
@Override
public void onRefresh() {
onRefreshStart();
}
@Override
public void onLoadMore() {
onLoadMoreStart();
}
};
- 在下拉刷新和上拉加载更多的回调中执行对应的业务操作,笔者在实例项目中写了一些演示界面,这里的操作就是贴的演示界面的代码,没有使用开发项目中的代码,这种没有业务在里面的代码读起来更容易一些。onRefresh()开始下拉刷新时,界面会出现刷新进度提示,在执行完成后需要执行onRefreshStop()函数,用来隐藏这个提示。onLoadMoreStart()开始上拉加载更多时,界面下方会出现加载提示,在执行完成后需要执行onLoadMoreStop()函数,用来隐藏加载提示。
private void onRefreshStart() {
new Thread(new Runnable() {
@Override
public void run() {
try {
List<LoadMore> list = MockLoadMore.getRefreshList(20);
Thread.sleep(2000);
//更新数据
onRefreshSuccess(list);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void onRefreshSuccess(List<LoadMore> list) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLoadMoreListAdapter.setData(list);
mCLoadMoreLayout.onRefreshStop();
}
});
}
private void onLoadMoreStart() {
new Thread(new Runnable() {
@Override
public void run() {
try {
List<LoadMore> list = MockLoadMore.getLoadMoreList(20, mLoadMoreListAdapter.getItemCount());
Thread.sleep(2000);
//更新数据
onLoadMoreSuccess(list);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void onLoadMoreSuccess(List<LoadMore> list) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLoadMoreListAdapter.addData(list);
mCLoadMoreLayout.onLoadMoreStop();
}
});
}
网友评论