昨天发了一篇GitHub版本控制的集成后,今天终于回归正事,继续我们的《一个Android工程的从零开始》,真心有点小开心呢。 今天也是base的BaseActivity完结掉了,昨天我也查了一下其他人的BaseActivity封装,发现却比我的篇幅少了不少,不过既然要从基础说起,自然废话也就多了一些,请大家见谅。 既然昨天已经发了GitHub的版本控制,那么这篇开始就发GitHub的链接了,码云的就暂停更新维护了,另外onstraintLayout的也相差不多,只是布局的部分不同,但是由于很少有用代码部分完成的,这里就不予以展示了。
[MyBaseApplication] (https://github.com/BanShouWeng/MyBaseApplication)
本次的内容总共有如下四点:
常用常量、变量;界面跳转;网络监听;提示框。这个顺序很显然是本着我个人一贯偷懒成瘾的套路排列的,不过这不重要,我们还是开始今天的内容吧。
这个部分的内容放在最简单的位置上,其实主要还是由于我所能写的东西有限,但实际上就那四点而言,这个部分是最复杂的,因为不同的工程项目,对这部分的需求往往是更不相同的,我们也无法一概而论的给出一个具体的实现方式,所以就再此做一个简要的分析。 这部分如果做简单分类呢,就是两个点:其一是用户相关;其二是编程相关
用户相关 用户相关的东西,在这里主要是一些常用的信息,例如用户id、用户联系方式、用户头像等等。这部分的做法有两种,其一,这些信息分别存储,用的时候直接调用;其二创建一个用户类,将常用的用户信息存储起来,在后面需要的时候,在用户类中去对应取值。 原因是,一般而言,我们会将用户信息存储起来,当然,也就是有两种方式,服务器存储、数据库存储。在我们后期使用用户信息的时候,基本都是从这三种途径获取,必不可少的,自然数服务器存储,但是如果每次我们使用,都要去请求一边服务器,是很浪费资源,而且完全没必要的。所以一般情况下(个别安全性严谨的除外),都是只有第一次登录账户的时候,获取一下个人的信息,存储在本地,用的时候直接调用。而至于在本地是使用数据库存储,或许有人比较喜欢SharedPreference,不过个人不建议,毕竟存储的数据量比较大。 但无论使用哪种,繁复访问数据库也是一个很耗时的操作,所以很容易影响到用户的体验,所以这个时候就可以选择在BaseActivity中保留一个用户信息的部分,方便之后使用,不过个人建议保留一些常用的即可,没必要将整个用户信息都暴露出来。 由于项目不同,所需也不尽相同,这部分就并予以展示了。编程相关 这部分呢,我主推Context (也就是传说中的上下文),创建一个Context 的共有变量,在onCreate中赋值 public Context context; public Activity activity; //在onCreate中 context = getApplicationContext(); activity = this;大家看到这部分可能或疑惑,毕竟关于BaseActivity的封装,也不是我一个人在写,之前看到的或许在BaseActivity中就不存在context,或者是使用的Activity。而无论是使用的Context还是Activity的,在onCreate中,也不过是使用的context = this;为什么到我这里却非要图个不一样,一定多要写一个context = getApplicationContext(),我们分开来阐述。
为什么定义Context 这个部分,最开始呢,我只是为了与BaseFragment相统一,方便后续编码,至于原因,就先剧透一下了,那就是在Fragment中使用getActivity()方法,容易导致空指针异常,至于原因,我们就先买个关子,到了BaseFragment中,再为大家剖析。 偷懒的解决方法就是在BaseFragment中获取一个上下文信息,后面需要使用的时候直接调用就可以,所以为了代码统一,在BaseActivity中就定义了一个,而且还是那句话,毕竟我们可以偷懒啊,不用每次getContext或者getApplicationContext来获取上下文了。随着查资料,我有发现了一个原因,就是为什么我写了一个Context的同时又写了一个Activity。为什么要多出来一个context = getApplicationContext(); 先需要说明的是,其实Application与Activity都是Context的子类,下面开始正是解释。 getApplicationContext主要的用处就是防止内存泄漏,尤其是一些静态的类需要持有context的时候,如果我们传入了Activity,那么这个静态类没有被销毁之前,对应的Activity也不会被销毁,这样就会导致内存泄漏,而getApplicationContext得到的是整个应用的Context,也就是说,只要Application还在,那么这个Context对象就一直在,哪怕是被静态或者其他可能会导致内存泄漏的方法或者类持有,也不会导致内存泄漏。 但是,如果完全使用getApplicationContext就可以吗?你可以试试,知道遇到“Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.”这个错误。 这是在使用AlerDialog的时候,出现的错误,我查到的是如下原因,所以对应这部分我们就要使用activity,也就是this。在语句 AlertDialog.Builder builder = new AlertDialog.Builder(this); 中,要求传递的 参数就是一个context,在这里传入的是this,那么这个this究竟指的是什么东东呢? 这里的this指的是Activity.this,是这个语句所在的Activity的this,是这个Activity 的上下文。网上有很多朋友在这里传入this.getApplicationContext(),这是不对的。 AlertDialog对象是依赖于一个View的,而View是和一个Activity对应的。 于是,这里涉及到一个生命周期的问题,this.getApplicationContext()取的是这个应 用程序的Context,Activity.this取的是这个Activity的Context,这两者的生命周期是不同 的,前者的生命周期是整个应用,后者的生命周期只是它所在的Activity。而AlertDialog应 该是属于一个Activity的,在Activity销毁的时候它也就销毁了,不会再存在;但是,如果传 入this.getApplicationContext(),就表示它的生命周期是整个应用程序,这显然超过了它 的生命周期了。 所以,在这里只能使用Activity的this。
所以,暂且理解为控件相关就使用activity,方法相关就使用context。不过如果实在难以区分的情况下,对与内存泄漏要求不严格,可是只使用activity,遇到内存泄漏时再替换成getApplicationContext,不过相对于这种解决方式,我还是比较倾向于,两个都创建,先使用context,直接报错的情况下,再换成activity。 再其他的与编程相关的就是一些数据库的引用、SharedPreference的引用,还有就是常量,大多数在BroadCast Reciever中使用的不较多。
这部分,除了activity与context的部分,其他均建议在utils目录下创建一个Const类,将这些存入其中调用,以减轻BaseActivity。
这部分说白了就是startActivity和startActivityForResult的封装,在Android开发中,这两个方法的使用频率就不用我多说了,那是相当多啊,所以发挥我一贯偷懒的准则,这么能忍每次都要创建Intent这么麻烦事呢。 所以封装了如下两个方法:
/** * 跳转页面 * * @param clz 所跳转的目的Activity类 */ public void startActivity(Class<?> clz) { startActivity(new Intent(this, clz)); } /** * 跳转页面 * * @param clz 所跳转的Activity类 * @param requestCode 请求码 */ public void startActivityForResult(Class<?> clz, int requestCode) { startActivityForResult(new Intent(this, clz), requestCode); }这样,我们在使用的时候,就可以传递目标Activity.class就可以实现跳转页面的目的了。 当然,我们除了这么跳转之外,还会有需要传参的时候,对于这部分内容,对于这部分,我们可以使用Bundle来完成,方法如下:
/** * 跳转页面 * * @param clz 所跳转的目的Activity类 * @param bundle 跳转所携带的信息 */ public void startActivity(Class<?> clz, Bundle bundle) { Intent intent = new Intent(this, clz); if (bundle != null) { intent.putExtra("bundle", bundle); } startActivity(intent); } /** * 跳转页面 * * @param clz 所跳转的Activity类 * @param bundle 跳转所携带的信息 * @param requestCode 请求码 */ public void startActivityForResult(Class<?> clz, int requestCode, Bundle bundle) { Intent intent = new Intent(this, clz); if (bundle != null) { intent.putExtra("bundle", bundle); } startActivityForResult(intent, requestCode); }当然,大家如果认为麻烦的话,传参的部分直接使用Intent的方法也是可以的,毕竟使用Bundle就代码量上而言,并没有得到实质性的优化。
除此之外,关于跳转页面还有一些运用。
在工程中,经常会出现退出程序,或者异地登录的情况,出现这种情况的时候,就需要将当前的所打开的Activity中除MainActivity外的其他Activity都关闭。 还有就是,完成一些列可回退的操作后,需要一次性关闭掉过程中所打开的所有Activity。 以上这些操作就需要我们使用下面的部分做处理。 首先需要创建一个List存储我们过程中打开的所有Activity,并在onCreate中添加到List中。
private static List<Activity> activities = new ArrayList<>(); //在onCreate添加 if (!(this instanceof MainActivity)) { activities.add(this); } //在onDestroy添加,防止空指针或者内存泄漏 activities.remove(this);可能大家会发现上面我使用了一个if判断,其作用就是防止MainActivity被添加到列表中,便于后面的运用,因为大多数的app在彻底退出app之前很少有会将MainActivity关闭的,这样就可以便于后面的运用,防止将所有的Activity都关闭了导致程序都退出了。 再就是运用部分了,首先是所有都关闭,这个比较简单,就是遍历一边,挨个关闭就可以
/** * 关闭所有Activity(除MainActivity以外) */ public void finishActivity() { for (Activity activity : activities) { activity.finish(); } }可能有人会疑惑,这样关闭就不需要将被关闭的Activity从Activity集合中移除吗?这点大家可以放心,因为finish方法执行的时候,会执行对应Activity的onDestroy方法,因此,就不需要额外添加remove方法了。 最后就是跳转到指定的以打开Activity,并将其堆栈上方的Activity全关闭的方法
/** * 跳转到指定的Activity * * @param clz 指定的Activity对应的class */ public void goTo(Class<?> clz) { if (clz.equals(MainActivity.class)) { finishActivity(); } else { for (int i = activities.size() - 1; i >= 0; i--) { if (clz.equals(activities.get(i).getClass())) { break; } else { activities.get(i).finish(); } } } }判断部分是看,是否是跳转到MainActivity,如果是,那么就将所有的都关闭即可,也就是调用finishActivity方法即可,当跳转的不是MainActivity的时候,就要遍历一下List了,for循环从i–的目的就是从上而下清理堆栈,到达我们想要跳转的目标Activity时,跳出循环即可。 而如果使用过程中,你发现,不是MainActivity却跳转到了MainActivity,那么恭喜你,你填写的Activity并不在List内,这样也是一个避免bug的检验。
想必大家在使用APP的时候,都体会过,在忽然断网的时候,自己还没反应到,APP就已经提醒你了,现在断网了。 或者是当看视频到一半,忽然提示你,wifi断了,当前使用的是流量是否继续观看。 还有就是我之前在工作中遇到的,实时显示出当前所连wifi的wifi名。 所以无论上面的哪种情况,都需要我们对网络做一个监听,而这部分内容,十分感谢mxiaoyem的Android 实时监测(监听)网络连接状态变化帮了个大忙,按照其博客所说的内容就可以实现,这里我就不加以赘述了,在我的git上也能找到具体的应用。 只是其中有一个地方需要大家注意一下,那就是一定要在AndroidMenifest.xml中注册你所创建的NetBroadcastReceiver。当然,也可以直接复制博客中所写的内容,但是需要注意的一点是,我们的包或许与作者的不同,所以需要做修改,不然会报红,方法很简单,如下图:
找到对应的NetBroadcastReceiver回车即可。
这部分呢,一共分为了两部分,分别是:1、Toast;2、网络加载提示框。
1、Toast Toast是简单的文本提示的时候使用,封装起来也很简单,直接上代码了。
/** * 消息提示框 * * @param message 提示消息文本 */ public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } /** * 消息提示框 * * @param messageId 提示消息文本ID */ public void showToast(int messageId) { Toast.makeText(context, messageId, Toast.LENGTH_SHORT).show(); }以上两个方法,对应的分别是,直接传递字符串和传递string.xml中的string对应的id。
2、网络加载
这部分实际上就是一个网络加载的ProgressBar,但是为了方便使用,所以嵌套在了ProgressDialog内。 懂我套路的一定会猜到,我又该提出感谢了,哈哈哈。十分感谢哇牛Aaron的Android progressdialog自定义背景透明的圆形进度条类似于Dialog帮了个大忙。当然这部分我就没有上面网络监听部分那么偷懒了,而是做了一定的简化,只要了实现我预期功能的部分,其他的内容暂时没有添加到项目中,如果大家也只是想实现一个效果的话,可以看一下我下面贴出来的代码,如果想深入的了解一下的话,可以看一下上面对应的这篇博客。 先上一下效果图:
没错,就是中间的那个圈,至于那个一如既往的丑的背景,辛苦大家暂且忍受一下。 首先是一个ProgressDialog的布局文件:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent"> <ProgressBar style="@style/progress_dialog_loading" android:id="@+id/progress_progress" android:indeterminateDrawable="@drawable/progressbar_load" android:layout_width="48dp" android:layout_height="48dp" android:padding="3dp" android:layout_centerInParent="true" android:visibility="visible" /> <TextView android:id="@+id/progress_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/progress_progress" android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/size_17" android:textSize="@dimen/size_17" /> </RelativeLayout>先说一下比较次要的TextView吧,它的功能主要就是一个文字提示,例如比较常见的“玩命加载中。。。”之类的,只要根据id找到控件,对应setText就可以了。 再就是我们主体ProgressBar,其中有两个属性之前博客没提到过:1、style,就是一个设计风格,也是一个比较常用的属性,对应内容下面会贴出;2、android:indeterminateDrawable,设置的是非进度条(也就是那个圈)的动画Drawable,代码后面也会贴出来。 贴代码喽。 style(里面只有一条,就是背景透明,这个style很快在后面会再用到):
<style name="progress_dialog_loading" parent="@android:style/Theme.Dialog"> <item name="android:windowBackground">@android:color/transparent</item> </style>然后是Drawable对应的动画xml:
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:toDegrees="360" > <!-- 这里画了一个灰色的环形 --> <shape android:innerRadiusRatio="3" android:shape="ring" android:thicknessRatio="8" android:useLevel="false" > <!-- 径向渐变 --> <gradient android:centerColor="#ffffff" android:centerX="1.0" android:centerY="1.0" android:endColor="@android:color/darker_gray" android:gradientRadius="90" android:startColor="@android:color/darker_gray" android:type="radial" android:useLevel="false" /> </shape> </rotate>drawable的主要内容就是绘制一个环,背景白色,渐变角度90°,开始与结束都设置成了深灰色。 以上几部分配合,就完成了我们的ProgressDialog的布局内容,当然,当然,如果大家看了上面的 哇牛Aaron 的博客就会发现,我做的化简只要将style中的属性做了删减。 下面就该进行下一部分了,就是ProgressDialog的自定义,还是先上打码在分析:
package com.mybaseapplication.widget; import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; import android.widget.TextView; import com.mybaseapplication.R; public class CustomProgressDialog extends ProgressDialog { private String message = ""; public CustomProgressDialog(Context context, int theme) { super(context, theme); } public CustomProgressDialog(Context context, int theme, String message) { super(context, theme); this.message = message; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.laod_progressbar_layout); //dialog弹出后点击物理返回键Dialog消失,但是点击屏幕不会消失 this.setCanceledOnTouchOutside(false); //dialog弹出后点击屏幕或物理返回键,dialog都不消失 //this.setCancelable(false); if (message != null){//message不为空,则设置 ((TextView)findViewById(R.id.progress_text)).setText(message); } } }其中大家可以看到,我使用了两个构造方法,一个是:
public CustomProgressDialog(Context context, int theme)另一个是:
public CustomProgressDialog(Context context, int theme, String message)先说一下共通的参数,context上下文信息,这个是每个控件都需要的一个参数,与当前的Activity绑定在一起,也就是前面所说的activity参数,而不能使用context。 theme主题,也就是前面我们定义的style,用途还是将ProgressDialog的背景设置为透明,如果不设置这个style的话,那么出来的就会有白色的背景,即便你将ProgressDialog解析的布局文件的背景和ProgressBar的背景都设置成了透明也一样。错误效果如图:
而第三个参数message,就是我们想要显示的文本,类似“玩命加载中。。。”通过构造方法传入进来,就可以显示了,如图:
然后是在BaseActivity中如何调用,第一步先创建一个私有的CustomProgressDialog
/** * 加载提示框 */ private CustomProgressDialog customProgressDialog;第二步,在onCreate中实例化(也就是new 一下):
//不传文本 customProgressDialog = new CustomProgressDialog(activity, R.style.progress_dialog_loading); //传递文本 customProgressDialog = new CustomProgressDialog(activity, R.style.progress_dialog_loading, "玩命加载中。。。");再就是显示与隐藏的方法了。
/** * 显示加载提示框 */ public void showLoadDialog() { customProgressDialog.show(); } /** * 隐藏加载提示框 */ public void hideLoadDialog() { if (customProgressDialog != null && customProgressDialog.isShowing()) { customProgressDialog.dismiss(); } }不过这里我们使用的是直接显示的或隐藏,如果在主线程中如此使用,自然没有问题。也能正常显示,就如同我上面截图一样,但是如果你想把显示或者隐藏的方法放在子线程中使用的话,这两个方法就没法起到作用了。 原因参见WongWoo1991的子线程中progress不显示问题,里面也给出了解决方法,那就是将ProgressDialog的操作嵌套在runOnUiThread内,这样就可以使其正常运行了,修改后的方法如下:
/** * 显示加载提示框 */ public void showLoadDialog() { runOnUiThread(new Runnable() { @Override public void run() { customProgressDialog.show(); } }); } /** * 隐藏加载提示框 */ public void hideLoadDialog() { runOnUiThread(new Runnable() { @Override public void run() { if (customProgressDialog != null && customProgressDialog.isShowing()) { customProgressDialog.dismiss(); } } }); }如此,这篇博客的内容终于都写完了,BaseActivity的封装到此也就结束了,当然还有一些方法,例如Log日志输出,还有一些工具类的创建,不过就像是前面所说第一部分常用常量、变量中所说的一样,这些方法还是建议在Utils包下创建Const完成,免得BaseActivity中内容太过冗杂。 还有就是一些数据库访问的或者全局SharedPreference的访问接口,也可以放在BaseActivity中,但是这部分比较复杂,在对应使用的地方再加以说明。 以上内容均为我个人的粗浅理解,大家如果发现问题或者可以添加的部分,可以留言,大家一起加油进步哦。
《一个Android工程的从零开始》目录
