如何使用用于 Android 的 Azure 移动应用 SDKHow to use the Azure Mobile Apps SDK for Android

Note

Visual Studio App Center 正在投资于对移动应用开发至关重要的新集成服务。Visual Studio App Center is investing in new and integrated services central to mobile app development. 开发人员可以使用生成测试分发服务来设置持续集成和交付管道。Developers can use Build, Test and Distribute services to set up Continuous Integration and Delivery pipeline. 部署应用后,开发人员可以使用分析诊断服务监视其应用的状态和使用情况,并使用推送服务与用户互动。Once the app is deployed, developers can monitor the status and usage of their app using the Analytics and Diagnostics services, and engage with users using the Push service. 开发人员还可以利用 Auth 对用户进行身份验证,利用数据服务在云中持久保存和同步应用数据。Developers can also leverage Auth to authenticate their users and Data service to persist and sync app data in the cloud. 立即查看 App CenterCheck out App Center today.

本指南说明如何使用用于移动应用的 Android 客户端 SDK 来实现常见方案,例如:This guide shows you how to use the Android client SDK for Mobile Apps to implement common scenarios, such as:

  • 查询数据(插入、更新和删除)。Querying for data (inserting, updating, and deleting).
  • 身份验证。Authentication.
  • 处理错误。Handling errors.
  • 自定义客户端。Customizing the client.

本指南侧重于客户端 Android SDK。This guide focuses on the client-side Android SDK. 若要详细了解移动应用的服务器端 SDK,请参阅 Work with .NET backend SDK(使用 .NET 后端 SDK)或 How to use the Node.js backend SDK(如何使用 Node.js 后端 SDK)。To learn more about the server-side SDKs for Mobile Apps, see Work with .NET backend SDK or How to use the Node.js backend SDK.

参考文档Reference Documentation

可以在 GitHub 上找到有关 Android 客户端库的 Javadocs API 参考You can find the Javadocs API reference for the Android client library on GitHub.

支持的平台Supported Platforms

用于 Android 的 Azure 移动应用 SDK 支持手机和平板电脑外形规格的 API 级别 19 到 24(KitKat 到 Nougat)。The Azure Mobile Apps SDK for Android supports API levels 19 through 24 (KitKat through Nougat) for phone and tablet form factors. 具体而言,身份验证利用通用 Web 框架方法收集凭据。Authentication, in particular, utilizes a common web framework approach to gather credentials. 服务器流身份验证不适用于手表等小型设备。Server-flow authentication does not work with small form factor devices such as watches.

安装与先决条件Setup and Prerequisites

完成移动应用快速入门教程。Complete the Mobile Apps quickstart tutorial. 此任务可确保满足开发 Azure 移动应用的所有先决条件。This task ensures all pre-requisites for developing Azure Mobile Apps have been met. 快速入门还帮助配置帐户及创建第一个移动应用后端。The Quickstart also helps you configure your account and create your first Mobile App backend.

如果决定不完成快速入门教程,请完成以下任务:If you decide not to complete the Quickstart tutorial, complete the following tasks:

更新 Gradle 生成文件Update the Gradle build file

更改以下两个 build.gradle 文件:Change both build.gradle files:

  1. 将以下代码添加到项目 级别的 build.gradle 文件:Add this code to the Project level build.gradle file:

    buildscript {
        repositories {
            jcenter()
            google()
        }
    }
    
    allprojects {
        repositories {
            jcenter()
            google()
        }
    }
    
  2. 将以下代码添加到依赖关系标记内模块应用级别的 build.gradle 文件:Add this code to the Module app level build.gradle file inside the dependencies tag:

    implementation 'com.microsoft.azure:azure-mobile-android:3.4.0@aar'
    

    目前的最新版本为 3.4.0。Currently the latest version is 3.4.0. bintray 中列出了支持的版本。The supported versions are listed on bintray.

启用 Internet 权限Enable internet permission

若要访问 Azure,应用中必须已启用 Internet 权限。To access Azure, your app must have the INTERNET permission enabled. 如果尚未启用,请将以下代码行添加到 AndroidManifest.xml 文件:If it's not already enabled, add the following line of code to your AndroidManifest.xml file:

<uses-permission android:name="android.permission.INTERNET" />

创建客户端连接Create a Client Connection

Azure 移动应用为移动应用程序提供四项功能:Azure Mobile Apps provides four functions to your mobile application:

  • 使用 Azure 移动应用服务进行数据访问和脱机同步。Data Access and Offline Synchronization with an Azure Mobile Apps Service.
  • 调用使用 Azure 移动应用服务器 SDK 编写的自定义 API。Call Custom APIs written with the Azure Mobile Apps Server SDK.
  • 使用 Azure 应用服务身份验证和授权进行身份验证。Authentication with Azure App Service Authentication and Authorization.
  • 使用通知中心进行推送通知注册。Push Notification Registration with Notification Hubs.

若要使用其中的每项功能,首先需要创建一个 MobileServiceClient 对象。Each of these functions first requires that you create a MobileServiceClient object. 只应在移动客户端中创建一个 MobileServiceClient 对象(即,该客户端应该采用单一实例模式)。Only one MobileServiceClient object should be created within your mobile client (that is, it should be a Singleton pattern). 若要创建 MobileServiceClient 对象,请执行以下操作:To create a MobileServiceClient object:

MobileServiceClient mClient = new MobileServiceClient(
    "<MobileAppUrl>",       // Replace with the Site URL
    this);                  // Your application Context

<MobileAppUrl> 是一个字符串,或者是指向移动后端的 URL 对象。The <MobileAppUrl> is either a string or a URL object that points to your mobile backend. 如果使用 Azure 应用服务来托管移动后端,请确保使用 URL 的安全 https:// 版本。If you are using Azure App Service to host your mobile backend, then ensure you use the secure https:// version of the URL.

客户端还需要能够访问活动或上下文 - 本示例中的 this 参数。The client also requires access to the Activity or Context - the this parameter in the example. MobileServiceClient 构造应发生在 AndroidManifest.xml 文件中引用的活动的 onCreate() 方法内。The MobileServiceClient construction should happen within the onCreate() method of the Activity referenced in the AndroidManifest.xml file.

最佳做法是将服务器通信抽象化为其自身的(单一实例模式)类。As a best practice, you should abstract server communication into its own (singleton-pattern) class. 在本例中,应该传递构造函数中的活动,以便适当地配置服务。In this case, you should pass the Activity within the constructor to appropriately configure the service. 例如:For example:

package com.example.appname.services;

import android.content.Context;
import com.microsoft.windowsazure.mobileservices.*;

public class AzureServiceAdapter {
    private String mMobileBackendUrl = "https://myappname.chinacloudsites.cn";
    private Context mContext;
    private MobileServiceClient mClient;
    private static AzureServiceAdapter mInstance = null;

    private AzureServiceAdapter(Context context) {
        mContext = context;
        mClient = new MobileServiceClient(mMobileBackendUrl, mContext);
    }

    public static void Initialize(Context context) {
        if (mInstance == null) {
            mInstance = new AzureServiceAdapter(context);
        } else {
            throw new IllegalStateException("AzureServiceAdapter is already initialized");
        }
    }

    public static AzureServiceAdapter getInstance() {
        if (mInstance == null) {
            throw new IllegalStateException("AzureServiceAdapter is not initialized");
        }
        return mInstance;
    }

    public MobileServiceClient getClient() {
        return mClient;
    }

    // Place any public methods that operate on mClient here.
}

现在,可以调用主活动的 onCreate() 方法中的 AzureServiceAdapter.Initialize(this);You can now call AzureServiceAdapter.Initialize(this); in the onCreate() method of your main activity. 需要访问客户端的其他任何方法使用 AzureServiceAdapter.getInstance(); 获取对服务适配器的引用。Any other methods needing access to the client use AzureServiceAdapter.getInstance(); to obtain a reference to the service adapter.

数据操作Data Operations

Azure 移动应用 SDK 的核心作用是让你访问移动应用后端上的 SQL Azure 中存储的数据。The core of the Azure Mobile Apps SDK is to provide access to data stored within SQL Azure on the Mobile App backend. 可以使用强类型化类(首选)或非类型化查询(不建议)访问此数据。You can access this data using strongly typed classes (preferred) or untyped queries (not recommended). 本部分重点介绍如何使用强类型化类。The bulk of this section deals with using strongly typed classes.

定义客户端数据类Define client data classes

若要访问 SQL Azure 表的数据,可定义对应于移动应用后端中的表的客户端数据类。To access data from SQL Azure tables, define client data classes that correspond to the tables in the Mobile App backend. 本主题中的示例采用名为 MyDataTable 的表,其中包含以下列:Examples in this topic assume a table named MyDataTable, which has the following columns:

  • idid
  • texttext
  • completecomplete

相应的类型化客户端对象驻留在名为 MyDataTable.java 的文件中:The corresponding typed client-side object resides in a file called MyDataTable.java:

public class ToDoItem {
    private String id;
    private String text;
    private Boolean complete;
}

为添加的每个字段添加 getter 和 setter 方法。Add getter and setter methods for each field that you add. 如果 SQL Azure 表包含多个列,请将相应的字段添加到此类。If your SQL Azure table contains more columns, you would add the corresponding fields to this class. 例如,如果 DTO(数据传输对象)包含整数 Priority 列,则可以添加此字段,以及其 getter 和 setter 方法:For example, if the DTO (data transfer object) had an integer Priority column, then you might add this field, along with its getter and setter methods:

private Integer priority;

/**
* Returns the item priority
*/
public Integer getPriority() {
    return mPriority;
}

/**
* Sets the item priority
*
* @param priority
*            priority to set
*/
public final void setPriority(Integer priority) {
    mPriority = priority;
}

若要了解如何在移动应用后端中创建更多表,请参阅如何:定义表控制器(.NET 后端)或使用动态架构定义表(Node.js 后端)。To learn how to create additional tables in your Mobile Apps backend, see How to: Define a table controller (.NET backend) or Define Tables using a Dynamic Schema (Node.js backend).

Azure 移动应用后端表定义了五个特殊字段,其中四个字段可用于客户端:An Azure Mobile Apps backend table defines five special fields, four of which are available to clients:

  • String id:记录的全局唯一 ID。String id: The globally unique ID for the record. 最佳做法是将 ID 设为 UUID 对象的字符串表示形式。As a best practice, make the id the String representation of a UUID object.
  • DateTimeOffset updatedAt:上次更新日期/时间。DateTimeOffset updatedAt: The date/time of the last update. updatedAt 字段由服务器设置,永远不可由客户端代码设置。The updatedAt field is set by the server and should never be set by your client code.
  • DateTimeOffset createdAt:对象的创建日期/时间。DateTimeOffset createdAt: The date/time that the object was created. createdAt 字段由服务器设置,永远不可由客户端代码设置。The createdAt field is set by the server and should never be set by your client code.
  • byte[] version:通常以字符串表示,版本也由服务器设置。byte[] version: Normally represented as a string, the version is also set by the server.
  • boolean deleted:指示记录已被删除,但尚未清除。boolean deleted: Indicates that the record has been deleted but not purged yet. 不要使用 deleted 作为类中的属性。Do not use deleted as a property in your class.

id 字段是必填的。The id field is required. updatedAt 字段和 version 字段用于脱机同步(分别用于增量同步和冲突解决)。The updatedAt field and version field are used for offline synchronization (for incremental sync and conflict resolution respectively). createdAt 字段是一个引用字段,客户端不会使用它。The createdAt field is a reference field and is not used by the client. 名称是属性的“全局”名称且不可调整。The names are "across-the-wire" names of the properties and are not adjustable. 但是,可以使用 gson 库在对象与“全局”名称之间创建映射。However, you can create a mapping between your object and the "across-the-wire" names using the gson library. 例如:For example:

package com.example.zumoappname;

import com.microsoft.windowsazure.mobileservices.table.DateTimeOffset;

public class ToDoItem
{
    @com.google.gson.annotations.SerializedName("id")
    private String mId;
    public String getId() { return mId; }
    public final void setId(String id) { mId = id; }

    @com.google.gson.annotations.SerializedName("complete")
    private boolean mComplete;
    public boolean isComplete() { return mComplete; }
    public void setComplete(boolean complete) { mComplete = complete; }

    @com.google.gson.annotations.SerializedName("text")
    private String mText;
    public String getText() { return mText; }
    public final void setText(String text) { mText = text; }

    @com.google.gson.annotations.SerializedName("createdAt")
    private DateTimeOffset mCreatedAt;
    public DateTimeOffset getCreatedAt() { return mCreatedAt; }
    protected void setCreatedAt(DateTimeOffset createdAt) { mCreatedAt = createdAt; }

    @com.google.gson.annotations.SerializedName("updatedAt")
    private DateTimeOffset mUpdatedAt;
    public DateTimeOffset getUpdatedAt() { return mUpdatedAt; }
    protected void setUpdatedAt(DateTimeOffset updatedAt) { mUpdatedAt = updatedAt; }

    @com.google.gson.annotations.SerializedName("version")
    private String mVersion;
    public String getVersion() { return mVersion; }
    public final void setVersion(String version) { mVersion = version; }

    public ToDoItem() { }

    public ToDoItem(String id, String text) {
        this.setId(id);
        this.setText(text);
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof ToDoItem && ((ToDoItem) o).mId == mId;
    }

    @Override
    public String toString() {
        return getText();
    }
}

创建表引用Create a Table Reference

若要访问表,请先通过对 MobileServiceClient 调用 getTable 方法来创建一个 MobileServiceTable 对象。To access a table, first create a MobileServiceTable object by calling the getTable method on the MobileServiceClient. 此方法有两个重载:This method has two overloads:

public class MobileServiceClient {
    public <E> MobileServiceTable<E> getTable(Class<E> clazz);
    public <E> MobileServiceTable<E> getTable(String name, Class<E> clazz);
}

在以下代码中, mClient 是对 MobileServiceClient 对象的引用。In the following code, mClient is a reference to your MobileServiceClient object. 如果类名称与表名称相同,则使用第一个重载,这也是快速入门中使用的重载:The first overload is used where the class name and the table name are the same, and is the one used in the Quickstart:

MobileServiceTable<ToDoItem> mToDoTable = mClient.getTable(ToDoItem.class);

如果表名称与类名称不同,则使用第二个重载:第一个参数是表名称。The second overload is used when the table name is different from the class name: the first parameter is the table name.

MobileServiceTable<ToDoItem> mToDoTable = mClient.getTable("ToDoItemBackup", ToDoItem.class);

查询后端表Query a Backend Table

首先,请获取表引用。First, obtain a table reference. 然后对表引用执行查询。Then execute a query on the table reference. 查询是以下元素的任意组合:A query is any combination of:

子句必须按上述顺序提供。The clauses must be presented in the preceding order.

筛选结果Filtering Results

查询的一般形式为:The general form of a query is:

List<MyDataTable> results = mDataTable
    // More filters here
    .execute()          // Returns a ListenableFuture<E>
    .get()              // Converts the async into a sync result

上面的示例返回所有结果(结果数上限为服务器设置的最大页面大小)。The preceding example returns all results (up to the maximum page size set by the server). .execute() 方法在后端上执行查询。The .execute() method executes the query on the backend. 查询先转换为 OData v3 查询,再传输到移动应用后端。The query is converted to an OData v3 query before transmission to the Mobile Apps backend. 移动应用后端收到查询后,会先将查询转换为 SQL 语句,然后在 SQL Azure 实例上执行该语句。On receipt, the Mobile Apps backend converts the query into an SQL statement before executing it on the SQL Azure instance. 由于网络活动需要一段时间,因此 .execute() 方法返回 ListenableFuture<E>Since network activity takes some time, The .execute() method returns a ListenableFuture<E>.

筛选返回的数据Filter returned data

以下查询执行从 complete 等于 falseToDoItem 表返回所有项目。The following query execution returns all items from the ToDoItem table where complete equals false.

List<ToDoItem> result = mToDoTable
    .where()
    .field("complete").eq(false)
    .execute()
    .get();

mToDoTable 是对前面创建的移动服务表的引用。mToDoTable is the reference to the mobile service table that we created previously.

对表引用使用 where 方法调用定义筛选器。Define a filter using the where method call on the table reference. 然后,依次执行 where 方法、field 方法和用于指定逻辑谓词的方法。The where method is followed by a field method followed by a method that specifies the logical predicate. 可能的谓词方法包括 eq(等于)、ne(不等于)、gt(大于)、ge(大于或等于)、lt(小于)、le(小于或等于)。Possible predicate methods include eq (equals), ne (not equal), gt (greater than), ge (greater than or equal to), lt (less than), le (less than or equal to). 使用这些方法可将数字和字符串字段与特定的值相比较。These methods let you compare number and string fields to specific values.

可以按日期筛选。You can filter on dates. 使用以下方法可以比较整个日期字段或日期的某些部分:yearmonthdayhourminutesecondThe following methods let you compare the entire date field or parts of the date: year, month, day, hour, minute, and second. 以下示例针对 截止日期 等于 2013 的项添加一个筛选器。The following example adds a filter for items whose due date equals 2013.

List<ToDoItem> results = MToDoTable
    .where()
    .year("due").eq(2013)
    .execute()
    .get();

以下方法支持对字符串字段使用复杂筛选器:startsWithendsWithconcatsubStringindexOfreplacetoLowertoUppertrimlengthThe following methods support complex filters on string fields: startsWith, endsWith, concat, subString, indexOf, replace, toLower, toUpper, trim, and length. 以下示例筛选 text 列以“PRI0”开头的表行。The following example filters for table rows where the text column starts with "PRI0."

List<ToDoItem> results = mToDoTable
    .where()
    .startsWith("text", "PRI0")
    .execute()
    .get();

支持对数字字段使用以下运算符方法:addsubmuldivmodfloorceilingroundThe following operator methods are supported on number fields: add, sub, mul, div, mod, floor, ceiling, and round. 以下示例筛选其中的 duration 为偶数的表行。The following example filters for table rows where the duration is an even number.

List<ToDoItem> results = mToDoTable
    .where()
    .field("duration").mod(2).eq(0)
    .execute()
    .get();

可将谓词与以下逻辑方法相组合:andornotYou can combine predicates with these logical methods: and, or and not. 以下示例组合前面的两个示例。The following example combines two of the preceding examples.

List<ToDoItem> results = mToDoTable
    .where()
    .year("due").eq(2013).and().startsWith("text", "PRI0")
    .execute()
    .get();

组和嵌套逻辑运算符:Group and nest logical operators:

List<ToDoItem> results = mToDoTable
    .where()
    .year("due").eq(2013)
    .and(
        startsWith("text", "PRI0")
        .or()
        .field("duration").gt(10)
    )
    .execute().get();

有关筛选操作的更详细介绍和示例,请参阅 Exploring the richness of the Android client query model(探索 Android 客户端查询模型的丰富功能)。For more detailed discussion and examples of filtering, see Exploring the richness of the Android client query model.

对返回的数据进行排序Sort returned data

以下代码返回 ToDoItems 表中的所有项,返回的结果已按 text 字段的升序排序。The following code returns all items from a table of ToDoItems sorted ascending by the text field. mToDoTable 是对前面创建的后端表的引用:mToDoTable is the reference to the backend table that you created previously:

List<ToDoItem> results = mToDoTable
    .orderBy("text", QueryOrder.Ascending)
    .execute()
    .get();

orderBy 方法的第一个参数是与要排序的字段名称相同的字符串。The first parameter of the orderBy method is a string equal to the name of the field on which to sort. 第二个参数使用 QueryOrder 枚举来指定是按升序还是按降序排序。The second parameter uses the QueryOrder enumeration to specify whether to sort ascending or descending. 如果使用 where 方法筛选,则必须在调用 orderBy 方法之前调用 where 方法。If you are filtering using the where method, the where method must be invoked before the orderBy method.

选择特定列Select specific columns

以下代码演示了如何返回 ToDoItems 表中的所有项,但只显示 completetext 字段。The following code illustrates how to return all items from a table of ToDoItems, but only displays the complete and text fields. mToDoTable 是对前面创建的后端表的引用。mToDoTable is the reference to the backend table that we created previously.

List<ToDoItemNarrow> result = mToDoTable
    .select("complete", "text")
    .execute()
    .get();

select 函数的参数是要返回的表列的字符串名称。The parameters to the select function are the string names of the table's columns that you want to return. select 方法需要接在 whereorderBy 等方法的后面。The select method needs to follow methods like where and orderBy. 它可以后接 skiptop 等分页方法。It can be followed by paging methods like skip and top.

在页中返回数据Return data in pages

数据始终在页面中返回。Data is ALWAYS returned in pages. 返回的最大记录数由服务器设置。The maximum number of records returned is set by the server. 如果客户端请求更多记录,则服务器返回最大记录数。If the client requests more records, then the server returns the maximum number of records. 默认情况下,服务器上的最大页面大小为 50 个记录。By default, the maximum page size on the server is 50 records.

第一个示例说明如何选择表中的前 5 个项。The first example shows how to select the top five items from a table. 该查询返回 ToDoItems表中的项。The query returns the items from a table of ToDoItems. mToDoTable 是对前面创建的后端表的引用:mToDoTable is the reference to the backend table that you created previously:

List<ToDoItem> result = mToDoTable
    .top(5)
    .execute()
    .get();

以下查询跳过前 5 个项,返回接下来的 5 个项:Here's a query that skips the first five items, and then returns the next five:

List<ToDoItem> result = mToDoTable
    .skip(5).top(5)
    .execute()
    .get();

如果想要获取表中的所有记录,请实现循环访问所有页面的代码:If you wish to get all records in a table, implement code to iterate over all pages:

List<MyDataModel> results = new ArrayList<>();
int nResults;
do {
    int currentCount = results.size();
    List<MyDataModel> pagedResults = mDataTable
        .skip(currentCount).top(500)
        .execute().get();
    nResults = pagedResults.size();
    if (nResults > 0) {
        results.addAll(pagedResults);
    }
} while (nResults > 0);

使用此方法请求所有记录会针对移动应用后端至少创建两个请求。A request for all records using this method creates a minimum of two requests to the Mobile Apps backend.

Tip

选择适当的页面大小可在执行请求时使用的内存量、带宽用量以及完全接收数据时产生的延迟之间进行平衡。Choosing the right page size is a balance between memory usage while the request is happening, bandwidth usage and delay in receiving the data completely. 默认值(50 个记录)适用于所有设备。The default (50 records) is suitable for all devices. 如果以独占方式在较大的内存设备上运行,最高可将页面大小增大到 500。If you exclusively operate on larger memory devices, increase up to 500. 我们发现,将页面大小增大到超过 500 条记录会导致出现不可接受的延迟,以及消耗大量内存的问题。We have found that increasing the page size beyond 500 records results in unacceptable delays and large memory issues.

如何:连接查询方法How to: Concatenate query methods

用于查询后端表的方法是可以连接的。The methods used in querying backend tables can be concatenated. 通过链接查询方法,可以选择已排序并分页的筛选行的特定列。Chaining query methods allows you to select specific columns of filtered rows that are sorted and paged. 可以创建复杂的逻辑筛选器。You can create complex logical filters. 每个查询方法都会返回一个查询对象。Each query method returns a Query object. 若要结束方法序列并真正运行查询,可以调用 execute 方法。To end the series of methods and actually run the query, call the execute method. 例如:For example:

List<ToDoItem> results = mToDoTable
        .where()
        .year("due").eq(2013)
        .and(
            startsWith("text", "PRI0").or().field("duration").gt(10)
        )
        .orderBy(duration, QueryOrder.Ascending)
        .select("id", "complete", "text", "duration")
        .skip(200).top(100)
        .execute()
        .get();

必须按照以下顺序排序已链接的查询方法:The chained query methods must be ordered as follows:

  1. 筛选 (where) 方法。Filtering (where) methods.
  2. 排序 (orderBy) 方法。Sorting (orderBy) methods.
  3. 选择 (select) 方法。Selection (select) methods.
  4. 分页(skiptop)方法。paging (skip and top) methods.

将数据绑定到用户界面Bind data to the user interface

数据绑定涉及到三个组件:Data binding involves three components:

  • 数据源The data source
  • 屏幕布局The screen layout
  • 将两者关联起来的适配器。The adapter that ties the two together.

以下示例代码将移动应用 SQL Azure 表 ToDoItem 中的数据返回到一个数组中。In our sample code, we return the data from the Mobile Apps SQL Azure table ToDoItem into an array. 此活动是数据应用程序的常见模式。This activity is a common pattern for data applications. 数据库查询通常会返回行的集合,客户端在列表或数组中获取该集合。Database queries often return a collection of rows that the client gets in a list or array. 在此示例中,该数组就是数据源。In this sample, the array is the data source. 代码将指定屏幕布局,用于定义设备中显示的数据视图。The code specifies a screen layout that defines the view of the data that appears on the device. 这两者与适配器绑定在一起,在此代码中,适配器是 ArrayAdapter<ToDoItem> 类的扩展。The two are bound together with an adapter, which in this code is an extension of the ArrayAdapter<ToDoItem> class.

定义布局Define the Layout

布局由多个 XML 代码段定义。The layout is defined by several snippets of XML code. 以某个现有布局为例,以下代码表示要在其中填充服务器数据的 ListViewGiven an existing layout, the following code represents the ListView we want to populate with our server data.

    <ListView
        android:id="@+id/listViewToDo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:listitem="@layout/row_list_to_do" >
    </ListView>

在上述代码中, listitem 属性指定列表中单个行的布局 ID。In the preceding code, the listitem attribute specifies the id of the layout for an individual row in the list. 此代码指定复选框及其关联文本,并针对列表中的每个项进行一次实例化。This code specifies a check box and its associated text and gets instantiated once for each item in the list. 此布局不显示 id 字段,如果使用更复杂的布局,则会在屏幕中指定更多的字段。This layout does not display the id field, and a more complex layout would specify additional fields in the display. 以下代码摘自 row_list_to_do.xml 文件。This code is in the row_list_to_do.xml file.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <CheckBox
        android:id="@+id/checkToDoItem"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/checkbox_text" />
</LinearLayout>

定义适配器Define the adapter

由于视图的数据源是 ToDoItem 的数组,因此我们需要基于 ArrayAdapter<ToDoItem> 类子类化适配器。Since the data source of our view is an array of ToDoItem, we subclass our adapter from an ArrayAdapter<ToDoItem> class. 此子类使用 row_list_to_do 布局为每个 ToDoItem 生成一个视图。This subclass produces a View for every ToDoItem using the row_list_to_do layout. 在代码中,可以定义以下类作为 ArrayAdapter<E> 类的扩展:In our code, we define the following class that is an extension of the ArrayAdapter<E> class:

public class ToDoItemAdapter extends ArrayAdapter<ToDoItem> {
}

替代适配器的 getView 方法。Override the adapters getView method. 例如:For example:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = convertView;

    final ToDoItem currentItem = getItem(position);

    if (row == null) {
        LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
        row = inflater.inflate(R.layout.row_list_to_do, parent, false);
    }

    row.setTag(currentItem);

    final CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkToDoItem);
    checkBox.setText(currentItem.getText());
    checkBox.setChecked(false);
    checkBox.setEnabled(true);

    checkBox.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View arg0) {
            if (checkBox.isChecked()) {
                checkBox.setEnabled(false);
                if (mContext instanceof ToDoActivity) {
                    ToDoActivity activity = (ToDoActivity) mContext;
                    activity.checkItem(currentItem);
                }
            }
        }
    });

    return row;
}

在活动中创建此类的实例,如下所示:We create an instance of this class in our Activity as follows:

    ToDoItemAdapter mAdapter;
    mAdapter = new ToDoItemAdapter(this, R.layout.row_list_to_do);

ToDoItemAdapter 构造函数的第二个参数是对布局的引用。The second parameter to the ToDoItemAdapter constructor is a reference to the layout. 我们现在可以实例化 ListView 并将适配器分配到 ListViewWe can now instantiate the ListView and assign the adapter to the ListView.

    ListView listViewToDo = (ListView) findViewById(R.id.listViewToDo);
    listViewToDo.setAdapter(mAdapter);

使用适配器绑定到 UIUse the Adapter to Bind to the UI

现在,可以使用数据绑定了。You are now ready to use data binding. 下面的代码说明如何获取表中的项,以及使用返回的项填充本地适配器。The following code shows how to get items in the table and fills the local adapter with the returned items.

    public void showAll(View view) {
        AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>(){
            @Override
            protected Void doInBackground(Void... params) {
                try {
                    final List<ToDoItem> results = mToDoTable.execute().get();
                    runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            mAdapter.clear();
                            for (ToDoItem item : results) {
                                mAdapter.add(item);
                            }
                        }
                    });
                } catch (Exception exception) {
                    createAndShowDialog(exception, "Error");
                }
                return null;
            }
        };
        runAsyncTask(task);
    }

请在每次修改 ToDoItem 表时调用适配器。Call the adapter any time you modify the ToDoItem table. 修改是逐条记录进行的,因此,要处理的是单个行而不是一个集合。Since modifications are done on a record by record basis, you handle a single row instead of a collection. 插入项时,需要对适配器调用 add 方法;删除项时,需要调用 remove 方法。When you insert an item, call the add method on the adapter; when deleting, call the remove method.

可以在 Android 快速入门项目中找到完整示例。You can find a complete example in the Android Quickstart Project.

将数据插入后端Insert data into the backend

实例化 ToDoItem 类的实例并设置其属性。Instantiate an instance of the ToDoItem class and set its properties.

ToDoItem item = new ToDoItem();
item.text = "Test Program";
item.complete = false;

然后,使用 insert() 插入对象:Then use insert() to insert an object:

ToDoItem entity = mToDoTable
    .insert(item)       // Returns a ListenableFuture<ToDoItem>
    .get();

返回的实体将匹配插入后端表的数据,包括 ID 和后端上设置的任何其他值(例如 createdAtupdatedAtversion 字段)。The returned entity matches the data inserted into the backend table, included the ID and any other values (such as the createdAt, updatedAt, and version fields) set on the backend.

移动应用表需要名为 id的主键列。此列必须是字符串。Mobile Apps tables require a primary key column named id. This column must be a string. ID 列的默认值为 GUID。The default value of the ID column is a GUID. 可以提供其他唯一的值,例如电子邮件地址或用户名。You can provide other unique values, such as email addresses or usernames. 如果没有为插入的记录提供字符串 ID 值,后端会生成新的 GUID。When a string ID value is not provided for an inserted record, the backend generates a new GUID.

字符串 ID 值提供以下优势:String ID values provide the following advantages:

  • 无需往返访问数据库即可生成 ID。IDs can be generated without making a round trip to the database.
  • 更方便地合并不同表或数据库中的记录。Records are easier to merge from different tables or databases.
  • ID 值能够更好地与应用程序的逻辑集成。ID values integrate better with an application's logic.

若要支持脱机同步, 必需 提供字符串 ID 值。String ID values are REQUIRED for offline sync support. 将 ID 存储到后端数据库后,无法对它进行更改。You cannot change an Id once it is stored in the backend database.

更新移动应用中的数据Update data in a mobile app

要更新表中的数据,请将新对象传递给 update() 方法。To update data in a table, pass the new object to the update() method.

mToDoTable
    .update(item)   // Returns a ListenableFuture<ToDoItem>
    .get();

在此示例中,item 是对 ToDoItem 表中某个行的引用,该表包含一些更改。In this example, item is a reference to a row in the ToDoItem table, which has had some changes made to it. 具有相同 id 的行会更新。The row with the same id is updated.

删除移动应用中的数据Delete data in a mobile app

以下代码演示如何通过指定数据对象来删除表中的数据。The following code shows how to delete data from a table by specifying the data object.

mToDoTable
    .delete(item);

也可以通过指定要删除的行的 id 字段来删除项。You can also delete an item by specifying the id field of the row to delete.

String myRowId = "2FA404AB-E458-44CD-BC1B-3BC847EF0902";
mToDoTable
    .delete(myRowId);

按 ID 查找特定项Look up a specific item by Id

使用 lookUp() 方法查找具有特定 ID 字段的项:Look up an item with a specific id field with the lookUp() method:

ToDoItem result = mToDoTable
    .lookUp("0380BAFB-BCFF-443C-B7D5-30199F730335")
    .get();

如何:处理非类型化数据How to: Work with untyped data

使用非类型化编程模型可以准确控制 JSON 序列化。The untyped programming model gives you exact control over JSON serialization. 在某些常见方案中,可能会希望使用非类型化编程模型。There are some common scenarios where you may wish to use an untyped programming model. 例如,如果后端表包含很多列,并且只需引用列的子集。For example, if your backend table contains many columns and you only need to reference a subset of the columns. 类型化模型需要在数据类中定义移动应用后端中定义的所有列。The typed model requires you to define all the columns defined in the Mobile Apps backend in your data class. 用于访问数据的大多数 API 调用都与类型化编程调用类似。Most of the API calls for accessing data are similar to the typed programming calls. 主要差别在于,在非类型化模型中,要对 MobileServiceJsonTable 对象而不是 MobileServiceTable 对象调用方法。The main difference is that in the untyped model you invoke methods on the MobileServiceJsonTable object, instead of the MobileServiceTable object.

创建非类型化表的实例Create an instance of an untyped table

与使用类型化模型相似,首先需要获取表引用,不过,此时该引用的是一个 MobileServicesJsonTable 对象。Similar to the typed model, you start by getting a table reference, but in this case it's a MobileServicesJsonTable object. 对客户端的实例调用 getTable 方法来获取引用:Obtain the reference by calling the getTable method on an instance of the client:

private MobileServiceJsonTable mJsonToDoTable;
//...
mJsonToDoTable = mClient.getTable("ToDoItem");

创建 MobileServiceJsonTable的实例后,它就几乎具有与类型化编程模型所能使用的 API 相同的 API。Once you have created an instance of the MobileServiceJsonTable, it has virtually the same API available as with the typed programming model. 在某些情况下,这些方法会采用非类型化参数,而不采用类型化参数。In some cases, the methods take an untyped parameter instead of a typed parameter.

插入到非类型化的表Insert into an untyped table

以下代码演示了如何执行插入。The following code shows how to do an insert. 第一步是创建属于 gson 库的 JsonObjectThe first step is to create a JsonObject, which is part of the gson library.

JsonObject jsonItem = new JsonObject();
jsonItem.addProperty("text", "Wake up");
jsonItem.addProperty("complete", false);

然后,使用 insert() 将非类型化对象插入表。Then, Use insert() to insert the untyped object into the table.

JsonObject insertedItem = mJsonToDoTable
    .insert(jsonItem)
    .get();

如果需要获取所插入对象的 ID,请使用 getAsJsonPrimitive() 方法调用。If you need to get the ID of the inserted object, use the getAsJsonPrimitive() method.

String id = insertedItem.getAsJsonPrimitive("id").getAsString();

从非类型化表中删除Delete from an untyped table

以下代码演示了如何删除一个实例,在本例中,该实例就是我们在前一个 insert 示例中创建的 JsonObject 的实例。The following code shows how to delete an instance, in this case, the same instance of a JsonObject that was created in the prior insert example. 该代码与类型化案例相同,但方法具有不同的签名,因为它引用了 JsonObjectThe code is the same as with the typed case, but the method has a different signature since it references an JsonObject.

mToDoTable
    .delete(insertedItem);

还可以使用某个实例的 ID 来直接删除该实例:You can also delete an instance directly by using its ID:

mToDoTable.delete(ID);

从非类型化表中返回所有行Return all rows from an untyped table

以下代码演示了如何检索整个表。The following code shows how to retrieve an entire table. 由于使用的是 Json 数据表,可以选择性地只检索某些表的列。Since you are using a JSON Table, you can selectively retrieve only some of the table's columns.

public void showAllUntyped(View view) {
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            try {
                final JsonElement result = mJsonToDoTable.execute().get();
                final JsonArray results = result.getAsJsonArray();
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter.clear();
                        for (JsonElement item : results) {
                            String ID = item.getAsJsonObject().getAsJsonPrimitive("id").getAsString();
                            String mText = item.getAsJsonObject().getAsJsonPrimitive("text").getAsString();
                            Boolean mComplete = item.getAsJsonObject().getAsJsonPrimitive("complete").getAsBoolean();
                            ToDoItem mToDoItem = new ToDoItem();
                            mToDoItem.setId(ID);
                            mToDoItem.setText(mText);
                            mToDoItem.setComplete(mComplete);
                            mAdapter.add(mToDoItem);
                        }
                    }
                });
            } catch (Exception exception) {
                createAndShowDialog(exception, "Error");
            }
            return null;
        }
    }.execute();
}

类型化模型使用的相同筛选设置(筛选和分页方法)适用于非类型化模型。The same set of filtering, filtering and paging methods that are available for the typed model are available for the untyped model.

实现脱机同步Implement Offline Sync

Azure 移动应用客户端 SDK 还可使用 SQLite 数据库在本地存储服务器数据的副本,从而实现脱机数据同步。The Azure Mobile Apps Client SDK also implements offline synchronization of data by using a SQLite database to store a copy of the server data locally. 无需建立移动连接即可针对脱机表执行操作。Operations performed on an offline table do not require mobile connectivity to work. 脱机同步有助于提高恢复能力和性能,代价是用于解决冲突的逻辑变得更复杂。Offline sync aids in resilience and performance at the expense of more complex logic for conflict resolution. Azure 移动应用客户端 SDK 实现以下功能:The Azure Mobile Apps Client SDK implements the following features:

  • 增量同步:仅下载已更新记录和新记录,从而减少了带宽和内存消耗。Incremental Sync: Only updated and new records are downloaded, saving bandwidth and memory consumption.
  • 乐观并发:假设操作成功。Optimistic Concurrency: Operations are assumed to succeed. 冲突解决推迟到在服务器上执行更新之后。Conflict Resolution is deferred until updates are performed on the server.
  • 冲突解决方法:SDK 检测何时在服务器上进行了有冲突的更改,并提供挂钩来提醒用户。Conflict Resolution: The SDK detects when a conflicting change has been made at the server and provides hooks to alert the user.
  • 软删除:将已删除的记录标记为已删除,使其他设备能够更新其脱机缓存。Soft Delete: Deleted records are marked deleted, allowing other devices to update their offline cache.

初始化脱机同步Initialize Offline Sync

在使用每个脱机表之前,必须先在脱机缓存中定义该表。Each offline table must be defined in the offline cache before use. 通常,在创建客户端之后立即执行表定义:Normally, table definition is done immediately after the creation of the client:

AsyncTask<Void, Void, Void> initializeStore(MobileServiceClient mClient)
    throws MobileServiceLocalStoreException, ExecutionException, InterruptedException
{
    AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
        @Override
        protected void doInBackground(Void... params) {
            try {
                MobileServiceSyncContext syncContext = mClient.getSyncContext();
                if (syncContext.isInitialized()) {
                    return null;
                }
                SQLiteLocalStore localStore = new SQLiteLocalStore(mClient.getContext(), "offlineStore", null, 1);

                // Create a table definition.  As a best practice, store this with the model definition and return it via
                // a static method
                Map<String, ColumnDataType> toDoItemDefinition = new HashMap<String, ColumnDataType>();
                toDoItemDefinition.put("id", ColumnDataType.String);
                toDoItemDefinition.put("complete", ColumnDataType.Boolean);
                toDoItemDefinition.put("text", ColumnDataType.String);
                toDoItemDefinition.put("version", ColumnDataType.String);
                toDoItemDefinition.put("updatedAt", ColumnDataType.DateTimeOffset);

                // Now define the table in the local store
                localStore.defineTable("ToDoItem", toDoItemDefinition);

                // Specify a sync handler for conflict resolution
                SimpleSyncHandler handler = new SimpleSyncHandler();

                // Initialize the local store
                syncContext.initialize(localStore, handler).get();
            } catch (final Exception e) {
                createAndShowDialogFromTask(e, "Error");
            }
            return null;
        }
    };
    return runAsyncTask(task);
}

获取对脱机缓存表的引用Obtain a reference to the Offline Cache Table

对于联机表,可以使用 .getTable()For an online table, you use .getTable(). 对于脱机表,可以使用 .getSyncTable()For an offline table, use .getSyncTable():

MobileServiceSyncTable<ToDoItem> mToDoTable = mClient.getSyncTable("ToDoItem", ToDoItem.class);

可用于联机表的所有方法(包括筛选、排序、分页、插入数据、更新数据和删除数据)同样适用于脱机表。All the methods that are available for online tables (including filtering, sorting, paging, inserting data, updating data, and deleting data) work equally well on online and offline tables.

同步本地脱机缓存Synchronize the Local Offline Cache

同步在应用的控制范围内。Synchronization is within the control of your app. 下面是一个示例同步方法:Here is an example synchronization method:

private AsyncTask<Void, Void, Void> sync(MobileServiceClient mClient) {
    AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>(){
        @Override
        protected Void doInBackground(Void... params) {
            try {
                MobileServiceSyncContext syncContext = mClient.getSyncContext();
                syncContext.push().get();
                mToDoTable.pull(null, "todoitem").get();
            } catch (final Exception e) {
                createAndShowDialogFromTask(e, "Error");
            }
            return null;
        }
    };
    return runAsyncTask(task);
}

如果为 .pull(query, queryname) 方法提供了查询名称,则使用增量同步,以便仅返回自上次成功完成提取以来创建或更改的记录。If a query name is provided to the .pull(query, queryname) method, then incremental sync is used to return only records that have been created or changed since the last successfully completed pull.

在脱机同步期间处理冲突Handle Conflicts during Offline Synchronization

如果在执行 .push() 操作期间发生冲突,会引发 MobileServiceConflictExceptionIf a conflict happens during a .push() operation, a MobileServiceConflictException is thrown. 服务器发出的项嵌入在异常中,可以通过针对该异常执行 .getItem() 来检索该项。The server-issued item is embedded in the exception and can be retrieved by .getItem() on the exception. 通过针对 MobileServiceSyncContext 对象调用以下项来调整推送:Adjust the push by calling the following items on the MobileServiceSyncContext object:

  • .cancelAndDiscardItem()
  • .cancelAndUpdateItem()
  • .updateOperationAndItem()

根据需要标记所有冲突后,可以再次调用 .push() 来解决所有冲突。Once all conflicts are marked as you wish, call .push() again to resolve all the conflicts.

调用自定义 APICall a custom API

自定义 API 可让你定义自定义终结点,这些终结点会公开不映射到插入、更新、删除或读取操作的服务器功能。A custom API enables you to define custom endpoints that expose server functionality that does not map to an insert, update, delete, or read operation. 使用自定义 API 能够以更大的力度控制消息传送,包括读取和设置 HTTP 消息标头,以及定义除 JSON 以外的消息正文格式。By using a custom API, you can have more control over messaging, including reading and setting HTTP message headers and defining a message body format other than JSON.

从 Android 客户端调用 invokeApi 方法,以调用自定义 API 终结点。From an Android client, you call the invokeApi method to call the custom API endpoint. 以下示例演示了如何调用名为 completeAll 的 API 终结点,从而返回名为 MarkAllResult 的集合类。The following example shows how to call an API endpoint named completeAll, which returns a collection class named MarkAllResult.

public void completeItem(View view) {
    ListenableFuture<MarkAllResult> result = mClient.invokeApi("completeAll", MarkAllResult.class);
    Futures.addCallback(result, new FutureCallback<MarkAllResult>() {
        @Override
        public void onFailure(Throwable exc) {
            createAndShowDialog((Exception) exc, "Error");
        }

        @Override
        public void onSuccess(MarkAllResult result) {
            createAndShowDialog(result.getCount() + " item(s) marked as complete.", "Completed Items");
            refreshItemsFromTable();
        }
    });
}

invokeApi 方法在客户端上调用,该客户端向新的自定义 API 发送 POST 请求。The invokeApi method is called on the client, which sends a POST request to the new custom API. 与任何错误相同,自定义 API 返回的结果也显示在消息对话框中。The result returned by the custom API is displayed in a message dialog, as are any errors. 使用其他版本的 invokeApi 可以选择性地在请求正文中发送对象、指定 HTTP 方法,以及随请求一起发送查询参数。Other versions of invokeApi let you optionally send an object in the request body, specify the HTTP method, and send query parameters with the request. 此外还提供了非类型化的 invokeApi 版本。Untyped versions of invokeApi are provided as well.

向应用添加身份验证Add authentication to your app

教程已详细说明如何添加这些功能。Tutorials already describe in detail how to add these features.

应用服务支持使用各种外部标识提供者对应用用户进行身份验证:Microsoft 帐户和 Azure Active Directory。App Service supports authenticating app users using various external identity providers: Microsoft Account, and Azure Active Directory. 可以在表中设置权限,以便将特定操作的访问权限限制给已经过身份验证的用户。You can set permissions on tables to restrict access for specific operations to only authenticated users. 还可以在后端中使用已经过身份验证的用户的标识来实施授权规则。You can also use the identity of authenticated users to implement authorization rules in your backend.

支持两种身份验证流:服务器流和客户端流。Two authentication flows are supported: a server flow and a client flow. 服务器流依赖于标识提供者 Web 界面,因此可提供最简便的身份验证体验。The server flow provides the simplest authentication experience, as it relies on the identity providers web interface. 无需其他 SDK 即可实现服务器流身份验证。No additional SDKs are required to implement server flow authentication. 服务器流身份验证不会与移动设备深度集成,因此仅建议用于概念验证方案。Server flow authentication does not provide a deep integration into the mobile device and is only recommended for proof of concept scenarios.

客户端流依赖于标识提供者提供的 SDK,因此允许与设备特定的功能(例如单一登录)进行更深入的集成。The client flow allows for deeper integration with device-specific capabilities such as single sign-on as it relies on SDKs provided by the identity provider. 例如,可以将 Microsoft 帐户 SDK 集成到移动应用程序中。For example, you can integrate the Microsoft Account SDK into your mobile application. 移动客户端会切换到 Microsoft 帐户应用并确认你登录,然后再切换回移动应用。The mobile client swaps into the Microsoft Account app and confirms your sign-on before swapping back to your mobile app.

在应用中启用身份验证需要执行以下四个步骤:Four steps are required to enable authentication in your app:

  • 向标识提供者注册应用,以进行身份验证。Register your app for authentication with an identity provider.
  • 配置应用服务后端。Configure your App Service backend.
  • 限制只有应用服务后端上经过身份验证的用户才能拥有表权限。Restrict table permissions to authenticated users only on the App Service backend.
  • 将身份验证代码添加到应用。Add authentication code to your app.

可以在表中设置权限,以便将特定操作的访问权限限制给已经过身份验证的用户。You can set permissions on tables to restrict access for specific operations to only authenticated users. 还可以使用已经过身份验证的用户的 SID 来修改请求。You can also use the SID of an authenticated user to modify requests. 有关详细信息,请查看 身份验证入门 和服务器 SDK 操作方法文档。For more information, review Get started with authentication and the Server SDK HOWTO documentation.

身份验证:服务器流Authentication: Server Flow

以下代码使用 MicrosoftAccount 启动服务器流登录过程。The following code starts a server flow login process using the MicrosoftAccount. 由于 MicrosoftAccount 的安全要求,需要指定其他配置:Additional configuration is required because of the security requirements for the MicrosoftAccount:

MobileServiceUser user = mClient.login(MobileServiceAuthenticationProvider.MicrosoftAccount, "{url_scheme_of_your_app}", MICROSOFTACCOUNT_LOGIN_REQUEST_CODE);

此外,请将以下方法添加到主活动类:In addition, add the following method to the main Activity class:

// You can choose any unique number here to differentiate auth providers from each other. Note this is the same code at login() and onActivityResult().
public static final int MICROSOFTACCOUNT_LOGIN_REQUEST_CODE = 1;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // When request completes
    if (resultCode == RESULT_OK) {
        // Check the request code matches the one we send in the login request
        if (requestCode == MICROSOFTACCOUNT_LOGIN_REQUEST_CODE) {
            MobileServiceActivityResult result = mClient.onActivityResult(data);
            if (result.isLoggedIn()) {
                // login succeeded
                createAndShowDialog(String.format("You are now logged in - %1$2s", mClient.getCurrentUser().getUserId()), "Success");
                createTable();
            } else {
                // login failed, check the error message
                String errorMessage = result.getErrorMessage();
                createAndShowDialog(errorMessage, "Error");
            }
        }
    }
}

在主活动中定义的 MICROSOFTACCOUNT_LOGIN_REQUEST_CODE 用于 login() 方法,并在 onActivityResult() 方法中使用。The MICROSOFTACCOUNT_LOGIN_REQUEST_CODE defined in your main Activity is used for the login() method and within the onActivityResult() method. 可以选择任意唯一编号,只要在 login() 方法和 onActivityResult() 方法中使用相同的编号即可。You can choose any unique number, as long as the same number is used within the login() method and the onActivityResult() method. 如果将客户端代码抽象化为服务适配器(如前所示),应在服务适配器上调用相应的方法。If you abstract the client code into a service adapter (as shown earlier), you should call the appropriate methods on the service adapter.

还需要为 customtabs 配置项目。You also need to configure the project for customtabs. 首先指定重定向 URL。First specify a redirect-URL. 将以下代码片段添加到 AndroidManifest.xmlAdd the following snippet to AndroidManifest.xml:

<activity android:name="com.microsoft.windowsazure.mobileservices.authentication.RedirectUrlActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="{url_scheme_of_your_app}" android:host="easyauth.callback"/>
    </intent-filter>
</activity>

redirectUriScheme 添加到应用程序的 build.gradle 文件:Add the redirectUriScheme to the build.gradle file for your application:

android {
    buildTypes {
        release {
            // … …
            manifestPlaceholders = ['redirectUriScheme': '{url_scheme_of_your_app}://easyauth.callback']
        }
        debug {
            // … …
            manifestPlaceholders = ['redirectUriScheme': '{url_scheme_of_your_app}://easyauth.callback']
        }
    }
}

最后,将 com.android.support:customtabs:28.0.0 添加到 build.gradle 文件中的依赖项列表:Finally, add com.android.support:customtabs:28.0.0 to the dependencies list in the build.gradle file:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.code.gson:gson:2.3'
    implementation 'com.google.guava:guava:18.0'
    implementation 'com.android.support:customtabs:28.0.0'
    implementation 'com.squareup.okhttp:okhttp:2.5.0'
    implementation 'com.microsoft.azure:azure-mobile-android:3.4.0@aar'
    implementation 'com.microsoft.azure:azure-notifications-handler:1.0.1@jar'
}

可以使用 getUserId 方法从 MobileServiceUser 获取已登录用户的 ID。Obtain the ID of the logged-in user from a MobileServiceUser using the getUserId method. 有关如何使用 Futures 调用异步登录 API 的示例,请参阅 身份验证入门For an example of how to use Futures to call the asynchronous login APIs, see Get started with authentication.

Warning

所述的 URL 方案区分大小写。The URL Scheme mentioned is case-sensitive. 请确保出现的所有 {url_scheme_of_you_app} 的大小写匹配。Ensure that all occurrences of {url_scheme_of_you_app} match case.

缓存身份验证令牌Cache authentication tokens

缓存身份验证令牌需要将用户 ID 和身份验证令牌存储在设备本地。Caching authentication tokens requires you to store the User ID and authentication token locally on the device. 下次启动应用时,只需检查缓存,如果这些值存在,则可以跳过登录过程,并使用这些数据重新进入客户端。The next time the app starts, you check the cache, and if these values are present, you can skip the log in procedure and rehydrate the client with this data. 但是,这些数据是敏感的,为安全起见,应该以加密形式存储,以防手机失窃。However this data is sensitive, and it should be stored encrypted for safety in case the phone gets stolen. 可以在缓存身份验证令牌部分中了解有关如何缓存身份验证令牌的完整示例。You can see a complete example of how to cache authentication tokens in Cache authentication tokens section.

尝试使用过期的令牌时,会收到“401 未授权” 响应。When you try to use an expired token, you receive a 401 unauthorized response. 可以使用筛选器处理身份验证错误。You can handle authentication errors using filters. 筛选器会截获对应用服务后端提出的请求。Filters intercept requests to the App Service backend. 筛选器代码会测试 401 响应,触发登录进程,并恢复生成 401 响应的请求。The filter code tests the response for a 401, triggers the sign-in process, and then resumes the request that generated the 401.

使用刷新令牌Use Refresh Tokens

Azure 应用服务身份验证和授权返回的令牌定义了一小时的生存期。The token returned by Azure App Service Authentication and Authorization has a defined life time of one hour. 在此期限过后,必须重新验证用户的身份。After this period, you must reauthenticate the user. 如果使用通过客户端流身份验证收到的长生存期令牌,则可以使用相同的令牌在 Azure 应用服务身份验证和授权中重新进行身份验证。If you are using a long-lived token that you have received via client-flow authentication, then you can reauthenticate with Azure App Service Authentication and Authorization using the same token. 另外,还会生成一个具有新生存期的 Azure 应用服务令牌。Another Azure App Service token is generated with a new lifetime.

还可以将提供程序注册为使用刷新令牌。You can also register the provider to use Refresh Tokens. 刷新令牌不一定始终可用。A Refresh Token is not always available. 需要指定其他配置:Additional configuration is required:

  • 对于 Azure Active Directory,请为 Azure Active Directory 应用配置客户端机密。For Azure Active Directory, configure a client secret for the Azure Active Directory App. 配置 Azure Active Directory 身份验证时,请在 Azure 应用服务中指定客户端机密。Specify the client secret in the Azure App Service when configuring Azure Active Directory Authentication. 调用 .login() 时,请传递 response_type=code id_token 作为参数:When calling .login(), pass response_type=code id_token as a parameter:

    HashMap<String, String> parameters = new HashMap<String, String>();
    parameters.put("response_type", "code id_token");
    MobileServiceUser user = mClient.login
        MobileServiceAuthenticationProvider.AzureActiveDirectory,
        "{url_scheme_of_your_app}",
        AAD_LOGIN_REQUEST_CODE,
        parameters);
    
  • 对于 Microsoft 帐户,请选择 wl.offline_access 范围。For Microsoft Account, select the wl.offline_access scope.

若要刷新令牌,请调用 .refreshUser()To refresh a token, call .refreshUser():

MobileServiceUser user = mClient
    .refreshUser()  // async - returns a ListenableFuture<MobileServiceUser>
    .get();

最佳做法是创建一个筛选器用于检测来自服务器的 401 响应,并尝试刷新用户令牌。As a best practice, create a filter that detects a 401 response from the server and tries to refresh the user token.

使用客户端流身份验证登录Log in with Client-flow Authentication

使用客户端流身份验证登录的一般过程如下:The general process for logging in with client-flow authentication is as follows:

  • 像配置服务器流身份验证一样配置 Azure 应用服务身份验证和授权。Configure Azure App Service Authentication and Authorization as you would server-flow authentication.

  • 集成用于身份验证的身份验证提供程序 SDK,以生成访问令牌。Integrate the authentication provider SDK for authentication to produce an access token.

  • 按如下所示调用 .login() 方法(result 应为 AuthenticationResult):Call the .login() method as follows (result should be an AuthenticationResult):

    JSONObject payload = new JSONObject();
    payload.put("access_token", result.getAccessToken());
    ListenableFuture<MobileServiceUser> mLogin = mClient.login("{provider}", payload.toString());
    Futures.addCallback(mLogin, new FutureCallback<MobileServiceUser>() {
        @Override
        public void onFailure(Throwable exc) {
            exc.printStackTrace();
        }
        @Override
        public void onSuccess(MobileServiceUser user) {
            Log.d(TAG, "Login Complete");
        }
    });
    

请参阅下一部分中的完整代码示例。See the complete code example in the next section.

onSuccess() 方法替换为成功登录后要使用的任何代码。Replace the onSuccess() method with whatever code you wish to use on a successful login. {provider} 字符串是有效的提供程序:aad (Azure Active Directory) 或 microsoftaccountThe {provider} string is a valid provider: aad (Azure Active Directory) or microsoftaccount. 如果已实现自定义身份验证,则还可以使用自定义身份验证提供程序标记。If you have implemented custom authentication, then you can also use the custom authentication provider tag.

使用 Active Directory 身份验证库 (ADAL) 对用户进行身份验证Authenticate users with the Active Directory Authentication Library (ADAL)

可以借助 Active Directory 身份验证库 (ADAL) 使用 Azure Active Directory 将用户登录到应用程序。You can use the Active Directory Authentication Library (ADAL) to sign users into your application using Azure Active Directory. 使用客户端流登录通常比使用 loginAsync() 方法更有利,因为它提供更直观的 UX 风格,并允许进行其他自定义。Using a client flow login is often preferable to using the loginAsync() methods as it provides a more native UX feel and allows for additional customization.

  1. 根据如何为 Active Directory 登录配置应用服务教程的说明,为 AAD 登录配置移动应用。Configure your mobile app backend for AAD sign-in by following the How to configure App Service for Active Directory login tutorial. 请务必完成注册本机客户端应用程序的可选步骤。Make sure to complete the optional step of registering a native client application.

  2. 可通过修改 build.gradle 文件并包含以下定义来安装 ADAL:Install ADAL by modifying your build.gradle file to include the following definitions:

    repositories {
        mavenCentral()
        flatDir {
            dirs 'libs'
        }
        maven {
            url "YourLocalMavenRepoPath\\.m2\\repository"
        }
    }
    packagingOptions {
        exclude 'META-INF/MSFTSIG.RSA'
        exclude 'META-INF/MSFTSIG.SF'
    }
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation('com.microsoft.aad:adal:1.16.1') {
            exclude group: 'com.android.support'
        } // Recent version is 1.16.1
        implementation 'com.android.support:support-v4:28.0.0'
    }
    
  3. 将以下代码添加到应用程序并进行以下替换:Add the following code to your application, making the following replacements:

    • INSERT-AUTHORITY-HERE 替换为在其中预配应用程序的租户的名称。Replace INSERT-AUTHORITY-HERE with the name of the tenant in which you provisioned your application. 格式应为 https://login.microsoftonline.com/contoso.onmicrosoft.comThe format should be https://login.microsoftonline.com/contoso.onmicrosoft.com.
    • INSERT-RESOURCE-ID-HERE 替换移动应用后端的客户端 ID。Replace INSERT-RESOURCE-ID-HERE with the client ID for your mobile app backend. 可以在门户中“Azure Active Directory 设置” 下面的“高级” 选项卡获取此客户端 ID。You can obtain the client ID from the Advanced tab under Azure Active Directory Settings in the portal.
    • INSERT-CLIENT-ID-HERE 替换为从本机客户端应用程序复制的客户端 ID。Replace INSERT-CLIENT-ID-HERE with the client ID you copied from the native client application.
    • INSERT-REDIRECT-URI-HERE 替换为站点的 /.auth/login/done 终结点(使用 HTTPS 方案)。Replace INSERT-REDIRECT-URI-HERE with your site's /.auth/login/done endpoint, using the HTTPS scheme. 此值应类似于 https://contoso.chinacloudsites.cn/.auth/login/doneThis value should be similar to https://contoso.chinacloudsites.cn/.auth/login/done.
private AuthenticationContext mContext;

private void authenticate() {
    String authority = "INSERT-AUTHORITY-HERE";
    String resourceId = "INSERT-RESOURCE-ID-HERE";
    String clientId = "INSERT-CLIENT-ID-HERE";
    String redirectUri = "INSERT-REDIRECT-URI-HERE";
    try {
        mContext = new AuthenticationContext(this, authority, true);
        mContext.acquireToken(this, resourceId, clientId, redirectUri, PromptBehavior.Auto, "", callback);
    } catch (Exception exc) {
        exc.printStackTrace();
    }
}

private AuthenticationCallback<AuthenticationResult> callback = new AuthenticationCallback<AuthenticationResult>() {
    @Override
    public void onError(Exception exc) {
        if (exc instanceof AuthenticationException) {
            Log.d(TAG, "Cancelled");
        } else {
            Log.d(TAG, "Authentication error:" + exc.getMessage());
        }
    }

    @Override
    public void onSuccess(AuthenticationResult result) {
        if (result == null || result.getAccessToken() == null
                || result.getAccessToken().isEmpty()) {
            Log.d(TAG, "Token is empty");
        } else {
            try {
                JSONObject payload = new JSONObject();
                payload.put("access_token", result.getAccessToken());
                ListenableFuture<MobileServiceUser> mLogin = mClient.login("aad", payload.toString());
                Futures.addCallback(mLogin, new FutureCallback<MobileServiceUser>() {
                    @Override
                    public void onFailure(Throwable exc) {
                        exc.printStackTrace();
                    }
                    @Override
                    public void onSuccess(MobileServiceUser user) {
                        Log.d(TAG, "Login Complete");
                    }
                });
            }
            catch (Exception exc){
                Log.d(TAG, "Authentication error:" + exc.getMessage());
            }
        }
    }
};

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (mContext != null) {
        mContext.onActivityResult(requestCode, resultCode, data);
    }
}

调整客户端与服务器之间的通信Adjust the Client-Server Communication

客户端连接通常是使用 Android SDK 随附的基础 HTTP 库实现的基本 HTTP 连接。The Client connection is normally a basic HTTP connection using the underlying HTTP library supplied with the Android SDK. 更改这种连接的原因有多种:There are several reasons why you would want to change that:

  • 想要使用备用的 HTTP 库来调整超时。You wish to use an alternate HTTP library to adjust timeouts.
  • 想要提供进度栏。You wish to provide a progress bar.
  • 想要添加自定义标头来支持 API 管理功能。You wish to add a custom header to support API management functionality.
  • 想要截获失败的响应,以便可以实现重新身份验证。You wish to intercept a failed response so that you can implement reauthentication.
  • 想要将后端请求记录到分析服务。You wish to log backend requests to an analytics service.

使用备用 HTTP 库Using an alternate HTTP Library

创建客户端引用后立即调用 .setAndroidHttpClientFactory() 方法。Call the .setAndroidHttpClientFactory() method immediately after creating your client reference. 例如,要将连接超时设置为 60 秒(而不是默认的 10 秒):For example, to set the connection timeout to 60 seconds (instead of the default 10 seconds):

mClient = new MobileServiceClient("https://myappname.chinacloudsites.cn");
mClient.setAndroidHttpClientFactory(new OkHttpClientFactory() {
    @Override
    public OkHttpClient createOkHttpClient() {
        OkHttpClient client = new OkHttpClient();
        client.setReadTimeout(60, TimeUnit.SECONDS);
        client.setWriteTimeout(60, TimeUnit.SECONDS);
        return client;
    }
});

实现进度筛选器Implement a Progress Filter

可以通过实现 ServiceFilter 来截获每个请求。You can implement an intercept of every request by implementing a ServiceFilter. 例如,以下代码将更新预先创建的进度栏:For example, the following updates a pre-created progress bar:

private class ProgressFilter implements ServiceFilter {
    @Override
    public ListenableFuture<ServiceFilterResponse> handleRequest(ServiceFilterRequest request, NextServiceFilterCallback next) {
        final SettableFuture<ServiceFilterResponse> resultFuture = SettableFuture.create();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mProgressBar != null) mProgressBar.setVisibility(ProgressBar.VISIBLE);
            }
        });

        ListenableFuture<ServiceFilterResponse> future = next.onNext(request);
        Futures.addCallback(future, new FutureCallback<ServiceFilterResponse>() {
            @Override
            public void onFailure(Throwable e) {
                resultFuture.setException(e);
            }
            @Override
            public void onSuccess(ServiceFilterResponse response) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (mProgressBar != null)
                            mProgressBar.setVisibility(ProgressBar.GONE);
                    }
                });
                resultFuture.set(response);
            }
        });
        return resultFuture;
    }
}

可按如下所示将此筛选器附加到客户端:You can attach this filter to the client as follows:

mClient = new MobileServiceClient(applicationUrl).withFilter(new ProgressFilter());

自定义请求标头Customize Request Headers

使用以下 ServiceFilter,并像附加 ProgressFilter 一样附加筛选器:Use the following ServiceFilter and attach the filter in the same way as the ProgressFilter:

private class CustomHeaderFilter implements ServiceFilter {
    @Override
    public ListenableFuture<ServiceFilterResponse> handleRequest(ServiceFilterRequest request, NextServiceFilterCallback next) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                request.addHeader("X-APIM-Router", "mobileBackend");
            }
        });
        SettableFuture<ServiceFilterResponse> result = SettableFuture.create();
        try {
            ServiceFilterResponse response = next.onNext(request).get();
            result.set(response);
        } catch (Exception exc) {
            result.setException(exc);
        }
    }
}

配置自动序列化Configure Automatic Serialization

可以使用 gson API,指定适用于每个列的转换策略。You can specify a conversion strategy that applies to every column by using the gson API. 将数据发送到 Azure 应用服务之前,Android 客户端库会在后台使用 gson 将 Java 对象序列化为 JSON 数据。The Android client library uses gson behind the scenes to serialize Java objects to JSON data before the data is sent to Azure App Service. 下面的代码使用 setFieldNamingStrategy() 方法设置策略。The following code uses the setFieldNamingStrategy() method to set the strategy. 此示例删除初始字符(“m”),并将每个字段名称的下一个字符小写。This example will delete the initial character (an "m"), and then lower-case the next character, for every field name. 例如,它将“mId”变为“id”。For example, it would turn "mId" into "id." 实现转换策略,减少在大多数字段中使用 SerializedName() 批注的需求。Implement a conversion strategy to reduce the need for SerializedName() annotations on most fields.

FieldNamingStrategy namingStrategy = new FieldNamingStrategy() {
    public String translateName(File field) {
        String name = field.getName();
        return Character.toLowerCase(name.charAt(1)) + name.substring(2);
    }
}

client.setGsonBuilder(
    MobileServiceClient
        .createMobileServiceGsonBuilder()
        .setFieldNamingStrategy(namingStrategy)
);

必须在使用 MobileServiceClient 创建移动客户端引用之前执行此代码。This code must be executed before creating a mobile client reference using the MobileServiceClient.