今天看新闻,发现 GreenDao 的东家 greenrobot 出了一个新的 NoSQL 数据库,greenrobot 称它是目前性能最好且易用的 NoSQL 数据库,且优于其它数据库 5~15 倍的性能。
今天看新闻,发现 GreenDao 的东家 greenrobot 出了一个新的 NoSQL 数据库,greenrobot 称它是目前性能最好且易用的 NoSQL 数据库,且优于其它数据库 5~15 倍的性能。
Android开发最佳实践——4.Repository 层实现
转眼就 2017 年了,原本定的博客计划因为太忙而搁置了两个月,最近终于可以恢复更新了。今天要阐述的是我理解的完美 Repository 层的实现,Repository 层也就是我们的数据层,在这里封装了获取网络数据、缓存数据、数据库数据以及数据转换逻辑
项目的架构是一个项目的基础,其决定了我们项目后期的可扩展性,开发过程中的便捷性等。一个好的项目架构应该是结构清晰,可维护性好且可扩展性强的。这次我们就来探讨一下如何架构一个项目。
最近公司的项目用到了友盟的统计和推送,在集成的过程中遇到了一点小坑,这里记录一下方便以后查阅。
Android开发最佳实践——2.使用Kotlin开发Android
Android 的官方开发语言是 Java,那为什么我们不继续使用 Java 开发 Android 呢?可能有人会说出很多理由,如:
但实际上我觉得让我们选择 Kotlin 而不是 Java 的原因只有一个:Kotlin 拥有更高的生产力。
下面我就介绍一下 Kotlin 这个语言和它的好处,以及如何使用它编写 Android 程序。
Kotlin 是公司 JetBrains 研发的语言(他们家代表产品有 IntellJ Idea、Android Studio 等)。他们的网站上,他们是这样描述 Kotlin 的:
为 JVM、Android 和浏览器而生的静态编程语言。
相比起其它 JVM 上的语言,它拥有无数的优点:
下面简单介绍一些 Kotlin 的习语,看看 Kotlin 是如何简化我们的编码的。
Kotlin 中创建数据类非常简单,我们只需要如下编写代码即可:
1 | data class Customer(val name: String, val email: String) |
使用了data class
关键词之后,Kotlin 会自动帮我们生成getter
(如果属性是使用var
声明的则还会生成setter
)、equals()
、hashCode()
、toString()
、copy()
等常用方法。
而在 Java 中写一个 JavaBean,我们需要一个个声明属性并写出它们对应的getter
、setter
以及其它代码,相比之下 Kotlin 的代码量小了一大半。
因为历史原因我们还在使用 Java6 编写 Android 代码,无法使用到 Java8 的新特性之 Lambda 表达式。而使用 Kotlin 的话是天生支持 Lambda 的,可以大大减少代码量。
Java ver:
1 | mTextView.setOnClickListener(new View.OnClickListener(){ |
Kotlin ver:
1 | mTextView.setOnClickListener { v -> // todo...} |
Kotlin 里声明的类型都是默认非空的,可空的类型必须要在声明类型的时候在类型后面加一个?
,而 Kotlin 也提供了语法糖来搞定null
判断
1 | Student aStudent |
该aStudent
永远不会为空
而如果是一个可空的 Student
1 | Student? bStudent |
因为声明类型的时候添加了一个?
表示可空,所以bStudent
是可能为null
的。但 Kotlin 提供的语法糖在使用的时候bStudent?.name
不会产生 NullPointException。
Java 中格式化字符串是这样的:
1 | String.format(Locale.getDefault(), "Name: %s", student.getName()); |
Kotlin 中我们可以这样:
1 | "Name: ${student.name}" |
Kotlin 里的 when 表达式非常强大,可以替换掉if-elseif-else
1 | when (x) { |
上面讲了一些常用的 Kotlin 特性,还有更多没有写出的如:不可变集合
,扩展函数
,参数默认值
等。
我个人的体验是使用 Kotlin 可以大大减少模版代码,让 Coding 更加愉悦。推荐在没有历史包袱的项目中使用。
但如果是有历史包袱的项目或者项目组成员不愿意去另外学习一门语言的话,那可能就无法享受到 Kotlin 的好处了,大家酌情选择即可。
一个项目刚开始的时候,最需要确认的就是接口设计了:数据如何传递,使用什么格式什么协议乃至如何保证安全性。如果一个项目的接口设计不合理——比如没有考虑到安全性,后期为了增加安全验证又要对部分 API 推倒重做,那么前端(泛指 Android、iOS 以及 Web)就必须对整个项目进行改动,甚至可能导致之前发布的版本无法使用的囧事。
那么本文就谈谈我认为的一个好的接口应该是如何设计的。
使用 RESTful 风格的 API 设计。
使用 HTTPS 协议,保证 HTTP 的方便的同时保证一定的安全性。
尽量部署在专属域名下,如 github:
1 | https://api.github.com/ |
应该把版本号放到 URL 中,如 API 有改版的时候,应保证老版的 API 持续提供服务一段时间。
1 | https://api.example.com/v1/ |
在 Restful 风格的 API 中,每个路径都代表着互联网中的一个资源。所以 URL 地址中应该使用名词,并且因为大多是资源集合,所以应该使用复数形式。如果是有从属关系的资源,应该服从从属关系,下面给出几个例子:`
https://api.example.com/v1/posts
https://api.example.com/v1/posts/{postId}
https://api.example.com/v1/posts/{postId}/comments
HTTP 动词可以完美对应数据库的增删查改操作,于是我们就把 HTTP 动词和我们的增删查改操作对应起来:
下面是结合 HTTP 动词和路径提供一些示例:
/posts
:获取所有文章/posts
:创建一篇文章/posts/{postId}
:获取指定 Id 的文章信息/posts/{postId}
:修改指定 Id 的文章信息(客户端需要提供全部属性)/post/{postId}
:修改指定 Id 的文章信息(客户端提供需要修改的部分属性)/post/{postId}
:删除指定 Id 的文章/posts/{postId}/comments
:获取指定 Id 文章的所有评论/posts/{postId}/comments
:在指定 Id 文章下创建一条评论在 GET 查询的时候我们不可能一次性获取所有资源,那么我们需要提供一些查询条件。
下面是一些常用的查询:
?index=2&size=20
:第二页每页20条
?sortby=name&order=asc
:按指定规则与顺序排序
……
全局通用信息应该放在请求头里,避免使用 Query 拼接,如:
等
使用 json 格式传输数据,如果需要上传文件则使用表单的形式提交。
使用 json 格式传输数据,Content-Type
一致设定为application/json
。
响应格式应该统一,下面给出一个例子:
名字 | 类型 | 含义 |
---|---|---|
code | int | 状态码 |
message | String | 状态信息 |
data | List or Object | 数据 |
time | long | 时间戳 |
具体的响应如下:
1 | { |
或者返回某一个数据:
1 | { |
为了保证客户端与服务端通信的安全,我们使用 HTTPS 协议。
在身份认证上使用 Oauth 2.0 协议,用户登录之后在客户端保存一份 token,避免在客户端持久化存储用户名和密码。之后每次访问需要身份认证的 API 时,必须携带 token 访问。
API 设计的时候应该合理帮助前端避免空指针异常,在一些字段或者属性为空的时候应该返回默认值:如 String 返回""
, int 返回0
, Object 返回 {}
, Array or List 返回 []
。
RecyclerView.Adapter:全能notify解决方案
在之前我们用 ListView 或者 GridView 的时候,通知适配器刷新是这样的:
1 | adapter.notifyDataSetChanged(); |
但是当我们使用了更强大的 RecyclerView 之后,如果直接这样通知适配器刷新将不会显示动画效果。它会直接将所有的 item 重新绘制。
我们需要使用如下的方法来通知适配器刷新,这样 RecyclerView 才会显示对应的动画效果:
1 | adapter.notifyItemInserted(); |
在这次更新的 Support Library 24.2.0 中添加了一个新的工具类,可以用来方便快捷的处理 RecyclerView.Adapter 的通知刷新。
DifUtil 就是这次引入的工具类,它会找出 Adapter 中每一个 Item 对应发生的变化,然后对每一个变化给予对应的刷新。
最重要的就是如下的两个重载方法
1 | DifUtil.calculateDiff(Callback cb, boolean detectMoves); |
其中DifUtil.calculateDiff(Callback cb);
实际上就是DifUtil.calculateDiff(callback, true);
所以我们着重研究第一个方法即可。
该方法会接收两个参数,其中第二个参数是一个 boolean 值,查看源码注释我们知道这个参数有如下作用:
True if DiffUtil should try to detect moved items, false otherwise.
如果 DiffUtil 尝试检测移动的项目就设为 true,否则设为 false。
这个参数实际上是指定是否需要项目移动的检测,如果设为 false ,那么一个项目移动了会先判定为 remove,再判定为 insert。
而Callback
是一个抽象类,它有四个方法需要实现:
1 | public abstract static class Callback { |
如上所述,我们四个需要实现的方法的作用都在注释中写出了。前两个方法都很好理解,需要重点说明的是后两个
areItemsTheSame
:这个方法用来判断两个 Object 是否是相同的 Item,此处最好不要简单的用equals
方法判断,我们可以根据 Object 的唯一标识或者自己指定一个规则来判断两个 Object 是否是展示的相同的 Item。areContentsTheSame
:该方法只有在areItemsTheSame
返回true
之后才会被调用,我们在重写该方法的时候,只需要判断两个 Object 显示的元素是否一致即可。如我们有两个 Object,它们可能拥有很多属性,但是其中只有两个属性需要被显示出来,那只要这两个属性一致我们这个方法就要返回true
。下面我们写一个简单的例子来学习使用 DiffUtil
首先我们来一个 Item 对应的数据类:
1 | public class Student { |
然后写一个 Adapter:
1 | class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { |
其对应的布局文件就是一个简单的 TextView:
1 |
|
然后我们在 Activity 里使用它们并显示出来:
1 | class MainActivity extends AppCompatActivity { |
这样我们就获得了一个简单的展示学生数据的 RecyclerView 了。
然后我们对 Adapter 的数据源进行更改,并通知刷新:
1 | mFab.setOnClickListener(new View.OnClickListener() { |
效果如下:
DiffUtil 的使用就是这样,根据 DiffUtil.Callback 计算出 Result,然后应用更新到 Adapter。
有的人可能说了,这样其实并不好用啊,我们原来数据的改变就直接使用对应的方法就可以了,你这里每次还要写得这么麻烦。那么我们就使用 DiffUtil 和 Adapter 结合再进行一次封装吧。
我们抽取一个 BaseAdapter 出来:
1 | public abstract class BaseAdapter<T, V extends RecyclerView.ViewHolder> |
然后我们只需要令 Adapter 实现 BaseAdapter即可:
1 | class MyAdapter extends BaseAdapter<Student, MyAdapter.ViewHolder> { |
之后我们如果数据源 List 中的数据有任何改动,我们只需要调用notifyDiff()
就可以了:
1 | mFab.setOnClickListener(new View.OnClickListener() { |
最新 Support 包中的 DiffUtil 类给我们带来了一个对 RecyclerView 的不同数据变化的统一处理方案,可以对所有数据变化之后的通知刷新简化,非常好用,强烈推荐使用。
Android Support Library 23.2.0 版为我们带来了官方的夜间模式,现在我们可以很容易地为 App 开发夜间模式了。
使用起来非常简单,我们只需要将主题继承其即可
1 | <!-- parent 为 Theme.AppCompat.DayNight --> |
然后我们在程序中调用方法设置模式即可,推荐在 Application 的onCreate()
中进行设置
1 | AppCompatDelegate.setDefaultNightMode(int mode); |
它有四个可选值,分别是:
MODE_NIGHT_NO
: 使用亮色(light)主题MODE_NIGHT_YES
:使用暗色(dark)主题MODE_NIGHT_AUTO
:根据当前时间自动切换 亮色(light)/暗色(dark)主题MODE_NIGHT_FOLLOW_SYSTEM
(默认选项):设置为跟随系统,通常为 MODE_NIGHT_NO我们也可以为某一个组件设置主题,通过getDelegate().setLocalNightMode(int mode);
即可。注意如果改变了 Activity 的主题,我们需要调用recreate()
重启来显示改变后的效果。
和设置相对应,非常简单
1 | AppCompatDelegate.getDefaultNightMode(); |
如果没有为组件单独设置主题,那么将会获取全局主题,否则获取到组件的主题。
1 | int currentNightMode = getResources().getConfiguration().uiMode |
对应夜间模式,我们会需要在不同的模式下使用不同的资源文件或不同的属性,此时我们可以新建一个带-night
后缀的资源文件夹,然后再创建对应的资源文件即可,比如:drawable-night
、values-night
等。
此时如果应用切换到了夜间模式,将会自动使用-night
后缀中对应的资源。
非夜间模式的后缀是-notnight
,但是因为不是夜间模式就不会使用-night
里的资源所以一般我们没必要使用这个后缀。
按照如上设置了之后还可能会出现一些问题如夜间模式下文字颜色还是黑色的所以看不清了(直接给 TextView 设置了textColor="@color/xxx"
,而比较建议的是直接引用主题属性或者给不同模式设置不同的资源。
如字体颜色一般使用?android:attr/textColorPrimary
,图标颜色一般使用?attr/colorControlNormal
等。
WebView 因为没有特别的处理,所以我们需要通过加载特殊的 css 来完成夜间模式的适配。通过判断现在处于哪种主题然后切换对应的 css 即可。
之前已经讲过了 Android 中 View 的绘制流程,上次主要讲的是
onDraw
方法,这次主要讲的就是在onMeasure
方法中对 View 的大小进行测量。
要了解如何在onMeasure
方法中对 View 进行测量,我们首先需要了解的就是onMeasure
方法传入的两个 int 值:widthMeasureSpec 和 heightMeasureSpec。
它们都是32位的 int 值,高2位代表 SpecMode(测量模式),低30位代表 SpecSize(对应模式下的测量大小)。通过以下的代码我们可以了解到 MeasureSpec 的原理:
1 | private static final int MODE_SHIFT = 30; // Mode 的移位(高2位也就是左移30位) |
因为 Android 中会有大量的 View 存在,所以必然会有很多 MeasureSpec,如果将 MeasureSpec 封装成一个对象必然会造成大量的对象内存分配,这也不难理解为什么要将其包装成一个 int 了。
SpecMode 有三类,我们在前面的代码定义中看到了有五个常量,其中两个是作为工具存在的(MODE_SHIFT 和 MODE_MASK),另外三个就是 SpecMode 了。
该模式下父容器不对 View 的大小有任何限制,一般不做处理。
父容器已经检测出 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 指定的大小。
对应 LayoutParams 中match_parent
以及具体数值。
父容器指定了一个 SpecSize,View 不能大于这个值。
它对应于 LayoutParams 中的wrap_content
。
在 View 测量的时候,会将 Layout_Params 在父容器的约束下转换成对应的 MeasureSpec,然后根据这个 MeasureSpec 确认 View 测量后的宽高。一旦 MeasureSpec 确认了,在onMesure
中就可以确认 View 的测量宽高了。
measure 过程要分为 View 和 ViewGroup,它们的测量是不同的
由其measure
方法完成,该方法是final
关键字修饰的,无法重写。但measure
会调用onMeasure
,所以只需要看onMeasure
如何实现即可。
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
其逻辑很简单,getDefaultSize
方法中可以看出,View 的宽高由 SpecSize 决定。于是我们知道:直接继承 View 的自定义控件需要重写onMeasure
方法并设置wrap_content
时的自身大小,否则使用wrap_content
属性是无效的(等同于match_parent
)。
所以我们可以这样实现来使得wrap_content
生效:
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
在如上代码中我们只需要指定默认最小时的mWidth
,mHeight
即可(wrap_content
的默认宽高),其它模式下交给系统测量即可。
需要注意的是
onMeasure
方法中获取到的测量宽高并不一定就是控件的最终宽高,比如 RelativeLayout 中的控件会有多次测量,LinearLayout 中的子控件如果设置了weight
也会有多次测量,那么第一次onMeasure
的就不会准了。
其实就是在测量自己的宽高之后还会调用measureChildren
来遍历子控件并且测量子控件的大小。
1 | protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { |
ViewGroup 是一个抽象类,其onMeasure
方法是没有具体实现的,所以我们继承 ViewGroup 必须重写onMeasure
,重写该方法需要进行的步骤如下:
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
处理非wrap_content
的情况wrap_content
,即 SpecMode 为AT_MOST
的情况测量子 View 我们可以使用这几个方法
1 | // 使用子view自身的测量方法 |
View 的测量基本就是如上所述了,自定义 View 需要重写onMeasure
方法并对wrap_content
进行特殊处理,其实说起来需要做的并不多,但原理还是满复杂的,全部了解了之后还是觉得学到了不少东西。