Google 포토 및 Flutter로 사진 공유 앱 빌드

빌드할 프로그램

이 Codelab에서는 사용자가 사진을 공유할 수 있는 현장 학습 앱인 Field Trippa를 빌드합니다.

Google 포토 라이브러리 API를 사용하여 자체 애플리케이션에서 미디어 공유 환경을 지원하는 방법을 알아봅니다.

이 Codelab의 앱은 단일 코드베이스에서 멋진 네이티브 컴파일 모바일 애플리케이션, 웹 애플리케이션, 데스크톱 애플리케이션을 빌드할 수 있는 Google의 UI 도구 키트인 Flutter를 사용하여 빌드되었습니다. Flutter에 관한 자세한 내용은 https://flutter.dev를 참고하세요.

6571e359f222ccf6.png

학습할 내용

요구사항

  • Flutter 개발 환경
  • Google 포토에 액세스할 수 있는 다양한 에뮬레이터 또는 기기에 설정된 2개의 Google 사용자 계정 - 사용자 간에 공유를 테스트할 수 있음
  • Android 기기, 에뮬레이터 또는 실제 iOS 기기 - iOS 시뮬레이터는 카메라 하드웨어 누락으로 인해 지원되지 않음

이 Codelab에서는 Google 포토 라이브러리 API를 사용하여 빌드되었으며 여행 또는 현장 학습 사진을 공유하는 앱을 빌드합니다.

사용자는 Google 로그인을 사용하여 로그인하고 애플리케이션이 Google 포토 라이브러리 API를 사용하도록 승인합니다.

그러면 사용자가 설명과 함께 사진을 업로드하기 위한 trip을 만들 수 있습니다. 그리고 각 trip을 애플리케이션의 다른 멤버(이 멤버도 사진 공유에 참여할 수 있음)와 공유할 수 있습니다.

146953eced1f4f92.png

내부적으로 각 trip은 Google 포토 내의 공유 앨범으로 저장됩니다. 앱이 이 앨범과 관련된 공유 및 업로드를 처리합니다. 그러나 앱이 없는 다른 사용자와도 Google 포토 URL을 통해 직접 앨범을 공유할 수 있습니다.

c4af82aa4bf8cc31.png

이 Codelab에서 학습하고 싶은 내용은 무엇인가요?

이 주제를 처음 접하므로 개요를 파악하고 싶습니다. 이 주제에 관해 약간 알고 있지만 한 번 더 확인하고 싶습니다. 프로젝트에 사용할 코드 예를 찾고 있습니다. 특정 항목에 관한 설명을 찾고 있습니다.

b2f84ff91b0e1396.png이 Codelab의 소스 코드 다운로드:

소스 코드 다운로드 GitHub에서 탐색

(시작 앱 코드는 저장소main 분기에서 제공됩니다.)

다운로드한 ZIP 파일의 압축을 해제합니다. 그러면 시작하는 데 필요한 모든 코드 및 리소스가 포함된 루트 폴더인 photos-sharing-main이 생성됩니다.

원하는 Flutter IDE(예: Dart 및 Flutter 플러그인이 설치된 VSCode 또는 Android 스튜디오)에서 추출된 폴더를 엽니다.

최종 구현 코드

다음 링크는 완전히 구현된 최종 애플리케이션 버전을 가리킵니다. 문제가 있거나 구현을 비교해 보려는 경우 이 코드를 사용할 수 있습니다.

최종 소스 코드 다운로드 GitHub에서 최종 소스 코드 탐색

(최종 앱 코드는 저장소final 분기에서 제공됩니다.)

이전에 Flutter를 사용한 개발 경험이 없는 경우 이 문서의 단계에 따라 개발 환경을 설정합니다.

'Field Trippa' 앱을 실행하려면 개발 IDE의 '실행' 버튼을 클릭하거나 소스 코드의 루트 디렉터리에서 다음 명령어를 사용합니다.

flutter run

다음과 같이 'Connect with Google Photos' 화면이 표시됩니다.

6bfc7e3fab746b8d.png

Google 포토 라이브러리 API를 사용하려면 OAuth 2.0을 사용하여 사용자를 인증해야 합니다. 사용자는 애플리케이션에 로그인하고 애플리케이션이 자신을 대신하여 API와 상호작용하도록 승인합니다.

이 단계의 끝부분에서 몇 가지 추가 문제 해결 도움말을 확인할 수 있습니다.

새 Firebase 프로젝트 만들기 및 앱 등록

b2f84ff91b0e1396.png Firebase Console로 이동하여 '+ 프로젝트 추가'를 선택합니다. 프로젝트 이름을 입력하고 '프로젝트 만들기'를 선택하여 계속 진행합니다. Firebase Console에서 다른 단계는 따르지 않습니다. 대신, 이 Codelab으로 돌아와서 아래의 'Android' 또는 'iOS' 부분을 진행하여 애플리케이션을 구성합니다.

Android만 해당: Android에서 앱을 실행하는 경우 다음과 같이 Android 앱을 등록합니다.

b2f84ff91b0e1396.png Android 아이콘을 클릭하여 Android 앱 등록 화면을 엽니다.

b2f84ff91b0e1396.png 패키지com.google.codelab.photos.sharing을 입력합니다.

b2f84ff91b0e1396.png 다음과 같이 머신에서 서명 인증서 SHA-1을 검색합니다.

Windows의 경우 다음 명령어를 실행합니다.

keytool -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore -list -v -storepass android

Mac 또는 Linux의 경우 다음 명령어를 실행합니다.

keytool -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v -storepass android

b2f84ff91b0e1396.png '앱 등록'을 클릭하여 계속 진행합니다.

b2f84ff91b0e1396.png google-service.json 파일을 컴퓨터에 다운로드하여 'android/app/' 디렉터리로 이동합니다. (도움말: Android 스튜디오에서 프로젝트 측면 패널의 올바른 위치로 다운로드한 파일을 직접 드래그할 수 있습니다.)

이 파일에는 방금 설정한 Firebase 및 Google Developers 프로젝트의 프로젝트 구성이 포함되어 있습니다.

(자세한 내용은 google_sign_in 패키지에 관한 문서를 참고하세요.)

Firebase Console에서 다른 단계를 완료할 필요는 없습니다. Firebase SDK가 이미 애플리케이션에 추가되었습니다.

iOS만 해당: iOS에서 앱을 실행하는 경우 다음과 같이 Firebase에 iOS 앱을 등록합니다.

b2f84ff91b0e1396.png iOS 아이콘을 클릭하여 iOS 앱 등록 화면을 엽니다.

b2f84ff91b0e1396.png iOS 번들 IDcom.google.codelab.photos.sharing을 입력합니다.

b2f84ff91b0e1396.png '다음'을 클릭하여 계속 진행합니다.

b2f84ff91b0e1396.png GoogleService-Info.plist 파일을 컴퓨터에 다운로드합니다.

b2f84ff91b0e1396.png Xcode에서 Flutter 프로젝트를 엽니다.

b2f84ff91b0e1396.png Runner 디렉터리를 마우스 오른쪽 버튼으로 클릭하여 'Add Files to Runner(실행기에 파일 추가)'를 선택한 후 다운로드한 GoogleService-Info.plist 파일을 선택하여 Runner(실행기) 모듈에 추가합니다.

b2f84ff91b0e1396.png ios/Runner/Info.plist 파일의 소스 코드를 수정하고 GoogleService-Info.plist 파일의 REVERSED_CLIENT_ID 속성값을 추가합니다. 다음과 같이 파일 하단의 항목을 바꿉니다.

ios/Runner/Info.plist

<!-- Google Sign-in Section -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>COPY_REVERSED_CLIENT_ID_HERE</string>
    </array>
  </dict>
</array>
<!-- End of the Google Sign-in Section -->

(자세한 내용은 google_sign_in 패키지에 관한 문서를 참고하세요.)

Google 포토 라이브러리 API 사용 설정

b2f84ff91b0e1396.png Google Developers Console에서 API 화면을 열고 'Google 포토 라이브러리 API'를 사용 설정합니다. ('사용 설정' 버튼이 사용 중지된 경우 먼저 화면 상단에서 Firebase 프로젝트를 선택해야 할 수 있습니다.)

b2f84ff91b0e1396.png Google Developers Console에서 OAuth 동의 화면 구성을 열어 Google 포토 라이브러리 API 범위 및 이메일 주소를 추가합니다. (이 구성은 Google 포토 라이브러리 API에서 사용하는 범위의 OAuth 확인 검토에 필요합니다.) 확인을 위해 제출할 필요는 없지만 양식을 작성하고 응답을 저장해야 합니다. 이렇게 하면 개발 및 테스트에 범위를 사용할 수 있습니다.

  • '애플리케이션 이름'을 입력합니다(예: Field Trippa Codelab).
  • '지원 이메일 주소'를 선택합니다.
  • '범위 추가'를 선택한 후 '수동으로 범위 붙여넣기'를 선택하여 다음 범위를 입력합니다.
https://www.googleapis.com/auth/photoslibrary
https://www.googleapis.com/auth/photoslibrary.sharing
  • '저장'을 선택합니다.
  • 이 Codelab을 계속 진행하려고 확인을 위해 제출할 필요는 없습니다. 이는 애플리케이션을 시작할 때만 필요하며 개인적인 테스트에는 필요하지 않습니다.

앱 실행 및 로그인

Google 로그인은 이미 google_sign_in flutter 패키지를 사용하여 구현되어 있습니다. 이 패키지에는 이전에 프로젝트에 복사한 google-services.json 또는 GoogleService-Info.plist 파일이 필요합니다.

b2f84ff91b0e1396.png 애플리케이션을 다시 실행하고 'Connect to Google Photos'를 선택합니다. 사용자 계정을 선택하고 인증 범위에 동의하라는 메시지가 표시됩니다.

모든 사항이 성공적으로 설정되었다면 다음 화면에 빈 목록이 표시됩니다.

9f3bcae1f8e7cd0d.png

로그인 문제 해결

애플리케이션에 로그인하는 데 문제가 있다면 다음 사항을 확인해 보세요.

b2f84ff91b0e1396.png 기기가 인터넷에 연결되어 있는지 확인합니다.

b2f84ff91b0e1396.png PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10: , null) 오류가 발생하면 Google 포토 라이브러리 API 사용 설정 섹션의 모든 단계를 따랐는지 확인합니다. Google 포토 라이브러리 API 범위를 추가하고 지원 이메일 주소를 입력한 후 저장을 선택해야 합니다.

b2f84ff91b0e1396.png r PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 12500: , null) 오류가 발생하면 Firebase Console에서 지원 이메일 주소를 추가했는지 확인합니다. Firebase Console을 열고 프로젝트 제목 옆의 톱니바퀴 아이콘을 선택하여 프로젝트 설정으로 이동합니다. 일반 설정 화면의 지원 이메일에서 이메일 주소를 선택합니다.

b2f84ff91b0e1396.png Firebase Console에 구성된 서명 인증서 SHA-1을 확인합니다. 첫 번째 단계의 keytool 명령어 출력과 일치하나요? android/ 프로젝트에서 실행 시 ./gradlew signingReport 명령어의 출력과 일치하나요? Console에서 서명 인증서 SHA-256을 포함해야 할 수도 있습니다.

b2f84ff91b0e1396.png Firebase Console에 구성된 패키지 이름iOS 번들 ID를 확인합니다. 이는 com.google.codelab.photos.sharing으로 설정되어 있어야 합니다.

b2f84ff91b0e1396.png Firebase Console에서 다운로드한 구성 파일의 위치를 확인합니다. Android의 경우 파일을 android/app/google-service.json에 복사해야 합니다. iOS의 경우 파일을 Runner(실행기) 모듈에 추가해야 합니다.

b2f84ff91b0e1396.png Google을 Firebase 프로젝트의 로그인 제공업체로 사용 설정해야 할 수 있습니다. Firebase Console을 열고 Authentication > 로그인 방법으로 이동합니다. 로그인 제공업체로 Google이 사용 설정되어 있는지 확인합니다.

Google 포토 라이브러리 API에 대한 첫 번째 API 호출을 구현하기 전에 'Field Trippa' 앱에서 사용하는 데이터 아키텍처를 살펴보겠습니다.

앱 아키텍처

  • 각 화면은 별도의 페이지로 구현됩니다. 71b9a588fb1bbb41.png
  • PhotosLibraryApiModel은 애플리케이션의 데이터 모델을 설명하고 Google 포토 라이브러리 API 호출을 추상화합니다.
  • 라이브러리 API에 대한 HTTPS REST 호출은 PhotosLibraryApiClient에서 구현됩니다. 이 클래스에서 제공하는 각 호출은 호출에 관한 매개변수 및 옵션을 지정하는 *Request 객체를 사용합니다.
  • 라이브러리 API에는 OAuth2를 통한 사용자 인증이 필요합니다. 모든 API 호출에 포함되어야 하는 액세스 토큰은 google_sign_in 패키지에서 직접 PhotosLibraryApiClient에 설정합니다.

앨범 만들기 API 호출 구현

각 여행은 Google 포토에 앨범으로 저장됩니다. 'CREATE A TRIP ALBUM' 버튼을 선택하면 사용자에게 여행의 이름을 묻는 메시지를 표시하고 이 이름을 제목으로 사용하여 새 앨범을 만듭니다.

b2f84ff91b0e1396.png create_trip_page.dart에서 라이브러리 API에 앨범 생성을 요청하는 로직을 작성합니다. 파일의 끝부분에 _createTrip(...) 메서드를 구현하여 사용자가 입력한 여행 이름으로 PhotosLibraryApiModel을 호출합니다.

lib/pages/create_trip_page.dart

Future<void> _createTrip(BuildContext context) async {
  // Display the loading indicator.
  setState(() => _isLoading = true);

  await ScopedModel.of<PhotosLibraryApiModel>(context)
      .createAlbum(tripNameFormController.text);

  // Hide the loading indicator.
  setState(() => _isLoading = false);
  Navigator.pop(context);
}

b2f84ff91b0e1396.png 앨범을 만드는 라이브러리 API 호출을 구현합니다. API 모델에서 앨범 제목을 매개변수로 사용하는 createAlbum(...) 메서드를 구현합니다. 이 메서드는 실제 REST 호출이 이루어지는 PhotosLibraryApiClient를 호출합니다.

lib/model/photos_library_api_model.dart

Future<Album> createAlbum(String title) async {
  final album = await client.createAlbum(CreateAlbumRequest.fromTitle(title));
  updateAlbums();
  return album;
}

b2f84ff91b0e1396.png photos_library_api_client.dart에서 앨범을 만드는 REST 호출을 구현합니다. 아시다시피 CreateAlbumRequest에는 이 호출에 필요한 title 속성이 이미 포함되어 있습니다.

다음으로, 이 요청을 JSON으로 인코딩하고 인증 헤더를 추가하여 요청을 승인합니다. 마지막으로, API에서 만든 앨범을 반환합니다.

lib/photos_library_api/photos_library_api_client.dart

Future<Album> createAlbum(CreateAlbumRequest request) async {
  final response = await http.post(
    Uri.parse('https://photoslibrary.googleapis.com/v1/albums'),
    body: jsonEncode(request),
    headers: await _authHeaders,
  );

  printError(response);

  return Album.fromJson(jsonDecode(response.body));
}

사용해 보기

b2f84ff91b0e1396.png 앱을 배포하고 '+ Create Trip'을 선택합니다.

2f0bec785bec1710.gif

여행 목록에 앱에서 만들지 않은 Google 포토의 다른 앨범이 표시되는 것을 알 수 있습니다. (Google 포토에 다른 앨범이 없을 때 이 동작을 확인하려면 Google 포토 앱을 열고 앨범을 만듭니다. 그러나 이 Codelab을 계속 진행하기 위해 그렇게 할 필요는 없습니다.)

아시다시피 각 여행은 Google 포토에 앨범으로 저장됩니다. 그러나 다른 방법으로 만든 Google 포토의 다른 앨범을 표시하는 것은 의미가 없습니다. Field Trippa에서는 이 앱으로 만든 여행만 표시해야 합니다.

표시되는 여행 목록을 제한하는 API를 사용하여 이 앱에서 만든 여행만 표시되도록 할 수 있습니다.

b2f84ff91b0e1396.png photos_library_api_client.dart에서 listAlbums() 메서드(listSharedAlbums() 메서드가 아님)를 수정합니다. 이 메서드는 앨범 목록을 검색하는 REST 호출을 실행합니다. 반환되는 데이터를 제한하는 excludeNonAppCreatedData=true 매개변수를 추가하여 이 앱에서 만들지 않은 앨범을 제외합니다.

lib/photos_library_api/photos_library_api_client.dart

Future<ListAlbumsResponse> listAlbums() async {
  final response = await http.get(
        Uri.parse('https://photoslibrary.googleapis.com/v1/albums?'
            'pageSize=50&excludeNonAppCreatedData=true'),
        headers: await _authHeaders);

       ...
}

사용해 보기

이제 첫 번째 페이지에는 앱에서 만든 여행만 표시됩니다.

c7c20b76dcbfbfea.png

다음 단계에서는 여행에 사진을 업로드합니다. 데이터는 사용자의 Google 포토 계정에 저장되므로 직접 데이터를 저장하거나 처리하는 작업에 관해 걱정할 필요가 없습니다.

Flutter에서 사진 촬영

b2f84ff91b0e1396.png 먼저, contribute_photo 대화상자에서 _getImage(...) 메서드를 구현합니다. 이 메서드는 사용자가 '+ADD PHOTO' 버튼을 클릭할 때 호출됩니다.

다음 코드는 image_picker 패키지를 사용하여 사진을 촬영하고 UI를 업데이트하며 API 모델을 호출하여 이미지를 업로드합니다. (다음 단계에서 이를 구현합니다.) _getImage(...) 호출은 나중에 Google 포토에 사진을 만드는 데 필요한 업로드 토큰을 저장합니다.

lib/components/contribute_photo_dialog.dart

Future _getImage(BuildContext context) async {
  // Use the image_picker package to prompt the user for a photo from their
  // device.
  final pickedImage = await _imagePicker
      .getImage(
        source: ImageSource.camera,
      );
  final pickedFile = File(pickedImage.path);

  // Store the image that was selected.
  setState(() {
    _image = pickedFile;
    _isUploading = true;
  });

  // Make a request to upload the image to Google Photos once it was selected.
  final uploadToken = await ScopedModel.of<PhotosLibraryApiModel>(context)
      .uploadMediaItem(pickedFile);

  setState(() {
    // Once the upload process has completed, store the upload token.
    // This token is used together with the description to create the media
    // item later.
    _uploadToken = uploadToken;
    _isUploading = false;
  });
}

이미지를 업로드하는 라이브러리 API 호출을 구현하여 업로드 토큰 가져오기

사진 및 동영상을 라이브러리 API에 업로드하는 과정은 다음과 같은 두 단계로 이루어집니다.

  1. 업로드 토큰을 수신할 미디어 바이트 업로드
  2. 업로드 토큰에서 사용자 라이브러리의 미디어 항목 만들기

b2f84ff91b0e1396.png 미디어를 업로드하는 REST 요청을 구현합니다. 업로드 요청 유형과 파일 이름을 지정하려면 일부 헤더를 설정해야 합니다. 다음과 같이 photos_library_api_client.dart 파일에서 파일을 업로드하는 uploadMediaItem(...) 메서드를 구현하여 HTTP 호출이 반환하는 업로드 토큰을 반환합니다.

lib/photos_library_api/photos_library_api_client.dart

Future<String> uploadMediaItem(File image) async {
  // Get the filename of the image
  final filename = path.basename(image.path);

  // Set up the headers required for this request.
  final headers = <String, String>{};
  headers.addAll(await _authHeaders);
  headers['Content-type'] = 'application/octet-stream';
  headers['X-Goog-Upload-Protocol'] = 'raw';
  headers['X-Goog-Upload-File-Name'] = filename;

  // Make the HTTP request to upload the image. The file is sent in the body.
  final response = await http.post(
    Uri.parse('https://photoslibrary.googleapis.com/v1/uploads'),
    body: image.readAsBytesSync(),
    headers: await _authHeaders,
  );

  printError(response);

  return response.body;
}

업로드 토큰에서 미디어 항목 만들기

다음으로, 업로드 토큰에서 사용자 라이브러리의 미디어 항목 만들기를 구현합니다.

미디어 항목을 만들려면 업로드 토큰, 사진 설명 또는 동영상 자막과 같은 설명(선택사항), 앨범 식별자(선택사항)가 필요합니다. Field Trippa는 항상 업로드된 사진을 여행 앨범에 직접 추가합니다.

b2f84ff91b0e1396.png 업로드 토큰, 설명, 앨범 ID를 사용하여 mediaItems.batchCreate를 호출하는 photos_library_api_client 호출을 구현합니다. API 모델에서 라이브러리 API를 호출하는 createMediaItem(...) 메서드를 구현합니다. 이 메서드는 미디어 항목을 반환합니다.

(이 호출의 photos_library_client는 이미 구현되어 있습니다.)

lib/model/photos_library_api_model.dart

Future<BatchCreateMediaItemsResponse> createMediaItem(
    String uploadToken, String albumId, String description) async {
  // Construct the request with the token, albumId and description.
  final request =
      BatchCreateMediaItemsRequest.inAlbum(uploadToken, albumId, description);

  // Make the API call to create the media item. The response contains a
  // media item.
  final response = await client.batchCreateMediaItems(request);

  // Print and return the response.
  print(response.newMediaItemResults[0].toJson());
  return response;
}

사용해 보기

b2f84ff91b0e1396.png 앱을 열고 여행을 선택합니다. contribute를 클릭하고 이전에 촬영한 사진을 선택합니다. 설명을 입력하고 upload를 선택합니다. 이미지가 몇 초 후에 여행에 표시됩니다.

b2f84ff91b0e1396.png Google 포토 앱에서 앨범을 엽니다. 그러면 이 여행의 앨범에 새 이미지가 표시됩니다.

526ede994fcd5d8d.gif

지금까지 여행을 만들고 여행에 설명이 있는 사진을 업로드하는 기능을 구현했습니다. 백엔드에서 각 여행은 Google 포토에 앨범으로 저장됩니다.

다음으로, 이 애플리케이션을 사용하지 않는 다른 사람들과 여행을 공유합니다.

각 여행은 Google 포토의 앨범으로 지원됩니다. 따라서 URL을 통해 앨범을 '공유'하고 이 URL을 알고 있는 사람은 누구나 앨범을 이용할 수 있게 할 수 있습니다.

앨범을 공유하는 호출 구현

앨범 상단의 share 버튼을 누르면 여행 페이지에서 앨범이 공유됩니다.

b2f84ff91b0e1396.png 모델을 호출하여 앨범을 공유한 후 표시되는 앨범을 새로고침하는 비동기 호출인 _shareAlbum(...)을 구현합니다. 앨범을 새로고침하면 나중에 대화상자에서 사용자에게 표시할 shareableUrl이 포함된 shareInfo 속성이 전파됩니다.

lib/pages/trip_page.dart

Future<void> _shareAlbum(BuildContext context) async {
  // Show the loading indicator
  setState(() => _inSharingApiCall = true);

  const snackBar = SnackBar(
    duration: Duration(seconds: 3),
    content: Text('Sharing Album...'),
  );
  Scaffold.of(context).showSnackBar(snackBar);

  // Share the album and update the local model
  await ScopedModel.of<PhotosLibraryApiModel>(context).shareAlbum(album.id);
  final updatedAlbum =
      await ScopedModel.of<PhotosLibraryApiModel>(context).getAlbum(album.id);

  print('Album has been shared.');
  setState(() {
    album = updatedAlbum;
    // Hide the loading indicator
    _inSharingApiCall = false;
  });
}

b2f84ff91b0e1396.png 사용자가 페이지 상단의 'SHARE WITH ANYONE' 버튼을 클릭할 때 호출되는 _showShareableUrl(...) 메서드를 구현합니다. 먼저, 앨범이 이미 공유되었는지 확인하고 앨범이 공유되었다면 _showUrlDialog(...) 메서드를 호출합니다.

lib/pages/trip_page.dart

Future<void> _showShareableUrl(BuildContext context) async {
  if (album.shareInfo == null || album.shareInfo.shareableUrl == null) {
    print('Not shared, sharing album first.');
    // Album is not shared yet, share it first, then display dialog
    await _shareAlbum(context);
    _showUrlDialog(context);
  } else {
    // Album is already shared, display dialog with URL
    _showUrlDialog(context);
  }
}

b2f84ff91b0e1396.png 대화상자에 shareableUrl을 표시하는 _showUrlDialog(...) 메서드를 구현합니다.

lib/pages/trip_page.dart

void _showUrlDialog(BuildContext context) {
  print('This is the shareableUrl:\n${album.shareInfo.shareableUrl}');

  _showShareDialog(
      context,
      'Share this URL with anyone. '
      'Anyone with this URL can access all items.',
      album.shareInfo.shareableUrl);
}

사용해 보기

앱의 기본 화면에는 아직 공유되지 않은 여행만 나열됩니다. 걱정하지 마세요. 다음 단계에서 이를 구현합니다. 지금은 이 화면을 벗어나는 경우 새 여행을 만들 수 있습니다.

b2f84ff91b0e1396.png 앱을 열고 여행을 선택합니다. 화면 상단의 'SHARE WITH ANYONE'을 선택하고 브라우저에서 반환된 URL을 엽니다. (도움말: URL도 로그에 출력되므로 컴퓨터에서 URL을 쉽게 복사할 수 있습니다. Android 스튜디오에서 로그는 'Run' 탭에 표시됩니다.)

1d1a40c1078e4221.gif

Google 포토에서는 URL에 액세스할 수 있는 누구나 액세스 가능한 URL을 통해 앨범을 공유할 수 있습니다. 또한 라이브러리 API를 사용하면 공유 토큰을 통해 앨범을 공유할 수도 있습니다. 공유 토큰은 애플리케이션 내에서 API를 통해 사용자를 공유 앨범에 참여시키는 데 사용되는 문자열입니다.

애플리케이션에서 라이브러리 API를 통해 앨범을 공유하는 프로세스는 다음과 같습니다.

  1. 사용자 A가 애플리케이션에 로그인하고 라이브러리 API를 승인합니다.
  2. 앨범을 만듭니다.
  3. 앨범 식별자를 사용하여 앨범을 공유합니다.
  4. 공유 토큰을 다른 사용자에게 전송합니다.

참여 프로세스는 다음과 비슷합니다.

  1. 사용자 B가 애플리케이션에 로그인하고 라이브러리 API를 승인합니다.
  2. 사용자가 참여해야 하는 앨범의 공유 토큰을 검색합니다.
  3. 공유 토큰을 사용하여 앨범에 참여합니다.

공유 앨범은 Google 포토 내 '공유' 탭에 표시됩니다.

공유 토큰 표시

이전 단계에서 이미 앨범을 공유하는 _shareAlbum(...) 메서드를 구현했습니다. shareInfo 속성에는 화면에 표시될 '공유 토큰'도 포함되어 있습니다.

b2f84ff91b0e1396.png 여행 페이지에서 사용자가 화면의 'SHARE WITH FIELD TRIPPA' 버튼을 누를 때 호출되는 _showShareToken(...) 메서드를 구현합니다.

lib/pages/trip_page.dart

Future<void> _showShareToken(BuildContext context) async {
  if (album.shareInfo == null) {
    print('Not shared, sharing album first.');
    // Album is not shared yet, share it first, then display dialog
    await _shareAlbum(context);
    _showTokenDialog(context);
  } else {
    // Album is already shared, display dialog with token
    _showTokenDialog(context);
  }
}

다음으로, _showTokenDialog(...) 메서드에서 '공유 토큰' 표시를 구현합니다. 토큰은 앨범의 shareInfo 속성의 일부입니다.

lib/pages/trip_page.dart

void _showTokenDialog(BuildContext context) {
  print('This is the shareToken:\n${album.shareInfo.shareToken}');
  _showShareDialog(
      context, 'Use this token to share', album.shareInfo.shareToken);
}

공유 앨범 나열

현재 애플리케이션은 사용자가 소유한 앨범만 나열하고 공유 앨범은 나열하지 않습니다.

사용자가 만들었거나 Google 포토 라이브러리에 명시적으로 추가한 앨범만 Google 포토 앱 내의 '앨범' 화면에 표시됩니다. 라이브러리 API의 albums.list를 호출할 때 이러한 앨범만 반환됩니다. 그러나 앱에서 사용자는 다른 사용자의 공유 앨범에 참여할 수 있으며, 이는 공유 앨범을 나열하는 호출에서만 반환됩니다. 라이브러리 API에서 여행 목록(앨범)을 검색하는 방법을 소유 앨범과 공유 앨범을 모두 포함하도록 변경해야 합니다.

b2f84ff91b0e1396.png 앨범은 API 모델에 로드되고 캐시됩니다. 모델에서 updateAlbums()의 구현을 앨범과 공유 앨범을 모두 로드한 후 하나의 목록으로 저장하도록 변경합니다.

이 구현은 여러 Future를 사용하여 앨범을 캐시된 앨범 목록에 결합하기 전에 비동기식으로 나열합니다. 이전 구현을 삭제하고 새 코드를 주석 처리합니다.

lib/model/photos_library_api_model.dart

void updateAlbums() async {
  // Reset the flag before loading new albums
  hasAlbums = false;
  // Clear all albums
  _albums.clear();
  // Skip if not signed in
  if (!isLoggedIn()) {
    return;
  }
  // Add albums from the user's Google Photos account
  // var ownedAlbums = await _loadAlbums();
  // if (ownedAlbums != null) {
  //   _albums.addAll(ownedAlbums);
  // }

  // Load albums from owned and shared albums
  final list = await Future.wait([_loadSharedAlbums(), _loadAlbums()]);

  _albums.addAll(list.expand((a) => a ?? []));

  notifyListeners();
  hasAlbums = true;
}

공유 앨범에 참여

공유 토큰을 사용하여 애플리케이션 사용자를 앨범에 참여시킬 수 있습니다. 이 Codelab에서는 간단한 텍스트 대화상자를 통해 이 작업을 완료합니다.

b2f84ff91b0e1396.png join_trip 페이지에서 사용자가 입력한 공유 토큰으로 API 모델을 호출하는 _joinTrip 메서드를 구현합니다. 먼저, 로드 중 표시기를 표시한 후 텍스트 양식의 입력으로 공유 앨범에 참여하도록 호출합니다. 그런 다음, 로드 중 표시기를 숨기고 이전 화면으로 돌아갑니다.

lib/pages/join_trip_page.dart

Future<void> _joinTrip(BuildContext context) async {
  // Show loading indicator
  setState(() => _isLoading = true);

  // Call the API to join an album with the entered share token
  await ScopedModel.of<PhotosLibraryApiModel>(context)
      .joinSharedAlbum(shareTokenFormController.text);

  // Hide loading indicator
  setState(() => _isLoading = false);

  // Return to the previous screen
  Navigator.pop(context);
}

사용해 보기

Codelab의 이 부분을 직접 해보려면 다른 사용자 계정을 사용하는 보조 기기 또는 에뮬레이터가 필요합니다.

b2f84ff91b0e1396.png 한 사용자 계정으로 여행을 만들고 공유한 후 'SHARE IN FIELD TRIPPA' 옵션을 선택하여 공유 토큰을 검색합니다. 이 공유 토큰을 다른 기기나 에뮬레이터에 복사하고 홈페이지의 'JOIN A TRIP ALBUM' 옵션을 통해 입력합니다. (도움말: 에뮬레이터와 호스트 컴퓨터 간에 클립보드가 공유됩니다.)

8043086cc00eaa16.gif 55c1e75014d4d2a4.gif

실제 구현 도움말

Codelab이 아닌 실제 애플리케이션에서 공유를 구현하는 경우 공유 토큰을 사용하여 사용자를 앨범에 참여시키는 방식에 관해 신중하게 생각해야 합니다. 공유 토큰을 보안 백엔드에 저장하는 것을 고려하고 앨범을 만들고 앨범에 참여시키는 데 사용자 간의 관계를 활용하는 것이 좋습니다.

예를 들어 축구 클럽 모임 애플리케이션의 경우 예약된 특정 일정의 참석자를 추적하고 참여 메시지가 표시된 후에만 참석자가 앨범에 참여하도록 할 수 있습니다.

사용자의 Google 포토 계정에 변화를 주기 전에 사용자에게 알리고 동의를 구하는 것이 중요합니다. 자세한 내용은 Google 포토 라이브러리 API UX 가이드라인을 참고하세요.

빌드한 사항

  • Google 포토에서 지원하는 공유 기능을 애플리케이션에 구현했습니다.
  • 인프라 또는 저장용량에 관해 걱정할 필요 없이 Google 포토 라이브러리 API를 기반으로 고유한 사진 및 동영상 공유 환경을 만들었습니다.
  • 흥미롭고 참신한 방식으로 API의 일부인 공유 기능을 사용하여 콘텐츠를 사용자에게 직접 공유했습니다.
  • 라이브러리 API의 몇 가지 주요 요소를 사용했습니다.
  • 새 앨범을 만들고 새 사진을 업로드했습니다.
  • 애플리케이션에서 만든 앨범으로 제한된 공유 앨범을 나열했습니다.

다음 단계

미디어 공유 및 라이브러리 API의 다른 요소에 관해 자세히 알아보려면 https://developers.google.com/photos에서 Google 포토 API에 관한 개발자 문서를 참고하세요. 예를 들어 머신러닝에서 제공하는 스마트 콘텐츠 필터를 사용하면 적절한 사진 및 동영상을 찾는 데 도움이 됩니다.

통합을 시작할 준비가 된 경우 Google 포토 파트너 프로그램에 참여하세요.

UX 가이드라인기술 권장사항을 검토하세요. 시작을 지원하기 위해 일부 언어에서는 클라이언트 라이브러리도 제공합니다.