为什么使用MVP,
MVP框架相对于MVC框架来说相对复杂一些,代码量相对也要更大一些。但是MVP框架使得model层和view层之间分割开来,使用presenter作为两者之间交互的桥梁。耦合度更低更方便后期维护。
最主要的原因是:使用MVP框架即使是项目开发一般让另一个人进行开发也可以很好的入手,而不会出现MVC那种Activity分工不明确,一个类中调用几百个方法作者自己都难以搞清的情况。
下面这个是契约类,model层负责持有数据,presenter层持有数据,view层对数据进行展示。
public interface IHomeContract {
interface IModel extends IBaseModel{
void getFlowdata(String url,ModelCallback modelCallback);
void getProductdata(String url,ModelCallback modelCallback);
interface ModelCallback{
void success(Object data);
void error(Throwable throwable);
}
}
interface IView extends IBaseView {
void success(Object data);
void error(Throwable throwable);
}
interface IPresenter {
void getFlowdata(String url);
void getProductdata(String url);
}
}
下边开始基类的抽取,首先抽取Presenter层的基类。我们在 Presenter 层直接绑定了 View 才可以拿到 View 层的引用,它们之间是强引用的关系,如果不进行解绑的话,那就会造成内存泄漏的情况发生。为了防止内存泄漏,我们在最后要解绑view。
public abstract class BasePresenter<M extends IBaseModel,V extends IBaseView> {
public M model;
public WeakReference<V> weakReference;
public BasePresenter(){
model = initModel();
}
/**
* 绑定vieww
* @param v
*/
public void attach(V v){
weakReference = new WeakReference<>(v);
}
protected abstract M initModel();
/**
* 解绑view,解决内存泄漏问题
*/
public void dettach(){
if (weakReference!=null){
weakReference.clear();
weakReference = null;
}
}
public V getView(){
return weakReference.get();
}
}
接下来对Activity进行基类抽取,封装泛型同时解决内存泄漏问题。
public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity implements IBaseView{
public P presenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(layoutId());
presenter = initPresenter();
if (presenter!=null){
presenter.attach(this);//绑定view
}
initView();
initData();
}
//让子类创建
protected abstract P initPresenter();
protected abstract void initData();
protected abstract void initView();
protected abstract int layoutId();
@Override
protected void onDestroy() {
super.onDestroy();
if (presenter!=null){
presenter.dettach();//解决内存泄漏
}
}
}
同上,抽取ragment对泛型进行封装,同时解决内存泄漏问题
public abstract class BaseFragment<P extends BasePresenter> extends Fragment implements IBaseView{
public P presenter;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(LayoutId(),container,false);
presenter = initPresenter();
if (presenter!=null){
presenter.attach(this);//绑定view
}
initView(view);
return view;
}
protected abstract P initPresenter();
protected abstract void initView(View view);
protected abstract int LayoutId();
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initData();
}
protected abstract void initData();
@Override
public void onDestroy() {
super.onDestroy();
if (presenter!=null){
presenter.dettach();//解决内存泄漏
}
}
}
下面对model层进行封装,基类的作用就是对公共的代码进行封装,减少代码的重复性,增强代码的维护性,但是model层没有公共的方法,所以model层为空。
public interface IBaseModel {
/*没用公共的方法*/
}
同上,view层只封装一个显示和一个隐藏即可。
public interface IBaseView {
void showLoading();
void hideLoading();
}
下面对工具类进行封装,为了解决多线程同时也是为了安全,这里使用了双重检验锁的单例模式,同时建议对属性进行私有化设置,这样可以有效防止外界new出新的对象。这里同时写了get请求和post请求,可以根据情况需要进行选择。
public class VolleyUtils {
private RequestQueue requestQueue;//volley请求队列
//双重检验锁的单例模式
//属性私有
private static VolleyUtils instance;
//构造方法私有,防止外界(调用者)new 出新的对象
private VolleyUtils() {
requestQueue = Volley.newRequestQueue(App.getContext());
}
/**
* 暴露公共方法,创建私有对象,供外部调用,双重:两次判断,检验锁:同步锁
*
* @return
*/
public static VolleyUtils getInstance() {
//第一重
if (instance == null) {
//加锁:为了解决多线程并发安全
synchronized (VolleyUtils.class) {
//二重
if (instance == null) {
instance = new VolleyUtils();
}
}
}
return instance;
}
/**
* volley的get请求
*/
public void doGet( String url, final VolleyCallback volleyCallback) {
//第二步
StringRequest stringRequest = new StringRequest(StringRequest.Method.GET, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
if (volleyCallback != null) {
volleyCallback.success(response);
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (volleyCallback != null) {
volleyCallback.failure(error);
}
}
});
//第三步
requestQueue.add(stringRequest);
}
/**
* volley的post请求
*/
public void doPost(final Map<String, String> params, String url, final VolleyCallback volleyCallback) {
//第二步
StringRequest stringRequest = new StringRequest(StringRequest.Method.POST, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
if (volleyCallback != null) {
volleyCallback.success(response);
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (volleyCallback != null) {
volleyCallback.failure(error);
}
}
}) {
@Override
protected Map<String, String> getParams() throws AuthFailureError {
return params;
}
};
//第三步
questQueue.add(stringRequest);
}
public interface VolleyCallback {
void success(String response);
void failure(Throwable error);
}
}
在完成基类的封装和工具类以后我们要完成MVP框架的搭建。首先是model层的代码
model层主要就是请求数据并把数据传给p层,最后由P层传给view层进行展示。
public class HomeModel implements IHomeContract.IModel {
@Override
public void getFlowdata(String url, final ModelCallback modelCallback) {
VolleyUtils.getInstance().doGet(url, new VolleyUtils.VolleyCallback() {
@Override
public void success(String response) {
FlowEntity flowEntity = new Gson().fromJson(response,FlowEntity.class);
modelCallback.success(flowEntity);
}
@Override
public void failure(Throwable error) {
modelCallback.error(error);
}
});
}
@Override
public void getProductdata(String url, final ModelCallback modelCallback) {
VolleyUtils.getInstance().doGet(url, new VolleyUtils.VolleyCallback() {
@Override
public void success(String response) {
System.out.println("resonse:"+response);
ProductEntity productEntity = new Gson().fromJson(response,ProductEntity.class);
modelCallback.success(productEntity);
}
@Override
public void failure(Throwable error) {
System.out.println("resonseerror:"+error.getMessage());
modelCallback.error(error);
}
});
}
}
这个是presenter层的代码,presenter主要是model层和view层之间通讯的桥梁。
public class HomePresenter extends BasePresenter<HomeModel, IHomeContract.IView> implements IHomeContract.IPresenter {
@Override
protected HomeModel initModel() {
return new HomeModel();
}
@Override
public void getFlowdata(String url) {
model.getFlowdata(url, new IHomeContract.IModel.ModelCallback() {
@Override
public void success(Object data) {
getView().success(data);
}
@Override
public void error(Throwable throwable) {
getView().error(throwable);
}
});
}
@Override
public void getProductdata(String url) {
model.getProductdata(url, new IHomeContract.IModel.ModelCallback() {
@Override
public void success(Object data) {
getView().success(data);
}
@Override
public void error(Throwable throwable) {
getView().error(throwable);
}
});
}
}
接下来的是首页面的布局,由fragment和viewpager联合组成,确保用户无论是点击下方的按钮还是单纯的滑动,都可以对页面进行切换。
《MainActivity---------首页的布局文件》
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.activity.MainActivity">
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<RadioGroup
android:id="@+id/radiogroup"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:padding="10dp"
android:text="首页"
android:gravity="center"
android:id="@+id/radio_home"
android:layout_width="0dp"
android:layout_weight="1"
android:button="@null"
android:layout_height="wrap_content"/>
<RadioButton
android:padding="10dp"
android:gravity="center"
android:text="列表"
android:button="@null"
android:id="@+id/radio_news"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<RadioButton
android:padding="10dp"
android:button="@null"
android:gravity="center"
android:text="我的"
android:id="@+id/radio_my"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
</RadioGroup>
</LinearLayout>
接下来是首页面的Activity代码部分,主要是viewpager和fragment进行展示,所有的fragment对象都添加进同一个队列中,方便进行切换。点击按钮切换通过RadioGroup和RadioButton的点击监听以及多分支选完成。
public class MainActivity extends BaseActivity {
private ViewPager viewPager;
private RadioGroup radioGroup;
private List<Fragment> fragmentList ;
@Override
protected BasePresenter initPresenter() {
return null;
}
@Override
protected void initData() {
fragmentList = new ArrayList<>();
fragmentList.add(new HomeFragment());
fragmentList.add(new BjFragment());
fragmentList.add(new MyFragment());
viewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@NonNull
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
});
}
@Override
protected void initView() {
viewPager = findViewById(R.id.viewpager);
radioGroup = findViewById(R.id.radiogroup);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
RadioButton radioButton = (RadioButton) radioGroup.getChildAt(position);
radioButton.setChecked(true);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId){
case R.id.radio_home:
viewPager.setCurrentItem(0);
break;
case R.id.radio_news:
viewPager.setCurrentItem(1);
break;
case R.id.radio_my:
viewPager.setCurrentItem(2);
break;
}
}
});
}
@Override
protected int layoutId() {
return R.layout.activity_main;
}
@Override
public void showLoading() {
}
@Override
public void hideLoading() {
}
}
为了是页面开起来整体性更高,页面更加平滑。这里推荐对展示的数据使用流式布局
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
int count = getChildCount();//得到子控件的数量
if (count>0){
for (int i = 0; i < count; i++) {
//循环每一个子控件
View view = getChildAt(i);
//子view添加进去的,所以要让系统测量一下每个子控件的大小
view.measure(0,0);
int childWidth = view.getMeasuredWidth();//view的宽
int childHeight = view.getMeasuredHeight();//view的高
//累加right
right =left+childWidth;
//屏幕宽度,px像素单位
int widthPixels = getResources().getDisplayMetrics().widthPixels;
//如果每一行的right大于屏幕宽度,开始折行判断
if (right>widthPixels){
left = 0;//折行后第一个控件距离左边距离为0
right = left+childWidth;
top = bottom+30;
}
bottom = top+childHeight;
//就是对view进行摆放
view.layout(left,top,right,bottom);
left+=childWidth+30;
}
}
}
/**
* 加入数据
*/
public void add(List<String> tags){
if (tags!=null&&tags.size()>0){
for (String tag : tags) {
//动态创建textview
final TextView textView = new TextView(getContext());
textView.setText(tag);//设置文本
//添加子控件到当前流式布局中
addView(textView);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
flowCallback.flowClick(textView.getText().toString());
}
});
}
}
}
/**
* 添加单个view到流式布局
*/
public void addTextView(String name){
final TextView textView = new TextView(getContext());
textView.setText(name);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), textView.getText().toString(), Toast.LENGTH_SHORT).show();
flowCallback.flowClick(textView.getText().toString());
}
});
//加入到流式布局中
addView(textView);
}
private FlowCallback flowCallback;
public void setFlowCallback(FlowCallback flowCallback) {
this.flowCallback = flowCallback;
}
public interface FlowCallback{
void flowClick(String name);
}
}
下面是流式布局的的布局文件
《HomeFragment——layout布局文件》
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:orientation="horizontal">
<EditText
android:id="@+id/et_keyword"
android:hint="请输入搜索关键词"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_search"
android:text="搜索"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<com.laoxu.yuekao.view.widgets.FlowLayout
android:id="@+id/flowlayout"
android:layout_width="match_parent"
android:layout_height="150dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
电商功能要求根据用户输入的关键词进行索引相关内容,为了方便用户使用记录用户的搜索记录,添加用户每一次搜索的关键词,在用户第二次点击时可以直接进行跳转,这里使用了网格布局。先写适配器,用户方法的回调使用接口回调进行。
列表中图片的展示使用Glide加载,完成错位图、占位图、原型的加载。
先是一个设定一个布局,设定列表展示的样子。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv"
android:layout_width="80dp"
android:layout_height="80dp"/>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
接下来是适配器部分。
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.MyViewHolder> {
private Context context;
private List<ProductEntity.Product> list;
public ProductAdapter(Context context, List<ProductEntity.Product> list) {
this.context = context;
this.list = list;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = View.inflate(context,R.layout.product_item_layout,null);
MyViewHolder myViewHolder = new MyViewHolder(view);
return myViewHolder;
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
holder.nameTv.setText(list.get(position).commodityName);
Glide.with(context).load(list.get(position).masterPic)
.placeholder(R.mipmap.ic_launcher)
.error(R.mipmap.ic_launcher)
.circleCrop()
.into(holder.iconIv);
//点击事件,通过下面声明的接口,发送商品标题
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
rvItemClick.onClick(list.get(position).commodityName);
}
});
}
@Override
public int getItemCount() {
return list.size();
}
class MyViewHolder extends RecyclerView.ViewHolder{
private ImageView iconIv;
private TextView nameTv;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
iconIv = itemView.findViewById(R.id.iv);
nameTv = itemView.findViewById(R.id.name);
}
}
//声明接口
private RvItemClick rvItemClick;
public void setRvItemClick(RvItemClick rvItemClick) {
this.rvItemClick = rvItemClick;
}
public interface RvItemClick{
void onClick(String name);
}
}
下面是本次项目的主体部分,同时也是整个项目的核心,是我们之前的布局要展示的内容。同时也是我们项目最核心的代码。在用户点击搜索按钮一个获取关键字,根据关键词来进行搜索,若是关键字为空则对用户进行输入提示。根据关键词进行点击搜索。点击这一部分使用接口回调完成。
点击商品详情后Toast提示,同时跳转到第二个页面。
public class HomeFragment extends BaseFragment<HomePresenter> implements IHomeContract.IView {
private FlowLayout flowLayout;
private EditText editText;
private Button btn;
private RecyclerView rv;
@Override
protected HomePresenter initPresenter() {
return new HomePresenter();
}
@Override
protected void initView(View view) {
flowLayout =view.findViewById(R.id.flowlayout);
rv =view.findViewById(R.id.rv);
rv.setLayoutManager(new GridLayoutManager(getActivity(),2));
editText =view.findViewById(R.id.et_keyword);
btn =view.findViewById(R.id.btn_search);
//点击流式布局的tag,请求搜索接口
flowLayout.setFlowCallback(new FlowLayout.FlowCallback() {
@Override
public void flowClick(String name) {
String url = "http://172.17.8.100/small/commodity/v1/findCommodityByKeyword?keyword="+URLEncoder.encode(name)+"&count=10&page=1";
presenter.getProductdata(url);
}
});
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(editText.getText().toString())){
Toast.makeText(getActivity(), "不能为空", Toast.LENGTH_SHORT).show();
return;
}
flowLayout.addTextView(editText.getText().toString());
String url = "http://172.17.8.100/small/commodity/v1/findCommodityByKeyword?keyword="+URLEncoder.encode(editText.getText().toString())+"&count=10&page=1";
presenter.getProductdata(url);
}
});
}
@Override
protected int LayoutId() {
return R.layout.fragment_home_layout;
}
@Override
protected void initData() {
String url = "http://blog.zhaoliang5156.cn/baweiapi/"+ URLEncoder.encode("手机");
presenter.getFlowdata(url);
}
@Override
public void showLoading() {
}
@Override
public void hideLoading() {
}
/**
* 成功
* @param data
*/
@Override
public void success(Object data) {
if (data instanceof FlowEntity){//instance
//
FlowEntity flowEntity = (FlowEntity) data;
flowLayout.add(flowEntity.tags);
}else if (data instanceof ProductEntity){
ProductEntity productEntity = (ProductEntity) data;
//适配器,recyclerview
ProductAdapter productAdapter = new ProductAdapter(getActivity(),productEntity.result);
rv.setAdapter(productAdapter);
//接收接口传过来的数据
productAdapter.setRvItemClick(new ProductAdapter.RvItemClick() {
@Override
public void onClick(String name) {
Toast.makeText(getActivity(), name, Toast.LENGTH_SHORT).show();
startActivity(new Intent(getActivity(), SecondActivity.class));
}
});
}
}
/**
* 失败
* @param throwable
*/
@Override
public void error(Throwable throwable) {
}
}
下边是点击以后跳转的第二个activity的布局情况
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn"
android:text="去调用js空参方法"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
下边是代码主要实现的功能:
主要功能是应用内加载本地图片,Android和JavaScript进行交互,点击以后进传值并吐司。
public class SecondActivity extends BaseActivity {
private WebView webView;
private Button button;
@Override
protected BasePresenter initPresenter() {
return null;
}
@Override
protected void initData() {
}
@Override
protected void initView() {
webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("file:///android_asset/h.html");
//把android的user的对象映射到webview里面
User user = new User();
webView.addJavascriptInterface(user,"ccc");
button = findViewById(R.id.btn);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String s = "我来自于adnroid的世界";
webView.loadUrl("javascript:jsFunction2('"+s+"')");
}
});
}
@Override
protected int layoutId() {
return R.layout.activity_second;
}
@Override
public void showLoading() {
}
@Override
public void hideLoading() {
}
}
这个是assets文件下要加载的本地网页的代码
<html>
<head>
<meta content="charset=utf-8" />
<script type="text/javascript">
function jsFunction2(hello){
document.getElementsByClassName("name").innerHTML = hello;
alert(hello);
}
</script>
</head>
<body>
<p>我是一个段落</p>
<p class="name">我是从android过来的</p>
<button onclick="window.ccc.aaa()" >无参调用android</button>
<button onclick="window.ccc.bbb('电脑')">有参调用android</button>
</body>
</html>
下面是android和js交互的部分
public class User {
/**
* 注解
*/
@JavascriptInterface
public void aaa(){
Log.e("aaa","js无参数传递过来的方法");
Toast.makeText(App.getContext(), "js无参数传递过来的方法", Toast.LENGTH_SHORT).show();
}
@JavascriptInterface
public void bbb(String name){
Log.e("aaa",name);
Toast.makeText(App.getContext(), name, Toast.LENGTH_SHORT).show();
}
}
最后是一个缩减版的全局异常捕获类,因完整版代码太长,这里进行了一定删减。
仅仅需要实现一个接口即可。
public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e("bug====",e.getMessage());
}
}
最后把异常捕获和上下文传递在一起。最后不要忘了在清单文件中进行添加。
最后再写全局异常捕获是为了防止在编译的时候报错被捕获,导致我们不知道自己的代码出现了问题。所在建议全局异常捕获类在我们的所有功能都实现以后再进行添加。
public class App extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
// MyCrashHandler myCrashHandler = new MyCrashHandler();
// Thread.setDefaultUncaughtExceptionHandler(myCrashHandler);
}
public static Context getContext() {
return context;
}
}