Android MVP BaseFragment 通用式封装的实现
这篇已经是我们的 BaseMVP 基础框架系列文章的第六篇了,BaseMVP 已经被我们封装了快差不多了,从上篇的文章(Android MVP 架构(五)MVP 多个 Presenter 依赖注入)中,我们解决了多的 Presenter 的问题,这是利用依赖注入及反射的方式,动态的去实例化不同 Presenter 层实现类,并且与同一个 View 做了绑定和解绑的操作,这就是一个 View 对多 Presenter 的需要手动 new 的解决方案。
BaseMVP 基础框架的搭建,到这里的话,还缺少一点东西。我们之前只封装过了一个基类的 BaseActivity 类,这个类是提供给 Activity 来继承的,但是,我们的实际项目中,难免会有 Fragment 的出现,于是乎,今天我们又带大家来封装一个 BaseFragment 基类吧,尽量的把基础框架给完善。
要封装 BaseFragment 基类,参考 BaseActivity 的封装并不难,因为 Activity 和 Fragment 的生命周期很相似,而且 Fragment 是寄托在 Activity 上的,所以说白了都差不多,不过代码肯定有一点偏差。对比之前的版本,这一次我在包中添加了一个 BaseFragment 基类,以及添加了几个测试它的类。
下面我们来看看 BaseFragment 基类吧,直接上代码:
新建 BaseFragment 基类:
package com.test.mvp.mvpdemo.mvp.v6.basemvp; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.test.mvp.mvpdemo.mvp.v6.inject.InjectPresenter; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; public abstract class BaseFragment extends Fragment implements IBaseView { private List<BasePresenter> mInjectPresenters; private View mLayoutView; protected abstract @LayoutRes int setLayout(); protected abstract void initViews(@Nullable Bundle savedInstanceState); protected abstract void initData(); @SuppressWarnings("ConstantConditions") protected <T extends View> T $(@IdRes int viewId) { return this.getView().findViewById(viewId); } @SuppressWarnings({"unchecked", "TryWithIdenticalCatches"}) @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(setLayout(), container, false); mInjectPresenters = new ArrayList<>(); //获得已经申明的变量,包括私有的 Field[] fields = this.getClass().getDeclaredFields(); for (Field field : fields) { //获取变量上面的注解类型 InjectPresenter injectPresenter = field.getAnnotation(InjectPresenter.class); if (injectPresenter != null) { try { Class<? extends BasePresenter> type = (Class<? extends BasePresenter>) field.getType(); BasePresenter mInjectPresenter = type.newInstance(); //绑定 mInjectPresenter.attach(this); field.setAccessible(true); field.set(this, mInjectPresenter); mInjectPresenters.add(mInjectPresenter); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (java.lang.InstantiationException e) { e.printStackTrace(); } catch (ClassCastException e) { e.printStackTrace(); throw new RuntimeException("SubClass must extends Class:BasePresenter"); } } } return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initViews(savedInstanceState); initData(); } @Override public void onDestroy() { super.onDestroy(); for (BasePresenter presenter : mInjectPresenters) { presenter.detach(); } mInjectPresenters.clear(); mInjectPresenters = null; } }
由于上篇文章中,我们使用了依赖注入,所以这里的 BaseFragment 类的泛型参数就给我们去掉了。还有 BaseActivity 在这一版本中,我也去除了这个泛型参数,如图:
去除之后:
这里的 BaseActivity 就显得干净简洁了一点,不然每次都需要传入一个参数,我觉得想想都累。好了,我们的 BaseFragment 与 BaseActivity 几乎都一样吧,这里也就不做多的解释了,可以去看前面的几篇文章中有对代码的讲解。
写完了一个 BaseFragment 基类后,然后就是迫不及待的去测试一些,到底能不能工作。这里,我新建了一个 SecondActivity 类,目的就是为了在新的 Activity 中存放一个 Fragment 用于测试。SecondActivity 没有什么难度的代码,就是在里面存放这一个 SecondFragment,对了这里的 SecondActivity 并不是继承我们的 BaseActivity 类,这就是一个普通的 Activity ,要特别注意。代码很简单,如下:
新建 SecondActivity 类:
public class SecondActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); /** * 开启一个 fragment */ getSupportFragmentManager().beginTransaction().replace(R.id.second_container, new SecondFragment()).commit(); } }
SecondActivity 的布局:是一个 FrameLayout 用于存放 SecondFragment。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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" tools:context=".mvc.MainActivity"> <FrameLayout android:id="@+id/second_container" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.constraint.ConstraintLayout>
接下来,才是我们的 BaseFragment 类的正真使用。我们新建一个 SecondFragment 实现类,继承与 BaseFragment 类,这里的 SecondFragment 就是 MVP 的 View 层了,与我们的 Activity 一样,同属于 View 层。这里,我偷懒,把 MainActivity 类的基本代码都考过来了。这里就不要太在意什么业务逻辑了,我们只要能测试 MVP 中的 BaseFragment 能够工作就好了。来看代码:
View 层:新建 SecondFragment 实现类:
package com.test.mvp.mvpdemo.mvp.v6.view; import android.os.Bundle; import android.support.annotation.Nullable; import android.widget.TextView; import android.widget.Toast; import com.test.mvp.mvpdemo.R; import com.test.mvp.mvpdemo.mvp.v6.SecondContract; import com.test.mvp.mvpdemo.mvp.v6.basemvp.BaseFragment; import com.test.mvp.mvpdemo.mvp.v6.inject.InjectPresenter; import com.test.mvp.mvpdemo.mvp.v6.presenter.SecondPresenter; public class SecondFragment extends BaseFragment implements SecondContract.ISecondView { private TextView tvFragment; @InjectPresenter private SecondPresenter mPresenter; @Override protected int setLayout() { return R.layout.fragment_second; } @Override protected void initViews(@Nullable Bundle savedInstanceState) { tvFragment = $(R.id.tv_fragment); } @Override protected void initData() { mPresenter.handlerData(); } @Override public void showDialog() { // Toast.makeText(getContext(), "this is Fragment", Toast.LENGTH_SHORT).show(); } @SuppressWarnings("ConstantConditions") @Override public void succes(String content) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getContext(), "" + content, Toast.LENGTH_SHORT).show(); tvFragment.setText(content); } }); } }
与之对应的就是 SecondPresenter 了,我们的 Presenter 层代码如下,代码与前面几篇文章一样,这里不做介绍了,代码如下所示:
###Presenter 层:新建 SecondPresenter 实现类: package com.test.mvp.mvpdemo.mvp.v6.presenter; import com.test.mvp.mvpdemo.mvp.v6.SecondContract; import com.test.mvp.mvpdemo.mvp.v6.basemvp.BasePresenter; import com.test.mvp.mvpdemo.mvp.v6.model.SecondModel; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Response; public class SecondPresenter extends BasePresenter<SecondContract.ISecondView, SecondModel> implements SecondContract.ISecondPresenter { @Override public void handlerData() { getView().showDialog(); getModel().requestBaidu(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String content = response.body().string(); getView().succes(content); } }); } }
接下来剩余的就是我们的 Model 层了,我们与之对应的是 SecondModel 类,还是请求网络数据,因为我们之前请求的是百度首页的网页文本,为了形成区别,我这里将 URL 改成了我的 博客 地址,哈哈。代码如下:
Model 层:新建 SecondModel 实现类:
package com.test.mvp.mvpdemo.mvp.v6.model; import com.test.mvp.mvpdemo.mvp.v6.SecondContract; import com.test.mvp.mvpdemo.mvp.v6.basemvp.BaseModel; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; public class SecondModel extends BaseModel implements SecondContract.ISecondModel { @Override public void requestBaidu(Callback callback) { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://blog.csdn.net/smile_running") .build(); client.newCall(request).enqueue(callback); } }
最后,还有一个它们的契约类,其中都是接口类型。代码如下:
新建 SecondContract 接口类:
package com.test.mvp.mvpdemo.mvp.v6; import com.test.mvp.mvpdemo.mvp.v6.basemvp.IBasePresenter; import com.test.mvp.mvpdemo.mvp.v6.basemvp.IBaseView; import okhttp3.Callback; public interface SecondContract { interface ISecondModel { void requestBaidu(Callback callback); } interface ISecondView extends IBaseView { void showDialog(); void succes(String content); } interface ISecondPresenter extends IBasePresenter { void handlerData(); } }
分包情况就是文章篇头的那张包图,好了,把代码写完了,就跑起来试试吧。
这里的运行情况是,从 MainActivity 中点击 textview 跳转到 SecondActivity,由于在 SecondActivity 显示的是我们的 SecondFragment ,所以会从网络上获取我的博客的地址文本,返回将数据设置到 SecondFragment 的 textview 上,运行效果就是这样,如下图:
好吧,效果虽然简单了点,但我们的 BaseFragment 算是封装完成了,经过测试,也是能够派上用场的了。经过我们的不懈努力,又把 BaseMVP 基础框架的搭建工作推进了一小步,在 BaseFragment 的封装过程中,我写的代码确实出现了一些小失误,这个是我们,原因是,我没有去拷贝代码!哈哈哈哈,好气啊,花了我好大把时间去改这个错误。
记录错误原因:在子线程中更新 UI 操作。
错误代码如下:在 SecondFragment 中更新 UI
@Override public void succes(String content) { Toast.makeText(getContext(), "" + content, Toast.LENGTH_SHORT).show(); tvFragment.setText(content); }
这个不是很简单嘛,这都不会改!
这可不一样,它报的错误信息可并不是子线程修改主线程异常,而是这么一堆错误日志:
07-09 23:51:21.887 9769-9788/com.test.mvp.mvpdemo E/EGL_adreno: tid 9788: eglSurfaceAttrib(1319): error 0x3009 (EGL_BAD_MATCH) 07-09 23:51:21.915 9769-9788/com.test.mvp.mvpdemo E/EGL_adreno: tid 9788: eglSurfaceAttrib(1319): error 0x3009 (EGL_BAD_MATCH) 07-09 23:51:23.362 9769-9788/com.test.mvp.mvpdemo E/EGL_adreno: tid 9788: eglSurfaceAttrib(1319): error 0x3009 (EGL_BAD_MATCH) 07-09 23:51:27.742 9769-9788/com.test.mvp.mvpdemo E/EGL_adreno: tid 9788: eglSurfaceAttrib(1319): error 0x3009 (EGL_BAD_MATCH) 07-09 23:51:28.069 9769-9798/com.test.mvp.mvpdemo E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher Process: com.test.mvp.mvpdemo, PID: 9769 java.lang.reflect.UndeclaredThrowableException at $Proxy2.succes(Unknown Source) at com.test.mvp.mvpdemo.mvp.v6.presenter.SecondPresenter$1.onResponse(SecondPresenter.java:25) at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) at java.lang.Thread.run(Thread.java:818) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at java.lang.reflect.Method.invoke(Method.java:372) at com.test.mvp.mvpdemo.mvp.v6.basemvp.BasePresenter$1.invoke(BasePresenter.java:31) at java.lang.reflect.Proxy.invoke(Proxy.java:397) at $Proxy2.succes(Unknown Source) at com.test.mvp.mvpdemo.mvp.v6.presenter.SecondPresenter$1.onResponse(SecondPresenter.java:25) at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) at java.lang.Thread.run(Thread.java:818) Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at android.os.Handler.<init>(Handler.java:200) at android.os.Handler.<init>(Handler.java:114) at android.widget.Toast$TN.<init>(Toast.java:359) at android.widget.Toast.<init>(Toast.java:100) at android.widget.Toast.makeText(Toast.java:273) at com.test.mvp.mvpdemo.mvp.v6.view.SecondFragment.succes(SecondFragment.java:44) at java.lang.reflect.Method.invoke(Native Method) at java.lang.reflect.Method.invoke(Method.java:372) at com.test.mvp.mvpdemo.mvp.v6.basemvp.BasePresenter$1.invoke(BasePresenter.java:31) at java.lang.reflect.Proxy.invoke(Proxy.java:397) at $Proxy2.succes(Unknown Source) at com.test.mvp.mvpdemo.mvp.v6.presenter.SecondPresenter$1.onResponse(SecondPresenter.java:25) at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) at java.lang.Thread.run(Thread.java:818) 07-09 23:51:28.126 9769-9788/com.test.mvp.mvpdemo E/EGL_adreno: tid 9788: eglSurfaceAttrib(1319): error 0x3009 (EGL_BAD_MATCH)
首先,我看了标记中的第一个和第二个错误原因,原来是反射那块有问题,根据它代码中提示的位置,说我的 Presenter 中的 getView() 方法出错了,如:
点击去看了下,是动态代理的代码,这里搞什么鬼,我又没修改这里的代码,怎么就错了呢?
一脸懵逼的我,回头看了看,在这里尝试了断点调试,没有什么结果。后来意外发现,我的把上面图中的 getView().succes(content) 注释掉了就不报错了。这才找到了原因,原来是这里的数据是通过网络请求传过来的,我们的 okhttp 需要转到 ui 线程中去更新,这个我是知道的。
所以要记得,切到主线程去更新 UI 操作。虽然发生了一点小失误,刚开始以为是动态代理的问题,所以查了好多关于动态代理的知识,借此还能学到一点额外的知识,美滋滋,哈哈。
这里的 BaseFragment 还是有代码重复的问题,比如我们的依赖注入那块代码,就放到下篇文章中去解决这个问题吧,这篇文章已经完成我们该完成的任务了,明天的事情放到后天干吧,哈哈哈哈。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。