Android 프로젝트를 작성하려면 워크 스테이션에 Java JDK가 설치되어 있어야 합니다. Android Studio를 다운로드하여 설치하고 Android SDK 관리자에서 Android 6.0 SDK(API 레벨 23 이상)를 다운로드합니다.
또한 에뮬레이터 이미지도 다운로드합니다. 이를 위해 Android Studio에서 AVD Manager를 선택해야 합니다. + Create Virtual Device를 선택하고 지침에 따라 설치를 완료합니다.
1 부에서 계속
1부에서 우리는 애완 동물의 목록을 표시하고 새로운 애완 동물을 추가할 수 있는 Android 앱을 만들었습니다. 새 애완 동물을 추가하면 앱은 다음과 같은 형태가 됩니다.
최적 업데이트 및 오프라인 지원
최적 업데이트 기능을 통해 반응성이 뛰어난 최종 사용자 환경을 제공할 수 있습니다. 서버가 결국 우리가 기대하는 데이터를 반환하는 것처럼 동작하도록 UI를 구성합니다. 업데이트가 성공적이라는 것은 낙관적입니다.
이 섹션에서는 변형 이후에 메모리에 반환될 것으로 예상되는 데이터를 만들고 Android 디바이스가 관리하는 영구 SQL 저장소에 작성합니다. 그런 다음, 서버 업데이트가 반환되면 SDK가 데이터를 통합합니다.
이 접근법은 데이터 수정을 시도하는 동안 인터넷 연결이 끊기는 시나리오에 매우 적합합니다. AWS AppSync SDK는 앱이 온라인 상태가 되면 자동으로 다시 연결하여 변형을 전송합니다.
이제 실습해 봅시다. AddPetActivity.java를 열고 save()가 끝날 때 오프라인 지원을 추가하십시오.
private void save() {
// ... Other code ...
ClientFactory.appSyncClient().mutate(addPetMutation).
refetchQueries(ListPetsQuery.builder().build()).
enqueue(mutateCallback);
// Enables offline support via an optimistic update
// Add to event list while offline or before request returns
addPetOffline(input);
}
이제 addPetOffline 메서드를 추가해 봅시다. 우리는 로컬 캐시에 기록한 후에 연결성을 확인하고 추가 작업이 성공적인 것처럼 활동을 종료합니다.
private void addPetOffline(CreatePetInput input) {
final CreatePetMutation.CreatePet expected =
new CreatePetMutation.CreatePet(
"Pet",
UUID.randomUUID().toString(),
input.name(),
input.description());
final AWSAppSyncClient awsAppSyncClient = ClientFactory.appSyncClient();
final ListPetsQuery listEventsQuery = ListPetsQuery.builder().build();
awsAppSyncClient.query(listEventsQuery)
.responseFetcher(AppSyncResponseFetchers.CACHE_ONLY)
.enqueue(new GraphQLCall.Callback<ListPetsQuery.Data>() {
@Override
public void onResponse(@Nonnull Response<ListPetsQuery.Data> response) {
List<ListPetsQuery.Item> items = new ArrayList<>();
if (response.data() != null) {
items.addAll(response.data().listPets().items());
}
items.add(new ListPetsQuery.Item(expected.__typename(),
expected.id(),
expected.name(),
expected.description()));
ListPetsQuery.Data data = new ListPetsQuery.Data(new ListPetsQuery.ListPets("ModelPetConnection", items, null));
awsAppSyncClient.getStore().write(listEventsQuery, data).enqueue(null);
Log.d(TAG, "Successfully wrote item to local store while being offline.");
finishIfOffline();
}
@Override
public void onFailure(@Nonnull ApolloException e) {
Log.e(TAG, "Failed to update event query list.", e);
}
});
}
private void finishIfOffline(){
// Close the add activity when offline otherwise allow callback to close
ConnectivityManager cm =
(ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null &&
activeNetwork.isConnectedOrConnecting();
if (!isConnected) {
Log.d(TAG, "App is offline. Returning to MainActivity .");
finish();
}
}
query() 메서드가 CACHE_AND_NETWORK 방식을 사용하기 때문에 MainActivity를 변경할 필요가 없습니다. 네트워크 호출을 하는 동안 먼저 로컬 캐시에서 읽습니다. 이전에 추가한 애완 동물은 최적 업데이트 때문에 이미 로컬 캐시에 있습니다.
앱을 구축하고 실행합니다. 로그인 한 후 비행기 모드를 켭니다.
앱으로 돌아가서 새 항목을 추가하십시오. UI 환경은 이전에 온라인 상태였던 때와 동일해야 합니다. 이름 및 설명을 입력합니다.
Save를 선택합니다. 앱에 목록의 두 번째 항목이 표시되어야 합니다.
이제 비행기 모드를 해제하십시오. 항목은 자동으로 저장되어야 합니다. 앱을 닫았다가 다시 열어서 동일한 두 항목이 계속 표시되는지 확인하면 저장이 완료되었는지 확인할 수 있습니다.
구독 기능
AWS AppSync를 사용하면 실시간 알림을 위해 구독을 사용할 수 있습니다.
이 섹션에서는 구독을 사용하여 다른 사람이 새 애완 동물을 추가할 때 즉시 알려줍니다. 이를 위해 MainActivity.java 클래스의 끝에 다음 블록을 추가해 보겠습니다.
private AppSyncSubscriptionCall subscriptionWatcher;
private void subscribe(){
OnCreatePetSubscription subscription = OnCreatePetSubscription.builder().build();
subscriptionWatcher = ClientFactory.appSyncClient().subscribe(subscription);
subscriptionWatcher.execute(subCallback);
}
private AppSyncSubscriptionCall.Callback subCallback = new AppSyncSubscriptionCall.Callback() {
@Override
public void onResponse(@Nonnull Response response) {
Log.i("Response", "Received subscription notification: " + response.data().toString());
// Update UI with the newly added item
OnCreatePetSubscription.OnCreatePet data = ((OnCreatePetSubscription.Data)response.data()).onCreatePet();
final ListPetsQuery.Item addedItem = new ListPetsQuery.Item(data.__typename(), data.id(), data.name(), data.description());
runOnUiThread(new Runnable() {
@Override
public void run() {
mPets.add(addedItem);
mAdapter.notifyItemInserted(mPets.size() - 1);
}
});
}
@Override
public void onFailure(@Nonnull ApolloException e) {
Log.e("Error", e.toString());
}
@Override
public void onCompleted() {
Log.i("Completed", "Subscription completed");
}
};
그런 다음, onResume 메서드를 수정하여 마지막에 subscribe를 호출하여 새 애완 동물 생성을 구독하십시오. 또한 활동이 끝나면 구독을 취소해야 합니다.
그런 다음, AddPetActivity.java에서 TransferUtility를 이용해 사진을 업로드하는 코드를 추가해 보겠습니다.
private String getS3Key(String localPath) {
//We have read and write ability under the public folder
return "public/" + new File(localPath).getName();
}
public void uploadWithTransferUtility(String localPath) {
String key = getS3Key(localPath);
Log.d(TAG, "Uploading file from " + localPath + " to " + key);
TransferObserver uploadObserver =
ClientFactory.transferUtility().upload(
key,
new File(localPath));
// Attach a listener to the observer to get state update and progress notifications
uploadObserver.setTransferListener(new TransferListener() {
@Override
public void onStateChanged(int id, TransferState state) {
if (TransferState.COMPLETED == state) {
// Handle a completed upload.
Log.d(TAG, "Upload is completed. ");
// Upload is successful. Save the rest and send the mutation to server.
save();
}
}
@Override
public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
float percentDonef = ((float) bytesCurrent / (float) bytesTotal) * 100;
int percentDone = (int)percentDonef;
Log.d(TAG, "ID:" + id + " bytesCurrent: " + bytesCurrent
+ " bytesTotal: " + bytesTotal + " " + percentDone + "%");
}
@Override
public void onError(int id, Exception ex) {
// Handle errors
Log.e(TAG, "Failed to upload photo. ", ex);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddPetActivity.this, "Failed to upload photo", Toast.LENGTH_LONG).show();
}
});
}
});
}
사진 저장
애완 동물 객체에 새 속성을 추가했으므로 이를 수용하도록 있도록 코드를 수정해야 합니다. AddPetActivity.java에서 다음과 같은 메서드를 추출하여 사진이 선택되었는지 여부에 따라 다른 유형의 CreatePetInput을 생성합니다.
private CreatePetInput getCreatePetInput() {
final String name = ((EditText) findViewById(R.id.editTxt_name)).getText().toString();
final String description = ((EditText) findViewById(R.id.editText_description)).getText().toString();
if (photoPath != null && !photoPath.isEmpty()){
return CreatePetInput.builder()
.name(name)
.description(description)
.photo(getS3Key(photoPath)).build();
} else {
return CreatePetInput.builder()
.name(name)
.description(description)
.build();
}
}
그런 다음, 추출된 메소드를 호출하도록 save()를 수정합니다.
private void save() {
CreatePetInput input = getCreatePetInput();
CreatePetMutation addPetMutation = CreatePetMutation.builder()
.input(input)
.build();
ClientFactory.appSyncClient().mutate(addPetMutation).
refetchQueries(ListPetsQuery.builder().build()).
enqueue(mutateCallback);
// Enables offline support via an optimistic update
// Add to event list while offline or before request returns
addPetOffline(input);
}
CreatePet 변형을 변경했으므로 addPetOffline 코드도 수정해야 합니다.
private void addPetOffline(final CreatePetInput input) {
final CreatePetMutation.CreatePet expected =
new CreatePetMutation.CreatePet(
"Pet",
UUID.randomUUID().toString(),
input.name(),
input.description(),
input.photo());
final AWSAppSyncClient awsAppSyncClient = ClientFactory.appSyncClient();
final ListPetsQuery listPetsQuery = ListPetsQuery.builder().build();
awsAppSyncClient.query(listPetsQuery)
.responseFetcher(AppSyncResponseFetchers.CACHE_ONLY)
.enqueue(new GraphQLCall.Callback<ListPetsQuery.Data>() {
@Override
public void onResponse(@Nonnull Response<ListPetsQuery.Data> response) {
List<ListPetsQuery.Item> items = new ArrayList<>();
if (response.data() != null) {
items.addAll(response.data().listPets().items());
}
items.add(new ListPetsQuery.Item(expected.__typename(),
expected.id(),
expected.name(),
expected.description(),
expected.photo()));
ListPetsQuery.Data data = new ListPetsQuery.Data(
new ListPetsQuery.ListPets("ModelPetConnection", items, null));
awsAppSyncClient.getStore().write(listPetsQuery, data).enqueue(null);
Log.d(TAG, "Successfully wrote item to local store while being offline.");
finishIfOffline();
}
@Override
public void onFailure(@Nonnull ApolloException e) {
Log.e(TAG, "Failed to update event query list.", e);
}
});
}
그런 다음, uploadAndSave()라는 새로운 메서드를 만들어 사진 및 사진 저장 작업을 모두 처리할 수 있습니다.
private void uploadAndSave(){
if (photoPath != null) {
// For higher Android levels, we need to check permission at runtime
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
Log.d(TAG, "READ_EXTERNAL_STORAGE permission not granted! Requesting...");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
1);
}
// Upload a photo first. We will only call save on its successful callback.
uploadWithTransferUtility(photoPath);
} else {
save();
}
}
MyAdapter.java를 여십시오. 사진 속성에 해당하는 코드를 추가하고 사진이 있는 경우 사진을 다운로드하십시오.
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
// ... other code ...
// stores and recycles views as they are scrolled off screen
class ViewHolder extends RecyclerView.ViewHolder {
TextView txt_name;
TextView txt_description;
ImageView image_view;
String localUrl;
ViewHolder(View itemView) {
super(itemView);
txt_name = itemView.findViewById(R.id.txt_name);
txt_description = itemView.findViewById(R.id.txt_description);
image_view = itemView.findViewById(R.id.image_view);
}
void bindData(ListPetsQuery.Item item) {
txt_name.setText(item.name());
txt_description.setText(item.description());
if (item.photo() != null) {
if (localUrl == null) {
downloadWithTransferUtility(item.photo());
} else {
image_view.setImageBitmap(BitmapFactory.decodeFile(localUrl));
}
}
else
image_view.setImageBitmap(null);
}
private void downloadWithTransferUtility(final String photo) {
final String localPath = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/" + photo;
TransferObserver downloadObserver =
ClientFactory.transferUtility().download(
photo,
new File(localPath));
// Attach a listener to the observer to get state update and progress notifications
downloadObserver.setTransferListener(new TransferListener() {
@Override
public void onStateChanged(int id, TransferState state) {
if (TransferState.COMPLETED == state) {
// Handle a completed upload.
localUrl = localPath;
image_view.setImageBitmap(BitmapFactory.decodeFile(localPath));
}
}
@Override
public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
float percentDonef = ((float) bytesCurrent / (float) bytesTotal) * 100;
int percentDone = (int) percentDonef;
Log.d(TAG, " ID:" + id + " bytesCurrent: " + bytesCurrent + " bytesTotal: " + bytesTotal + " " + percentDone + "%");
}
@Override
public void onError(int id, Exception ex) {
// Handle errors
Log.e(TAG, "Unable to download the file.", ex);
}
});
}
}
}
사진을 다운로드하기 때문에 권한을 부여 받을 수 있어야 합니다. MainActivity.java로 이동하여 query()에서 권한 찾기 블록을 추가합니다.
public void query(){
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
Log.d(TAG, "WRITE_EXTERNAL_STORAGE permission not granted! Requesting...");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
2);
}
ClientFactory.appSyncClient().query(ListPetsQuery.builder().build())
.responseFetcher(AppSyncResponseFetchers.CACHE_AND_NETWORK)
.enqueue(queryCallback);
}
드디어 모든 작업을 마쳤습니다. 다시 앱을 구축하고 실행합니다. 사진을 추가하고 멋진 애완 동물을 볼 수 있는지 확인하십시오! 앱은 다음과 같은 형태로 나타날 것입니다.
기타 기능
앱의 기능을 강화할 수 있는 다른 기능도 있습니다. 다음 내용을 직접 연습해 보십시오.
애완 동물의 정보를 업데이트하는 기능을 추가하십시오.
애완 동물을 삭제하는 기능을 추가하십시오.
변형을 업데이트하고 삭제하려면 구독하십시오.
Android 앱에 오프라인 지원, 실시간 구독 및 객체 저장소를 추가 할 수 있게 되었습니다. 더 자세한 것은 AWS Amplify 웹 사이트에서 AWS Amplify에서 살펴 볼 수 있습니다.
– Jane Shen, AWS 프로페셔널 서비스 애플리케이션 아키텍트
혹시 만약 React를 사용하여 사용자들이 사진을 업로드하고 공유하는 data-driven 기반의 안전한 사진 갤러리 웹 애플리케이션 개발을 하고자 하신다면 단계별 실습 과정을 살펴 보시기 바랍니다.