先扯两句
这次已经记不清距离上次发博客有多久了,总归是好久了吧。而这次要写的内容,之前也多次开始,不过又都随着自己后面的应用,重新作出了调整,如果收藏了我的demo的或许能够看到修改的过程,说实在的,现在的base封装与之前的实在是相差太多,多到我自己都快找不到之前的痕迹了。
虽然也不能肯定之后还会不会继续调整,不过也不能这么无休止的拖下去了,如果有需要的可以收藏一下我的demo,我的修改都会第一时间发到上面的,以后我也尽量在修改的同时一同发博客说明。
闲言少叙,老规矩还是先上我的Git库,然后开始正文吧。
MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)
正文
正文的开头,我先说说自己这次修改base的初衷,实际上目录中的2~7篇博客,加上阶段总结与修改1,已经能够基本实现了当前的功能,所以之前的博客我并没有删除掉,依然留给大家做个参考。
而既然基本能够完成这功能,我这里为什么要多费事还要重新封装底层呢?除了一些性能上的优化以外,更重要的原因了解我的人一定都知道,就因为一个字“懒”!至于这么个懒法,下面我就为大家说明一下。
抽象类
如果是看过我之前demo的朋友,再对比一下当前的demo,一定会发现现在的BaseActivity、BaseNetActivity、BaseFragment、BaseNetFragment以及新增加的BaseFragmentActivity都从原本的普通类变成了如今的抽象类。作为一个菜鸟,你如果让我说明一下,从性能上这么封装会不会有什么优势,我还真一点也说不出来,我就只从“懒”上来做解释了。
之前的普通类,当我们需要创建一个Activity或者Fragment去继承Base的时候,往往需要我们自己去记,需要把setContentView改成setBaseContentView(布局嵌套),然后还需要自己去创建一些initView(初始化视图)、initData(初始化数据)等方法,这些虽然说麻烦也不是很麻烦,但是对于一个懒人来说,实在是太繁琐了。而使用抽象类封装,就可以把这些方法变成抽象方法,在继承的时候,只需要我们实现即可。
BaseActivity
抽象方法如下:
/**
* 获取布局ID
*
* @return 获取的布局ID
*/
protected abstract int getLayoutId();
/**
* 获取所有View信息
*/
protected abstract void findViews();
/**
* 初始化布局信息
*/
protected abstract void formatViews();
/**
* 初始化数据信息
*/
protected abstract void formatData();
/**
* 初始化Bundle
*/
protected abstract void getBundle(Bundle bundle);
除此之外,这些抽象方法,需要在onCreate中进行调用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activities.add(this);
context = getApplicationContext();
activity = this;
event = this;
setContentView(R.layout.activity_base);
initSDK();
setBaseContentView(getLayoutId());
findViews();
getBundle(getIntent().getBundleExtra("bundle"));
formatViews();
formatData();
}
关于onCreate方法中的其他参数activities、context、activity、event,以及方法setBaseContentView的用处,如果不知道的,请参见我之前的博客《一个Android工程的从零开始》-5、base(四) BaseActivity——方法封装,里面会有详细的描述的。
getLayoutId()
顾名思义,获取布局Id,也就是原本的setBaseContentView()方法,而实际上,我们在onCreate()方法中也很容易发现,它就是setBaseContentView(getLayoutId());作为一个懒汉的我,在使用的时候,只需要将我们的布局Id作为返回值返回即可。
例如:
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
这样,我们就实现了布局的设置,同时也能使用之前封装好的Title。
findViews()
获取所有View信息,也就是刚刚前面说到的initView的一部分功能(至于为什么说是一部分功能,我们后面会说到),而在其中的操作其实也很简单,就是我们熟悉的不能在熟悉的findViewById。当然,这个findViewById我们也进行了进一步的封装,具体怎么封装的,我们这里先卖个关子,后面会为大家提到。
formatViews()
初始化布局信息,这就是initView的令一部分功能,比如设置图片啊,设置文字啊,设置布局适配器,设置一些监听接口之类的方法,我个人都将其划分到这个部分来完成了。
formatData()
初始化数据信息,这里我个人的定义是设置一些数据的初始化信息,例如定时器之类的初始化、数据库工具的初始化、或者图形验证码等工具类的初始化,不过该方法使用频率较低
getBundle(Bundle bundle)
初始化Bundle,这里之所以选择初始化bundle,最重要的一个原因是我在Activity跳转的部分做了一个封装,所以Activity之间传值就被约束成了通过bundle传递,而如果你是直接使用的Intent传值,那么初始化intent也可以,这个方法就不加以限制了。而通过上面的说明,想必大家也知道了,这个方法的主要用处就是Activity之间传递参数的一个取值方法,当然,为了防止不必要的麻烦,在实现getBundle()方法的时候,还是建议大家添加一步bundle的判空以及相对应的处理。
initSDK()
当然,细心的还会发现,在onCreate方法中,我调用的方法其实并不仅仅是前面说到的抽象方法,还有一个就是initSDK()方法,说来,这个方法的添加也是让我很无奈啊,其实原本是没有这个方法的,直到我使用Baidu地图的时候。
看过百度地图开发文档的想必都看到过这么一段代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//在使用SDK各组件之前初始化context信息,传入ApplicationContext
//注意该方法要再setContentView方法之前实现
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
//获取地图控件引用
mMapView = (MapView) findViewById(R.id.bmapView);
}
而排出掉可以移动到前面说的抽象方法中的方法后,其中一行就显得尤为突兀:SDKInitializer.initialize(getApplicationContext()); 它的功能在官方注释中也可以很看得出来,就是SDK的初始化。这个方法,我们如果放置在子Activity的onCreate方法中,那你很荣幸的就会在调用mMapView = (MapView) findViewById(R.id.bmapView); 看到万里江上一片红,全都是错误日志,翻译过来就是让我们去初始化百度SDK。至于放到onCreate的super前面,逻辑上确实是提前初始化了,不过依然会报错崩溃掉,至于日志是什么,有兴趣的话,大家可以自行尝试一下。
想必大家看到的我的顺序,在看到官方的注释就会发现,我并没有放到setContentView的前面,不过我的顺序在执行过程中,确实也没有报错,同时也能够正常使用,原因我也没有去深究,不过还是建议大家用官方建议的顺序去执行。
不过由于SDK的初始化,在使用频率上,比formatData()方法还要低,甚至一部分APP使用的都是类似于极光推送之类的SDK,只需要进行一次全局的初始化,而并不需要在使用它的Activity中进行初始化。所以这里选择了在BaseActivity方法中写了一个空方法,在需要的时候重写即可。
/**
* SDK初始化
*/
protected void initSDK() {
}
onCreate方法中的顺序
前面的属性部分,大家随着心情来就好了,是没有关系的,不过一定要放到这些抽象方法的调用前面,虽然setContentView()与getBundle(Bundle bundle)是不受这些参数影响的,但也是为了代码排版更好看容易理解一些。
下面,我这里setContentView(),不过前面斜体字已经说明了,还是建议大家先使用initSDK(),在使用setContentView()。
下一个,setBaseContentView(getLayoutId())或者getBundle(Bundle bundle),这两个方法则没有顺序前后的要求,只不过这两个方法都需要放在findViews()方法与formatViews()方法之前,因为在formatViews()赋值时,很可能使用的是getBundle(Bundle bundle)传递来的值,而逻辑上findViews()最好与formatViews()相邻。而最后,则是使用频率相对较低的formatData()。
其中可以看得出来,有一些方法的顺序并不是一成不变的,大家也是可以根据自己的需求进行增删改
其他
首先需要说明一点,大家可以看一下标题,这一篇博客所说的内容只是BaseActivity的上,所以这个其他并不是除了上述抽象方法的其他所有,而是与抽象方法相关的一些其他方法,总结起来有两类三个方法:
/**
* 简化获取View
*
* @param viewId View的ID
* @param <T> 将View转化为对应泛型,简化强转的步骤
* @return ID对应的View
*/
@SuppressWarnings("unchecked")
public <T extends View> T getView(int viewId) {
return (T) findViewById(viewId);
}
/**
* 简化获取View
*
* @param view 父view
* @param viewId View的ID
* @param <T> 将View转化为对应泛型,简化强转的步骤
* @return ID对应的View
*/
@SuppressWarnings("unchecked")
public <T extends View> T getView(View view, int viewId) {
return (T) view.findViewById(viewId);
}
/**
* 设置点击事件
*
* @param layouts 点击控件Id
*/
protected void setOnClickListener(int... layouts) {
for (int layout : layouts) {
getView(layout).setOnClickListener(this);
}
}
getView
还记得前面我说过吧,在这次BaseActivity的封装中,我对findViewById也进行了封装,这也就是两个getView方法。
这里我们使用到了一个神奇的内容“T”,在注释中也能看出来,这货叫泛型,当然别看我这里写的T,大家就当成“T”就是泛型,T只不过是一个代指,就好像如果我把“T”替换成“半寿翁”,那么我“半寿翁”就成了泛型(别说你还不知道在AS中可以使用中文变量,并嘲讽我使用中文)
/**
* 简化获取View
*
* @param viewId View的ID
* @param <半寿翁> 将View转化为对应泛型,简化强转的步骤
* @return ID对应的View
*/
@SuppressWarnings("unchecked")
public <半寿翁 extends View> 半寿翁 getView(int viewId) {
return (半寿翁) findViewById(viewId);
}
其使用方法就是,找到所有泛型类的一个共同父类,然后用泛型指代变量(“T”或者“半寿翁”,或者你喜欢的其他什么称呼),<T extends View>继承这个父类,这样,当我们用这个指代“T”表示这个父类的任何一个子类,都不需要进行强转,又或者说我们已经这里进行了强转:
(T) findViewById(viewId);
这里需要说明一下,当我们使用泛型的时候,我们可以使用TextView textView = (T)view,然后使用textView.setText()方法,而不能直接使用(T)view.setText(),因为泛型并不知道你究竟要将这个父类转换成具体的哪个子类,所以你无法直接使用子类所特有的方法。所以我们需要用特有的子类去进行接收,接收后就可以使用其特有的方法了。
所以使用getView方法,我们就能在formatViews()方法中获取其对应的控件了。至于为什么会有getView(View view, int viewId),毕竟不是所有的情况下都可以直接使用findViewById(),至少在BaseAdapter的getView方法中,我们就难免使用到convertView.findViewById(),所以这里我也添加了一条:
return (T) view.findViewById(viewId);
当然,getView(View view, int viewId)放在demo中的Const工具类中也是可以的,又或者在我们的view只需要获取一次的时候,我们也可以getView(View view, int viewId)方法进行进一步的封装:
/**
* 简化获取View
*
* @param layoutId 父布局Id
* @param viewId View的ID
* @param <T> 将View转化为对应泛型,简化强转的步骤
* @return ID对应的View
*/
@SuppressWarnings("unchecked")
public <T extends View> T getView(int layoutId, int viewId) {
return (T) LayoutInflater.from(context).inflate(layoutId, null).findViewById(viewId);
}
setOnClickListener
这个想必大家一眼就能看出来,单纯的就是一个点击事件的设置,没错,它的功能同样也是设置点击事件,只是与普通的View.setOnClickListener()不同的是,这部分我是拿来批量设置点击事件。
首先,需要我们的BaseActivity实现View.OnClickListener接口,不过onClick方法却不必须实现,谁让BaseActivity是抽象类呢,只需要与上面那些抽象方法一样,在继承BaseActivity的子Activity中实现就好了。
其次就是setOnClickListener的参数int... layouts,这个参数的意思就是传入一个类型为int数组,并且这个数组的数量是不确定,范围是“大于等于0”,也就是说我们可以不传值,也可以传很多值,只要类型都是“...”前的即可。既然是数组,只需要对传入的view id参数进行一个foreach循环设置点击事件即可:
@Override
protected void formatViews() {
setOnClickListener(R.id.button1, R.id.button2, R.id.button3, R.id.button4)
}
我一般是在formatViews()方法中使用setOnClickListener的。
注意事项
由于网络请求的部分我拆解出来放置在了BaseNetActivity中,所以当我们执行网络请求时,切记在onCreate之后执行,如果在上述抽象方法的实现中执行,由于BaseNetActivity的onCreate方法要在BaseActivity的onCreate方法之后执行,将会导致空指针的发生。
当然,这部分也可以在BaseNetActivity中添加一个网络访问的抽象方法,不过有很多Activity将会有重新获取焦点时刷新页面的需求,也就是网络请求需要在onResume()方法中调用,所以在BaseNetActivity中就没有设置对应的抽象方法,如果大家在使用过程中没有这个需求,不放BaseNetActivity中封装一个网络请求的方法,毕竟能偷懒何必那么勤快不是。