Ernest的Blog

一个有理想的Android程序员

0%

在新版本安卓版本(API23以上)中,谷歌把android.net.conn.CONNECTIVITY_CHANGE 这个广播废弃了。总的来说,应用不应该依赖这个广播来执行相关任务,而应该使用 JobScheduler 或者 GCMNetworkManager。计划任务的执行方案还可以用 AlarmManager。 对于国内的开发者来说,这是一个悲剧。Android 现在提供这三种不同的API来执行计划任务.这三种API都有各自的优缺点 。 android.net.conn.CONNECTIVITY_CHANGE 在高版本的系统中不可用了,JobScheduler 在低版本的系统中又不可用, GCMNetworkManager 是什么?在国内不存在。而且这些API之间的不同语义和用法对开发者是一种额外的负担。这种负担体现在我们不仅仅要懂得什么时候使用合适的API,而且要为不同的环境执行同样任务提供不同分支的代码。APP需要在运行时候用合适的条件判断到底要使用哪种方式执行计划任务。幸运的是,开源的世界有伟大的公司为我们提供了灵活的轮子。

Evernote 开源的 Android-Job 为我们带来兼容这三种API的方案,高效,简单,灵活。Android-Job 在运行判断使用哪种API,它提供 AlarmManager, JobSchedulerGcmNetworkManager功能的超集,比如说,我们可以定义计划任务在网络连通且在充电时候执行。

为兼容而生

上面提到的三种方案,兼容维护非常麻烦。为了彰显Android-Job的价值,我们有必要对这三种方案做一个分析。

AlarmManager

AlarmManager 的API一直在改变 ,所以我们需要非常注意我们APP目标运行系统是哪个版本。

** 优点 **

  • 所有设备上可用
  • 发送广播来启动一个服务执行计划很容易

** 缺点 **

  • API在不同系统版本表现不一致
  • 太多样板代码
  • 忽略了设备的状态
    alarmManager

JobScheduler

JobScheduler 只在Lollipop以上版本可用。

** 优点 **

  • 流式API简洁易用
  • 检查设备状态

** 缺点 **

  • 只在API 21+ 可用 (有些功能要求API24+)
  • 平台bug
  • 也是太多样板代码

GcmNetworkManager

GcmNetworkManager 只有在安装有Google Play的设备上可用,而且,没有被墙。

** 优点 **

  • JobScheduler相似的API
  • 要求minSdkVersion 9

** 缺点 **

  • 是 GooglePlay ServicesSDK 的一部分
  • 不能脱离 Play Services 运行
  • API 有很多陷阱

例子

Android-Job项目作者在一篇PPT中详细的说明了这三种方案以及Android-Job的用法。由于篇幅有限且排版原因,强烈建议点击链接去看看。例子简单易懂,在这里给出PPT对应的PDF文件。还在犹豫是否要使用 Android-Job ,看了这个PDF,相信你心里就有肯定的答案了 PDF传送门

使用Android-Job(翻译自项目说明)

请保证点击以下链接检查你使用的是否是最新版本Android-Job最新版链接
添加以下的Gradle配置到安卓项目:

1
2
3
4
// 在app 项目的 build.gradle 文件:
dependencies {
compile 'com.evernote:android-job:1.1.7'
}

如果没有在 Gradle build tools 中关闭 manifest merger,那就不用更多步骤来配置这个工具库了。否则需要手动添加权限生命和services,参考这个AndroidManifest

用法

JobManager 类作为入口。我们自定义的Jobs 需要继承 Job 类。使用对应的Builder类创建一个 JobRequest 然后使用 JobManager 创建这个计划请求。

使用 JobManager 之前必须向初始化这个单例。我们需要提供一个 Context 然后添加一个 JobCreator 实现类. JobCreator 把一个job tag 映射到一个特定的job 类。推荐在 ApplicationonCreate() 方法中 初始化 JobManager 。如果没有 Application 类的访问权限,这是可选的方案

1
2
3
4
5
6
7
8
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
JobManager.create(this).addJobCreator(new DemoJobCreator());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class DemoJobCreator implements JobCreator {

@Override
public Job create(String tag) {
switch (tag) {
case DemoSyncJob.TAG:
return new DemoSyncJob();
default:
return null;
}
}
}

然后我们可以开始计划要执行的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DemoSyncJob extends Job {

public static final String TAG = "job_demo_tag";

@Override
@NonNull
protected Result onRunJob(Params params) {
// run your job here
return Result.SUCCESS;
}

public static void scheduleJob() {
new JobRequest.Builder(DemoSyncJob.TAG)
.setExecutionWindow(30_000L, 40_000L)
.build()
.schedule();
}
}

进阶用法

JobRequest.Builder 类有很多额外的配置选项,例如,我们可以声明条件:需要网络连接。为了使任务周期性执行,用 bundle 传递额外的参数,使得任务在重启后或在特定的时间恢复。
每一个Job 有一个唯一的 ID 。这个 ID 帮助我们辨别 job ,以便将来更新条件或者取消 job。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void scheduleAdvancedJob() {
PersistableBundleCompat extras = new PersistableBundleCompat();
extras.putString("key", "Hello world");

int jobId = new JobRequest.Builder(DemoSyncJob.TAG)
.setExecutionWindow(30_000L, 40_000L)
.setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL)
.setRequiresCharging(true)
.setRequiresDeviceIdle(false)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setExtras(extras)
.setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true)
.build()
.schedule();
}

private void schedulePeriodicJob() {
int jobId = new JobRequest.Builder(DemoSyncJob.TAG)
.setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
.setPersisted(true)
.build()
.schedule();
}

private void scheduleExactJob() {
int jobId = new JobRequest.Builder(DemoSyncJob.class)
.setExact(20_000L)
.setPersisted(true)
.build()
.schedule();
}

private void cancelJob(int jobId) {
JobManager.instance().cancel(jobId);
}

如果非周期性的 Job 执行失败,我们可以设定恢复标准重新计划这个 job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RescheduleDemoJob extends Job {

@Override
@NonNull
protected Result onRunJob(Params params) {
// 一些异常情况发生,稍后重试这个Job
return Result.RESCHEDULE;
}

@Override
protected void onReschedule(int newJobId) {
// 恢复的Job有一个新的 ID
}
}

提醒: 谷歌在安卓 Marshmallow 版本引入自动备份功能,所有的 job 信息保存在一个文件名为 evernote_jobs.xml的 shared preference , 以及文件名为evernote_jobs.db 的数据库中. 我们应该排除这些文件,避免他们被自动备份。
我们可以定义一个resource XML 文件 (比如, res/xml/backup_config.xml) 内容如下:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="evernote_jobs.xml" />
<exclude domain="database" path="evernote_jobs.db" />
</full-backup-content>

然后在应用的 AndroidManifest.xml指向这个文件:

1
<application ...  android:fullBackupContent="@xml/backup_config">

代码混淆

这个库没有使用反射,但是它依赖两个 Service 以及两个 BroadcastReceiver 为了避免任何可能出现的问题,不应该混淆这四个类。这个库打包了它自己的混淆配置,所以我们不需要做任何额外的配置。以防万一,也可以在我们项目的混淆配置中添加这些规则

FAQ

点击这里查看更多的疑问解答。

参考资料

evernote的Blog
作者Ralf的PPT

前两天逛GitHub时候发现了一个命令行手册项目jaywcjlove/linux-command 手册网站,觉得非常实用。GitHub的Pages很方便,但是国内访问有时候比较慢。虽然网上类似的在线手册很多了,但是我觉得一个手册应该是随手可用,而且即使在没有网时候,也可随时翻阅的。于是打算用一天半天把手册做成个离线版简易手册APP。

准备工作

翻了一下GitHub上项目的README,发现作者很贴心的把爬虫抓到的数据保存成MarkDown格式了。不得不说一下,MarkDown是程序员最喜欢的文档格式之一。把项目clone到本地,手册的数据来源就准备好了。再次 感谢作者的辛勤付出。

MarkDown解析

安卓没有原生支持MarkDown的控件,在GitHub上用markdown为关键字搜了一下,pegdown 这个MarkDownn解析库是最多star的。遗憾的是,尽管目前仍然是最流行的MarkDown解析库之一,作者在三个多月前声明这个项目停止维护了。不过作者推荐了@vschflex-mark-java,另一个性能优越的markdown解析库。

flexmark-java解析架构基于commonmark-java,另一款解析库(开源的世界就是这么任性啊),想到罗永浩的那句生命不息,折腾不止,决定先用commonmark-java解析,后面再切换到flexmark-java。尽管是最简单的引用,并没有怎么折腾。

搜索功能

手册的数据,解析都准备好了,还有一个手册必不可少的功能——搜索。以前做项目的时候都是用EditText+Button来实现搜索功能的。虽然一直知道安卓有个原生的searchView,但是项目做视觉设计的小伙伴设计出来的视觉不给searchView用武之地。自从进TV应用开发的坑后,也好久没有开发手机APP了。基于这两个原因,决定在这个小应用中学习一下searchView的使用。
计划实现的是带搜索建议的searchView,这样可以尽量的简化在手机上的输入。searchView的配置很简单,但是搜索建议需要有一定的学习。
安卓现在推荐的是ActionBar中搜索,除了要增加一点actionView的配置,其他的跟searchView大同小异。正所谓磨刀不误砍柴工,何况之前没有系统的学习过searchView,于是开始找百度,找博客补习功课。
以searchView搜索看了几篇配置博文后,发现一个博客把谷歌的searchView介绍做了一个专题的介绍,于是把这几篇文章细读了一遍。在读这几篇译文的时候想起很多大牛的教导,要读就读一手的文档,最好是出自代码作者之手的,再次一点就是读一手文档的翻译。除了这两种文章干货较多,其他类型的文章往往介绍得不够全面。
英文原文(需要翻墙)
译文第一篇:[Searchable(一)](剩下的几篇链接就不贴了,在文末有上下篇的链接跳转)

依照译文的介绍,我对搜索功能有了大概的方向。主要做了以下几点:

  • 把前面获取到的MD文件放到assets目录下,用AssetsManager进行管理,这样就可以不用数据库了。展示命令详情的时候,也可以直接读取到命令对应文件流,再方便不过了。
  • searchView的搜索建议需要有一个ContentProvider来提供搜索建议的数据源,没有数据库怎么用ContentProvider? 答案是MatrixCursorMatrixCursor可以做到模拟数据库的作用,也提供方法方便插入数据等。
  • 有了实现ContentProvider的思路,只需把AssetsManager获取到的命令文件名作为数据,提供给searchView来查询就可以了。

总结

手册的实现并没有什么难度,在补充了相关的知识后,只需要不到半天的时间就可以搞定了。项目虽小,但是还是给我不少收获的。之前看到有一篇文章,名字叫《提问的智慧》。搜索其实就是向机器提问的一种方式,科学的搜索关键词,合适的网站搜索事半功倍。搜索还是首选 Google.comstackoverflow.com

项目地址:
GitHub
coding.net

airbnb开源了一套适用于Android和iOS动效库。虽然尚在快速迭代中,但是已经忍不住在项目中小范围试用。无奈新开源的项目文档比较少。在airbnb设计部门的网站上看到一篇介绍Lottie的文章,试着翻一下。本文是第一次翻译英文文档,翻译不当或不专业的地方欢迎指教。

在过去,为Android,IOS,React Native应用创建复杂的动效,是一个困难又费时的过程。您要么为每种屏幕分辨率添加笨重的图像文件,要么写上千行难以扩展维护的代码。正因为如此,尽管动效是一个沟通的想法和创造引人注目的用户体验的强大工具,大多数应用程序并不使用动效。一年前,我们开始改变这种情况。

今天,我们很高兴介绍我们的解决方案。Lottie是一个可用于iOS,Android和React Native 的库,实时渲染 After Effects动效,并允许开发者在原生应用程序中使用动效像使用图片等静态资源一样简单。Lottie使用一个开源的After Effects插件(Bodymovin)导出的JSON文件形式的动效数据,这个插件捆绑了一个JavaScript播放器,可以在网络上渲染动画。从2015二月开始,Bodymovin的作者Hernan Torrisi,每月通过给插件增加新功能,改进现有功能,为我们的Lottie项目建立了一个坚实的基础。我们的团队(Brandon Withrow负责iOS,Gabriel Peal负责Android,Leland Richardson负责React Native,我负责体验设计)在Torrisi的杰出工作基础上开始Lottie开发之旅。

Lottie使得程序猿无痛实现丰富的动效。如上图所示的Nick Butcher跳跃动效,Bartek Lipinski的汉堡式菜单,Miroslaw Stanek的推特心等等,如果用代码从头开始实现四多么困难和费时。有了Lottie,诸如深入了解框架,预估动画时间,手动创建贝赛尔曲线,重新制作GIF参考动效将成为过去式。现在程序猿可以准确地使用设计师设计的动效,确切地说动效是如何制造的,我们实现的就是怎样。为了展示这一点,我们已经重新创建上面的动画,并在我们的示例应用程序提供After Effects和JSON文件。

我们的目标是尽我们所能支持尽可能多的After Effects 的功能,而不仅仅是简单的图标动画。我们还创建了一些其他示例来显示Lottie库的灵活性、丰富性和进阶功能集。在示例应用程序中,还有各种不同类型动画的源文件,包括基本线条艺术、基于字符的动效以及多角度的logo动效。

我们已经在屏幕的各个角落开始使用我们设计的Lottie动效,包括下图所示的应用程序的通知,全帧动画插图,回访评价页面等。我们计划通过有趣且有用的方式大幅度扩大动画使用。

灵活高效的解决方案

Airbnb是支持数以百万计的房东和房客的全球化公司,所以有一个灵活的动画格式,且可以在多种平台上使用,对我们而言非常重要。现在已经有与Lottie相似的动效库,如Marcus Eckert的Squall和Facebook的Keyframes。但我们的目标稍有不同。Facebook挑选了一小部分After Effects的功能提供支持,因为他们主要集中在交互,但我们想支持尽可能多。至于Squall,Airbnb的设计师用它和Lottie组合使用,因为它有一个神奇的After Effects预览APP,所以成为我们工作中必不可少的一部分。然而,它只支持iOS,但是我们的工程团队需要一个跨平台的解决方案。

Lottie有几个功能内置了它的API,使其更加灵活和高效。它支持从网络上加载JSON文件,这对A / B测试非常有用。它也有一个可选的缓存机制,所以经常使用的动画,如愿望列表的心型图标可以每次加载缓存的副本。Lottie也可以使用动效的进度功能实现用手势调整动画的进度,以及通过改变一个简单的值从而改变动画的速度。iOS版本甚至支持在运行时向动画添加额外的本地UI,可用于复杂的动画转换。

我们工作到目前为止,除了所有的After Effects功能和API的补充,也有很多未来的想法。这些包括映射View到Lottie动画,使用Lottie控制的View的转换,提供Battle Axe的 RubberHose,梯度,类型和图像的支持(译者注:最新版已经支持图像)。最难的部分是下一步先挑选哪些功能实现。

构建社区

发布开源的东西不仅仅是把它放在那里供大家使用。它是连接人们和创造社区的桥梁。随着我们通过GitHub发布Lottie给设计师和程序猿而走得更近,我们希望确保与动画分支连接。(As we got closer to releasing Lottie to designers and engineers via GitHub, we wanted to be sure to connect with the animation folks as well.)

我们的灵感来自9 Squares, Motion Corpse, 以及 Animography 创造的社区。这三个社区都汇集了来自世界各地的人,如果没有这些社区他们将不能在公共动画项目上合作。这些项目需要数月的工作以及组织各自的团队跟人们进行讨论,但这些毋容置疑作为一个整体为动画社区提供了巨大的价值。Motion Corpse 和Animography 公开分享After Effects 源文件,提供关于人们如何创作的不同见解。

追随他们的协作的领先脚步,我们达成协议,这三个团队贡献动画到我们的示例应用程序中。如下图所示,我们已经包括了由J.R. Canest创建一个动画(来自Motion Corpse),一个来自Al Boardman squares的动效 ,和一个使用animography Mobilo动画字体的动效键盘,包含20多个艺术家设计的字母。我们希望整合这些动画社区,强大的工程社区将擦除独特的火花。

我们很乐意听到你如何使用Lottie-不管你是一名设计师,动画设计师还是程序猿。随时联系我们直接与你们的思想,lottie@airbnb.com反馈和见解。我们很高兴看到世界各地的社区以我们从未想过的方式开始使用Lottie。

译者注:airbnb已上线一个Lottie动效采集分享网站
lottiefiles.com

下载 Bodymovin, LottieiOS, Android 以及 React Native.

这篇文章由 Brandon Withrow, Gabriel Peal 以及 Salih Abdul-Karim 共同完成

英文版原文链接
译文原文链接

在上一篇文章greenDAO项目实战中,我们对GreenDao有了初步的认识。今天我们熟悉一下GreenDao中使用ToOneToMany实现一对一,一对多关联映射。本文在上篇的基础上进行。

添加Category类

我们现在对上一篇的Note进行扩展,添加类别。我们假设一个Note属于一个类别Category,一个类别Category可以拥有多个Note.

1
2
3
4
5
6
7
@Entity
public class Category {
@Id
private Long id;
private String name;
@ToMany(referencedJoinProperty = "categoryId")
private List<Note> notes;

修改Note类,Category信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Entity
public class Note {
@Id(autoincrement = true)
private Long id;
@NotNull
private String text;
private String comment;
private java.util.Date date;

@Convert(converter = NoteTypeConverter.class, columnType = String.class)
private NoteType type;

private Long categoryId;

@ToOne(joinProperty = "categoryId")
private Category category;
//省略了其他的geter,setter
}

注解分析

上面的Category类代码中:
@ToMany(referencedJoinProperty = "categoryId")这个注解表明,一个Category可以拥有多个Note(一对多).referencedJoinProperty= "categoryId"表示使用Note类中的categoryId字段进行关联。注意这个字段应该是Long类型.在Category中用 List<Note> notes表示拥有的Note.

上面的Note类代码中:
@ToOne(joinProperty = "categoryId")这个注解表明,一个Note属于一个Category。注意我们添加了private Long categoryId这字段,然后在@ToOne注解中使用joinProperty = "categoryId"CategorycategoryId关联起来。

经过在一的端(Category)使用@ToMany,在多的端(Note)使用@ToOne,我们就把一对多和多对一关联关系建立起来了。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//添加一个类别:技术分享.
Category category = new Category(1L,"技术分享");
categoryDao.insert(category);

//添加一个类别:心灵鸡汤.
category = new Category(2L,"心灵鸡汤");
categoryDao.insert(category);

//添加一条技术分享note记录
Note note = new Note();
note.setText("这是note内容文本,技术分享");
note.setComment("这是note的comment");
note.setDate(new Date());
note.setType(NoteType.TEXT);
note.setCategoryId(1L);//设置分类为技术分类
noteDao.insert(note);

//再添加一条技术分享note记录
note = new Note();
note.setText("这是note内容文本,也是技术分享");
note.setComment("这是note的comment");
note.setDate(new Date());
note.setType(NoteType.TEXT);
note.setCategoryId(1L);//设置分类为技术分类
noteDao.insert(note);

//接着添加一条属于心灵鸡汤Note记录
note = new Note();
note.setText("这是note内容文本,心灵鸡汤");
note.setComment("这是note的comment");
note.setDate(new Date());
note.setType(NoteType.TEXT);
note.setCategoryId(2L);//设置分类为心灵鸡汤
noteDao.insert(note);

//查询所有的分类为技术分享(id=1)的note数据
List<Note> notes = categoryDao.load(1L).getNotes();
System.out.println(notes);
//通过Note反查Category
noteDao.load(1L).getCategory().getName();

//顺便演示一下通过对文本内容的模糊查询匹配返回包含"心灵鸡汤"文本的Note。
notes = noteDao.queryBuilder().where(NoteDao.Properties.Text.like("%心灵鸡汤%")).list();
System.out.println(notes);

前言

老婆在一个艺术培训中心上班,最近她的领导说想要开一个公众号。由于他们对这一块一窍不通,于是咨询起我这个专业人士来。说实话我只是个人捣鼓过微信公众号开发,没有运营或者开发商业微信公众号。但是对于其中的原理还是大概了解的。这不说不要紧,经过跟老婆的一番讨论,发现无论是开发还是运营一个微信公众号,都没有想象中那么简单。

这篇文章不讨论大企业的微信公众号,我们讨论一下像他们这种小微型企业的需求以及他们可能遇到的坑。

需求分析

首先我们明确一下大概的背景和需求。公司有两三个领导,员工不多,几个人负责教务管理,属于培训中心的后勤保障。剩下的就是几十个老师定期上课培训。培训课有中国舞,芭蕾舞,书法画画,围棋等等。每个培训班人数从几个到十几个不等。那使用这套系统的人就有以下几类:

  • 培训中心的领导
  • 教务员工
  • 培训老师
  • 家长学生
  • 目前没有孩子参加培训,将来可能参加的潜家长

系统的使用人群种类不算多,人数顶多也就是上千。我们大概梳理一下可能的需求。

对于领导:

  • 查看中心运营情况。包括财务收支,招生情况,课程开设情况等
  • 管理审批。主要是培训老师的管理,员工管理,以及财务和开设课程管理

对于员工:

  • 查看管理招生情况,课程开设情况等。跟领导的需求差不多,只不过有些数据只有领导有权限查看操作。
  • 上传下达。主要是录入信息向领导汇报财务收支,招生情况,课程运营情况等。向老师和家长通知开停课通知,收费缴费及活动通知等。

对于家长:

  • 中心信息查询。包括中心的办学情况,课程开设情况,老师简介等。
  • 课程查询。包括课程费用,上课时间,剩余课时等。
  • 费用查询。包括缴费记录,剩余费用,续费等。
  • 通知查询。主要是通过微信推送通知信息。

附上跟老婆讨论时候画的一张思维导图,反映了目前大部分需求。
培训管理系统需求

方案选择

摆在他们面前的有两种方案。

  • 购买专门为这个行业定制的一整套系统。
  • 请一个开发团队针对他们的需求进行开发。

两种方案各有优劣,我们用表格的方式对比一下。

购买行业已有系统 找团队定制开发
价格 参差不齐,根据系统功能和公司销售策略而定 根据需求定,可议价性高
后期费用 主要是后期的维护服务费,相关硬件设备(IC卡,打卡机等)购置费 主要是保证系统正常运行的主机,带宽,域名费用等。
功能 功能丰富,能满足大部分需要,但可定制化不高,很多功能用不上 由需求方提出,一般没有累赘的功能。但需求不够明确容易漏掉某些需要的功能
维护 一般由开发公司维护,每个月收取维护服务费 开发完成后有一段维护期,到期后不负责维护或要根据情况收取维护费用
系统稳定性 取决于开发公司,一般有专人维护,能够保证系统正常运行.由于系统不止一家同行业公司使用,系统出现重大问题概率不高 取决于开发质量。在系统试运行期间可能问题较多
数据安全性 取决于开发公司。如果终止合同,使用其他公司产品,或者开发公司倒闭,数据得不到保障 数据保存在云服务器上,可按需备份。
数据可用性 只能在系统内使用,不可定制,进一步分析利用。部分系统可导出到文件 可根据需要对数据进行进一步定制,分析,利用
扩展性 取决于开发公司。有的开发公司提供有限的有偿的二次开发。可扩展性不高 可按需由原开发者或找新的开发者开发定制

结论

经过上面的对比,我们可以看出无论是购买行业已有系统还是找团队定制开发,都不是像去超市购买商品那样一次性搞定。从方案选择就有很多因素需要长远考虑。

总的来说,购买行业已有系统前期要付出的精力会少很多,但是对系统的依赖较大,一旦选择了,对系统就产生了高度的依赖,很难切换到其他的产品。这种依赖会随着时间和数据的积累越来越难摆脱。终止合同或更换系统对已有数据几乎是毁灭性的从头再来。

而找团队定制开发很大程度取决于需求够不够明确,以及开发团队的质量。对于技术没有大概了解的,不容易看出潜在的风险。这风险包括由于开发团队一般不熟悉这个行业,对需求的理解会有所偏差,开发出来的系统是否符合现有需求以及将来的扩展需要,系统的质量也要一段时间的使用才能得知。

其实无论是开一个微信公众号还是开发一个APP,或者是上一套系统,都会面临着同样的问题。这也就很好理解为什么那么多公司迟迟没有用上现代化的系统,甚至仍然停留在纸和笔或者word,excel来处理各种事务了。

在安卓移动APP开发过程中,一般使用安卓默认的sqlite数据库做数据持久化存储。虽然android提供了ContentProvider等框架支持,但是写SQL语句依然是一件不少程序猿的头疼的事。无论是简单的SQL语句,还是复杂的多表查询,都是影响开发效率的苦力活。幸运的是,我们有类似于Hibernate/MyBatis的ORM框架用于安卓开发中。今天我们介绍gitHub上热门的一个专注于Android开发的ORM框架——greenDAO.

greenDAO简介

greenDAO是一个轻量&快速的安卓ORM框架,把对象和Sqlite数据库关联映射起来。它专门为Android做了高度的优化,高性能,消耗极少的内存。
greenDAO首页, 开发文档以及技术支持链接: http://greenrobot.org/greendao/
greendao图解
GreenDAO特色功能

  • 稳健:GreenDAO从2011开始,就被无数著名的应用程序使用
  • 超级简单:简洁直截了当的API,支持注释(V3以上版本)
  • 小巧:框架小于150K,并且只是普通的java jar(没有CPU相关的本地部分)减少构建时间避免超出 65k方法数限制
  • 快速:可能是全球最快的Android平台ORM框架,得益于gradle插件,框架能够自动智能执行代码生成
  • 安全且人性化的查询API:QueryBuilder使用属性常量,避免拼写错误(这是SQL语句拼接查询常见的错误)
  • 强大的连接:跨实体的查询,甚至嵌套复杂的多表链接和多条件关系查询
  • 灵活的属性类型:在实体中使用自定义的类或枚举表示数据
  • 加密:支持SQLCipher加密数据库
  • 活跃的社区:超过7000 GitHub stars

    在项目中使用GreenDAO

    greenDAO 在 Maven 仓库可用. 请保证点击以下链接检查你使用的是否是最新版本greenDAO最新版链接 | greenDAO-generator最新版链接

添加以下的Gradle配置到安卓项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 在项目根目录的 build.gradle 文件:
buildscript {
repositories {
jcenter()
mavenCentral() <-- 添加这个仓库
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1' <-- 添加greendao用到的gradle插件,用于生成GreenDao用到的代码(注意使用最新版本)
}
}

// 在app 项目的 build.gradle 文件:
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' <-- 应用greendao的gradle插件(用于生成GreenDao用到的代码)
//------------
//(可选)配置数据库版本,生成的Dao包路径(无特殊要求除了schemaVersion,daoPackage和targetGenDir一般不用配置,使用默认的)
greendao {
schemaVersion 1
daoPackage 'com.example.greendao'
targetGenDir 'src/main/java'
}

//------------

dependencies {
compile 'org.greenrobot:greendao:3.2.0' <-- 添加greenDao框架依赖
//如果需要数据库加密,要引入一下sqlcipher加密库
//compile 'net.zetetic:android-database-sqlcipher:3.5.4'
}

注意 这些配置使得GreenDao插件在Gradle构建过程中触发,当项目构建时.插件生成 DaoMaster, DaoSession 以及实体类对应的DAO.这些类是GreenDao框架的核心类

声明实体类

首先,我们声明一个实体类Note,一个自定义的枚举类型NoteType。

1
2
3
4
5

public enum NoteType {
TEXT, LIST, PICTURE
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

@Entity
public class Note {

@Id(autoincrement = true)
private Long id;

@NotNull
private String text;
private String comment;
private java.util.Date date;

@Convert(converter = NoteTypeConverter.class, columnType = String.class)
private NoteType type;

@Generated(hash = 1272611929)
public Note() {
}

public Note(Long id) {
this.id = id;
}

@Generated(hash = 1686394253)
public Note(Long id, @NotNull String text, String comment, java.util.Date date, NoteType type) {
this.id = id;
this.text = text;
this.comment = comment;
this.date = date;
this.type = type;
}


@NotNull
public String getText() {
return text;
}

/** 非空值声明,保证这个只在保存到数据之前是非空的**/
public void setText(@NotNull String text) {
this.text = text;
}

//省略了其他的geter,setter

}

上面的实体类代码中:

@Entity注解表明这是一个GreenDao映射到数据的实体类,括号内的配置是可选的。

@Id注解表明这是主键,autoincrement = true表示自增,注意GreenDao主键需要是Long类型,不能是long或者int之类的其他类型。(这也是安卓推荐的主键类型)

@NotNull 注解表明这是一个非空值,在它的setter参数也注明了非空,保证这个只在保存到数据之前是非空的。

@Convert(converter = NoteTypeConverter.class, columnType = String.class)这个注解实现自定义类型到数据库支持的类型的转换。converter = NoteTypeConverter.class指定转换器是NoteTypeConverter这个类(下面马上介绍),columnType = String.class指定数据库对应的列类型是文本类型。通过Convert我们可以用自然的方式表达各种属性。

@Transient上面的例子没有标出。这个注解表明此字段不存储到数据库中,用于不用持久化的字段

值得说明的是,实体类的构造函数和getter/setter是greendao在项目build的时候自动生成的,是不是很方便呢?

接下来我们再看看转换器的写法。转换器听起来很高级有没有?但是写起来出乎意料的简单。

1
2
3
4
5
6
7
8
9
10
11
12
13

public class NoteTypeConverter implements PropertyConverter<NoteType, String> {
@Override
public NoteType convertToEntityProperty(String databaseValue) {
return NoteType.valueOf(databaseValue);
}

@Override
public String convertToDatabaseValue(NoteType entityProperty) {
return entityProperty.name();
}
}

每个转换器都实现了PropertyConverter<P, D> 这个接口。P是实体类中自定义的类型,D是数据库支持的类型。

接口方法convertToEntityProperty接受一个数据库支持类型的参数,返回实体类中自定义的类型。

接口方法convertToDatabaseValue接受一个实体类中自定义的类型的参数,返回数据库支持类型。

通过实现这两个方法,我们就完成了实体类的自定义类型到数据库支持类型的映射。在本例中,NoteType这个自定义的枚举类型得以用文本方式存储到数据库。

在Application中配置DaoSession

接着,我们在App中配置DaoSession,作为数据统一操作的对象,注意DaoSession所在的包如果未配置,默认跟声明的实体类包名相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class App extends Application {
/**是否加密标志。通过这个例子展示给我们greendao从用SQLCipher加密数据库到标准的sqlite切换时多么简单.打开数据库加密需要引入SQLCipher相关的库*/
public static final boolean ENCRYPTED = false;
/**全局公用的DaoSession**/
private DaoSession daoSession;

@Override
public void onCreate() {
super.onCreate();
//DevOpenHelper简单的在数据库需要升级的时候重建数据表,在实际项目中,如果我们需要保存旧版本的数据,需要自己实现一个OpenHelper,把旧版本数据迁移到新版本中。

DevOpenHelper helper = new DevOpenHelper(this, ENCRYPTED ? "notes-db-encrypted" : "notes-db");
Database db = ENCRYPTED ? helper.getEncryptedWritableDb("super-secret") : helper.getWritableDb();
daoSession = new DaoMaster(db).newSession();
}

public DaoSession getDaoSession() {
return daoSession;
}
}

** 提醒 **
记得在AndroidManifest.xml中配置App.

1
2
3
<application
android:name=".App">
</application>

到这里,我们从实体类的声明,数据库操作入口都已经建立好了。没有写一行SQL,是不是有一种畅快淋漓的感觉?下面我们用几个代码片段展示一下如何通过greendao进行数据的增删改查。

查询所有的note列表

1
2
3
4
5
6
7
8
9
10
11

// 获得note对应的DAO。Dao在build的时候自动生成
DaoSession daoSession = ((App) getApplication()).getDaoSession();
//我们通过这个noteDao进行增删改查的操作
NoteDao noteDao = daoSession.getNoteDao();

//查询所有的note数据
List<Note> notes = noteDao.loadAll();
// 查询所有的note数据, 根据note的文本内容根据a-z升序排序.这里的NoteDao.Properties.Text是greendao自动生成的,上面提到的属性常量。
notes = noteDao.queryBuilder().orderAsc(NoteDao.Properties.Text).build().list();

根据id来查询一个note记录,修改后再保存到数据库里

1
2
3
4
5
6
7

Long id = 1L;
Note note = noteDao.load(id);

note.setText("这是修改后的内容文本");
note.setComment("这是修改后的comment");
noteDao.save(note);

删除所有note记录

1
noteDao.deleteAll();

根据id删除一条note记录

1
2
Long id = 1;
noteDao.deleteByKey(id);

添加一条note记录

1
2
3
4
5
6
Note note = new Note();
note.setText("这是note 内容文本");
note.setComment("这是note的comment");
note.setDate(new Date());
note.setType(NoteType.TEXT);
noteDao.insert(note);

后记

greenDao的简单介绍就到这里。后续介绍GreenDao的进阶使用,包括@ToOne 一对一, @ToMany 一对多等使用介绍。同时介绍如何在现有项目中迁移到GreenDao,包括用统一的数据库管理类来对GreenDao进行轻度封装等。

相信大家在移动APP开发中,无论是做安卓还是iOS开发,都会遇到一个很尴尬的问题,接口没有开发好。而请求网络接口数据是开发中开发流程的第一步,因此方便快捷的模拟接口返回数据给APP,有助于提高开发效率。

下面介绍2个工具来搭建模拟接口服务器。

  • moco
  • ngrok

moco

在过去,我们通常部署一个war包到应用服务器,如Jetty或Tomcat等。我们都知道,开发一个war包然后部署到应用程序服务器太无聊,即使我们使用内嵌服务器,只要我们改变一点点,war包就要重新部署。

moco是一款开源的专注于模拟接口数据的开源工具。它有以下优势:

  • 使用简单:提供下载一个jar文件外加简单的json配置就可运行的方式
  • 配置灵活:可以用独立jar包加载json方式配置,也可以写java代码配置
  • 高度可配置:可以对url,参数,请求方法,返回内容等进行配置
  • 扩展强大:HTTPS,Socket,JUnit 集成,Maven 插件,Gradle 插件,Shell,Scala,满足你不同爱好

例子

  • 下载 moco独立运行程序

  • 新建moco配置文件如下(foo.json):

    1
    2
    3
    4
    5
    6
    7
    8
    [
    {
    "response" :
    {
    "text" : "Hello, Moco"
    }
    }
    ]
  • 使用配置文件运行moco独立运行文件.

    1
    java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json
  • 现在,打开浏览器访问 http://localhost:12306 ,你会看见配置的返回内容”Hello, Moco”

更多使用方法,请参考moco的github说明文档


ngrok

如果是个人开发,一个简单灵活的接口服务器就已经搭建成功了。APP只要把上文的localhost换成运行moco电脑的IP就能请求到模拟的数据。但是,手机和电脑需要同一个局域网。如果是团队开发,或者手机和电脑不在同一个局域网怎么办?ngrok就是用来解决这个问题的。

ngrok提供了一个能够在公网安全访问内网Web主机的工具,能捕获所有HTTP请求的内容,也支持TCP端口映射,支持Linux、Windows、Mac OS X 等平台。

注意
ngrok V1.X的版本是可以免费支持将一个固定的二级域名指向本机的,不过作者已经把 V2.X的版本商业化,我们使用一个国内免费提供的服务。网址 http://qydev.com

  1. 下载windows/linux/mac版本的客户端,解压到你喜欢的目录

  2. 在命令行下进入到解压目录下

  3. 执行 ngrok -config=ngrok.cfg -subdomain xxx 12306 //(xxx 是你自定义的域名前缀,12306是moco的端口)

  4. 如果开启成功 你就可以使用 xxx.tunnel.qydev.com 来访问你本机的 127.0.0.1:12306的服务啦

小技巧

打开 http://127.0.0.1:4040 可以查看ngrok访问情况。

在linux下,ngrok 不能用 & 实现后台运行,我们可以使用使用screen这个命令,步骤如下:

安装screen apt-get install screen 运行 screen -S

任意名字(例如:ngork) 然后运行ngrok启动命令

最后按快捷键ctrl+A+D即可保持ngrok后台运行