美文网首页Android开发Android技术知识Android开发
《一个Android工程的从零开始》-5、base(四) Bas

《一个Android工程的从零开始》-5、base(四) Bas

作者: 半寿翁 | 来源:发表于2017-07-11 20:09 被阅读0次

    先扯两句

    昨天发了一篇GitHub版本控制的集成后,今天终于回归正事,继续我们的《一个Android工程的从零开始》,真心有点小开心呢。
    今天也是base的BaseActivity完结掉了,昨天我也查了一下其他人的BaseActivity封装,发现却比我的篇幅少了不少,不过既然要从基础说起,自然废话也就多了一些,请大家见谅。
    既然昨天已经发了GitHub的版本控制,那么这篇开始就发GitHub的链接了,码云的就暂停更新维护了,另外onstraintLayout的也相差不多,只是布局的部分不同,但是由于很少有用代码部分完成的,这里就不予以展示了。

    [MyBaseApplication] (https://github.com/BanShouWeng/MyBaseApplication)

    正文

    本次的内容总共有如下四点:

    1. 常用常量、变量;
    2. 界面跳转;
    3. 网络监听;
    4. 提示框。

    这个顺序很显然是本着我个人一贯偷懒成瘾的套路排列的,不过这不重要,我们还是开始今天的内容吧。

    常用常量、变量

    这个部分的内容放在最简单的位置上,其实主要还是由于我所能写的东西有限,但实际上就那四点而言,这个部分是最复杂的,因为不同的工程项目,对这部分的需求往往是更不相同的,我们也无法一概而论的给出一个具体的实现方式,所以就再此做一个简要的分析。
    这部分如果做简单分类呢,就是两个点:其一是用户相关;其二是编程相关

    1. 用户相关
      用户相关的东西,在这里主要是一些常用的信息,例如用户id、用户联系方式、用户头像等等。这部分的做法有两种,其一,这些信息分别存储,用的时候直接调用;其二创建一个用户类,将常用的用户信息存储起来,在后面需要的时候,在用户类中去对应取值。
      原因是,一般而言,我们会将用户信息存储起来,当然,也就是有两种方式,服务器存储、数据库存储。在我们后期使用用户信息的时候,基本都是从这三种途径获取,必不可少的,自然数服务器存储,但是如果每次我们使用,都要去请求一边服务器,是很浪费资源,而且完全没必要的。所以一般情况下(个别安全性严谨的除外),都是只有第一次登录账户的时候,获取一下个人的信息,存储在本地,用的时候直接调用。而至于在本地是使用数据库存储,或许有人比较喜欢SharedPreference,不过个人不建议,毕竟存储的数据量比较大。
      但无论使用哪种,繁复访问数据库也是一个很耗时的操作,所以很容易影响到用户的体验,所以这个时候就可以选择在BaseActivity中保留一个用户信息的部分,方便之后使用,不过个人建议保留一些常用的即可,没必要将整个用户信息都暴露出来。
      由于项目不同,所需也不尽相同,这部分就并予以展示了。
    2. 编程相关
      这部分呢,我主推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(),我们分开来阐述。

    1. 为什么定义Context
      这个部分,最开始呢,我只是为了与BaseFragment相统一,方便后续编码,至于原因,就先剧透一下了,那就是在Fragment中使用getActivity()方法,容易导致空指针异常,至于原因,我们就先买个关子,到了BaseFragment中,再为大家剖析。
      偷懒的解决方法就是在BaseFragment中获取一个上下文信息,后面需要使用的时候直接调用就可以,所以为了代码统一,在BaseActivity中就定义了一个,而且还是那句话,毕竟我们可以偷懒啊,不用每次getContext或者getApplicationContext来获取上下文了。随着查资料,我有发现了一个原因,就是为什么我写了一个Context的同时又写了一个Activity。
    2. 为什么要多出来一个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中使用的不较多。

    Tips

    这部分,除了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名。
    所以无论上面的哪种情况,都需要我们对网络做一个监听,而这部分内容,十分感谢mxiaoyemAndroid 实时监测(监听)网络连接状态变化帮了个大忙,按照其博客所说的内容就可以实现,这里我就不加以赘述了,在我的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内。
    懂我套路的一定会猜到,我又该提出感谢了,哈哈哈。十分感谢哇牛AaronAndroid 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工程的从零开始》目录

    相关文章

      网友评论

        本文标题:《一个Android工程的从零开始》-5、base(四) Bas

        本文链接:https://www.haomeiwen.com/subject/wcomhxtx.html