튜토리얼: Kotlin용 Atlas Device Sync
이 페이지의 내용
예상 완료 시간: 30분, Kotlin 사용 경험에 따라 다름
Realm 은 Jetpack Compose 를 사용하여 코틀린 (Kotlin) 으로 Android 모바일 애플리케이션 을 만들 수 있는 코틀린 SDK (Kotlin SDK) 를 제공합니다. . 이 튜토리얼은 kotlin.todo.flex
라는 이름의 코틀린 (Kotlin) Flexible Sync Template 앱을 기반으로 하며, To-do 항목 목록 관리 애플리케이션 을 만드는 방법을 설명합니다. 이 애플리케이션 을 통해 사용자는 다음을 수행할 수 있습니다.
이메일을 새 사용자 계정으로 등록합니다.
이메일과 비밀번호를 사용하여 계정에 로그인하고 나중에 로그아웃합니다.
자신의 작업을 보고, 만들고, 수정하고, 삭제할 수 있습니다.
사용자가 소유자가 아닌 경우에도 모든 작업을 볼 수 있습니다.
템플릿 앱은 장치가 '오프라인 모드'에 있는 것을 시뮬레이션하는 토글도 제공합니다. 이 토글을 사용하면 인터넷에 연결되지 않은 사용자를 에뮬레이션하여 시뮬레이터에서 Device Sync 기능을 빠르게 테스트할 수 있습니다. 그러나 프로덕션 애플리케이션에서는 이 토글을 제거할 가능성이 높습니다.
이 튜토리얼에서는 템플릿 앱에 기능을 추가합니다. 기존 Item
모델에 새 Priority
필드를 추가하고 Flexible Sync 구독을 업데이트하여 우선 순위 범위 내의 항목만 표시합니다. 이 예에서는 필요에 맞게 템플릿 앱을 조정하는 방법을 보여줍니다.
학습 목표
이 튜토리얼에서는 필요에 맞게 템플릿 앱을 조정하는 방법을 보여줍니다.
이 튜토리얼에서는 다음을 알아봅니다.
호환성이 손상되지 않는 변경을 적용하여 Realm 객체 모델을 업데이트합니다.
Device Sync 구독 업데이트.
동기화되는 데이터를 변경하려면 서버의 Device Sync 구성에 쿼리 가능한 필드를 추가하십시오.
참고
빠른 시작 확인
가이드 튜토리얼을 따르지 않고 직접 애플리케이션을 시작하는 것을 선호하는 경우 Kotlin 빠른 시작을 확인하세요. 여기에는 복사 가능한 코드 예시와 Atlas App Services 백엔드를 설정하는 데 필요한 필수 정보가 포함되어 있습니다.
전제 조건
Android Studio Bumblebee 2021.1.1 또는 그 이상입니다.
JDK 11 이상.
Android Studio 전용 코틀린(Kotlin) 플러그인, 버전 1.6.10 이상.
지원되는 CPU 아키텍처를 사용하는 AVD(Android Virtual Device, Android 가상 디바이스)입니다.
이 튜토리얼은 템플릿 앱으로 시작합니다. 템플릿 앱을 만들려면 Atlas 계정, API 키, Atlas App Services CLI가 필요합니다.
Atlas 계정 만들기에 관한 자세한 사항은 Atlas 시작하기 문서에서 확인할 수 있습니다. 이 튜토리얼을 사용하려면 프리 티어 클러스터가 있는 Atlas 계정이 필요합니다.
로그인하려는 MongoDB Cloud 계정에 대한 Atlas API 키도 필요합니다. App Services CLI를 사용해 템플릿 앱을 만들려면 프로젝트 소유자여야 합니다.
App Services CLI 설치에 대해 자세히 알아보려면 App Services CLI 설치를 참조하세요. 설치 후 Atlas 프로젝트의 API 키를 사용하여 login 명령을 실행합니다.
템플릿으로 시작하기
이 튜토리얼은 kotlin.todo.flex
라고 명명된 Kotlin SDK Flexible Sync 템플릿 앱을 기반으로 합니다. 기본 앱으로 시작하여 그 위에 새로운 기능을 빌드합니다.
템플릿 앱에 관한 자세한 사항은 템플릿 앱을 참조하십시오.
아직 Atlas 계정이 없는 경우 등록하여 템플릿 앱을 배포합니다.
앱 만들기 가이드 에 설명된 절차에 따라 Create App from Template 을 선택합니다. Real-time Sync 템플릿을 선택합니다. 이렇게 하면 Device Sync 템플릿 앱 클라이언트 중 하나와 함께 사용하도록 사전 구성된 App Services App 이 생성됩니다.
템플릿 앱을 만들면 UI에 Get the Front-end Code for your Template이라는 레이블이 붙은 모달이 표시됩니다. 이 모달은 템플릿 앱 클라이언트 코드를 .zip
파일로 다운로드하거나 App Services CLI를 사용하여 클라이언트를 가져오는 방법에 대한 지침을 제공합니다.
.zip
또는 App Services CLI 메서드를 선택한 후 화면의 안내에 따라 클라이언트 코드를 가져옵니다. 이 튜토리얼에서는 Kotlin (Android) 클라이언트 코드를 선택합니다.
참고
기본값 Windows ZIP 유틸리티에 .zip이 표시될 수 있습니다. 파일 을 빈 상태로 표시합니다. 이 문제가 발생하면 사용 가능한 타사 zip 프로그램 중 하나를 사용하세요.
appservices apps create 명령은 백엔드를 설정하고 이 튜토리얼의 기반으로 사용할 코틀린(Kotlin) 템플릿 앱을 만듭니다.
터미널 창에서 다음 명령을 실행하여 US-VA
리전에 배포되고 환경이 'development(production 또는 QA가 아닌)'로 설정된 'MyTutorialApp'이라는 앱을 만듭니다.
appservices app create \ --name MyTutorialApp \ --template kotlin.todo.flex \ --deployment-model global \ --environment development
이 명령은 --name
플래그의 값과 동일한 이름으로 현재 경로에 새 디렉토리를 작성합니다.
클라이언트 코드가 포함된 리포지토리 를 Github 포크하고 복제할 수 있습니다.Device Sync 코틀린( 코틀린 (Kotlin) ) 클라이언트 코드는 https://github.com/mongodb/ Template-app-kotlin-todo에서 확인할 수 있습니다.
이 프로세스를 사용하여 클라이언트 코드를 가져오는 경우 클라이언트와 함께 사용할 템플릿 앱을 만들어야 합니다. 템플릿 앱 만들기의 지침에 따라 Atlas App Services UI, App Services CLI, 또는 Admin API를 사용하여 Device Sync 템플릿 앱을 만듭니다.
템플릿 앱 설정하기
앱 구조 살펴보기
Android Studio에서 프로젝트의 인덱스를 생성하는 동안 몇 분 정도 시간을 내어 프로젝트 구성을 살펴봅니다. app/java/com.mongodb.app
디렉토리 내에서 주목할 만한 파일 몇 가지를 볼 수 있습니다.
file | 목적 |
---|---|
ComposeItemActivity.kt | 영역 열기, 영역에 항목 쓰기, 사용자 로그아웃, 영역 닫기 기능을 제공하고 레이아웃을 정의하는 활동 클래스입니다. |
ComposeLoginActivity.kt | 사용자 등록 및 로그인 기능을 제공하고 레이아웃을 정의하는 활동 클래스입니다. |
TemplateApp.kt | App Services 앱을 초기화하는 클래스입니다. |
이 튜토리얼에서는 다음 파일에서 작업하게 됩니다.
file | 목적 |
---|---|
Item.kt | domain 디렉토리에 있습니다. 데이터베이스에 저장하는 Realm 객체를 정의합니다. |
AddItem.kt | ui/tasks 디렉토리에 있습니다. 항목을 추가할 때 사용되는 레이아웃을 정의하는 구성 가능 함수가 포함되어 있습니다. |
AddItemViewModel.kt | presentation/tasks 디렉토리에 있습니다. 비즈니스 로직을 포함하고 항목을 추가할 때 상태를 관리하는 보기 모델입니다. |
SyncRepository.kt | data 디렉토리에 있습니다. Realm Sync에 액세스하는 데 사용되는 리포지토리로 Flexible Sync 구독을 정의합니다. |
Strings.xml | res/values 디렉토리에 있습니다. string 텍스트 리소스 를 정의합니다. 앱에서 사용됩니다. |
앱 실행하기
코드를 변경하지 않고도 Android Studio를 사용하는 Android 에뮬레이터 또는 실제 기기에서 앱을 실행할 수 있어야 합니다.
앱을 실행하고 새 사용자 계정을 등록한 다음 할 일 목록에 새 항목을 추가합니다.
백엔드 확인하기
Atlas App Services에 로그인합니다. Data Services 탭에서 Browse Collections를 클릭합니다. 데이터베이스 목록에서 todo 데이터베이스를 찾아 펼친 다음 Item 컬렉션을 찾아 펼칩니다. 해당 컬렉션에 만든 문서가 표시되어야 합니다.
애플리케이션 수정하기
새 속성 추가
모델에 새 속성 추가
이제 모든 것이 예상대로 작동하는지 확인했으므로 변경 사항을 추가할 수 있습니다. 이 튜토리얼에서는 우선 순위에 따라 항목을 필터링할 수 있도록 각 항목에 '우선 순위' 속성을 추가합니다. 우선 순위 속성은 PriorityLevel
열거형에 매핑되어 사용 가능한 값을 제한하고, 각 열거형의 서수를 우선 순위 정수에 대응하도록 하여 나중에 숫자 우선 순위 수준을 기준으로 쿼리할 수 있도록 합니다.
이렇게 하려면 다음 단계를 따라 진행합니다.
app/java/com.mongodb.app/domain
폴더에서Item
클래스 파일을 엽니다.PriorityLevel
열거형을 추가하여 사용 가능한 값을 제한합니다. 또한Item
클래스에priority
속성을 추가합니다. 이 속성은 기본 우선 순위를 3으로 설정하여 우선 순위가 낮은 할 일 항목임을 나타냅니다.domain/Item.kt// ... imports enum class PriorityLevel() { Severe, // priority 0 High, // priority 1 Medium, // priority 2 Low // priority 3 } class Item() : RealmObject { var _id: ObjectId = ObjectId.create() var isComplete: Boolean = false var summary: String = "" var owner_id: String = "" var priority: Int = PriorityLevel.Low.ordinal constructor(ownerId: String = "") : this() { owner_id = ownerId } // ... equals() and hashCode() functions }
새 항목 생성 시 우선 순위 설정하기
ui/tasks
폴더에서AddItem.kt
파일을 엽니다. 이 파일은 사용자가 새 할 일 항목을 추가하기 위해 '+' 버튼을 클릭할 때 표시되는 UI의 구성 가능한 기능을 정의합니다.먼저
package com.mongodb.app
아래에 다음 가져오기를 추가합니다:ui/tasks/AddItem.ktimport androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.mongodb.app.domain.PriorityLevel 이제 구성 가능한
AddItemPrompt
함수에 드롭다운 필드를 추가하면 사용자가 PriorityLevel 열거형을 사용 가능한 값으로 사용하여 목록에서 우선 순위 수준을 선택할 수 있습니다.ui/tasks/AddItem.kt// ... imports fun AddItemPrompt(viewModel: AddItemViewModel) { AlertDialog( containerColor = Color.White, onDismissRequest = { viewModel.closeAddTaskDialog() }, title = { Text(stringResource(R.string.add_item)) }, text = { Column { Text(stringResource(R.string.enter_item_name)) TextField( colors = ExposedDropdownMenuDefaults.textFieldColors(containerColor = Color.White), value = viewModel.taskSummary.value, maxLines = 2, onValueChange = { viewModel.updateTaskSummary(it) }, label = { Text(stringResource(R.string.item_summary)) } ) val priorities = PriorityLevel.values() ExposedDropdownMenuBox( modifier = Modifier.padding(16.dp), expanded = viewModel.expanded.value, onExpandedChange = { viewModel.open() }, ) { TextField( readOnly = true, value = viewModel.taskPriority.value.name, onValueChange = {}, label = { Text(stringResource(R.string.item_priority)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = viewModel.expanded.value) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), modifier = Modifier .fillMaxWidth() .menuAnchor() ) ExposedDropdownMenu( expanded = viewModel.expanded.value, onDismissRequest = { viewModel.close() } ) { priorities.forEach { DropdownMenuItem( text = { Text(it.name) }, onClick = { viewModel.updateTaskPriority(it) viewModel.close() } ) } } } } }, // ... buttons ) } Android Studio는 몇 가지 오류를 식별합니다. 다음 단계에서 관련 기능을 추가하여 해당 문제를 해결합니다.
다음으로, 드롭다운 필드 레이블을 문자열 리소스로 정의합니다.
res/values/strings.xml
파일을 열고 'resource' 요소 마지막 부분 앞에 다음을 추가합니다.res/values/strings.xml<string name="item_priority">Item Priority</string> 이제
presentation/tasks
폴더에서AddItemViewModel.kt
파일을 엽니다. 여기서는 새로운 드롭다운 필드와 관련된 비즈니스 로직을 추가합니다.package com.mongodb.app
아래에PriorityLevel
가져오기를 추가한 다음 드롭다운 내에서 상태 변경을 처리하는 데 필요한 변수와 함수를AddItemViewModel
클래스에 추가합니다.presentation/tasks/AddItemViewModel.kt// ... imports import com.mongodb.app.domain.PriorityLevel // ... events class AddItemViewModel( private val repository: SyncRepository ) : ViewModel() { private val _addItemPopupVisible: MutableState<Boolean> = mutableStateOf(false) val addItemPopupVisible: State<Boolean> get() = _addItemPopupVisible private val _taskSummary: MutableState<String> = mutableStateOf("") val taskSummary: State<String> get() = _taskSummary private val _taskPriority: MutableState<PriorityLevel> = mutableStateOf(PriorityLevel.Low) val taskPriority: State<PriorityLevel> get() = _taskPriority private val _expanded: MutableState<Boolean> = mutableStateOf(false) val expanded: State<Boolean> get() = _expanded private val _addItemEvent: MutableSharedFlow<AddItemEvent> = MutableSharedFlow() val addItemEvent: Flow<AddItemEvent> get() = _addItemEvent fun openAddTaskDialog() { _addItemPopupVisible.value = true } fun closeAddTaskDialog() { cleanUpAndClose() } fun updateTaskSummary(taskSummary: String) { _taskSummary.value = taskSummary } fun updateTaskPriority(taskPriority: PriorityLevel) { _taskPriority.value = taskPriority } fun open() { _expanded.value = true } fun close() { _expanded.value = false } // addTask() and cleanUpAndClose() functions } 이제
addTask()
및cleanUpAndClose()
함수를 업데이트하여 새로운taskPriority
매개 변수를 포함하고, 우선 순위 정보로 메시지를 업데이트하고, 항목 추가 보기가 닫히면 우선 순위 필드를 낮음으로 재설정합니다:fun addTask() { CoroutineScope(Dispatchers.IO).launch { runCatching { repository.addTask(taskSummary.value, taskPriority.value) }.onSuccess { withContext(Dispatchers.Main) { _addItemEvent.emit(AddItemEvent.Info("Task '$taskSummary' with priority '$taskPriority' added successfully.")) } }.onFailure { withContext(Dispatchers.Main) { _addItemEvent.emit(AddItemEvent.Error("There was an error while adding the task '$taskSummary'", it)) } } cleanUpAndClose() } } private fun cleanUpAndClose() { _taskSummary.value = "" _taskPriority.value = PriorityLevel.Low _addItemPopupVisible.value = false } 마지막으로
data
폴더에서SyncRepository.kt
파일을 열어 항목을 영역에 쓰는addTask()
함수의 동일한 변경 사항을 반영합니다.먼저
package com.mongodb.app
아래에PriorityLevel
가져오기를 추가한 다음addTask()
함수를 업데이트하여taskPriority
를 매개 변수로 전달하고priority
필드를 영역에 정수로 씁니다(열거형 서수 사용).data/SyncRepository.kt// ... imports import com.mongodb.app.domain.PriorityLevel interface SyncRepository { // ... Sync functions suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel) // ... Sync functions } class RealmSyncRepository( onSyncError: (session: SyncSession, error: SyncException) -> Unit ) : SyncRepository { // ... variables and SyncConfiguration initializer // ... Sync functions override suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel) { val task = Item().apply { owner_id = currentUser.id summary = taskSummary priority = taskPriority.ordinal } realm.write { copyToRealm(task) } } override suspend fun updateSubscriptions(subscriptionType: SubscriptionType) { realm.subscriptions.update { removeAll() val query = when (subscriptionType) { SubscriptionType.MINE -> getQuery(realm, SubscriptionType.MINE) SubscriptionType.ALL -> getQuery(realm, SubscriptionType.ALL) } add(query, subscriptionType.name) } } // ... additional Sync functions } class MockRepository : SyncRepository { override fun getTaskList(): Flow<ResultsChange<Item>> = flowOf() override suspend fun toggleIsComplete(task: Item) = Unit override suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel) = Unit override suspend fun updateSubscriptions(subscriptionType: SubscriptionType) = Unit override suspend fun deleteTask(task: Item) = Unit override fun getActiveSubscriptionType(realm: Realm?): SubscriptionType = SubscriptionType.ALL override fun pauseSync() = Unit override fun resumeSync() = Unit override fun isTaskMine(task: Item): Boolean = task.owner_id == MOCK_OWNER_ID_MINE override fun close() = Unit // ... companion object }
실행 및 테스트
이 시점에서 애플리케이션을 다시 실행할 수 있습니다. 이 튜토리얼의 앞부분에서 생성한 계정을 사용하여 로그인합니다. 이전에 생성한 하나의 항목이 표시됩니다. 새 항목을 추가하면 이제 우선 순위를 설정할 수 있습니다. 우선 순위를 High
로 선택하고 항목을 저장합니다.
이제 브라우저에서 Atlas 데이터 페이지로 다시 전환하고 Item
컬렉션을 새로 고침 하십시오. 그러면 priority
필드가 추가되고 1로 설정된 새 항목이 표시됩니다. 기존 항목에는 priority
필드가 없습니다.
참고
동기화가 해제되지 않은 이유는 무엇인가요?
Realm 객체에 속성을 추가하는 것은 호환성이 손상되는 변경 사항이 아니므로 클라이언트를 재설정할 필요가 없습니다. 템플릿 앱에는 개발 모드가 활성화되어 있으므로 클라이언트 Realm 객체에 대한 변경 사항이 서버 측 스키마에 반영됩니다. 자세한 내용은 개발 모드 및 데이터 모델 업데이트를 참조하세요.
구독 변경
구독 업데이트
app/java/com.mongodb.app/data
폴더 내에서 SyncRepository.kt
파일을 엽니다. 여기서 Flexible Sync 구독을 정의합니다. 구독은 사용자의 기기 및 계정과 동기화되는 문서를 정의합니다. getQuery()
함수를 찾습니다. 현재 두 개의 구독을 구독하고 있음을 알 수 있습니다.
MINE
:ownerId
속성이 인증된 사용자와 일치하는 모든 문서입니다.ALL
: 모든 사용자의 모든 문서.
오직 높음(High) 또는 심각(Severe)으로 표시된 항목만 동기화하도록 MINE
구독을 업데이트하려고 합니다.
기억하시겠지만 priority
필드는 int
유형으로, 가장 높은 우선 순위('심각(Severe)')의 값은 0이고 가장 낮은 우선 순위('낮음(Low)')의 값은 3입니다. 정수와 우선 순위 속성을 직접 비교할 수 있습니다. 이렇게 하려면 여기에 표시된 것처럼 우선 순위가 PriorityLevel.High(또는 1)보다 낮거나 같은 문서를 포함하도록 RQL 진술(statement)을 업데이트합니다.
private fun getQuery(realm: Realm, subscriptionType: SubscriptionType): RealmQuery<Item> = when (subscriptionType) { SubscriptionType.MINE -> realm.query("owner_id == $0 AND priority <= ${PriorityLevel.High.ordinal}", currentUser.id) SubscriptionType.ALL -> realm.query() }
또한 앱을 열 때마다 구독 쿼리가 동기화할 문서를 다시 계산하도록 강제합니다.
이렇게 하려면 애플리케이션이 시작할 때 호출하는 SyncConfiguration.Builder().initialSubscriptions()
함수를 찾습니다. 먼저 true
로 설정한 reRunOnOpen
매개 변수를 추가한 다음 updateExisting
을 true
로 설정하여 기존 쿼리에 대한 업데이트를 허용합니다.
config = SyncConfiguration.Builder(currentUser, setOf(Item::class)) .initialSubscriptions(rerunOnOpen = true) { realm -> // Subscribe to the active subscriptionType - first time defaults to MINE val activeSubscriptionType = getActiveSubscriptionType(realm) add(getQuery(realm, activeSubscriptionType), activeSubscriptionType.name, updateExisting = true) } .errorHandler { session: SyncSession, error: SyncException -> onSyncError.invoke(session, error) } .waitForInitialRemoteData() .build()
실행 및 테스트
애플리케이션을 다시 실행합니다. 이 튜토리얼의 앞부분에서 생성한 계정을 사용하여 로그인합니다.
Realm에서 문서 컬렉션을 다시 동기화하는 초기 시점 이후에는 사용자가 만든 새로운 High 우선 순위 항목을 볼 수 있습니다.
팁
개발자 모드가 활성화된 상태에서 구독 변경하기
이 튜토리얼에서는 구독을 변경하고 처음으로 우선 순위 필드에 대한 쿼리를 하면 해당 필드가 Device Sync Collection Queryable Fields에 자동으로 추가됩니다. 이는 템플릿 앱에 개발 모드가 기본적으로 활성화되어 있기 때문입니다. 개발 모드가 활성화되지 않은 경우 클라이언트 사이드 동기화 쿼리에서 필드를 사용하려면 쿼리 가능 필드로 수동으로 추가해야 합니다.
자세한 내용은 쿼리 가능 필드를 참조하세요.
기능을 추가로 테스트하려면 다양한 우선순위의 항목을 생성할 수 있습니다. 우선 순위가 높음보다 낮은 항목을 추가하려고 하면 권한이 없다는 알림 메시지가 표시됩니다. Logcat을 사용하여 로그를 확인하는 경우 항목이 '성공적으로 추가되었습니다'라는 메시지와 함께 동기화 오류가 표시됩니다.
ERROR "Client attempted a write that is outside of permissions or query filters; it has been reverted"
이는 Realm이 항목을 로컬에서 생성하고 백엔드와 동기화한 다음 구독 규칙을 충족하지 않는다는 이유로 쓰기를 되돌렸기 때문입니다.
또한 처음에 생성한 문서의 우선순위가 null
이므로 동기화되지 않습니다. 이 항목을 동기화하려면 Atlas UI에서 문서를 편집하고 우선 순위 필드에 값을 추가할 수 있습니다.
결론
기존 Realm 객체에 속성을 추가하는 것은 호환성이 손상되지 않는 변경이며, 개발 모드는 스키마 변경이 서버 측에 반영되도록 합니다.
다음 단계
Kotlin SDK 설명서를 읽어보세요.
개발자 중심 블로그 포스트 및 통합 튜토리얼은 MongoDB 개발자 허브에서 확인할 수 있습니다.
MongoDB Community 포럼에 참여해 다른 MongoDB 개발자와 기술 전문가들로부터 배워보세요.
엔지니어링 및 전문가가 제공하는 예시 프로젝트를 살펴보세요.