다음을 통해 공유


Android용 Azure Mobile Apps SDK를 사용하는 방법

이 가이드에서는 Mobile Apps용 Android 클라이언트 SDK를 사용하여 다음과 같은 일반적인 시나리오를 구현하는 방법을 보여줍니다.

  • 데이터 쿼리(삽입, 업데이트 및 삭제).
  • 인증.
  • 오류 처리.
  • 클라이언트 사용자 지정.

이 가이드에서는 클라이언트 쪽 Android SDK에 중점을 둡니다. Mobile Apps용 서버 쪽 SDK에 대해 자세히 알아보려면 .NET 백 엔드 SDK로 작업 또는 Node.js 백 엔드 SDK를 사용하는 방법을 참조하세요.

참조 설명서

GitHub에서 Android 클라이언트 라이브러리에 대한 Javadocs API 참조 를 찾을 수 있습니다.

지원되는 플랫폼

Android용 Azure Mobile Apps SDK는 휴대폰 및 태블릿 폼 팩터에 대해 API 수준 19~24(KitKat~Nougat)를 지원합니다. 특히 인증은 일반적인 웹 프레임워크 접근 방식을 활용하여 자격 증명을 수집합니다. 서버 흐름 인증은 시계와 같은 소형 폼 팩터 디바이스에서는 작동하지 않습니다.

설정 및 필수 구성 요소

Mobile Apps 빠른 시작 자습서를 완료합니다. 이 작업을 수행하면 Azure Mobile Apps 개발을 위한 모든 필수 구성 요소가 충족됩니다. 빠른 시작은 계정을 구성하고 첫 번째 모바일 앱 백 엔드를 만드는 데도 도움이 됩니다.

빠른 시작 자습서를 완료하지 않기로 결정한 경우 다음 작업을 완료합니다.

Gradle 빌드 파일 업데이트

build.gradle 파일을 모두 변경합니다.

  1. 프로젝트 수준 build.gradle 파일에 다음 코드를 추가합니다.

    buildscript {
        repositories {
            jcenter()
            google()
        }
    }
    
    allprojects {
        repositories {
            jcenter()
            google()
        }
    }
    
  2. 종속성 태그 내의 모듈 앱 수준 build.gradle 파일에 다음 코드를 추가합니다.

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

    현재 최신 버전은 3.4.0입니다. 지원되는 버전은 bintray에 나열 됩니다.

인터넷 사용 권한 사용

Azure에 액세스하려면 앱에 인터넷 사용 권한이 있어야 합니다. 아직 사용하지 않는 경우 코드의 다음 줄을 AndroidManifest.xml 파일에 추가합니다.

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

클라이언트 연결 만들기

Azure Mobile Apps는 모바일 애플리케이션에 다음 네 가지 기능을 제공합니다.

  • Azure Mobile Apps 서비스와의 데이터 액세스 및 오프라인 동기화.
  • Azure Mobile Apps 서버 SDK로 작성된 사용자 지정 API를 호출합니다.
  • Azure 앱 서비스 인증 및 권한 부여를 사용한 인증
  • Notification Hubs를 사용하여 푸시 알림 등록

이러한 각각의 함수는 먼저 MobileServiceClient 개체를 생성해야 합니다. 모바일 클라이언트 내에서 하나의 MobileServiceClient 개체만 만들어야 합니다(즉, Singleton 패턴이어야 합니다). MobileServiceClient 개체를 만들려면:

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

모바일 <MobileAppUrl> 백 엔드를 가리키는 문자열 또는 URL 개체입니다. Azure 앱 Service를 사용하여 모바일 백 엔드를 호스트하는 경우 보안 https:// 버전의 URL을 사용해야 합니다.

또한 클라이언트는 예제의 매개 변수인 Activity 또는 Context에 this 대한 액세스 권한이 필요합니다. MobileServiceClient 생성은 파일에서 AndroidManifest.xml 참조되는 활동의 메서드 내에서 onCreate() 발생해야 합니다.

모범 사례로 서버 통신을 자체(싱글톤 패턴) 클래스로 추상화해야 합니다. 이 경우 생성자 내에서 작업을 전달하여 서비스를 적절하게 구성해야 합니다. 예시:

package com.example.appname.services;

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

public class AzureServiceAdapter {
    private String mMobileBackendUrl = "https://myappname.azurewebsites.net";
    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); 할 수 있습니다. 클라이언트에 액세스해야 하는 다른 메서드는 서비스 어댑터에 대한 참조를 가져오는 데 사용합니다 AzureServiceAdapter.getInstance(); .

데이터 작업

Azure Mobile Apps SDK의 핵심은 모바일 앱 백 엔드에서 SQL Azure 내에 저장된 데이터에 대한 액세스를 제공하는 것입니다. 강력한 형식의 클래스(기본 설정) 또는 형식화되지 않은 쿼리(권장되지 않음)를 사용하여 이 데이터에 액세스할 수 있습니다. 이 섹션의 대부분은 강력한 형식의 클래스 사용을 다룹니다.

클라이언트 데이터 클래스 정의

SQL Azure 테이블에서 데이터에 액세스하려면 모바일 앱 백 엔드의 테이블에 해당하는 클라이언트 데이터 클래스를 정의합니다. 이 항목의 예제에서는 다음 열이 있는 MyDataTable이라는 테이블을 가정합니다.

  • id
  • text
  • 완료

해당하는 형식의 클라이언트 쪽 개체는 MyDataTable.java라는 파일에 있습니다.

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

추가하는 각 필드에 getter 및 setter 메서드를 추가합니다. SQL Azure 테이블이 더 많은 열을 포함하는 경우 이 클래스에 해당하는 필드를 추가합니다. 예를 들어 DTO(데이터 전송 개체)에 정수 우선 순위 열이 있는 경우 getter 및 setter 메서드와 함께 이 필드를 추가할 수 있습니다.

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;
}

Mobile Apps 백 엔드에서 추가 테이블을 만드는 방법을 알아보려면 방법: 동적 스키마(Node.js 백 엔드)를 사용하여 테이블 컨트롤러(.NET 백 엔드) 정의 또는 테이블 정의를 참조하세요.

Azure Mobile Apps 백 엔드 테이블은 5개의 특수 필드를 정의하며 이 중 4개는 클라이언트에서 사용할 수 있습니다.

  • String id: 레코드의 전역적으로 고유한 ID입니다. ID를 UUID 개체의 문자열 표현으로 만드는 것이 가장 좋습니다.
  • DateTimeOffset updatedAt: 마지막 업데이트의 날짜/시간입니다. updatedAt 필드는 서버에서 설정되며 클라이언트 코드로 설정해서는 안 됩니다.
  • DateTimeOffset createdAt: 개체가 만들어진 날짜/시간입니다. createdAt 필드는 서버에서 설정되며 클라이언트 코드로 설정해서는 안 됩니다.
  • byte[] version: 일반적으로 문자열로 표현되며 버전도 서버에서 설정합니다.
  • boolean deleted: 레코드가 삭제되었지만 아직 제거되지 않음을 나타냅니다. 클래스에서 속성으로 사용하지 deleted 마세요.

id 필드는 필수입니다. updatedAt 필드와 version 필드는 오프라인 동기화(각각 증분 동기화 및 충돌 해결)에 사용됩니다. 이 createdAt 필드는 참조 필드이며 클라이언트에서 사용되지 않습니다. 이름은 속성의 "전체" 이름이며 조정할 수 없습니다. 그러나 gson 라이브러리를 사용하여 개체와 "유선" 이름 간에 매핑을 만들 수 있습니다. 예시:

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();
    }
}

테이블 참조 만들기

테이블에 액세스하려면 먼저 MobileServiceClient에서 getTable 메서드를 호출하여 MobileServiceTable 개체를 만듭니다. 이 메서드에는 두 개의 오버로드가 있습니다.

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

다음 코드 에서 mClient 는 MobileServiceClient 개체에 대한 참조입니다. 첫 번째 오버로드는 클래스 이름과 테이블 이름이 동일한 위치에 사용되며 빠른 시작에서 사용되는 오버로드입니다.

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

두 번째 오버로드는 테이블 이름이 클래스 이름과 다를 때 사용됩니다. 첫 번째 매개 변수는 테이블 이름입니다.

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

백 엔드 테이블 쿼리

먼저 테이블 참조를 가져옵니다. 그런 다음 테이블 참조에 쿼리를 실행합니다. 쿼리는 다음의 조합입니다.

절은 이전 순서대로 표시되어야 합니다.

결과 필터링

쿼리의 일반적인 형태는 다음과 같습니다.

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

앞의 예제에서는 모든 결과를 반환합니다(서버에서 설정한 최대 페이지 크기까지). 메서드는 .execute() 백 엔드에서 쿼리를 실행합니다. 쿼리는 Mobile Apps 백 엔드로 전송되기 전에 OData v3 쿼리로 변환됩니다. 수신 시 Mobile Apps 백 엔드는 SQL Azure 인스턴스에서 실행하기 전에 쿼리를 SQL 문으로 변환합니다. 네트워크 활동에는 다소 시간이 걸리므로 메서드는 .execute() .를 반환합니다 ListenableFuture<E>.

반환된 데이터 필터링

다음 쿼리 실행은 완료false와 같은 ToDoItem 테이블에서 모든 항목을 반환합니다.

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

mToDoTable 은 이전에 만든 모바일 서비스 테이블에 대한 참조입니다.

테이블 참조에서 where 메서드 호출을 사용하여 필터를 정의합니다. where 메서드 뒤에는 field 메서드를 사용하고 그 뒤에 논리 조건자를 지정하는 메서드를 사용합니다. 가능한 조건자 메서드에는 eq(equals), ne(같지 않음), gt(보다 큼), ge(크거나 같음), lt(보다 작음), le(작거나 같음)가 포함됩니다. 이러한 메서드를 사용하면 숫자 및 문자열 필드를 특정 값과 비교할 수 있습니다.

날짜를 필터링할 수 있습니다. 다음 메서드를 사용하여 전체 날짜 필드 또는 날짜 부분(연도, 월, 일, 시간, 초)을 비교할 수 있습니다. 다음 예제에서는 기한2013과 같은 항목에 대한 필터를 추가합니다.

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

다음 메서드는 문자열 필드에 대한 복합 필터를 지원합니다. startsWith, endsWith, concat, subString, indexOf, replace, toLower, toUpper, trimlength. 다음 예제는 텍스트 열이 "PRI0"으로 시작되는 테이블 행을 필터링합니다.

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

숫자 필드에서 지원되는 연산자 메서드는 add, sub, mul, div, mod, floor, ceilinground입니다. 다음 예제는 기간이 짝수인 테이블 행을 필터링합니다.

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

조건자를 다음 논리 메서드와 결합할 수 있습니다 . 다음 예제는 앞의 예 중 두 가지를 결합합니다.

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

논리 연산자를 그룹화하고 중첩합니다.

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

필터링에 대한 자세한 내용과 예를 보려면 Android 클라이언트 쿼리 모델의 다양한 기능 알아보기를 참조하세요.

반환된 데이터 정렬

다음 코드는 텍스트 필드를 기준으로 오름차순으로 정렬된 ToDoItems 테이블의 모든 항목을 반환합니다. mToDoTable 은 이전에 만든 백 엔드 테이블에 대한 참조입니다.

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

orderBy 메서드의 첫 번째 매개 변수는 정렬할 필드의 이름과 동일한 문자열입니다. 두 번째 매개 변수는 QueryOrder 열거형을 사용하여 오름차순이나 내림차순으로 정렬할지를 지정합니다. where 메서드를 사용하여 필터링하는 경우 orderBy 메서드 앞에 where 메서드를 호출해야 합니다.

특정 열 선택

다음 코드는 ToDoItems 테이블에서 모든 항목을 반환하는 방법을 보여 주지만 전체텍스트 필드만 표시합니다. mToDoTable 은 이전에 만든 백 엔드 테이블에 대한 참조입니다.

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

select 함수의 매개 변수는 반환하려는 테이블 열의 문자열 이름입니다. select 메서드는 where 및 orderBy같은 메서드를 따라야 합니다. 건너뛰기 및 위쪽같은 페이징 메서드가 뒤따를 수 있습니다.

페이지에서 데이터 반환

데이터는 항상 페이지에 반환됩니다. 반환되는 최대 레코드 수는 서버에서 설정합니다. 클라이언트가 더 많은 레코드를 요청하면 서버는 최대 레코드 수를 반환합니다. 기본적으로 서버의 최대 페이지 크기는 50개의 레코드입니다.

첫 번째 예제에서는 테이블에서 상위 5개 항목을 선택하는 방법을 보여줍니다. 쿼리는 ToDoItems 테이블의 항목을 반환합니다. mToDoTable 은 이전에 만든 백 엔드 테이블에 대한 참조입니다.

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

다음은 처음 5개 항목을 건너뛰고 다음 5개를 반환하는 쿼리입니다.

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

테이블의 모든 레코드를 얻으려면 모든 페이지를 반복하는 코드를 구현합니다.

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);

이 메서드를 사용하는 모든 레코드에 대한 요청은 Mobile Apps 백 엔드에 대해 최소 두 개의 요청을 만듭니다.

적절한 페이지 크기를 선택하는 것은 요청이 발생하는 동안 메모리 사용량, 대역폭 사용량 및 데이터 수신 지연 간의 균형입니다. 기본값(50개 레코드)은 모든 디바이스에 적합합니다. 더 큰 메모리 디바이스에서만 작동하는 경우 최대 500개까지 증가합니다. 페이지 크기를 레코드 500개를 초과하도록 늘리면 허용되지 않는 지연과 큰 메모리 문제가 발생합니다.

방법: 쿼리 메서드 연결

백 엔드 테이블을 쿼리하는 데 사용되는 메서드를 연결할 수 있습니다. 쿼리 메서드를 연결하면 정렬 및 페이징되는 필터링된 행의 특정 열을 선택할 수 있습니다. 상당히 복잡한 논리 필터를 만들 수 있습니다. 각 쿼리 메서드는 Query 개체를 반환합니다. 일련의 메서드를 종료하고 실제로 쿼리를 실행하려면 execute 메서드를 호출합니다. 예시:

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();

연결된 쿼리 메서드는 다음과 같이 정렬되어야 합니다.

  1. 필터링(where) 메서드.
  2. Sorting(orderBy) 메서드.
  3. 선택(선택) 메서드.
  4. 페이징(skiptop) 메서드.

사용자 인터페이스에 데이터 바인딩

데이터 바인딩에는 세 가지 구성 요소가 필요합니다.

  • 데이터 원본
  • 화면 레이아웃
  • 둘을 연결하는 어댑터입니다.

샘플 코드에서는 Mobile Apps SQL Azure 테이블 ToDoItem 의 데이터를 배열로 반환합니다. 이 활동은 데이터 애플리케이션에 대한 일반적인 패턴입니다. 데이터베이스 쿼리는 종종 클라이언트가 목록 또는 배열에 가져오는 행 컬렉션을 반환합니다. 이 샘플에서 배열은 데이터 원본입니다. 이 코드는 디바이스에 표시되는 데이터의 보기를 정의하는 화면 레이아웃을 지정합니다. 이 두 요소는 어댑터(이 코드에서 ArrayAdapter<ToDoItem> 클래스의 확장)를 통해 바인딩됩니다.

레이아웃 정의

레이아웃은 XML 코드의 여러 코드 조각에 의해 정의됩니다. 기존 레이아웃을 고려할 때 다음 코드는 서버 데이터로 채울 ListView를 나타냅니다.

    <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를 지정합니다. 이 코드는 확인란과 연결된 텍스트를 지정하고 목록의 각 항목에 대해 한 번 인스턴스화됩니다. 이 레이아웃은 ID 필드를 표시하지 않으며, 더 복잡한 레이아웃은 디스플레이에 추가 필드를 지정합니다. 이 코드는 row_list_to_do.xml 파일에 있습니다.

<?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>

어댑터 정의

뷰의 데이터 원본은 ToDoItem의 배열이므로 ArrayAdapter<ToDoItem> 클래스에서 어댑터를 서브클래스합니다. 이 하위 클래스는 row_list_to_do 레이아웃을 사용하여 모든 ToDoItem에 대한 뷰를 생성합니다. 코드에는 ArrayAdapter<E> 클래스의 확장인 다음 클래스를 정의합니다.

public class ToDoItemAdapter extends ArrayAdapter<ToDoItem> {
}

어댑터 getView 메서드를 재정의합니다. 예시:

    @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;
    }

작업에서 다음과 같이 이 클래스의 인스턴스를 만듭니다.

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

ToDoItemAdapter 생성자의 두 번째 매개 변수는 레이아웃에 대한 참조입니다. 이제 ListView를 인스턴스화하고 ListView에 어댑터를 할당할 수 있습니다.

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

어댑터를 사용하여 UI에 바인딩

이제 데이터 바인딩을 사용할 준비가 되었습니다. 다음 코드에서는 테이블의 항목을 가져오고 로컬 어댑터를 반환된 항목으로 채우는 방법을 보여 줍니다.

    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 테이블을 수정할 때 언제든지 어댑터를 호출하세요. 레코드별로 수정이 수행되므로 컬렉션 대신 단일 행을 처리합니다. 항목을 삽입할 때 어댑터에서 add 메서드를 호출합니다. 삭제할 때 remove 메서드를 호출합니다.

Android 빠른 시작 프로젝트에서 전체 예제를 찾을 수 있습니다.

백 엔드에 데이터 삽입

ToDoItem 클래스의 인스턴스를 인스턴스화하고 해당 속성을 설정합니다.

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

그런 다음 insert()를 사용하여 개체를 삽입합니다.

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

반환된 엔터티는 백 엔드 테이블에 삽입된 데이터와 일치하며, ID 및 백 엔드에 설정된 다른 값(예: createdAt, updatedAtversion 필드)을 포함합니다.

Mobile Apps 테이블에는 ID라는 기본 키 열이 필요합니다. 이 열은 문자열이어야 합니다. ID 열의 기본값은 GUID입니다. 전자 메일 주소 또는 사용자 이름과 같은 다른 고유 값을 제공할 수 있습니다. 삽입된 레코드에 문자열 ID 값이 제공되지 않으면 백 엔드에서 새 GUID를 생성합니다.

문자열 ID 값은 다음과 같은 이점을 제공합니다.

  • 데이터베이스를 왕복하지 않고 ID를 생성할 수 있습니다.
  • 여러 테이블 또는 데이터베이스의 레코드를 병합하기가 더 쉽습니다.
  • ID 값은 애플리케이션의 논리와 더 잘 통합됩니다.

문자열 ID 값은 오프라인 동기화 지원에 필요합니다 . Id가 백 엔드 데이터베이스에 저장되면 변경할 수 없습니다.

모바일 앱에서 데이터 업데이트

테이블의 데이터를 업데이트하려면 update() 메서드에 새 개체를 전달합니다.

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

이 예제에서 항목은 ToDoItem 테이블의 행에 대한 참조로, 일부 변경 내용이 적용되었습니다. ID가 동일한 행이 업데이트됩니다.

모바일 앱의 데이터 삭제

다음 코드는 데이터 개체를 지정하여 테이블에서 데이터를 삭제하는 방법을 보여줍니다.

mToDoTable
    .delete(item);

삭제할 행의 ID 필드를 지정하여 항목을 삭제할 수도 있습니다.

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

ID로 특정 항목 조회

lookUp() 메서드를 사용하여 특정 id 필드 값을 가진 항목을 조회합니다.

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

방법: 형식화되지 않은 데이터 작업

형식화되지 않은 프로그래밍 모델은 JSON 직렬화를 정확하게 제어할 수 있습니다. 형식화되지 않은 프로그래밍 모델을 사용할 수 있는 몇 가지 일반적인 시나리오가 있습니다. 예를 들어 백 엔드 테이블에 많은 열이 포함되어 있고 열의 하위 집합만 참조하면 되는 경우입니다. 형식화된 모델을 사용하려면 데이터 클래스의 Mobile Apps 백 엔드에 정의된 모든 열을 정의해야 합니다. 데이터에 액세스하기 위한 대부분의 API 호출은 형식화된 프로그래밍 호출과 유사합니다. 주요 차이점은 형식화되지 않은 모델에서 MobileServiceTable 개체 대신 MobileServiceJsonTable 개체에서 메서드호출한다는 것입니다.

형식화되지 않은 테이블 인스턴스 만들기

형식화된 모델과 마찬가지로 테이블 참조를 가져오는 것으로 시작하지만, 이 경우 MobileServicesJsonTable 개체입니다. 클라이언트의 인스턴스에 대해 getTable 메서드를 호출하여 참조를 가져옵니다.

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

MobileServiceJsonTable 인스턴스를 만든 후에는 형식화된 프로그래밍 모델과 거의 동일한 API를 사용할 수 있습니다. 경우에 따라 메서드는 형식화된 매개 변수 대신 형식화되지 않은 매개 변수를 사용합니다.

형식화되지 않은 테이블에 삽입

다음 코드는 삽입하는 방법을 보여 줍니다. 첫 번째 단계는 gson 라이브러리의 일부인 JsonObject만드는 것입니다.

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

그런 다음 insert()를 사용하여 형식화되지 않은 개체를 테이블에 삽입합니다.

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

삽입된 개체의 ID를 가져와야 하는 경우 getAsJsonPrimitive() 메서드를 사용합니다.

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

형식화되지 않은 테이블에서 삭제

다음 코드에서는 이전 삽입 예제에서 만든 JsonObject동일한 인스턴스인 인스턴스를 삭제하는 방법을 보여 줍니다. 코드는 형식화된 사례와 동일하지만 JsonObject를 참조하므로 메서드의 서명이 다릅니다.

mToDoTable
    .delete(insertedItem);

또한 해당 ID를 사용하여 인스턴스를 직접 삭제할 수도 있습니다.

mToDoTable.delete(ID);

형식화되지 않은 테이블에서 모든 행 반환

다음 코드는 전체 테이블을 검색하는 방법을 보여줍니다. JSON 테이블을 사용하므로 테이블의 일부 열만 선택적으로 검색할 수 있습니다.

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();
}

형식화된 모델에 사용할 수 있는 동일한 필터링, 필터링 및 페이징 메서드 집합을 형식화되지 않은 모델에 사용할 수 있습니다.

오프라인 동기화 구현

또한 Azure Mobile Apps 클라이언트 SDK는 SQLite 데이터베이스를 사용하여 서버 데이터의 복사본을 로컬로 저장하여 데이터의 오프라인 동기화를 구현합니다. 오프라인 테이블에 수행되는 작업에는 모바일 연결이 필요하지 않습니다. 오프라인 동기화는 충돌 해결을 위해보다 복잡한 논리를 사용하는 대신 복원력과 성능을 향상시킵니다. Azure Mobile Apps 클라이언트 SDK는 다음 기능을 구현합니다.

  • 증분 동기화: 업데이트된 레코드와 새 레코드만 다운로드되어 대역폭 및 메모리 소비를 절약합니다.
  • 낙관적 동시성: 작업이 성공하는 것으로 간주됩니다. 충돌 해결은 서버에서 업데이트가 수행될 때까지 지연됩니다.
  • 충돌 해결: SDK는 서버에서 충돌하는 변경이 발생하면 이를 감지하고 사용자에게 경고하기 위해 후크를 제공합니다.
  • 일시 삭제: 삭제된 레코드가 삭제된 것으로 표시되기 때문에 다른 디바이스가 오프라인 캐시를 업데이트할 수 있습니다.

오프라인 동기화 초기화

각 오프라인 테이블은 사용하기 전에 오프라인 캐시에서 정의해야 합니다. 일반적으로 테이블 정의는 클라이언트를 만든 직후에 수행됩니다.

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);
}

오프라인 캐시 테이블에 대한 참조 가져오기

온라인 테이블의 경우 .getTable()을 사용합니다. 오프라인 테이블의 경우 다음을 사용합니다 .getSyncTable().

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

온라인 테이블에서 사용할 수 있는 모든 메서드(필터링, 정렬, 페이징, 데이터 삽입, 데이터 업데이트 및 데이터 삭제 포함)는 온라인 및 오프라인 테이블에서 동일하게 작동합니다.

로컬 오프라인 캐시 동기화

동기화는 앱의 제어 내에 있습니다. 다음은 동기화 방법의 예입니다.

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) 경우 증분 동기화는 마지막으로 성공적으로 풀을 완료한 이후 생성되거나 변경된 레코드만 반환하는 데 사용됩니다.

오프라인 동기화 중 충돌 처리

작업 중에 .push() 충돌이 발생하면 throw MobileServiceConflictException 됩니다. 서버에서 발급한 항목은 예외에 포함되며 예외에 의해 .getItem() 검색될 수 있습니다. MobileServiceSyncContext 개체에서 다음 항목을 호출하여 푸시를 조정합니다.

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

모든 충돌이 원하는 대로 표시되면 .push()를 다시 호출하여 모든 충돌을 해결합니다.

사용자 지정 API 호출

사용자 지정 API를 사용하면 삽입, 업데이트, 삭제 또는 읽기 작업에 매핑되지 않는 서버 기능을 노출하는 사용자 지정 엔드포인트를 정의할 수 있습니다. 사용자 지정 API를 사용하면 HTTP 메시지 헤더 읽기 및 설정, JSON 이외의 메시지 본문 형식 정의 등 메시징을 더 잘 제어할 수 있습니다.

Android 클라이언트에서 invokeApi 메서드를 호출하여 사용자 지정 API 엔드포인트를 호출합니다. 다음 예제에서는 completeAll이라는 API 엔드포인트를 호출하는 방법을 보여주며 이는 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 요청을 보내는 클라이언트에서 호출됩니다. 사용자 지정 API에서 반환된 결과는 오류와 마찬가지로 메시지 대화 상자에 표시됩니다. 다른 버전의 invokeApi 를 사용하면 필요에 따라 요청 본문에 개체를 보내고, HTTP 메서드를 지정하고, 요청과 함께 쿼리 매개 변수를 보낼 수 있습니다. 형식화되지 않은 버전의 invokeApi 도 제공됩니다.

앱에 인증 추가

자습서에서는 이러한 기능을 추가하는 방법을 자세히 설명합니다.

App Service는 Facebook, Google, Microsoft 계정, Twitter 및 Azure Active Directory와 같이 다양한 외부 ID 공급자를 사용하여 앱 사용자의 인증 을 지원합니다. 테이블에 대한 사용 권한을 설정하여 특정 작업에 대한 액세스를 인증된 사용자로만 제한할 수 있습니다. 인증된 사용자의 ID를 사용하여 백 엔드에서 권한 부여 규칙을 구현할 수도 있습니다.

서버 흐름과 클라이언트 흐름이라는 두 가지 인증 흐름이 지원됩니다. 서버 흐름의 경우 ID 공급자의 웹 인터페이스를 사용하므로 인증 경험이 가장 단순합니다. 서버 흐름 인증을 구현하기 위해 추가 SDK가 필요하지는 않습니다. 서버 흐름 인증은 모바일 디바이스에 대한 심층 통합을 제공하지 않으며 개념 증명 시나리오에만 권장됩니다.

클라이언트 흐름을 사용하면 ID 공급자가 제공하는 SDK를 사용함에 따라 Single Sign-On과 같은 디바이스별 기능과 더 심층 통합할 수 있습니다. 예를 들어 Facebook SDK를 모바일 애플리케이션에 통합할 수 있습니다. 모바일 클라이언트는 Facebook 앱으로 전환하고 모바일 앱으로 다시 교환하기 전에 로그온을 확인합니다.

앱에서 인증을 사용하도록 설정하려면 4단계가 필요합니다.

  • ID 공급자로 인증할 수 있도록 앱을 등록합니다.
  • App Service 백 엔드를 구성합니다.
  • App Service 백 엔드에서만 인증된 사용자로 테이블 권한을 제한합니다.
  • 앱에 인증 코드 추가

테이블에 대한 사용 권한을 설정하여 특정 작업에 대한 액세스를 인증된 사용자로만 제한할 수 있습니다. 인증된 사용자의 SID를 사용하여 요청을 수정할 수도 있습니다. 자세한 내용은 인증 시작 및 서버 SDK HOWTO 설명서를 검토하세요.

인증: 서버 흐름

다음 코드는 Google 공급자를 사용하여 서버 흐름 로그인 프로세스를 시작합니다. Google 공급자에 대한 보안 요구 사항으로 인해 추가 구성이 필요합니다.

MobileServiceUser user = mClient.login(MobileServiceAuthenticationProvider.Google, "{url_scheme_of_your_app}", GOOGLE_LOGIN_REQUEST_CODE);

또한 기본 활동 클래스에 다음 메서드를 추가합니다.

// 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 GOOGLE_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 == GOOGLE_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");
            }
        }
    }
}

기본 작업에 정의된 내용은 GOOGLE_LOGIN_REQUEST_CODE 메서드 및 메서드 내에서 onActivityResult() 사용됩니다login(). 메서드와 onActivityResult() 메서드 내에서 login() 동일한 숫자를 사용하는 한 고유 번호를 선택할 수 있습니다. 앞에서 설명한 것처럼 클라이언트 코드를 서비스 어댑터로 추상화하면 서비스 어댑터에서 적절한 메서드를 호출해야 합니다.

또한 customtabs에 대한 프로젝트를 구성해야 합니다. 먼저 리디렉션 URL을 지정합니다. 다음 코드 조각을 다음 코드 조각에 추가합니다 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>

애플리케이션의 파일에 redirectUriSchemebuild.gradle 추가합니다.

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

마지막으로 파일의 종속성 목록에 build.gradle 추가 com.android.support:customtabs:28.0.0 합니다.

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를 가져옵니다. 미래를 사용하여 비동기 로그인 API를 호출하는 방법의 예제는 인증 시작을 참조하세요.

Warning

언급된 URL 체계는 대/소문자를 구분합니다. 일치하는 경우의 {url_scheme_of_you_app} 모든 항목을 확인합니다.

인증 토큰 캐시

인증 토큰을 캐싱하려면 사용자 ID 및 인증 토큰을 디바이스에 로컬로 저장해야 합니다. 다음에 앱이 시작될 때 캐시를 확인하고 이러한 값이 있는 경우 로그인 프로시저를 건너뛰고 이 데이터로 클라이언트를 리하일레이션할 수 있습니다. 그러나 이 데이터는 민감하며 휴대폰이 도난당한 경우 안전을 위해 암호화된 상태로 저장해야 합니다. 캐시 인증 토큰 섹션에서 인증 토큰을 캐시하는 방법의 전체 예제를 볼 수 있습니다.

만료된 토큰을 사용하려고 하면 401 권한이 없는 응답을 받게 됩니다. 필터를 사용하여 인증 오류를 처리할 수 있습니다. App Service 백 엔드에 대한 가로채기 요청을 필터링합니다. 필터 코드는 401에 대한 응답을 테스트하고 로그인 프로세스를 트리거한 다음 401을 생성한 요청을 다시 시작합니다.

새로 고침 토큰 사용

Azure 앱 서비스 인증 및 권한 부여에서 반환되는 토큰의 수명은 1시간으로 정의됩니다. 이 기간이 지나면 사용자를 다시 인증해야 합니다. 클라이언트 흐름 인증을 통해 받은 수명이 긴 토큰을 사용하는 경우 동일한 토큰을 사용하여 Azure 앱 서비스 인증 및 권한 부여로 다시 인증할 수 있습니다. 또 다른 Azure 앱 서비스 토큰은 새 수명을 사용하여 생성됩니다.

공급자를 등록하여 새로 고침 토큰을 사용할 수도 있습니다. 새로 고침 토큰을 항상 사용할 수 있는 것은 아닙니다. 추가 구성이 필요합니다.

  • Azure Active Directory의 경우 Azure Active Directory 앱에 대한 클라이언트 암호를 구성합니다. Azure Active Directory 인증을 구성할 때 Azure 앱 서비스에서 클라이언트 비밀을 지정합니다. .login()을 호출하면 response_type=code id_token을 매개 변수로 전달합니다.

    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);
    
  • Googleaccess_type=offline 경우 매개 변수로 전달합니다.

    HashMap<String, String> parameters = new HashMap<String, String>();
    parameters.put("access_type", "offline");
    MobileServiceUser user = mClient.login
        MobileServiceAuthenticationProvider.Google,
        "{url_scheme_of_your_app}",
        GOOGLE_LOGIN_REQUEST_CODE,
        parameters);
    
  • Microsoft 계정wl.offline_access 경우 범위를 선택합니다.

토큰을 새로 고치려면 .refreshUser()를 호출합니다.

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

모범 사례로 서버에서 401 응답을 감지하고 사용자 토큰을 새로 고치려고 하는 필터를 만듭니다.

클라이언트 흐름 인증으로 로그인

클라이언트 흐름 인증으로 로그인하는 일반적인 프로세스는 다음과 같습니다.

  • 서버 흐름 인증과 마찬가지로 Azure 앱 서비스 인증 및 권한 부여를 구성합니다.

  • 인증을 위해 인증 공급자 SDK를 통합하여 액세스 토큰을 생성합니다.

  • 다음과 같이 메서드를 .login() 호출합니다( 이어야 합니다.AuthenticationResult)result

    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");
        }
    });
    

다음 섹션에서 전체 코드 예제를 참조하세요.

성공적인 로그인에 onSuccess() 사용하려는 코드로 메서드를 바꿉 있습니다. 문자열은 {provider} aad(Azure Active Directory), facebook, google, microsoftaccount 또는 twitter와 같은 유효한 공급자입니다. 사용자 지정 인증을 구현한 경우 사용자 지정 인증 공급자 태그를 사용할 수도 있습니다.

ADAL(Active Directory 인증 라이브러리)을 사용하여 사용자 인증

ADAL(Active Directory 인증 라이브러리)을 사용하여 Azure Active Directory를 사용하여 애플리케이션에 사용자를 로그인할 수 있습니다. 클라이언트 흐름 로그인은 UX 느낌을 그대로 제공하고 추가 사용자 지정이 가능하기 때문에 loginAsync() 메서드보다 선호도가 높습니다.

  1. Active Directory용 App Service를 구성하는 방법 로그인 자습서에 따라 AAD 로그인에 대한 모바일 앱 백 엔드를 구성합니다. 네이티브 클라이언트 애플리케이션을 등록하는 선택적 단계를 완료해야 합니다.

  2. 다음 정의를 포함하도록 build.gradle 파일을 수정하여 ADAL을 설치합니다.

    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. 애플리케이션에 아래 코드를 추가하여 다음과 같이 대체합니다.

    • INSERT-AUTHORITY-HERE를 애플리케이션을 프로비전한 테넌트 이름으로 바꿉다. 형식은 https://login.microsoftonline.com/contoso.onmicrosoft.com이어야 합니다.
    • INSERT-RESOURCE-ID-HERE를 모바일 앱 백 엔드의 클라이언트 ID로 바꿉니다. 포털의 Azure Active Directory 설정 아래 고급 탭에서 클라이언트 ID를 가져올 수 있습니다.
    • INSERT-CLIENT-ID-HERE를 네이티브 클라이언트 애플리케이션에서 복사한 클라이언트 ID로 바꿉니다.
    • HTTPS 체계를 사용하여 INSERT-REDIRECT-URI-HERE 를 사이트의 /.auth/login/done 엔드포인트로 바꿉니다. 이 값은 https://contoso.azurewebsites.net/.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);
    }
}

클라이언트-서버 통신 조정

클라이언트 연결은 일반적으로 Android SDK와 함께 제공되는 기본 HTTP 라이브러리를 사용하는 기본 HTTP 연결입니다. 변경하려는 몇 가지 이유가 있습니다.

  • 대체 HTTP 라이브러리를 사용하여 시간 제한을 조정하려고 합니다.
  • 진행률 표시줄을 제공하려고 합니다.
  • API 관리 기능을 지원하기 위해 사용자 지정 헤더를 추가하려고 합니다.
  • 다시 인증을 구현할 수 있도록 실패한 응답을 가로채려고 합니다.
  • 백 엔드 요청을 분석 서비스에 기록하려고 합니다.

대체 HTTP 라이브러리 사용

클라이언트 참조를 .setAndroidHttpClientFactory() 만든 직후 메서드를 호출합니다. 예를 들어 연결 시간 제한을 60초(기본값 10초 대신)로 설정하려면 다음을 수행합니다.

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

진행률 필터 구현

를 구현하여 모든 요청의 절편을 구현할 수 있습니다 ServiceFilter. 예를 들어, 다음은 미리 만든 진행률 표시줄을 업데이트합니다.

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;
    }
}

다음과 같이 이 필터를 클라이언트에 연결할 수 있습니다.

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

요청 헤더 사용자 지정

다음 ServiceFilter를 사용하고 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);
        }
    }
}

자동 Serialization 구성

gson API를 사용하여 모든 열에 적용되는 변환 전략을 지정할 수 있습니다. Android 클라이언트 라이브러리는 백그라운드에서 gson을 사용하여 데이터가 Azure 앱 Service로 전송되기 전에 Java 개체를 JSON 데이터로 직렬화합니다. 다음 코드는 setFieldNamingStrategy() 메서드를 사용하여 전략을 설정합니다. 이 예제에서는 모든 필드 이름에 대해 초기 문자("m")를 삭제한 다음 소문자 다음 문자를 삭제합니다. 예를 들어 "mId"를 "id"로 바꿔야 합니다. 대부분의 필드에서 주석의 필요성을 줄이기 위한 SerializedName() 변환 전략을 구현합니다.

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를 사용하여 모바일 클라이언트 참조를 만들기 전에 이 코드를 실행해야 합니다.