美文网首页androidAndroid开发经验谈Android技术知识
《一个Android工程的从零开始》阶段总结与修改4-BaseA

《一个Android工程的从零开始》阶段总结与修改4-BaseA

作者: 半寿翁 | 来源:发表于2018-04-05 23:14 被阅读493次

    先扯两句

    终于把档案的事搞定了,据传闻,如果毕业两年后档案还留校没有调到生源地人社局或者是工作单位所在城市人社局的话,档案就会变成“死档”,

    “死档”有两种情况,一是超过择业期未就业的同学,档案在打回原籍过程中丢失;二是档案拿在自己手里超过两年。如果档案处理不得当,很有可能影响你报考公务员、事业单位等,也会影响以后的职称评定和工龄认定。

    所以如果有还没有办理的可要快点喽,虽然我们码农一时半会都用不上,但是有备无患嘛。
    好了,闲言少叙,老规矩还是先上我的Git,然后开始正文吧。
    MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)

    正文

    Toast

    通过上一篇“《一个Android工程的从零开始》阶段总结与修改BaseActivity上(抽象处理),已经对BaseActivity的进行了面目全非的调整,若是在遇到一个奇葩错误之前,原本这次BaseActivity的封装已经结束了,下面就将进入BaseLayoutActivity的封装(也就是原本的BaseActivity的布局部分),可人算不如天算,公司拿APP出去演示,演示机华为畅享7忘记关闭飞行模式时打开APP,直接崩溃了,而导致崩溃的正是下面的错误。

    Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

    以我的英文水平翻译过来就是“在Looper没有准备好的时候,在子线程中无法创建handler。”,看了一眼代码,问题是出在了:

    /**
     * 消息提示框
     *
     * @param message 提示消息文本
     */
    public void toast(String message) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
    

    作为一个自成老头子的小菜鸟自然一脸懵逼,多亏Xuelong_li大神的Can't toast on a thread that has not called Looper.prepare()中发现了答案,原来是由于

    在子线程中弹出Toast,会报错:java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()。

    需要加以修改,所以这里,我将自己的Toast封装,做了如下调整:

    private Toast toast;
    
    /**
     * 消息提示框
     *
     * @param message 提示消息文本
     */
    @SuppressLint("ShowToast")
    public void toast(String message) {
        try {
            if (toast != null) {
                toast.setText(message);
            } else {
                toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
            }
            toast.show();
        } catch (Exception e) {
            // 解决在子线程中调用Toast的异常情况处理
            Looper.prepare();
            toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
            toast.show();
            Looper.loop();
        }
    }
    
    1. 首先将Toast提出来创建一个对象,防止每次toast都需要重新创建浪费资源;
    2. 当第一次调用,或者toast对象被回收时,toast为空,则创建新的toast:toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
    3. 从第二次开始直接调用toast.setText(message)替换显示文本;
    4. 无论toast之前处于什么状态,经过上面的判断或复制后,都确定toast存在,并有所要显示的文本信息,所以调用toast.show()显示toast信息;
    5. 当出现开篇所述的异常时,会被try-catch捕捉,此时采用Xuelong_li博客中所述的方法。

    细心的或许会发现,上面为什么会有@SuppressLint("ShowToast"),这个说实话,我也很无奈啊,由于上面调用的时候,有Toast.makeText(context, message, Toast.LENGTH_SHORT),很明显,这里我没有直接调用show展示,于是好心的AS专程提醒我了一下:
    [图片上传失败...(image-6303db-1522941221454)]
    所以强迫症的我为了不看到这个提示,只能加上忽略警告的注解,当然,这是因为在代码中,虽然没有直接饮用,但是后面肯定会调用到show方法,才添加的这条注解,在开发过程中,可不要习惯性不管三七二十一直接加上这条注解,到时候Toast真害羞不出来,都没地方哭去。

    jumpTo

    这个方法从翻译也能看出来,就是“跳转到”,原本的BaseActivity封装中也是有的,只是当初命名是startActivity,最基本的封装如下:

    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的目的Activity类
     */
    public void startActivity(Class<?> targetActivity) {
        startActivity(new Intent(this, targetActivity);
    }
    

    代码也很简单,意图(Intent)一共两个参数,当前Activity.this和目标activity的class——targetActivity。因为当前所定义的是将来所要调用后的activity的父类,所以这里的this就可以指代子类的this,就好像我们小时候出去,别人对我们的第一印象首先不是我们是谁谁谁,而是,我们是谁谁谁的儿子/女儿。
    所以其实这里我们需要的参数也就只有targetActivity而已(传参的暂不考虑,后面会添加),所以就有了上述方法的形式。不过这里就会有个问题,那就是我们无法判断自己传入的targetActivity的正确性(是否是Activity的class、该Activity是否在manifest文件中注册过),所以很可能因为我们的一时手误,填写了一个不正当的参数APP程序崩溃。所以当前的BaseActivity做了如下处理:

    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的目的Activity类
     */
    public void jumpTo(Class<?> targetActivity) {
        Intent intent = new Intent(this, targetActivity);
        if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            startActivity(intent);
        } else {
            Logger.e(getName(), "activity not found for " + targetActivity.getSimpleName());
        }
    }
    

    if中的判断条件就是验证当前Activity是正确,如果正确,则执行startActivity(intent);不正确,项目中只是不执行对应的跳转操作,但是会打印出哪个类无法获取的Error。
    下面列举的是当前的BaseActivity中的所有跳转界面封装方法,因为Intent封装到jumpTo方法中了,所以无法直接为其赋值。所以这里的传参选择了使用Bundle,看过我的《一个Android工程的从零开始》阶段总结与修改3-BaseActivity上(抽象处理)的也会知道,我在其中封装了一个抽象方法getBundle(Bundle bundle),正是用来接收这里传递过去的Bundle的。而如果想要调用startActivityForResult,自然需要在前面说到的this和targetActivity的基础上再加上一个int型的参数requestCode,在多个事件调用startActivityForResult时,可以用于区分,所以组合下来一共有下面的四种方法。

    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的目的Activity类
     */
    public void jumpTo(Class<?> targetActivity) {
        Intent intent = new Intent(this, targetActivity);
        if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            startActivity(intent);
        } else {
            Logger.e(getName(), "activity not found for " + getName(targetActivity))ß);
        }
    }
    
    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的目的Activity类
     * @param bundle         跳转所携带的信息
     */
    public void jumpTo(Class<?> targetActivity, Bundle bundle) {
        Intent intent = new Intent(this, targetActivity);
        if (bundle != null) {
            intent.putExtra("bundle", bundle);
        }
        if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            startActivity(intent);
        } else {
            Logger.e(getName(), "activity not found for " + getName(targetActivity)));
        }
    }
    
    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的Activity类
     * @param requestCode    请求码
     */
    public void jumpTo(Class<?> targetActivity, int requestCode) {
        Intent intent = new Intent(this, targetActivity);
        if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            startActivityForResult(intent, requestCode);
        } else {
            Logger.e(getName(), "activity not found for " + getName(targetActivity)));
        }
    }
    
    /**
     * 跳转页面
     *
     * @param targetActivity 所跳转的Activity类
     * @param bundle         跳转所携带的信息
     * @param requestCode    请求码
     */
    public void jumpTo(Class<?> targetActivity, int requestCode, Bundle bundle) {
        Intent intent = new Intent(this, targetActivity);
        if (bundle != null) {
            intent.putExtra("bundle", bundle);
        }
        if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            startActivityForResult(intent, requestCode);
        } else {
            Logger.e(getName(), "activity not found for " + getName(targetActivity));
        }
    }
    

    当然上述的判断条件添加前需要大家先斟酌一下:
    其一这里需要先声明一点,虽然使用上述的方法,可以避免方法targetActivity不存在时的崩溃问题,但是却也不会真正跳转到用户期望的Activity中去,治标不治本。
    其二虽然加上了提示,可是毕竟只是在日志中打印出一个找不到的类名而已,开发中,并没有崩溃日志中的定位信息来得那么直接,容易查找。
    所以,建议大家在上线时,为了避免意外崩溃时可以加上这些判断的,但是开发者过程中还是不要添加了,也尽量测试到所有跳转情况,让这些方法加上,也不要有用武之地

    getName

    最近两篇博客,大家一定不少看到这个方法,但是直接调用的话,系统是没有提供对应的方法,或许会有人疑问,这个方法是用来做什么的呢?解密之前先来看看封装的方法,或许有人就能知道具体是做什么的了:

    /**
     * 获取当前Activity类名
     *
     * @return 类名字符串
     */
    protected String getName() {
        return getClass().getSimpleName();
    }
    /**
     * 获取目标类的类名
     *
     * @param clz 需要获取名称的类
     * @return 类名字符串
     */
    protected String getName(Class<?> targetClass) {
        return targetClass.getSimpleName();
    }
    

    没错,这两个方法就是用来获取类名的,毕竟开发中出现了问题,却不知道究竟应该到哪个类中查找(崩溃日志除外)。
    当然,其实获取类名一共有三个方法,我们这里使用的是getSimpleName(),还有两个分别为getName()和getCanonicalName()(这第三个是我查前两个区别的时候才发现的,又学到了新知识啊),具体三者之间又什么区别呢?Ju_Sang简单比较 getName()、getCanonicalName()、getSimpleName() 的异同

    getName()方法,以String的形式,返回Class对象的‘实体’名称;
    getSimpleName()方法,是获取源代码中给出的‘底层类’简称;

    对于我们这些入门级选手基本就够用了,再具体的信息,就请大家自行百度
    (能翻墙的选手自行Google)。我个人的理解就是而这里,个人的习惯getSimpleName()就足够用,而不需要再加上完整的包名,当需要查对应的类在哪里时,我们只需要在AS界面双击Shift,就可以在弹出的弹窗中输入想要查找的类了。当然,以上的观点都是建立在你不会频繁的在不同包下创建同名类的前提下。

    而这里之所以要定义两个方法,完全是因为有的时候,我们想要获取的类名的类并不是当前的Activity本身,所以这个时候就需要第二个方法,我们将对应的类传入,就可以获取类名,更加的灵活。

    activities与activitiesMap

    刚看到这节的标题或许会有人比较困惑,本篇博客不是叫方法封装吗,可是从这节标题看,怎么出来一个List和一个Map,怎么都有点挂羊头卖狗肉的意思呢?
    好吧,首先我不得不承认,这两个缺失不是方法名,但是它们却与下面要说明的几个方法有着千丝万缕的关系。

    首先呢,我们要创建一个List集合activities,目的就是用来存储我们打开过的Activity;然后呢,新打开一个Activity就添加一条,没关闭一个Activity就删除对应的一条;最后呢,接收到要返回的Activity,就将该Activity后打开的所有Activity统统调用一遍finish即可。

    当我们调用backTo的时候,防止出现想要返回的不存在,多见于同一个Activity会返回多个Activity时的判断出现逻辑错误,返回了还未开启的Activity,虽然说一般情况下,这个错误都会随着自测或者公司测试的同事测试时反馈回来予以修改,可不怕一万就拍万一,尤其一些初创的小团队,团队的一切都透露着慢慢的随意的时候,这部分处理虽然还是治标不治本,但至少不会让用户看到崩溃的不好体验。

    首先是纯Activity集合的部分,这部分过了在来叙述为什么会比原本的BaseActivity封装多出了一个Map集合:

    /**
    * 当前打开Activity存储List
    */
    private static List<Activity> activities = new ArrayList<>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        activities.add(this);
        ...
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除当前的Activity
        activities.remove(this);
        ...
    }
    
    /**
     * 返回历史界面
     *
     * @param targetActivity 指定的Activity对应的class
     */
    public static void backTo(Class<?> targetActivity) {
        int size = activities.size();
        for (int i = size - 1; i >= 0; i--) {
            if (targetActivity.equals(activities.get(i).getClass())) {
                break;
            } else {
                activities.get(i).finish();
            }
        }
    }
    

    以上就是前面说到的基本流程,这样就能维持activities中的Activity能够与当前打开的页面层级保持一致。

    1. backTo中没有调用remove移除activities中的Activity是因为在调用finish()的时候,对应结束的Activity会调用自己的onDestroy(),这样可以确保:a、当Activity正常退出时,在onDestroy()方法中remove保证activities与打开的页面层级相同;b、当backTo被中调用时,其间的每个Activity都能够被activities remove;c、当backTo被调用时,不会因为在backTo中remove了对应Activity而导致在onDestroy中调用时由于不存在对应Activity而导致的异常。
    2. 采用静态方法,完全是因为非静态时,Activity集合只能存储当前的Activity,而而不会时完整的Activity层级关系。
    3. backTo使用public是因为在开发时,可能会出现不只是在Activity中调用该方法,也可能是在自定义列表适配器中、BroadcastReceiver和Service等其他环境下调用。

    一般而言,如上就完成了我们返回制定Activity的封装,做多再加上一个返回主页面的方法:

    /**
     * 关闭所有Activity(除MainActivity以外)
     */
    public void finishActivity() {
        for (Activity activity : activities) {
            if (activity.getClass().equals(MainActivity.class))
                break;
            activity.finish();
        }
    }
    

    之所以加上“activity.getClass().equals(MainActivity.class)”判断,主要是因为前面添加Activity时没有过滤掉主页面。也可以在onCreate方法中判断,如果是主页面就不添加,不过这么一来每次创建一个A是不是超简单!没错,之前我也是这么设计的。

    不过后来的使用呢,总体来说还是没有发生什么问题的,但是最为一个脑洞打开的老年人,就好像前面判断targetActivity是否存在一样,总会过度设计一些功能,所以下面的部分大家根据需要看就好,实在用不上,当前这一小节也可以跳过,下一节是getView,可以自行Ctrl+v/ƒcommand+v搜索。

    (刚买了个苹果本,发现各种快捷键不同,还要适应一段时间。另外,苹果本到手了,自然闲暇时间也要研究研究IOS开发,届时会同步更新学习笔记,有兴趣的朋友可以一起学习哦)

    关于验证activities中是否存在对应Activity的尝试

    1. 判断activities中是否有包含目标class对应的Activity————需要通过for循环,遍历每个Activity并作对比,浪费资源;
    2. backTo传递Activity参数————暂时无法实现;
    3. activities存储每一个Activity的class————通过class无法调用对应Activity的finish方法。

    根据以上的尝试,最后无奈之下,只能选择再添加一个参数也就是Map集合,其value为Activity自身,而key则是Activity对应的class:

    /**
    * 当前打开Activity存储List
    */
    private static List<Activity> activities = new ArrayList<>();
    
    **
    * 调用backTo方法时,验证该Activity是否已经存在
    */
    rivate static Map<Class<?>, Activity> activitiesMap = new HashMap<>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        activities.add(this);
        activitiesMap.put(getClass(), this);
        ...
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除当前的Activity
        activities.remove(this);
        activitiesMap.remove(getClass());
        ...
    }
    
    /**
     * 关闭所有Activity(除MainActivity以外)
     */
    public void finishActivity() {
        for (Activity activity : activities) {
            if (activity.getClass().equals(MainActivity.class))
                break;
            activity.finish();
        }
    }
    
    /**
     * 返回历史界面
     *
     * @param targetActivity 指定的Activity对应的class
     */
    public static void backTo(Class<?> targetActivity) {
        int size = activities.size();
        if (activitiesMap.get(targetActivity) != null)
            for (int i = size - 1; i >= 0; i--) {
                if (targetActivity.equals(activities.get(i).getClass())) {
                    break;
                } else {
                    activities.get(i).finish();
                }
            }
        else
            Logger.e(activities.get(size - 1).getClass().getSimpleName(), "activity not open for " + targetActivity.getSimpleName());
    }
    

    我们只需要取判断map中class作为key能否渠道对应的Activity即可,有的情况下就正常返回,没有时则输出一条log日志即可。看到这里可能会有人问,如果map这么强大的话,为什么这里不直接使用map,反而要多一个List集合呢?对此,老头子我也很无奈啊,谁让map是无序的呢,如果真的在这里直接使用map集合的话,说不好不该关掉的Activity关掉不少,而真正应该关掉的Activity还在原地对着你微笑呢!

    getView

    从字面理解,很简单,这就是获取View,相比看到这个说法,大家第一反应就会想到findViewById,不错,这里就是对于findViewById的封装。不过关于这个部分,我是思考考了很久才决定这篇博客还是写出来的,至于原因,我们先上代码:

    /**
     * 简化获取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);
    }
    
    /**
     * 简化获取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);
    }
    

    这部分的功能很简单,就是将findViewById获取到的View通过泛型传出来,这样就可以减少一步强转的操作。而至于为什么这么封装,其实很简单,毕竟大多数人还是与我一项“标榜”自己的一样————懒!

    实际上,对于初学者来说,这个强转的功能还是有必要的,毕竟如果没有类型的强转的话,初学者很容易将类型与Id对应错,倒不是说老鸟不会出这样的错误,只是初学者一旦这么使用,当运行的时候看到那鲜红的类转换异常绝对会一脸懵逼欲哭无泪的,而同样的问题老鸟只会狠狠拍一下额头,然后用远少于拍额头的时间改正。所以说,这个功能,对于有一定Android开发经营的朋友来说,实在是个鸡肋,就连Google都发觉了这个问题,因此在新发布的AS 3.0的AppCompatActivity中,已经添加了如下处理:

    @SuppressWarnings("TypeParameterUnusedInFormals")
    @Override
    public <T extends View> T findViewById(@IdRes int id) {
        return getDelegate().findViewById(id);
    }
    
    

    也就是说,使用AS 3.0的朋友,妈妈再也不用担心我们还要多一步View强转了。可是我封装的后两个方法在当前版本的AS 3.0中还没有查找到替代方法,这让我在这篇博客添加这部分的时候,负罪感减少了不少。只说明一点,那就是后两步这里传递进来的严谨来说实际上不应该是普通的View或id,而是ViewGroup或其id,也就是可以拥有子控件的控件,因为只有能有子控件的控件,我们才能通过喊它儿子名字的方式找到那个砸了自己家玻璃的捣蛋鬼,对于那些做了绝育手术的猫,我也就只能为它默哀三秒钟了。

    今天的博客就都这里了,当然不是说BaseActivity的封装只有这些内容,还有网络状态监听之类的方法,只不过这篇博客的内容是“阶段总结与修改”,前面有些相关的方法已经重复说明不少了,那些与之前博客中所说相同的内容就不多做赘述了,有兴趣的朋友可以翻看一下我之前的博客,也算见证一下我个人的成长吧。

    附录

    《一个Android工程的从零开始》- 目录

    相关文章

      网友评论

      • 沐小晨曦:持续关注大佬。🙂🙂🙂
      • JarryLeo:activity回退到某个页面,关闭它之上的页面:用sigleTask模式启动它即可,会自动移除它之上的页面,还可以携带数据过去,如果它不存在还会自己创建,一石三鸟,没必要维护list或者map
        半寿翁:@JarryLeo 受教了。我会在后续的迭代中加上对应的处理,谢谢了
      • IT人故事会:做开发很累,还的学习,之前你这个我也碰到过,但是没记录谢谢了

      本文标题:《一个Android工程的从零开始》阶段总结与修改4-BaseA

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