安卓专家级编程-六-

75 阅读58分钟

安卓专家级编程(六)

原文:Expert Android

协议:CC BY-NC-SA 4.0

十三、Parse 云存储简介

Abstract

一些移动应用可能需要在服务器上存储数据的能力。这些数据可能与用户简档一样简单。或者,如果您的应用是一个游戏,您可能希望存储用户分数。或者,您的应用可能是协作性的,在这种情况下,一个用户可能需要查看另一个用户创建的数据。或者,您可能希望为您的移动应用提供跨设备同步功能。

一些移动应用可能需要在服务器上存储数据的能力。这些数据可能与用户简档一样简单。或者,如果您的应用是一个游戏,您可能希望存储用户分数。或者,您的应用可能是协作性的,在这种情况下,一个用户可能需要查看另一个用户创建的数据。或者,您可能希望为您的移动应用提供跨设备同步功能。

这些类型的数据使用需要一个可供您的移动应用作为服务使用的服务器。这个服务器端技术空间,尤其是针对移动需求的,现在有了一个名字。它被称为 BaaS:后端即服务。现在在 BaaS 领域有大量的公司。这种 BaaS 平台提供的典型功能有:

  • 用户注册
  • 允许知名网站如脸书或 Twitter 的用户直接登录的能力
  • 能够在云中存储任意对象
  • 能够处理无模式对象(如 NoSQL)
  • 立即保存或在一段时间后保存的能力
  • 查询对象的能力
  • 能够在本地设备上缓存查询
  • 使用 Web 控制台来管理用户及其数据
  • 能够发送推送通知
  • 能够对用户及其行为进行分类,并相应地发送推送通知
  • 除了存储数据之外,还能够在云中编写可执行的服务,从而为移动应用提供三层架构

在这一章和接下来的几章中,我们将以一个流行的 BaaS 平台 Parse.com 为例,介绍其中的一些特性。BaaS 领域的其他新兴参与者包括:

  • ACS Appcelerator 云服务(以前为 Cocoafish)
  • 应用 a
  • 堆栈移动
  • 微软 Azure 移动服务
  • 金维
  • 脂肪分形

在这一章中,我们将通过编写一个简单的 Android 应用来探索 Parse cloud API,这个应用将介绍 Parse 的基本特性。这应该为您提供了足够的关于 Parse API 的信息。下一章将集中在关于从属对象的关键细微差别,以及如何通过地块传递解析对象。在接下来的章节中,我们将向您的用户推送通知,以持续吸引他们。

现在让我们转到我们计划使用 Parse 的示例应用。

规划一个示例解析应用

我们在这里创建的示例应用是一个多用户移动应用。也就是说,每个用户都能够创建一个单词,并看到其他用户创建的单词。除了使用 word,这款应用还需要具备基本的用户管理功能,比如注册、登录、重置密码和注销。

图 13-1 显示了当用户第一次遇到应用时,用户将看到的主屏幕。

A978-1-4302-4951-1_13_Fig1_HTML.jpg

图 13-1。

Parse sample application signup/login activity

因此,当用户第一次看到应用时,他或她可以选择注册或登录(如果该用户已经注册),如图 13-1 所示。

Note

Parse 还允许从脸书和 Twitter 等其他网站登录。然而,我们不在本章中讨论这些集成。

设计注册活动

我们先来规划一下注册页面。图 13-2 显示了注册页面的样子。

A978-1-4302-4951-1_13_Fig2_HTML.jpg

图 13-2。

Parse sample application signup activity

作为注册活动的一部分,您将收集用户 ID、电子邮件地址和密码,如图 13-2 所示。然后,您可以调用 Parse API 向用户注册这些详细信息。

设计登录屏幕

注册完成后,用户可以使用如图 13-3 所示的以下登录活动进行登录。(但是,请注意,当用户注册成功时,用户会自动登录!用户需要注销才能看到和使用这个登录屏幕。)

A978-1-4302-4951-1_13_Fig3_HTML.jpg

图 13-3。

Parse sample application login activity

对于图 13-3 所示的登录屏幕,没有什么值得惊讶的。您正在收集用户 ID 和密码,以便您可以调用解析 API 来登录。因为当联系服务器上的 Parse 进行登录时可能会有延迟,所以最好放置一个进度对话框。这种相互作用如图 13-4 所示。

A978-1-4302-4951-1_13_Fig4_HTML.jpg

图 13-4。

Waiting for login to complete

大多数解析 API 都有它们的异步变体。这些 API 提供了在调用返回时调用的回调。在这个回调中,我们可以通过编程关闭进度对话框。

设计密码重置活动

用户可能不记得以前使用过的密码。使用 Parse,很容易实现重置密码功能。重置密码工具的屏幕可能如图 13-5 所示。

A978-1-4302-4951-1_13_Fig5_HTML.jpg

图 13-5。

An activity to reset password

用户只需输入电子邮件地址即可重置密码。图 13-5 中的表单收集邮件地址,一旦用户点击发送密码重置按钮,你就可以调用 Parse API 通过传递邮件来重置密码。Parse 随后会向该电子邮件地址发送一个 web 链接,以便重置密码。

规划应用的主页

一旦用户登录,您将为该应用显示的屏幕是一个欢迎屏幕,如图 13-6 所示。

A978-1-4302-4951-1_13_Fig6_HTML.jpg

图 13-6。

Welcome activity after a successful login

使用这个欢迎屏幕,我们将演示三件事情。第一个是显示到目前为止已经注册的用户列表。这将探索可供用户查询的解析 API。

然后,我们将探索如何创建一个数据对象并将其存储在解析云中。为此,我们将创建一个可以存储在解析云中的“word”对象。

使用图 13-6 所示的创建单词按钮,我们将创建一些单词,然后使用显示单词列表按钮查看到目前为止创建的所有单词。

向用户展示

图 13-7 显示了列出所有注册用户的活动。

A978-1-4302-4951-1_13_Fig7_HTML.jpg

图 13-7。

An activity that shows a list of users

在图 13-7 中,只显示了一个用户,因为目前只有一个用户注册。如果有更多的注册用户,查询将显示所有的用户。如果用户太多,也可以翻页查看。然而,对于这一章,我们不打算显示分页。有关分页查询的详细信息,请参考解析文档。

创建和存储数据对象

图 13-8 显示了允许您创建一个单词并将其存储在解析云中的活动。

A978-1-4302-4951-1_13_Fig8_HTML.jpg

图 13-8。

Creating a word Parse object

一旦收集了单词及其含义,就可以调用解析 API 将其存储在解析云中。这个应用的目标是开发一个简单的基于社区的字典,其中的含义由其他用户提供。

查询单词

一旦用户创建了一个单词,该用户就可以浏览所有现有的单词。图 13-9 显示了显示可用单词集的列表活动。

A978-1-4302-4951-1_13_Fig9_HTML.jpg

图 13-9。

Querying for the list of registered users

图 13-9 中的活动列出了创建的单词、哪个用户创建了该单词以及创建时间。该屏幕还提供了删除单词的功能。“含义”按钮允许多个用户为一个单词提供各自的含义。在这一章中,我们不会进入用于创建和浏览含义的附加屏幕。这些概念类似于创建单词和浏览单词,因为单词和含义都是解析对象。

这就完成了我们计划用 Parse 实现的应用的快速概述。我们现在将介绍 Parse 中的一些基本概念,然后开始为 Parse 设置您的移动应用。

探索语法分析基础

在 Parse 中,对象被存储为一组键值对。对象不必像关系表中的列或类定义中的属性那样坚持预定义的模式。一旦有了 Parse 对象,就可以添加任意多的列;所有这些列的值都根据这些列名存储在该对象中。但是,每次在解析对象中存储它们的值时,都必须指定列名(属性名)。

了解基本 ParseObject

一个解析对象有一个与之关联的类型名。它就像关系数据库中的表名。但是,属于该类型或表名的许多对象可以有一组不同的列和相应的值。这与类型语言中的类型和关系数据库中的表形成了对比。

Parse 中的每个对象都保证有一个对象 ID。解析对象还保留字段(a)对象创建的时间和(b)对象最后更新的时间。您可以将大多数类型的对象作为特定键的值放置。这些对象(那些被指定为值的对象)通常被转换成某种流表示,并根据它们各自的键名进行存储。当这些对象值被检索时,它们作为它们的原始对象类型被检索。因此,一个解析对象可以存储其他解析对象作为给定键的目标值。这意味着一个解析对象可以与其他解析对象有关系,尤其是它的父对象。

比方说,您的应用有 30 种类型的 Java 对象。当存储在 Parse 中时,它们都被表示为无类型的基于键值对的解析对象。需要时,您可以将它们映射到各自的 Java 对象,以保证类型安全。

虽然在很多时候,将无类型对象转换为有类型对象是正确的,但是请记住,在某些情况下,无类型集合允许更多的动态性和灵活性,尤其是当您正在创建一个框架,其中这些对象必须通过各种防火墙时。

了解 ParseUser 对象

一个ParseUser也是一个ParseObject,它提供了一些额外的类型特征,比如用户名、电子邮件等。,而不恢复到基础键名。一个ParseUser还提供了登录、注销等必要的方法。

因为一个ParseUser是一个ParseObject,如果你愿意,你可以把额外的属性放到一个ParseUser对象中。

了解 ParseQuery 对象

与关系数据库不同,没有用于查询解析对象的 SQL。提供了一个 Java API,您可以使用它来检索解析对象。

使用解析查询 API,一次只能检索一种类型的对象。这意味着您不能有选择地连接多个解析对象类型并同时检索它们。

然而,某些类型的连接可以通过其他方式完成,同时遵守这样的约束,即通过 Parse 只能检索一种类型的根级对象。例如,如果我们有一个Word,并且如果这个单词有许多含义,那么我们将这个单词和单词含义表示为两种类型的对象。那么WordMeaning将与它的Word有一个关系作为附加属性。现在,我们可以说“得到所有的WordMeanings,它们的Word是如此如此。”

当您检索一个主对象时,如果需要,您可以让查询包含相关的解析对象。您还可以在解析查询上设置缓存策略,以减少延迟。您可以在保存对象或通过查询检索对象时执行此操作。

这些基础知识应该足以实现我们在本章开始时建议的应用。

设置支持解析的 Android 应用

到目前为止,我们描述的示例应用是使用 Parse API 进行开发的一个很好的选择。我们将实现陈述的用例中指出的所有功能。这将为您提供关于 Parse 平台及其 API 的精彩介绍。

在我们实现示例应用之前,让我们首先了解如何设置 Parse 并开始使用它。

创建解析帐户和应用

parse.com 开始很简单。访问 parse.com 并创建账户。在该帐户中,创建一个应用。一旦创建了应用,您将成为该应用的所有者。您可以创建任意多的应用。

Note

Parse.com 网站上,创建帐户和创建应用所需的链接可能会发生变化。你应该能够导航和完成这些基本任务。

一旦创建了一个应用,Parse 就会创建一组键,您需要用这些键来初始化您的移动应用。Parse 为您的应用提供了一个仪表板,可以方便地复制和粘贴这些键。图 13-10 ,显示了我们创建的第一个示例应用的仪表板。

A978-1-4302-4951-1_13_Fig10_HTML.jpg

图 13-10。

Parse dashboard to locate application keys

出于安全目的,我们截断了密钥,如图 13-10 所示。在这个面板中,您可以将这些键复制并粘贴到您的移动应用中。

我们将很快向您展示如何使用这些键,以便您的应用能够在云端与 Parse 进行通信。但是首先您应该下载必要的 jar 文件并创建 Android 移动应用。为了使用 Parse 引导应用开发,Parse 提供了一个示例 Android 应用,名为 Parse Starter(或 Android Blank Project ),您可以下载并作为起点使用。

Note

随着 Parse 经历多个版本,它可能会更改这个 starter 应用的名称,或者提供一个完全不同的机制来帮助您入门。然而,我们在这里讨论的内容应该适用。

您可以从以下 URL 下载这个样例初学者模板应用: https://www.parse.com/apps/quickstart

到达这个 URL 后,选择您的平台(例如,Android ),下载并在 eclipse ADT 中设置 Parse starter 应用。根据 Parse download 的发行版本,我们在这一章中介绍的内容可能与您下载的内容略有不同,但是一般的原则和方向应该适用。

一旦您在首选 IDE 中下载并设置了 Parse starter 应用,它很可能会失败,并在一个名为ParseApplication.java的文件中显示错误。这是因为 Parse 有意留下了占位符,用于放置我们前面展示的键。一旦你放置了这些键,这个文件将看起来如清单 13-1 所示(考虑到 Parse 的后续版本可能会有额外的代码段!).

清单 13-1。用解析键初始化应用

public class ParseApplication extends Application {

private static String tag = "ParseApplication";

private static String PARSE_APPLICATION_ID

= "vykek4ps.....";

private static String PARSE_CLIENT_KEY

= "w52SGUXv....";

@Override

public void onCreate() {

super.onCreate();

Log.d(tag,"initializing with keys");

// Add your initialization code here

Parse.initialize(this, PARSE_APPLICATION_ID, PARSE_CLIENT_KEY);

// This allows read access to all objects

ParseACL defaultACL = new ParseACL();

defaultACL.setPublicReadAccess(true);

ParseACL.setDefaultACL(defaultACL, true);

Log.d(tag,"initializing app complete");

}

}

在如上所示的代码中插入应用 ID 和客户机密钥后,Parse starter 应用就可以在服务器上使用 Parse 了。代码中的 ACL 代表“访问控制列表”对于本书,我们将其设置为用户创建的所有对象的默认读取权限。除此之外,这本书没有涉及 Parse 的安全方面,建议读者查阅 Parse 文档。

我们几乎准备好开始实现我们所描述的应用了。不过,在此之前,让我们先了解一下 Parse 的一些基本概念。

实现示例应用

在本章的这一部分,我们将使用到目前为止出现的每个屏幕,并展示演示如何使用 parse API 来完成该用例的关键代码片段。如果您对整个应用源代码感兴趣,可以从本章末尾的参考资料部分给出的项目下载 URL 下载。

实施注册

让我们从注册活动开始(见图 13-2 )。在注册活动中,我们将收集用户名、密码和电子邮件地址。使用这些值,清单 13-2 中的代码演示了我们如何使用 Parse API 来注册用户。

清单 13-2。解析用户注册的 API

private void signup(String userid, String email, String password)

{

ParseUser user = new ParseUser();

user.setUsername(userid);

user.setPassword(password);

user.setEmail(email);

//Show the progress dialog

turnOnProgressDialog("Signup", "Please wait while we sign you up");

//Go for signup with a callback

user.signUpInBackground(new SignUpCallback() {

public void done(ParseException e) {

turnOffProgressDialog();

if (e == null) {

// Hooray! Let them use the app now.

signupSuccessful();

} else {

// Sign up didn't succeed. Look at the ParseException

// to figure out what went wrong

signupFailed(e);

}

}

});

return;

}//signup-method

private void signupSuccessful()

{

//Go to signup successful page

//finish

gotoActivity(SignupSuccessActivity.class);

finish();

}

private void signupFailed(ParseException x)

{

//stay on the page

//Put an error message for the exception

String message = x.getMessage();

alert("Signup", "Failed:" + message);

}

请注意,这段代码(以及本章中的其他代码片段)取自一个更大的代码库的中间部分。它将帮助您理解如何使用解析 API。

不要试图编译,因为代码引用了一些本章没有列出的方法。然而,这些方法的意图从它们的命名中应该是清楚的。例如,alert()方法用于放置一个警告对话框。gotoActivity()方法用于将控制转移给另一个活动。turnOnProgressDialog()方法显示进度对话框,直到解析方法通过其回调返回。

现在,将注意力转回到清单 13-2 中解析注册代码的主要流程。我们首先创建一个名为ParseUser的新解析对象,并用我们通过用户注册表单收集的用户相关信息填充它。然后我们调用进度对话框,期待对解析 API signUpInBackground的调用。这个注册 API 是异步的。该方法接受一个回调对象,该对象提供了一个done()回调方法。在done()方法中,我们首先关闭进度对话框。然后我们判断注册方法是成功还是失败。如果注册成功,Parse 会自动将用户登录到设备上。

通过这一步,用户注册。现在,用户可以进行登录、注销等操作。

检测用户是否登录

要知道用户是否登录,您可以使用清单 13-3 中的代码片段:

清单 13-3。显示用户是否登录

private void setViewsProperly()

{

ParseUser pu = ParseUser.getCurrentUser();

if (pu == null)

{

//User is not logged in

showLoggedOutView();

return;

}

//User is logged in

showLoggedInView();

}

我们调用了ParseUsergetCurrentUser()上的静态方法。如果此方法返回有效用户,则该用户已登录。否则,用户不会登录。

注销

您可以使用清单 13-4 中的代码片段从当前会话中注销用户。

清单 13-4。从解析中注销

private void logoutFromParse()

{

ParseUser.logOut();

}

一旦调用这个 logout 方法,ParseUser.getCurrentUser()将返回一个 null,表示当前会话中没有用户。

实现登录

为了理解登录,请注意本章开头介绍的登录活动,并参见图 13-3 。在登录活动中,我们收集用户 ID 和密码。一旦我们有了这些字段,登录的代码就很简单了,如清单 13-5 所示。

清单 13-5。解析 API 以登录

public void login(View v)

{

if (validateForm() == false){

return;

}

//form is valid

String sUserid = getUserid();

String sPassword = getPassword();

turnOnProgressDialog("Login","Wait while we log you in");

ParseUser.logInInBackground(sUserid, sPassword, new LogInCallback() {

public void done(ParseUser user, ParseException e) {

turnOffProgressDialog();

if (user != null) {

reportSuccessfulLogin();

} else {

reportParseException(e);

}

}

});

}//eof-login

private void reportParseException(ParseException e)

{

String error = e.getMessage();

reportTransient("Login failed with:" + error);

}

private void reportSuccessfulLogin()

{

gotoActivity(ParseStarterProjectActivity.class);

finish();

}

同样,login 调用可以作为静态方法在ParseUser对象上使用。我们还通过提供回调在后台使用这种方法,这是 Android 与服务器端内容对话的常用模式。这种模式非常类似于使用 signup API 和几乎所有其他解析 API 的模式,因为它们都有回调,所以它们不会停止主线程。

实施重置密码

如果忘记了密码怎么办?当然,您提供了重置它的方法。参见本章开头介绍的复位活动,如图 13-5 所示。我们使用这个活动来收集帐户的电子邮件地址,并调用ParseUser类上的requestPasswordResetInBackground()静态方法。清单 13-6 中的代码展示了如何使用这个解析重置密码 API。

清单 13-6。通过解析 API 重置密码

public void resetPassword(View v)

{

if (validateForm() == false){

return;

}

String sEmail = email.getText().toString();

turnOnProgressDialog("Reset Password","Wait while we send you email with password reset");

//userid is there

ParseUser.requestPasswordResetInBackground(sEmail

new RequestPasswordResetCallback() {

public void done(ParseException e) {

turnOffProgressDialog();

if (e == null) {

reportSuccessfulReset();

} else {

reportResetError(e);

}

}

});

}//eof-reset

private void reportSuccessfulReset(){

gotoActivity(PasswordResetSuccessActivity.class);

finish();

}

private void reportResetError(ParseException e)

{

//stay on the page

//Put an error message for the exception

String message = e.getMessage();

alert("Reset Password", "Failed:" + message);

}

重置密码解析 API 的模式类似于其他解析 API。您调用方法,提供回调,并处理成功和失败。如果重置密码调用成功,Parse 将向用户发送一封电子邮件,其中包含输入新密码的 web URL。

查询用户

清单 13-7 显示了可以用来查看目前已经注册的用户的代码。参见本章开头的用户活动列表,如图 13-7 所示。

清单 13-7。查询用户

private void populateUserNameList()

{

ParseQuery query = ParseUser.getQuery();

this.turnOnProgressDialog("Going to get users", "Patience. Be Right back");

query.findInBackground(new FindCallback() {

public void done(List<ParseObject> objects, ParseException e) {

turnOffProgressDialog();

if (e == null) {

// The query was successful.

successfulQuery(objects);

} else {

// Something went wrong.

queryFailure(e);

}

}

});

}

private void successfulQuery(List<ParseObject> objects)

{

ArrayList<ParseUserWrapper> userList = new ArrayList<ParseUserWrapper>();

for(ParseObject po: objects)

{

ParseUser pu = (ParseUser)po;

ParseUserWrapper puw = new ParseUserWrapper(pu);

userList.add(puw);

}

ArrayAdapter<ParseUserWrapper> listItemAdapter =

new ArrayAdapter<ParseUserWrapper>(this

,android.R.layout.simple_list_item_1

,userList);

this.setListAdapter(listItemAdapter);

}

private void queryFailure(ParseException x)

{

this.setErrorView(x.getMessage());

}

首先使用ParseUser对象获得一个ParseQuery对象。然后在查询对象上使用 find 方法来查找用户。一旦该方法通过回调返回,您就可以检索到ParseUser对象的集合,并填充如图 13-7 所示的用户列表。当我们讨论查询Word对象时,我们将再次讨论查询对象。

在 Parse 中存储数据对象:创建一个单词

您现在知道了如何注册、登录、注销以及在需要时重置密码。这里显示的下两件事是如何在 Parse 中创建其他对象以及如何查询它们。

让我们从创建一个对象并将其存储在解析云中开始。为此,回想一下开始时出现的创建单词的屏幕(图 13-8 )。在创建单词活动中,您收集单词及其含义,并将其存储在解析云中。要使用 Parse API 做到这一点,你所要做的就是显示伪代码,如清单 13-8 所示。

清单 13-8。用于在云中保存对象的伪代码

ParseObject po = new ParseObject("word_table");

po.put("word", "prow");

po.put("word","I think it means something to do with boats and ships!");

po.saveInTheBackground(...withsomecallback-method...)

这是 Parse 提供的 API 级别。在清单 13-8 给出的 Java 伪代码中,你可以看到我们使用ParseObject作为主要的键值对的无类型集合。创建一个包含两个字段的类型化类Word可能是值得的。下面是一些伪代码(清单 13-9 ),展示了如何做到这一点。

清单 13-9。将有类型的对象作为无类型解析对象的替身的想法

public class Word extends ParseObjectWrapper

{

public Word(String word, String meaning);

public String getWord();

public String setWord(String word);

public String getMeaning();

public String setMeaning();

}

这种类型的正式类定义允许我们将单词视为 Java 对象,而不仅仅是字符串的集合。基类ParseObjectWrapper可以保存底层解析对象,并将该解析对象中的字段值存储为键值对。

下面是从我们可下载的示例项目中提取的ParseObjectWrapper的实际代码,如清单 13-10 所示。

清单 13-10。ParseObjectWrapper 类的源代码

public class ParseObjectWrapper

{

public static String f_createdAt = "createdAt";

public static String f_createdBy = "createdBy";

public static String f_updatedAt = "updatedAt";

public static String f_updatedBy = "updatedBy";

public ParseObject po;

public ParseObjectWrapper(String tablename)

{

po = new ParseObject(tablename);

po.put(f_createdBy, ParseUser.getCurrentUser());

}

public ParseObjectWrapper(ParseObject in)

{

po = in;

}

//Accessors

public ParseObject getParseObject() { return po; }

String getTablename()

{

return po.getClassName();

}

public ParseUser getCreatedBy()

{

return po.getParseUser(f_createdBy);

}

public void setCreatedBy(ParseUser in)

{

po.put(f_createdBy, in);

}

public void setUpdatedBy()

{

po.put(f_updatedBy, ParseUser.getCurrentUser());

}

public ParseUser getLastUpdatedBy()

{

return (ParseUser)po.getParseObject(f_updatedBy);

}

}//eof-class

清单 13-10 显示了我们如何设计这个解析对象包装类作为第一次尝试。它将保存一个对实际ParseObject的引用。解析对象包装器可以通过两种方式获取ParseObject。当您第一次创建一个解析对象时,您可以简单地说出该解析对象的表名是什么。或者,如果您碰巧从云中检索到了一个解析对象,并且想要修改它,那么您可以直接传入该解析对象。

这里是我们在这个解析对象包装器中构建的另一个特性:本机ParseObject并不携带哪个用户创建了它或者最后更新了它。因此,解析对象包装器为这两个附加属性提供了字段名称,以“f_...”开头。这些额外的字段允许我们存储这两种类型的用户(created bylast updated by)和每个ParseObject。给定这个解析对象包装器,清单 13-11 显示了我们如何创建Word类。

清单 13-11。使用 ParseObjectWrapper 表示类型化的对象词

public class Word

extends ParseObjectWrapper

{

//Name of the table or class for this type of object

public static String t_tablename = "WordObject";

//Only two fileds

public static String f_word = "word";

public static String f_meaning = "meaning";

public Word(String word, String meaning)

{

super(t_tablename);

setWord(word);

setMeaning(meaning);

}

public Word(ParseObject po)

{

super(po);

}

public String getWord()

{

return po.getString(f_word);

}

public void setWord(String in)

{

po.put(f_word,in);

}

public String getMeaning()

{

return po.getString(f_meaning);

}

public void setMeaning(String in)

{

po.put(f_meaning,in);

}

public String toString()

{

String word = getWord();

String user = getCreatedBy().getUsername();

return word + "/" + user;

}

}//eof-class

清单 13-11 中的Word类的代码很简单。我们使用了“t_”作为表名和“f_”作为字段名的惯例。这些静态常量与使用它们的方法一样重要。这是因为当您为该对象提供查询时,您将对查询所依据的字段使用这些字符串名称。

有了正确的Word类定义,现在就可以创建一个Word并将其存储在解析云中,如清单 13-12 所示。

清单 13-12。在云中创建和保存一个单词解析对象

public void createWord(View v){

if (validateForm() == false) {

return;

}

//form is valid

String sWord = getWord();

String sMeaning = getMeaning();

Word w = new Word(sWord, sMeaning);

turnOnProgressDialog("Saving Word", "We will be right back");

w.po.saveInBackground(new SaveCallback() {

@Override

public void done(ParseException e) {

turnOffProgressDialog();

if (e == null)    {

//no exception

wordSavedSuccessfully();

}

else    {

wordSaveFailed(e);

}

}

});

}//eof-login

private void wordSaveFailed(ParseException e)

{

String error = e.getMessage();

alert("Saving word failed", error);

}

private void wordSavedSuccessfully()

{

gotoActivity(WordListActivity.class);

//Don't finish it as back button is valid

//finish();

}

请注意,在清单 13-12 所示的代码中,您能够通过其构造函数创建一个Word对象,而不必担心在解析对象上显式设置字段名称。如果需要的话,这也可以确保您不会在每次需要字段名时都输入错误。因为 Parse 对象允许任何字段名,所以如果输入错误,最终会创建不需要的新属性。当然,可能有些地方你有理由这样做,但至少对于大多数常见的情况,这是一个很好的防火墙。

还要注意,在清单 13-12 中,您使用了来自底层解析对象包装器(w.po)的真正的 ParseObject 来启动诸如 save 之类的解析 API。

Note

这里给出的ParseObjectWrapperWord的代码被剥离出来,以向您展示我们当前需求所需的最少代码。如果您查找可下载的项目,您会看到这些类有更多的代码。当你开始通过 Android 包裹传递像“Word”这样的对象时,额外的代码是必要的。(那是下一章的主题。)所以,当您在下载的项目中看到原始代码时,请记住这一点。

现在,让我们将注意力转向如何查询这些word对象,并通过使用 Android 列表适配器将它们绘制在列表中。

查询和填充解析对象

清单 13-13 显示了如何查询类型为Word的对象。这段代码与查询 Parse 用户略有不同。

清单 13-13。查询解析对象列表

private void populateWordList()

{

ParseQuery query = new ParseQuery(Word.t_tablename);

query.orderByDescending(Word.f_createdAt);

query.include(Word.f_createdBy);

query.setCachePolicy(ParseQuery.CachePolicy.CACHE_ELSE_NETWORK);

//Milliseconds

query.setMaxCacheAge(100000L);

this.turnOnProgressDialog("Going to get words", "Patience. Be Right back");

query.findInBackground(new FindCallback() {

public void done(List<ParseObject> objects, ParseException e) {

turnOffProgressDialog();

if (e == null) {

// The query was successful.

successfulQuery(objects);

} else {

// Something went wrong.

queryFailure(e);

}

}

});

}

private void successfulQuery(List<ParseObject> objects)

{

ArrayList<Word> wordList = new ArrayList<Word>();

for(ParseObject po: objects)

{

Word puw = new Word(po);

wordList.add(puw);

}

WordListAdapter listItemAdapter =

new WordListAdapter(this

,wordList

,this);

this.setListAdapter(listItemAdapter);

}

private void queryFailure(ParseException x)

{

this.setErrorView(x.getMessage());

}

如前所述,Parse 使用一个名为ParseQuery的对象来执行查询。Parse query 对象是用您正在查询的对象类型初始化的,并且您使用了Word类来指示这是什么类型。在我们的例子中,变量Word.t_tablename指向表的名称。在 Parse 中,这个表名被称为与对象数据库一致的“类”,而不是关系数据库。

然后,使用作为排序依据的字段名称在查询中设置 order 子句。Word.f_createdBy。再次注意这里如何使用WordParseObjectWrapper上的静态字段定义来避免直接输入字符串名称。

然后以毫秒为单位设置缓存策略,这样就不会过于频繁地访问服务器。然后对查询对象调用findInBackground()方法。另外,请注意,当您被回调时,您获取返回的解析对象,并通过其构造函数将它们填充到一个Word中。这允许您在Word对象上调用常规的 Java 方法,而不用担心基于字符串的字段名。

Note

本章中没有列出但在代码中引用的所有实用函数和类都可以在参考资料部分提到的可下载项目中找到。

这基本上涵盖了使用 Parse 的一般模式,并记录了我们作为用例提出的应用是如何实现的。

下一步是什么

这一章只是对 Parse 的一点皮毛。它可能代表了 Parse 所能提供的四分之一或五分之一。尽管我们在第十五章中使用 Parse 讨论了推送通知,但我们没有空间或时间来讨论本书中的所有 Parse 主题,所以我们建议您参考 Parse 文档,这是非常好的。此外,Parse 论坛是我们见过的最好的论坛之一,包括响应有多快。

此外,在完成本章之前,我们想指出几件值得考虑的事情。首先,我们采用的将解析对象转换成 Java 对象的方法是基本的方法。您可能需要考虑增强它以满足您的需求。有一些努力,像 ParseFacade 和 BaaSFacade,正在尝试更聪明地做到这一点,使用更少的代码和更多的类型安全。考虑这些选项,看看它们如何满足您的需求。例如,BaaSFacade 不仅为 Parse backend 提供了一个 Facade,还为其他源/接收器提供了 facade,比如 StackMob,甚至是本地 SQLLite。

其次,直接解析对象的一个主要缺点是,它们不提供将对象序列化为字符串的方法。当你试图将它们通过额外的意图传递给其他活动时,这是一个问题。如果没有这种能力,您只有两个选择:要么将它们完全转换成普通的 Java 对象,然后通过 intent 防火墙传递它们,要么传递对象的 ID,然后根据它的 ID 重新查询真正的对象。我们希望,通过适当地调整缓存,后一种选择没有那么糟糕。但是手里有一个物体却不能使用它是不自然的,因为你不能通过一个意图把它传递给另一个活动。

第三,请记住,您可以使用 JSON 非常有效地序列化普通 Java 对象,并将它们作为额外的意图进行传输。但是,注意ParseObjects不能转换成 JSON 字符串。因此,您可能必须首先手动将 Parse 对象转换成普通的 Java 对象,然后使用 JSON 将它们作为 parcelables 进行传输。关于 JSON 本地存储的第四章同样适用于实现 JSON parcelables。是的,JSON 比纯手工编码的 parcelables 要冗长一些;然而,如果您的对象很小,这种方法非常有效,可以让您避免编写大量容易出错的代码。

在第十四章的中,我们将提供一个合理的解决方案,向您展示直接使用ParseObjects进行打包的中间方法。如果您迫不及待,可以看看本章的可下载项目,看看 Parse object 包装器是如何被增强以实现 parcelable 的,以及它是如何做出必要的规定以使这种方法起作用的。

因为我们已经实现了本章的示例应用,所以我们使用了第六章中提到的表单处理框架。这个高级表单处理框架为我们节省了大量时间,因为这个应用有许多表单,可以用于登录、注册、创建单词等等。

参考

我们发现以下链接对本章的研究很有帮助。

摘要

协作移动应用无处不在。在云中存储数据是这些类型的应用的基本需求。本章对流行的云存储平台之一 Parse 进行了精彩的介绍。这一章还列出了注册用户和与用户合作的标准模式。它为用户管理提供了必要的解析 API,还解释了如何存储和检索解析对象。最后,本章简要对比了关系连接和非 SQL 方法。

复习问题

以下问题应该有助于巩固您在本章中学到的知识:

What is a Parse application key and a client key?   How do you get the keys in question 1?   How do you initialize an application that needs to talk with Parse with the Parse generated keys?   What Parse APIs do you use to sign up a user?   What Parse APIs do you use to log in a user?   How do you reset a user password with Parse?   How do you log out using Parse?   Are you logged in when the signup is successful?   How is a Parse object different from a relational table?   Does a Parse object maintain the user that has either created the object or last updated it?   Why do you need to use progress dialogs when you are issuing Parse methods?   How do you cache Parse objects?   How do you tell a Parse query to include dependent objects?   Can you change an Android package name while keeping the Parse keys intact?

十四、使用 Parcelables 增强解析

Abstract

在第十三章中,我们记录了 Parse 的基本特性。我们向您展示了如何使用 Parse 创建一个帐户,以及如何使用该帐户注册用户并代表这些用户在云中存储对象。我们还向您展示了查询解析对象的基本机制。

在第十三章中,我们记录了 Parse 的基本特性。我们向您展示了如何使用 Parse 创建一个帐户,以及如何使用该帐户注册用户并代表这些用户在云中存储对象。我们还向您展示了查询解析对象的基本机制。

在这一章中,我们将讨论另外两个与解析相关的主题。首先是需要通过 intent extras 将解析对象作为 parcelables 传递。第二个是基于解析对象与其他解析对象的关系来查询解析对象的能力。

让我们更详细地讨论一下第一个话题。如果您还记得我们为解释 Parse 而介绍的类似字典的应用,有两种类型的对象是该应用特有的:单词及其含义。每个单词可以有不同用户提供的多种含义。我们现在想要创建一个屏幕,在这里我们显示一个给定单词的意思。事实证明,由于 Android 和 Parse 的原因,这有点棘手。

让我们来了解一下显示给定单词含义的屏幕有什么技巧。理想情况下,我们会选择一个Word解析对象,并将其传递给列出该单词含义的活动。一旦我们在 receiving meanings 活动中有了这个Word对象,我们就可以运行一个单词含义查询,它的父单词是传入的单词。

然而,Android SDK 有一个规定,即您不能轻易地将内存中对象的引用传递给另一个活动。只有某些礼仪对象可以这样做(比如实现IBinder的那些)。WordWordMeaning的对象一点礼仪性都没有!它们只是包含一些属性的普通 Java 对象。因此,在 Android 中,您通过 intent extras 将数据(而不是对象引用)传递给其他活动。您可以通过执行以下操作来尝试这一点:

ParseObject wordObject;

Intent i = new Intent(this,MeaningsListActivity.classname);

i.putExtra("word-parse-object", wordObject);

this.startActivity(i);

在这段代码中,变量this指向当前活动。如果成功的话,那么接收活动可以通过这种方式检索 word 对象:

Intent i = this.getIntent();

ParseObject parceledWord = (ParseObject)i.getParcelableExtra();

然而,为了让这个方法工作,类型ParseObject需要实现 Android SDK 接口Parcelable,而ParseObject不做这个!因此,为了克服这个限制,我们将向您展示:

  • 如何通过意图传递一个ParseObject
  • 如何在查询中使用 parceled ParseObject 来检索其子对象

然而,在我们开始探索这两个主题之前,让我们看一下我们将用来实现和说明这些概念的屏幕。

示例应用的用户体验

我们从第十三章中显示的单词列表开始,现在在下面的图 14-1 中给出。

A978-1-4302-4951-1_14_Fig1_HTML.jpg

图 14-1。

List of words activity

你在图 14-1 中看到的是一个单词列表。轻触单词的Meanings按钮,您将进入显示该单词含义的活动(如果有)。图 14-2 就是那个屏幕。

A978-1-4302-4951-1_14_Fig2_HTML.jpg

图 14-2。

List of meanings activity

你在图 14-2 中看到的是从上一个屏幕(图 14-1 )传入的一个单词的可用含义列表。显然,这个活动需要一个 word 对象来绘制自己。因此,当我们稍后实现该活动时,我们将展示 parcelables 在这里是如何发挥作用的。该屏幕还显示了一个为当前单词创建新含义的按钮。图 14-3 显示了为单词创建新含义的屏幕。

A978-1-4302-4951-1_14_Fig3_HTML.jpg

图 14-3。

Create a word meaning activity

虽然图 14-3 屏幕很简单,但是我们需要解决将父 word 对象作为 parcelable 传递的问题,以便我们创建的含义属于正确的 Parse word 对象。此外,因为该屏幕是从单词含义列表屏幕(图 14-2 调用的,我们需要打包一个已经从图 14-1 打包传递的单词解析对象。所以这里的任何解决方案都应该多次传递一个解析对象作为额外的意图。

现在让我们更详细地研究一下这个问题。我们还将在 Android SDK 中讨论包裹和 parcelables 背后的理论。然后,我们将展示如何实现将ParseObjects作为 parcelables 传递的解决方案。

将 ParseObjects 作为 Parcelables 传递

通过一个意图发送一个ParseObject就是让它成为一个Parcelable。但是ParseObject是一个已经被 Parse SDK 定义和冻结的类型。更复杂的是,ParseObject不仅不可打包,而且不可序列化。此外,它不能被转换成一个JSON字符串。如果它是可序列化的或者被转换成一个JSON字符串,我们就可以以那种形式将它传递给 parcelable。

这是一个可能的选择。从一个 Parse 对象中读取所有的值,并把它们放入一个 hashmap 中,然后将这个 hashmap 序列化或转换成一个字符串,然后可以在 parcelable 中传递。另一方面,您获取这个打包的散列表并构造一个解析对象——在某种程度上,您可以将它构造为原始的解析对象。但是,原始解析对象可能有一个您不知道的内部状态,因此它不能被原样复制。

然而,我们还没有使用这种将解析对象映射到 hashmaps 的方法来进行传输。一个原因是,在第十三章中,我们已经有了一个包装原始解析对象的对象。(它被称为ParseObjectWrapper。)所以,我们想用这个包装器作为一个可打包的东西,即使有一些限制。一个限制是,当这个ParseWrapperObject在另一边被重新创建时,它的核心会有一个重新创建的ParseObject,而不是原来的ParseObject。我们可以对此进行一点补偿,因为ParseObjectWrapper上的方法可以检测到嵌入的ParseObject是克隆的,并根据需要进行调整,以给人一种尽可能接近处理真实解析对象的印象。此外,终端用户对象如WordWordMeaning已经扩展了ParseObjectWrapper,所以从ParseObjectWrapper重新创建它们更容易。

所以,我们从ParseObjectWrapper开始,在那个水平上看起来是可行的。因此,我们将坚持使用 parcelable 支持来扩展ParseObjectWrapper。关于 hashmap 方法在其他方面是否更好的研究还没有完成。这些优势目前还不太明显,也不可能变得更好,所以我们坚持使用ParseObjectWrapper作为打包产品的载体。此外,正如第十三章中指出的,如果你要使用基于反射和基于接口的方法,比如 ParseFacade 或 BaaSFacade,你可能会有更好的选择。我们会把这项研究交给你。

让我们看看 parcelables 背后的理论,然后深入研究代码,看看如何使ParseObjectWrapper parcelable。

重温 Parcelables

在 Android 中,Parcels用于在进程间传递消息。它是 Android 中的 IPC(进程间通信)机制。因此,Parcel是一个消息数据流,可以通过进程间的存储转发机制进行实时通信和存储。

一个Parcel可以包含来自对象的数据,该数据在一侧被展平,然后在另一侧被展平,回到对象中。它还可以用来携带服务或文件流的对象引用或代理。一个Parcel可能包含以下内容:

  • 基元
  • 数组(4 字节长度+数据)
  • 实现 parcelable 接口的对象
  • 捆绑包(键值对,其中的值反映了上述任何内容)
  • 使用 IBinder 接口的代理对象
  • 代理的文件描述符对象

可以在 Android SDk 链接: http://developer.android.com/reference/android/os/Parcel.html 了解更多关于ParcelsParcelables的内容。

当一个对象实现Parcelable接口时,该对象向 Android SDK 承诺,它知道如何一个字段一个字段地将自己写入Parcel。这样的一个Parcelable对象也知道如何从Parcel中一个字段一个字段地创建和读取自己。

实现 Parcelable 的简单示例

让我们考虑一个简单的对象,看看它如何实现一个 parcelable,如果它想被传输的话。这个简单的对象,一个User,在清单 14-1 中给出。

清单 14-1。一个简单的可打包实现示例:用户类

public class User

implements Parcelable

{

//Add more field types later

public String userid;

public String username;

public User(String inuserid, String inusername)

{

userid = inuserid;

username = inusername;

}

public static final Parcelable.Creator<User> CREATOR

= new Parcelable.Creator<User>() {

public User createFromParcel(Parcel in) {

return new User(in);

}

public User[] newArray(int size) {

return new User[size];

}

}; //end of creator

//

@Override

public int describeContents() {

return 0;

}

public User(Parcel in)

{

userid = in.readString();

username = in.readString();

}

@Override

public void writeToParcel(Parcel dest, int flags)

{

dest.writeString(userid);

dest.writeString(username);

}

}//eof-class

这个类User只有两个字段:usernameuserid。当这个类实现Parcelable接口时,它需要能够做以下事情:

Understand to see if it needs to handle describeContents(  )   Know how to read and write its constituent attributes   Understand flags to alter the behavior of what and how to write if needed  

描述内容和文件描述符

显然对于 Android SDK 来说,知道一个Parceled对象是否包含文件描述符是很重要的。该信息用于实现方法Bundle.hasFileDescriptors()。该方法又用于防止带有描述符的对象被提供给系统进程。例如,像这样的代码可以在核心 Android SDK 源代码中看到:

//Taken from ActivityManagerService.java

if (intent.hasFileDescriptors()) {

throw new IllegalArgumentException("File descriptors passed in Intent");

}

在我们的例子中,User对象甚至不远程处理文件描述符,所以我们可以安全地返回一个零,表示我们不需要操作系统进行任何特殊处理。(如果你想了解如何处理包含文件描述符的对象,请参考 Android SDK 类ParcelFileDescriptor的源代码。)

向包中读写成员

parcelable 类的第二个职责是从 parcel 对象中读取其成员并将其写入 parcel 对象。如果您看到清单 14-1 中的writeToParcel()方法,您会注意到我们正在将字符串对象写入包流。类似地,要阅读User类的成员,看一看构造函数User(Parcel p)。这个构造函数只是将值读回到它的局部变量中。

writeParcel()方法不同,User类中没有对等的readParcel()方法。相反,Android SDK 要求实现parcelable接口的类提供对知道如何实例化特定类型对象的CREATOR对象的静态引用,比如我们例子中的User。这个CREATOR对象具有createFromParcel()方法,该方法负责通过调用适当的构造函数来实例化User对象,该构造函数将 parcel 对象作为输入。

写时间可打包标志

现在让我们考虑一下可包装旗帜的细节。当您使用parcel.writeParcelable(Parcelable p, int flags)将一个对象写入一个包时,您可以传递标志,以便正在写入的包可以改变要写入的内容。Android SDK 定义和识别的唯一标志是:

PARCELABLE_WRITE_RETURN_VALUE

要知道当这个标志被传递时,Parcelable是否做了什么不同的事情,您需要检查它的文档。例如,ParcelableFileDescriptor使用了这个标志,如果被传入,它将关闭文件描述符并仅仅通过包传递它的值。在我们的例子中,User类根本不使用这个标志。

API 的另一个建议是,如果您的 parcelable 表示一个有状态的对象——比方说,一个文件描述符或一个服务的引用——您可能希望回收资源,只将代理或值传递给那些底层资源。在这种情况下,Android API 推荐这种识别标志的好方法。因此,在打包核心 Android 对象时,请注意这些对象的文档,看看它们是否支持该标志,以及行为是否会受到该标志的影响。在User示例的情况下,确实没有理由识别该标志或对其做出反应。在大多数情况下,当您写入 parcelables 时,您可以始终为describeContents()返回 0,并在写入 parcelables 时忽略这些标志。

现在您已经了解了什么是 parcelable 以及它们是如何工作的,让我们看看如何将ParseObjectWrapper实现为一个 parcelable。

实现 Parcelable ParseObjectWrapper

清单 14-2 展示了ParseObjectWrapper的源代码。当清单中的代码都在一个地方时,最好理解,但是我们将在后面讨论相关的部分。清单很长;快速浏览,浏览各个部分,看看它们是如何组合在一起的。我们解释清单后面的所有关键部分。一旦你通读了这些解释,你就可以重新阅读清单来巩固这个类的结构组成。

清单 14-2。ParseObjectWrapper 的源代码

public class ParseObjectWrapper

implements Parcelable

{

public static String f_createdAt = "createdAt";

public static String f_createdBy = "createdBy";

public static String f_updatedAt = "updatedAt";

public static String f_updatedBy = "updatedBy";

//The parse object that is being wrapped

public ParseObject po;

//Constructors

//Use this when you are creating a new one from scratch

public ParseObjectWrapper(String tablename)    {

po = new ParseObject(tablename);

po.put(f_createdBy, ParseUser.getCurrentUser());

}

//Use this to create proper shell

//For example you can do this in parcelable

public ParseObjectWrapper(String tablename, String objectId)  {

po = ParseObject.createWithoutData(tablename, objectId);

}

//Use this when you are creating from an exsiting parse obejct

public ParseObjectWrapper(ParseObject in)  {

po = in;

}

//To create derived objects like Word using the

//ParseObjectWrapper that is unparceled

public ParseObjectWrapper(ParseObjectWrapper inPow)   {

//Parseobject underneath

po = inPow.po;

//parseobject essentials if it has it

poe = inPow.poe;

}

//Accessors

public ParseObject getParseObject() { return po; }

String getTablename(){return po.getClassName();    }

public ParseUser getCreatedBy(){return po.getParseUser(f_createdBy);}

public void setCreatedBy(ParseUser in){po.put(f_createdBy, in);}

public void setUpdatedBy(){po.put(f_updatedBy, ParseUser.getCurrentUser());}

public ParseUser getLastUpdatedBy(){return

(ParseUser)po.getParseObject(f_updatedBy);    }

//Parcelable stuff

@Override

public int describeContents() {

return 0;

}

public static final Parcelable.Creator<ParseObjectWrapper> CREATOR

= new Parcelable.Creator<ParseObjectWrapper>() {

public ParseObjectWrapper createFromParcel(Parcel in) {

return create(in);

}

public ParseObjectWrapper[] newArray(int size) {

return new ParseObjectWrapper[size];

}

};  //end of creator

@Override

public void writeToParcel(Parcel parcel, int flags)

{

//Order: tablename, objectId, fieldlist, field values, essentials

//write the tablename

parcel.writeString(this.getTablename());

//write the object id

parcel.writeString(this.po.getObjectId());

//write the field list and write the field names

List<ValueField> fieldList = getFieldList();

//See how many

int i = fieldList.size();

parcel.writeInt(i);

//write each of the field types

for(ValueField vf: fieldList)      {

parcel.writeParcelable(vf, flags);

}

//You need to write the field values now

FieldTransporter ft =

new FieldTransporter(this.po

parcel,FieldTransporter.DIRECTION_FORWARD);

for(ValueField vf: fieldList)      {

//This will write the field from parse object to the parcel

ft.transfer(vf);

}

//get the essentials and write to the parcel

ParseObjectEssentials lpoe = this.getEssentials();

parcel.writeParcelable(lpoe, flags);

}

//

private static ParseObjectWrapper create(Parcel parcel)

{

//Order: tablename, objectid, fieldlist, field values, essentials

String tablename = parcel.readString();

String objectId = parcel.readString();

ParseObjectWrapper parseObject =

new ParseObjectWrapper(tablename, objectId);

//Read the valuefiled list from parcel

List<ValueField> fieldList = new ArrayList<ValueField>();

int size = parcel.readInt();

for(int i=0;i<size;i++)

{

ValueField vf = (ValueField)

parcel.readParcelable(

ValueField.class.getClassLoader());

fieldList.add(vf);

}

//add the field values

FieldTransporter ft =

new FieldTransporter(

parseObject.po, parcel

IFieldTransport.DIRECTION_BACKWARD);

for(ValueField vf: fieldList)

{

ft.transfer(vf);

}

//read essentials

ParseObjectEssentials poe =

(ParseObjectEssentials)parcel.readParcelable(

ParseObjectEssentials.class.getClassLoader());

parseObject.setParseObjectEssentials(poe);

return parseObject;

}

//have the derived classes override this

public List<ValueField> getFieldList()

{

return new ArrayList<ValueField>();

}

//To represent createdby and lastupdatedby user objects

//when parceled. We don't recreate them as ParseObjects but save their

//essential attributes in separate objects.

private ParseObjectEssentials poe;

public void setParseObjectEssentials(ParseObjectEssentials inpoe)   {

poe = inpoe;

}

public ParseObjectEssentials getEssentials()

{

if (poe != null) return poe;

Date cat = po.getCreatedAt();

Date luat = po.getUpdatedAt();

ParseUser cby = getCreatedBy();

ParseUser luby = getLastUpdatedBy();

return new ParseObjectEssentials(

cat, User.fromParseUser(cby)

luat, User.fromParseUser(luby));

}

public boolean isParcelled()

{

if (poe != null) return true;

return false;

}

//Utility methods that take into account if this

//object is parceled or not

public User getCreatedByUser()    {

if (!isParcelled())

{

//it is not parcelled so it is original

return User.fromParseUser(getCreatedBy());

}

//it is parcelled

return poe.createdBy;

}

public Date getCreatedAt()    {

if (!isParcelled())

{

//it is not parcelled so it is original

return po.getCreatedAt();

}

//it is parcelled

return poe.createdAt;

}

}//eof-class

在前一章你已经看到了这个类的基础知识。这个类现在在清单 14-2 中被扩展,以实现通过包发送ParseObjectWrapper所需的 parcelable 方法。同样,清单 14-2 中的代码假设清单 14-1 中所示的User类有以下两个额外的静态方法:

public static User getAnnonymousUser() {

return new User("0","Annonynous");

}

public static User fromParseUser(ParseUser pu) {

if (pu == null) return getAnnonymousUser();

//pu is available

String userid = pu.getObjectId();

String username = pu.getUsername();

return new User(userid,username);

}

现在,让我们剖析清单 14-2 中 parcelable ParseObjectWrapper的代码。正如在 parcelable 的讨论中所指出的,这个类的describeContents()方法返回 0,并且这个类也忽略写时间 parcelable 标志。

这个类ParseObjectWrapper中的大部分代码来自于做下面清单 14-3 中伪代码所示的事情的愿望。

清单 14-3。可打包 Parcelable ParseObjectWrapper 的伪代码

public class Word extends ParseObjectWrapper {}

public class WordMeaning extends ParseObjectWrapper {}

//On the sending side

Word wordObject;

Intent i;

i.putExtra(Word.t_tablename, wordObject);

startActivity(i,...);

//In the receiving activity

Intent i = getIntent();

Word parceledWordObject = (Word)i.getExtra(Word.t_tablename);

//Use the parceledWordObject

现在,由清单 14-2 中的 parcelable ParseObjectWrapper代码提供的解决方案并不像它应该的那样准确、精确或纯粹,但是正如你将看到的,它已经非常接近了。如清单 14-3 所示,这种高层次的理解是快速掌握ParseObjectWrapper(清单 14-2)代码的关键。

实现 writeToParcel()

清单 14-2 的关键方法是writeToParcel()方法和静态对应方法createFromParcel()。我们从writeToParcel()开始讨论。在此方法中,您将按顺序将以下元素写入地块:

tablename, objectId, fieldlist, field values, essentials

在另一端重新创建ParseObject需要表名和解析对象 ID。如上所述,没有办法克隆或序列化一个ParseObject。因此,您最终只使用表名及其解析对象 ID 创建了一个新的ParseObject。然后将 Parse 对象拥有的每个属性放入包中。

要将这些字段值从解析对象传输到宗地,您需要两个帐户的帮助。首先,我们的 parcelable 实现背后的理念是,我们不需要像WordWordMeaning这样的派生类来实现Parceleble并流式传输它们自己的字段。我们希望基类ParseObjectWrappper为我们做这项工作。这使得派生类的负担最小。

为了允许基类打包属性,我们希望派生类使用名为getFieldList()的方法来声明它们的字段。该方法返回字段名及其类型的列表。然后,我们可以将这些字段名和类型存储在包中,并在另一端检索它们,以便在新创建的目标解析对象上设置它们。这些字段定义封装在一个名为ValueField的类中,该类有两个属性:字段名及其类型。清单 14-4 是ValueField的代码。

清单 14-4。ValueField:表示字段名和类型的类

public class ValueField

implements Parcelable

{

public static String FT_int = "Integer";

public static String FT_string = "String";

public static String FT_Object = "Object";

public static String FT_unknown = "Unknown";

//Add more field types later

public String name;

public String type;

public ValueField(String inName, String inFieldType)

{

name = inName;

type = inFieldType;

}

public static final Parcelable.Creator<ValueField> CREATOR

= new Parcelable.Creator<ValueField>() {

public ValueField createFromParcel(Parcel in) {

return new ValueField(in);

}

public ValueField[] newArray(int size) {

return new ValueField[size];

}

}; //end of creator

//

@Override

public int describeContents() {

return 0;

}

public ValueField(Parcel in)   {

name = in.readString();

type = in.readString();

}

@Override

public void writeToParcel(Parcel dest, int flags)    {

dest.writeString(name);

dest.writeString(type);

}

public String toString()   {

return name + "/" + type;

}

public static ValueField getStringField(String fieldName)  {

return new ValueField(fieldName, ValueField.FT_string);

}

}//eof-class

因为需要将一个ValueField存储在包中,所以我们通过实现Parcelable各自的方法,将ValueField变成了一个Parcelable。这个ValueField类还为必需的字段类型定义了常量。现在我们只定义了几个类型;您可以通过添加其他允许的基本类型来扩展它。

因此,回头参考一下ParseObjectWrapperwriteParcel()方法,您可以看到将字段名及其类型名写到包中非常简单。

野外运输工具

下一个任务是将ParseObject的每个属性或字段的值写入包中。ParcelParseObject都提供了获取和设置值的类型化方法。所以你需要一个媒人把值从一个传递到另一个。要完成这种转换,您需要使用一个接口和几个类。这些如清单 14-5 所示。

清单 14-5。支持在 ParseObject 和 Parcel 之间传输字段的类

//Transfer value from one source to another

public interface IFieldTransport

{

public static int DIRECTION_FORWARD = 1;

public static int DIRECTION_BACKWARD= 2;

//Transfer from one mode to another

public void transfer(ValueField f);

}

//A class to transport an integer between a

//ParseObject and a Parcel

//ParseObject is source and Parcel is target

//Direction indicates how this value should be transported

public class IntegerFieldTransport

implements IFieldTransport

{

ParseObject po;

Parcel p;

int d = IFieldTransport.DIRECTION_FORWARD;

public IntegerFieldTransport(ParseObject inpo, Parcel inp){

this(inpo,inp,DIRECTION_FORWARD);

}

public IntegerFieldTransport(ParseObject inpo, Parcel inp, int direction)

{

po = inpo;

p = inp;

d = direction;

}

@Override

public void transfer(ValueField f)

{

//1

if (d == DIRECTION_BACKWARD) {

//parcel to parseobject

int i = p.readInt();

po.put(f.name, i);

}

else {

//forward

//parseobject to parcel

int i = po.getInt(f.name);

p.writeInt(i);

}

}

}

public class StringFieldTransport

implements IFieldTransport

{

ParseObject po;

Parcel p;

int d = IFieldTransport.DIRECTION_FORWARD;

public StringFieldTransport(ParseObject inpo, Parcel inp){

this(inpo,inp,DIRECTION_FORWARD);

}

public StringFieldTransport(ParseObject inpo, Parcel inp, int direction)

{

po = inpo;

p = inp;

d = direction;

}

@Override

public void transfer(ValueField f) {

if (d == DIRECTION_BACKWARD)

{

//parcel to parseobject

String s = p.readString();

po.put(f.name, s);

}

else

{

//forward

//parseobject to parcel

String s = po.getString(f.name);

p.writeString(s);

}

}

}

给定这个接口和类型转换器,您可以将它们收集在注册表中,并让注册表处理所有类型的转换。清单 14-6 是一个FieldTransporter的代码,它可以传递所有已知的字段类型。

清单 14-6。FieldTransporter:单个字段传输的注册表

public class FieldTransporter

implements IFieldTransport

{

ParseObject po;

Parcel p;

int d = IFieldTransport.DIRECTION_FORWARD;

Map<String,IFieldTransport> transporterMap;

public FieldTransporter(ParseObject inpo, Parcel inp, int direction){

po = inpo;

p = inp;

d = direction;

//Register the all the translators/tranporters

register();

}

private void register()

{

transporterMap = new HashMap<String,IFieldTransport>();

//register integers

transporterMap.put(

ValueField.FT_int

new IntegerFieldTransport(po,p,d));

//register string transporter

transporterMap.put(

ValueField.FT_string

new StringFieldTransport(po,p,d));

//Other missing transporters

}

private IFieldTransport getTransportFor(String fieldType)

{

IFieldTransport ift = transporterMap.get(fieldType);

if (ift == null)

{

throw new RuntimeException("Problem with locating the type");

}

return ift;

}

@Override

public void transfer(ValueField f)

{

IFieldTransport ift = getTransportFor(f.type);

ift.transfer(f);

}

}//eof-class

使用这个FieldTransporter,您现在可以很容易地看到writeParcel()方法是如何神奇地为从parseobjectparcel的所有字段写入值的。清单 14-7 中重复了这段代码,以便快速查看。

清单 14-7。如何使用野外运输车

//add the field values

FieldTransporter ft =

new FieldTransporter(

parseObject.po, parcel, IFieldTransport.DIRECTION_BACKWARD);

for(ValueField vf: fieldList)

{

ft.transfer(vf);

}

看看清单 14-7 中的FieldTransporter是如何用源、目标和方向实例化的;然后,对于由ParseObjectWrapper给出的每个ValueField,字段值被传输。这种方法保持了ParseObjectparcel之间写操作的类型安全。但是请记住,由于您可能有更多的字段类型,您将需要为这些类型创建 transporters,并将它们添加到上面的FieldTransporter注册中。

什么是 ParseObjectEssentials?

到目前为止,我们已经收集了源ParseObject的原始属性,并且已经转移。然而,源解析对象有一些指向其他解析对象的属性。这尤其包括两个用户对象:创建解析对象的用户和最后更新它的用户。

潜在地,您也可以将 parcelables 概念扩展到这些子对象。然而,为了我们的目的,我们采用一种更简单的方法。我们剥离这两个用户对象的本质,并将它们封装到一个自己开发的User对象中,如本章开头所述。然后,我们在一个名为ParseObjectEssentials的合并对象中捕获两个用户。一旦我们从当前被打包的ParseObject中提取出这个ParseObjectEssentials,我们就可以打包ParseObjectEssentials来代替子对象或相关的解析对象。清单 14-8 给出了ParseObjectEssentials的定义。

清单 14-8。ParseObjectEssentials 的概念

public class ParseObjectEssentials

implements Parcelable

{

//Add more fields if desired from their respective ParseObjects

public Date createdAt;

public User createdBy;

public Date lastUpdatedAt;

public User lastUpdatedBy;

public ParseObjectEssentials(Date createdAt, User createdBy

Date lastUpdatedAt, User lastUpdatedBy) {

super();

this.createdAt = createdAt;

this.createdBy = createdBy;

this.lastUpdatedAt = lastUpdatedAt;

this.lastUpdatedBy = lastUpdatedBy;

}

public static final Parcelable.Creator<ParseObjectEssentials> CREATOR

= new Parcelable.Creator<ParseObjectEssentials>() {

public ParseObjectEssentials createFromParcel(Parcel in) {

return new ParseObjectEssentials(in);

}

public ParseObjectEssentials[] newArray(int size) {

return new ParseObjectEssentials[size];

}

}; //end of creator

@Override

public int describeContents() {

return 0;

}

public ParseObjectEssentials(Parcel in)

{

createdAt = new Date(in.readLong());

createdBy = (User)in.readParcelable(User.class.getClassLoader());

lastUpdatedAt = new Date(in.readLong());

lastUpdatedBy = (User)in.readParcelable(User.class.getClassLoader());

}

@Override

public void writeToParcel(Parcel dest, int flags)

{

dest.writeLong(this.createdAt.getTime());

dest.writeParcelable(createdBy, flags);

dest.writeLong(lastUpdatedAt.getTime());

dest.writeParcelable(lastUpdatedBy, flags);

}

public static ParseObjectEssentials getDefault()

{

Date cat = new Date(0);

User auser = User.getAnnonymousUser();

Date luat = new Date(0);

return new ParseObjectEssentials(cat,auser,luat,auser);

}

}//eof-class

为了完成ParseObjectWrapperwriteToParcel(),你在包裹中保存这些ParseObjectEssentials中的一个。

接下来,我们来看看ParseObjectWrapper代码,看看如何从ParseObjectWrapper的内核嵌入式ParseObject中获得这个ParseObjectEssentials

另一方面:重新创建 ParseObjectWrapper

作为一个可打包的包,ParseObjectWrapper需要能够从包中重新创建自己。参见静态的create()函数,该函数从给定的包裹中返回ParseObjectWrapper。在这种方法中,你逆向阅读,如下所示。

首先,读取属于被打包的旧解析对象的表名和对象 ID。根据这两个参数,可以创建一个解析 shell 对象。接下来,读取字段定义,查看有多少属性可用于这个解析对象并被打包。然后使用字段传输器将每个字段及其值传输到新创建的ParseObject。你用这个ParseObject创造一个新的ParseObjectWrapper。此时,您已经有了准备返回的ParseObjectWrapper。但是,你也需要读取ParseObjectEssentials并将其设置在ParseObjectWrapper上。

ParseObjectWrapper变得非常上下文敏感,因为它可能处于多个状态。当它最初被创建时,它只是持有一个仅仅用它的表名(甚至没有 ID)创建的ParseObject,因为ParseObject甚至没有被保存在解析云中。在下一个状态,ParseObjectWrapper可能持有完全保存在解析云中的ParseObject。然后,在第三种状态下,ParseObjectWrapper可能已经被打包并在另一端重新创建。在这最后一个状态中,它保存的ParseObject只是一个替身,并不与服务器绑定。ParseObjectWrapper也持有一个ParseObjectEssentials,所以可以询问ParseObjectWrapper是否打包。

我在ParseObjectWrapper上创建了一些方法,比如 last created user 和 last updated by,以便它们考虑到ParseObjectWrapper所处的状态,并相应地返回正确的值。

把这个词分成两半

我们来看看如何把一个Word物体打包成包裹带回去。清单 14-9 显示了基于我们新的ParseObjectWrapperWord的定义。

清单 14-9。扩展 Parcelable ParseObjectWrapper

public class Word

extends ParseObjectWrapper

{

public static String t_tablename = "WordObject";

public static String PARCELABLE_WORD_ID = "WordObjectId";

//Only two fileds

public static String f_word = "word";

public static String f_meaning = "meaning";

//Constructors: A new word from scratch

public Word(String word, String meaning){

super(t_tablename);

setWord(word);

setMeaning(meaning);

}

//Wrapping from a ParseObject gotten from the cloud

public Word(ParseObject po)    {

super(po);

}

//Recreated using a previously Parceled word

public Word(ParseObjectWrapper inPow)    {

super(inPow);

}

//Accessors

public String getWord()    {

return po.getString(f_word);

}

public void setWord(String in)    {

po.put(f_word,in);

}

public String getMeaning()    {

return po.getString(f_meaning);

}

public void setMeaning(String in)    {

po.put(f_meaning,in);

}

public String toString()

{

String word = getWord();

String user = getCreatedBy().getUsername();

return word + "/" + user;

}

//have the children override this

@Override

public List<ValueField> getFieldList()

{

ArrayList<ValueField> fields = new ArrayList<ValueField>();

fields.add(ValueField.getStringField(Word.f_word));

fields.add(ValueField.getStringField(Word.f_meaning));

return fields;

}

}//eof-class

如果你回头看第十三章,你会发现这个版本的Word非常相似。主要的增加是Word类现在通过覆盖getFieldList()来提供它的字段列表。它还有一个接受ParseObjectWrapper作为输入的构造函数。这个构造函数在重新创建穿过包裹的Word时非常有用。

我们现在准备实施本章开始时介绍的WordMeanings活动(见图 14-2 )。

实施单词含义列表活动

实施WordMeaningsListActivity的主体包括以下内容:

How to invoke this activity by passing a Word through an intent   How to retrieve that word from the intent   How to access the word so that you can change the title of the activity based on the input word   How you use the word to query for its word meanings  

传递单词作为额外的意图

图 14-1 中的单词列表活动屏幕显示每一行代表一个由列表 14-9 中给出的单词类定义的Word对象。如果点击图中的Meanings按钮,将需要调用图 14-2 所示的WordMeaningsListActivity。清单 14-10 显示了如何调用这个活动。

清单 14-10。传递单词作为额外的意图

private void respondToClick(WordListActivity activity, Word wordRef)

{

Intent i = new Intent(activity,WordMeaningsListActivity.class);

i.putExtra(Word.t_tablename,wordRef);

activity.startActivity(i);

}

从额外的意图中重新创造这个词

注意单词 object 是如何作为 parcelable 传递给 intent extra 的。让我们看看如何让Word对象回到另一边。清单 14-11 显示了从WordMeaningsListActivity中再次获取Word对象的代码片段。

清单 14-11。从 Intent Extra 中检索键入的单词

private Word getParceledWordFromIntent()

{

Intent i = this.getIntent();

ParseObjectWrapper pow =

(ParseObjectWrapper)i.getParcelableExtra(Word.t_tablename);

Word parceledWord = new Word(pow);

return parceledWord;

}

注意如何首先从 intent extra 中检索到一个ParseObjectWrapper,然后用它来包装Word对象。

在目标活动中使用检索到的 Word 对象

您可以使用 Word 对象来访问它的所有属性并使用它的方法。清单 14-12 显示了如何设置活动的标题。

清单 14-12。使用 Word 访问器方法的伪代码

Word parceledWord;

activity.setTitle(parceledWord.getWord());

请注意,您可以通过 intent 传递单词并使用它,而无需求助于底层的 Parse 对象。如果您不能通过 intent 传递单词,那么您必须只传递单词的解析对象 ID,并再次查询解析后端以检索单词对象,从而获得其单词字符串值并确定是谁在何时创建的。正是为了避免对服务器的第二次查询,我们才不厌其烦地编写了这么多打包代码。

使用检索到的单词来搜索其含义

让我们看看如何使用这个打包的Word对象来检索WordMeanings。首先,让我们看看一个Word和一个WordMeaning是如何连接的,如清单 14-13 所示。

清单 14-13。词义的源代码

public class WordMeaning extends ParseObjectWrapper

{

//Design the table first

public static String t_tablename = "WordMeaningObject";

public static String f_word = "word";

public static String f_meaning = "meaning";

public WordMeaning(String wordMeaning, Word inParentWord)

{

super(t_tablename);

setMeaning(wordMeaning);

setWord(inParentWord);

}

//Make sure there is a way to construct with a straight

//Parse object

public WordMeaning(ParseObject po)

{

//Create a check in the future if it is not of the same type

super(po);

}

public void setMeaning(String meaning)   {

po.put(f_meaning, meaning);

}

public void setWord(Word word)   {

po.put(f_word, word.po);

}

public String getMeaning()   {

return po.getString(f_meaning);

}

public Word getWord()   {

return new Word(po.getParseObject(f_word));

}

}

一个WordMeaning带有一个指向其父词的属性。您可以使用这个属性来查询一个给定单词的所有词义,如清单 14-14 所示。

清单 14-14。使用打包的单词查询其含义

private void populateWordMeaningsList(Word word)

{

ParseQuery query = new ParseQuery(WordMeaning.t_tablename);

query.whereEqualTo(WordMeaning.f_word, word.po);

query.orderByDescending(WordMeaning.f_createdAt);

//Include who created me

query.include(WordMeaning.f_createdBy);

//Include who the parent word is

query.include(WordMeaning.f_word);

//How can We include the owner of the word

query.include(WordMeaning.f_word + "." + Word.f_createdBy);

this.turnOnProgressDialog("Going to get word meanings for:" + word.getWord()

"Patience. Be Right back");

query.findInBackground(new FindCallback() {

public void done(List<ParseObject> objects, ParseException e) {

turnOffProgressDialog();

if (e == null) {

// The query was successful.

successfulQuery(objects);

} else {

// Something went wrong.

queryFailure(e);

}

}

});

}

private void successfulQuery(List<ParseObject> objects)

{

this.setEmptyViewToNoRows();

ArrayList<WordMeaning> wordMeaningList = new ArrayList<WordMeaning>();

for(ParseObject po: objects)

{

WordMeaning wordMeaning = new WordMeaning(po);

wordMeaningList.add(wordMeaning);

}

WordMeaningListAdapter listItemAdapter =

new WordMeaningListAdapter(this

,wordMeaningList

,this);

this.setListAdapter(listItemAdapter);

}

private void queryFailure(ParseException x)

{

this.setErrorView(x.getMessage());

}

这段代码与上一章给出的查询单词的代码非常相似。这里的区别在于如何指定涉及父单词解析对象的“where”子句。下面是刚刚给出代码中的一行:

query.whereEqualTo(WordMeaning.f_word, word.po);

请注意,您可以像在任何其他场合一样使用打包的 word 对象。其余代码与第十三章中给出的代码非常相似。

创造一个词的意义

让我们把注意力转向图 14-2 中的创建含义按钮,它调用了创建单词活动。在这里,你也可以直接从打包的单词中受益。清单 14-15 显示了如何将已经打包的单词再一次通过 intent 传递给 create word meaning 活动。

清单 14-15。将已打包的单词转移到另一个活动

public void createWordMeaning(View v)

{

Intent i = new Intent(this,CreateAMeaningActivity.class);

i.putExtra(Word.t_tablename,parceledWord);

startActivity(i);

}

注意被打包的单词是如何通过 intent extra 再次打包的。这意味着ParseObjectWrapper需要成功地知道它的状态和包裹,不管它以前没有被包裹还是已经被包裹。您可以在writeToParcel()方法中看到这一点,也可以在ParseObjectWrappercreate()方法中从包创建ParseObjectWrapper时看到这一点。

清单 14-16 显示了如何为CreateWordMeaning活动检索打包的单词。请注意,这段代码与第一次检索打包单词的代码相同,如清单 14-11 所示。

清单 14-16。检索被打包两次的单词

private Word getParceledWordFromIntent()

{

Intent i = this.getIntent();

ParseObjectWrapper pow =

(ParseObjectWrapper)i.getParcelableExtra(Word.t_tablename);

Word parceledWord = new Word(pow);

return parceledWord;

}

清单 14-17 显示了如何使用检索到的单词来填充显示在CreateWordMeaning活动中所需的单词细节。

清单 14-17。使用 ParseObjectEssentials 访问器方法

private String getWordDetail(Word pword)

{

String by = pword.getCreatedByUser().username;

Date d = pword.getCreatedAt();

DateFormat df = SimpleDateFormat.getDateInstance(DateFormat.SHORT);

String datestring =  df.format(d);

return by + "/" + datestring;

}

最后,清单 14-18 展示了如何使用传入的Word在解析云中创建一个WordMeaning

清单 14-18。打包的 ParseObjects 的进一步使用

public void createMeaning(View v)

{

if (validateForm() == false)   {

return;

}

//get meaning from the text box

String meaning = getUserEnteredMeaning();

WordMeaning wm = new WordMeaning(meaning, parceledWord);

turnOnProgressDialog("Saving Word Meaning", "We will be right back");

wm.po.saveInBackground(new SaveCallback() {

@Override

public void done(ParseException e) {

turnOffProgressDialog();

if (e == null)    {

wordMeaningSavedSuccessfully();

}

else {

wordMeaningSaveFailed(e);

}

}

});

}

private void wordMeaningSaveFailed(ParseException e) {

String error = e.getMessage();

alert("Saving word failed", error);

}

private void wordMeaningSavedSuccessfully(){

alert("word meaning saved", "Success");

}

请注意,在这段代码中,经过两次打包的单词被用作保存单词含义的父单词属性的直接目标。

参考

第十三章中给出的解析参考适用于此处。以下附加链接进一步支持本章中的内容。

摘要

本章涵盖了如何使用 parcelables 有效开发 Parse 的关键主题。我们已经展示了 parcelables 如何在 Android 中工作的详细架构。我们已经解释了为什么 parcelables 在用 Android 和 Parse 编码时很重要。我们已经展示了一个可行的框架,你可以直接使用它,也可以修改它来创建一个全新的框架,以满足既定的指导方针。在第十五章中,我们将介绍解析推送通知。

复习问题

以下问题有助于巩固您在本章中学到的知识:

Why are parcelables important while working with Parse in Android?   How do you implement a parcelable?   What is Parcelable.describeContents()?   What are parcelable flags?   What is the creator static method in a parcelable?   Are ParseObjects parcelable?   Are ParseObjects serializable?   Can ParseObjects be converted to JSON strings?   How can you query for Parse objects where an attribute points to another Parse object?   Can you create a Parse object using its Parse object ID?