실제 세계의 Android, MVVM 및 저장소

(Mike DeMaso) (2020 년 11 월 2 일)

코드를 깨끗하고, 정확하고, 읽기 쉽고, 유지 관리 할 수 ​​있도록 유지하면서 반응 형 사용자 인터페이스를 어떻게 달성합니까? 이 질문은 2008 년부터 Android 개발자의 머리에 떠오르는 질문입니다.

수년 동안 Android 개발자가 스스로 알아 내도록 한 끝에 Google은 최근 MVVM 의 변형을 홍보하는 div id = “dae5557846”> 앱 아키텍처 가이드 . 이것은 좋은 시작이지만 팀이 스스로 대답 할 수있는 많은 질문이 남아 있습니다. Google은 LiveData , Room DataBinding 은 상용구를 줄이고 코드 문제를 분리하는 데 도움이됩니다. 타사에서도 RxJava Retrofit 을 통해 도움의 손길을주었습니다. 비동기 작업을 처리하고 네트워크를 통해 물건을 가져 오는 방법. 이 모든 것들이 더 큰 문제의 작은 부분을 해결하면서 떠 다니는 상황에서 구현 및 유지 관리가 간단한 유동적 인 UI 경험을 제공하기 위해 어떻게 모두 통합 할 수 있을까요?

이 기사에서는 공유 할 것입니다. MVVM 아키텍처를 만들려는 이전 시도에서 배운 내용과 이러한 모든 퍼즐 조각을 하나로 모으는 방법에 대한 간단한 예를 보여줍니다.

MVVM에 대한 간략한 설명

리포지토리가 무엇을 해결할 것인지 이해하기 위해 먼저 기본 MVVM 패턴을 살펴 보겠습니다.

기본 MVVM 패턴
기본 MVVM 다이어그램

보기

다음에 대한 그래픽 사용자 인터페이스의 표현입니다. 귀하의 코드. 이는 XML (레이아웃 파일) 또는 코드 (Jetpack Compose)로 나타낼 수 있습니다. 일반적으로 View를 ViewModel에 연결하는 데이터 바인딩 형식이 있습니다. 뷰와 바인더 사이에 선을 그리는 위치에 따라 Activity 및 Fragment 객체를 둘 중 하나 또는 둘 다 고려할 수 있습니다.

ViewModel

이는 데이터 변환을 담당합니다. 보기가 표시하기에 적합한 형식으로 모델을 만듭니다. View와 ViewModel 사이의 이중 화살표로 끝나는 연결을 속이지 마십시오. MVP에서 ViewModel과 Presenter의 주요 차이점은 ViewModel에 뷰에 대한 참조가 포함되어 있지 않다는 것입니다. 단방향 관계이므로 다른 항목이 ViewModel 개체를 관리해야합니다.

The 모델

ViewModel이 View에 제공하는 정보의 소스로 사용되는 데이터 (또는 도메인 모델)를 나타냅니다.이 레이어는 일부 기사에서 언급하는 것처럼 약간 흐릿 해집니다. 다른 사람들은이를 데이터 액세스 레이어라고 부르는 반면, 다른 사람들은이를 데이터 액세스 레이어라고합니다. 여기에서 Google의 저장소 패턴이 작업을 정리합니다.

Enter the Repository

Google은 이 패턴을 한동안 언급 했습니다. 그들의 예제는 저장소와 함께 MVVM을 사용하는 기본 원칙을 이해하는 데 훌륭한 가이드이지만 작은 부분 ( 그리고 중요) 사람들이 이러한 스 니펫을 더 크고 복잡한 프로젝트로 번역 할 수 있도록 안내합니다.

Google의 MVVM 다이어그램

저장소 패턴은”깨끗한 API를 제공하여 나머지 앱은이 데이터를 쉽게 검색 할 수 있습니다. ” 아쉽게도 아키텍처에 리포지토리를 추가하는 것만으로는 코드가 깨끗해지지 않습니다. 레이어간에 명확한 계약을 적절하게 분리하고 제공하지 않고도 엉킨 엉망진창을 만들 수 있습니다.

DraftKings에서는 개발자가 일관되게 깨끗한 코드를 생성 할 수 있도록 몇 가지 추가 지침에 집중했습니다.

인터페이스로 레이어 분리

여기에 인터페이스를 추가하여 엔지니어가 공개적으로 무엇을 노출하는지 생각하도록 유도합니다.
인터페이스를 추가하여 Google 다이어그램 개선

우리는 이러한 레이어 사이에 인터페이스를 사용하면 모든 수준의 엔지니어가 좋은 코딩 원칙을 고수하는 데 도움이된다는 것을 확인했습니다. 이를 통해 단위 테스트가 실제로 한 번에 하나의 레이어 만 테스트하는지 확인하여 대규모 테스트 스위트를 작성하고 유지하는 오버 헤드를 줄일 수 있습니다. 또한 외부 API를 명확하게 정의하고 다른 계층의 구현 세부 정보를 난독 화하는 데 도움이됩니다. 이 개체의 기능에 대해 외부 세계에 전달하는 내용을 평가하라는 메시지를 표시하고 코드가 깨끗한 지 확인할 수있는 기본 제공 기회를 제공합니다.

또한 코드베이스의 여러 계층을보다 효과적으로 리팩토링 할 수있는 능력을 제공합니다. 인터페이스가 변경되지 않는 한이를 사용하는 코드베이스 영역을 그대로 둘 수 있습니다. 예를 들어 네트워킹 라이브러리를 Volley에서 Retrofit으로 마이그레이션하려는 경우 Dagger 클래스의 메서드를 간단히 변경할 수 있습니다. 원격 데이터 소스 위에 인터페이스를 생성하고 제공하며 해당 엔드 포인트를 사용하는 모든 저장소를 변경할 필요가 없습니다. 이렇게하면 이러한 변경의 범위가 크게 줄어들고 최종 제품에 버그가 발생할 가능성이 줄어 듭니다.

여기에 ViewModel이 저장소의 구체적인 클래스를 유지하도록하면 의도하지 않은 동작이 발생할 수있는 방법에 대한 예가 있습니다.이 예제는 약간 인위적이며 간단히 fooPublishSubject in FooRepositoryprivate로 표시되지만이 솔루션은 더 취약합니다. FooRepository는 해당 매개 변수에 대한 액세스가 필요한 다른 범위에서 사용해야하며 인스턴스에 대한 액세스를 열면 이제 혼란스러워집니다. ko 해당 멤버 변수를 직접 사용하는 것이 적절합니다.

이러한 종속성 전달

프로젝트의 복잡성이 커질수록 종속성 관계가 더 복잡해집니다. 즉, 사람들은 일반적으로 일종의 종속성 주입 라이브러리 (예 : Dagger 또는 Koin )를 사용합니다. .

DI 라이브러리는 필요한 종속성을 검색하는 깔끔하고 쉬운 방법을 제공 할뿐만 아니라 애플리케이션에 필요한 이러한 객체의 수를 생각할 수도 있습니다.

이 사고 과정을 통해 Dagger 그래프에 속하는 개체에 대한 모범 사례를 설정했습니다. 단일 인스턴스 만 원하는 것은 루트 / 글로벌 / 애플리케이션 범위 그래프에 있어야합니다. 많은 인스턴스가있을 수있는 모든 것은 온 디맨드로 생성하고 적절하게 보관해야합니다.

이것은 여러 ViewModel이 모델에 액세스 할 수 있기를 원하기 때문에 새로운 저장소 객체가 Dagger 그래프에 속한다는 것을 의미합니다. 기본 Room 또는 Retrofit 소스의 단일 인스턴스를 통해. 반면에 ViewModel은 필요로하는 각 View에 대해 새로 만들어야합니다. 카드 놀이의 스택과 같은 활동 스택을 생각하고 ViewModel은 슈트와 가치를 주도합니다. 우리는 상단에 3 개의 클럽을 추가하여 아래의 모든 카드를 3 ​​개의 클럽으로 변경하는 것을 원하지 않습니다. 따라서 각 뷰는 데이터를 보존하고 분리하기 위해 자체 ViewModel 인스턴스가 필요합니다.

이제 DI 솔루션이 보유 할 내용을 정의했습니다.
어떤 개체가 보유 할 것으로 예상되는지 보여줍니다. Dagger Graph

우리는 ViewModel을 Dagger 그래프에서 제외하기로 결정했습니다. 역사적으로 우리는이 선택에 대해 덜 명시 적이었지만 이것이 androidx.lifecycle에서 제공되는 ViewModelProvider 패턴과 이것이 활동 / 조각 간의 관계를 강화하는 데 도움이되는 올바른 방향 / XML 및 ViewModel은 “일대일”로 표시됩니다. 한편, ViewModel과 리포지토리 관계는 “다 대다”일 수 있습니다. 실제로 이는 모든 활동 / 조각 / XML에 대해 해당 뷰의 l을 처리하는 단일 ViewModel이 있음을 의미합니다. 그러나 필요한 데이터를 소싱하기 위해 많은 저장소에 접근 할 수 있습니다. 데이터는 일반적으로 애플리케이션 전체에서 재사용되고 표시되기 때문에 여러 다른 ViewModel이 Dagger Graph에서 저장소의 동일한 인스턴스를 쉽고 효율적으로 사용할 수 있습니다.

API 포함

모든 회사에서 대규모로 화이트 보드에서 고객의 손에 프로젝트를 전달하려면 많은 엔지니어, 팀 및 부서가 필요합니다. 여기 DraftKings에서도 마찬가지입니다. Android 애플리케이션으로 데이터를 가져 오려면 백엔드에서 몇 가지 다른 팀과 협력하여 데이터베이스에서 API, Android 클라이언트로 데이터를 가져와야합니다. 이 코드는 다른 팀이 소유하는 경우가 많기 때문에 일반적으로 백엔드가 클라이언트와 다른 속도로 “이동”한다는 것을 의미합니다.

이는 그렇지 않은 프로젝트를 시작할 때 특히 그렇습니다. 개발에 사용할 수있는 상태의 API가 있습니다. 백엔드에서 작업하는 엔지니어가 내리는 결정 충돌에 대해 너무 걱정하지 않고 내부적으로 클라이언트에게 전달되는 데이터 개체에 대한 설계 및 구현 결정을 내리고 싶습니다.

그 외에도 우리는 동일한 비즈니스 데이터를 클라이언트에게 반환하는 서비스는 거의 없지만 서로 다른 팀이 소유하고 서로 다른 문제를 해결하려고하기 때문에 API를 통해 반환되는 데이터의 구조와 유형에서 서로 멀어졌습니다.클라이언트 내부에서는 이러한 응답이 실제로 동일한 것을 나타내므로 이러한 응답을 클라이언트에게 보편적 인 것으로 변환하고 결합 할 수 있다는 것은 매우 의미가 있습니다.

여기에서 사용자의 변형을 반환하는 API에 두 개의 엔드 포인트가 있음을 알 수 있습니다. 로그인 및 프로필 엔드 포인트. 이를 하나의 데이터 유형으로 병합하기 위해 처리하려는 각 변형에 대한 생성자를 생성하기 만하면 이제 애플리케이션이 API가 단일 위치에 제공하는 두 가지 유형의 사용자에 대한 지식을 제한 할 수 있습니다. 따라서 API의 데이터 구조 및 유형의 변경이 하나의 엔드 포인트로 제한되도록 허용하면서 (모델 레이어를 통해) 기능간에 데이터를 훨씬 더 쉽게 공유 할 수 있습니다.

네트워크 응답 객체를 구분하고 있습니다. 우리 아키텍처의 비즈니스 모델 개체. 이는 또한 네트워크 응답을 가져와 나머지 애플리케이션을위한 비즈니스 모델로 변환하는 원격 데이터 소스의 역할을 정의하는 데 도움이됩니다.

이러한 레이어가 생성하는 데이터 유형을 정의하면 원격 데이터 소스의 역할을 정의하는데도 도움이됩니다.
레이어간에 전송되는 데이터 유형을 명확하게하여 더 많은 재사용을 가능하게합니다.

코드 예제

이제 Google UserRepository 예제를 살펴보고 자체 버전을 만들겠습니다. 위에서 변경 한 사항을 고수합니다.

먼저 Google의 최종 버전 인 UserRepository를 살펴 보겠습니다.

Dagger (또는 Hilt )를 사용하고 있음을 알 수 있습니다. , Kotlin Coroutines , Room 및 대부분의 경우 Retrofit. 리포지토리에 Retrofit 서비스, Coroutine 실행기 및 Dao (데이터 액세스 개체)를 제공합니다.

이 작업의 기본 흐름은 데이터에 대한 네트워크 요청을 만들고 데이터 (또는 감시중인 항목)를 반환하는 것입니다. 데이터 용) Room에서 즉시. 네트워크 요청이 완료되면 데이터에 필요한 모든 작업을 수행하고 새 개체를 테이블에 삽입합니다. 삽입은 이전에 반환 된 데이터에 변경되었음을 자동으로 알리고 새 검색을 요청하고 마지막으로 뷰를 업데이트합니다.

일부 설정

UserRepository, 먼저 실행할 스레드를 주입하는 유용한 방법과 같이 필요한 사항을 먼저 해결해야합니다.

나중에 테스트하는 데 도움이됩니다. Dagger 그래프에서이를 설정하면 이제 전체 코드베이스에 올바른 스레드를 쉽게 삽입 할 수 있으며 여전히 유닛에서 TestScheduler 로 교체 할 수 있습니다. 테스트 (단위 테스트를 작성 중입니다… 맞습니까?)

다음은 사용자 클래스입니다. UserResponse는 Retrofit 및 , 내부적으로 전달하는 비즈니스 모델입니다. 네트워크 객체를 단순하게 만들고 코드 생성기를 사용하여 생성 할 수 있으며 비즈니스 객체가 필요한 것에 더 부합 할 수 있음을 알 수 있습니다.

여기에서 Retrofit 서비스를 정의합니다. Single 대신 Observable를 반환하도록 할 수 있었는데, 이는 다운 스트림 로직을 조금 더 간단하게 만들었지 만 다음과 같은 유사점을 좋아했습니다. 네트워크 요청 및 Single의 작동 방식 (비동기 및 성공 또는 실패) 우리는 원격 데이터 소스 레이어를 통해서도 그 로직을 전달합니다.

다음은 우리의 방입니다. Dao. Room은 이미 인터페이스와 주석으로 작동하므로 영구 데이터를 처리하는 방법을 난독 화하기 위해 다른 인터페이스, 클래스 및 개체를 만들 필요가 없다고 생각했습니다.

우리는 Observable를 사용하여 User 개체의 방출에 반응하고 삽입 작업이 Completable를 반환하도록하면 발생할 수있는 회의실 오류를 처리하는 데 도움이됩니다.

마지막으로 UserRepository 자체에 대한 마지막 인터페이스입니다. 매우 간단하고 깔끔합니다. 필요한 것 이외의 유일한 추가 부분은 iv id =와 같은 하위 레이어의 기존 일회용품을 정리하는 데 도움이되는 onCleared() 메서드입니다. “51668785cb”> 도 삭제됩니다.

구현

생성자의 서명이 위의 Google 예제와 매우 유사하다는 것을 알 수 있습니다.네트워크를 통해 데이터를 검색 할 수있는 객체, Dao 및 실행할 스레드를 알려주는 객체를 제공합니다.

getUser(userId: Int) 메서드도 제공합니다. 위의 예에서 작동하는 방식과 유사합니다. 비동기 네트워크 요청을 만들고 데이터베이스에서 데이터를 감시하는 개체를 즉시 반환합니다. 네트워크 요청이 완료되면 해당 데이터를 테이블에 삽입하면 UI 업데이트가 트리거됩니다.

여기서 RxJava로 너무 멋진 작업을 수행하고 있지 않음을 알 수 있습니다. 데이터를 전달하는 새로운 PublishSubject를 만듭니다. 이를 영구 레이어의 선택 항목과 병합하여 Observable를 반환하며 네트워크 레이어에서도 오류를 보냅니다. 우리는 각 계층에서 위의 계층으로 원하는 오류 만 가져 오는 방법을 앞뒤로 진행했으며, 이것이 우리가 찾은 가장 간단하고 사용자 정의 가능한 솔루션이었습니다. 예, 추가 관찰 가능 항목을 생성하지만 오류 처리를보다 세밀하게 제어 할 수 있습니다.

마지막 퍼즐의 조각은 원격 데이터 소스입니다. 여기에서 UserResponseUser로 변환하여 나머지 코드베이스가 다른 속도로 이동하는 방법을 볼 수 있습니다. API에서 들어오는 데이터입니다.

전체 정리

이 시점에서 이러한 개체를 위에서 Dagger 그래프에 추가해야합니다. 제공된 메서드는 구현이 아닌 인터페이스 유형을 반환해야합니다. 이렇게하면 Dagger 그래프에서 위에 정의 된 모든 개체를 빌드 할 수 있으며 코드베이스의 모든 위치에서 UserRepository에서 데이터를 매우 쉽게 가져올 수 있습니다.

마지막으로 RxJava를 사용하고 있기 때문에 모든 시스템에서 clear() Disposable 개체를 ViewModel에 추가하고 iv id = “8016bcf949에서 onCleared()를 호출합니다. “> 인스턴스를 사용하여 내부 Disposable 개체를 정리합니다.

이제 저장소를 통해 제공되는 저장소를 사용하여 상당히 간단한 MVVM 구현이 있습니다. Dagger 그래프. 우리가 만든 레이어는 각각 명확한 목적을 가지고 있으며 외부 세계에 노출되는 것을 필터링하는 데 도움이되는 인터페이스에 의해 유지됩니다. 이를 통해 테스트 실행을 완벽하게 제어 할 수있는 RxJava TestScheduler를 사용하여 이러한 레이어를 서로 독립적으로 테스트 할 수 있습니다.

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다