본문 바로가기
개발/안드로이드

안드로이드 MVVM에 GraphQL 적용하기(1/2)

by 창이2 2020. 7. 19.

안녕하세요 찰스입니다.

이번 포스팅에서는 MVVM 패턴에 GraphQL을 적용하는 것을 알려드리려고 합니다.

GraphQL이 무엇인지를 안다는 전제 하에 포스팅을 진행하도록 하겠습니다.

안드로이드에서 GraphQL을 사용하기 위해 저는 Apollo 라이브러리를 활용했습니다.

 

일단 Apollo 깃헙에서 샘플 프로젝트를 확인하셔서 학습하시거나 해당 사이트에서 튜토리얼을 제공하니 공부하실때

같이 보시면 도움이 될거 같습니다. 

 

여기는 공식 홈페이지이고

https://www.apollographql.com/docs/android/

 

Introduction

A strongly-typed, caching GraphQL client for the JVM, Android and Kotlin multiplatform

www.apollographql.com

 

샘플프로젝트 주소입니다.

https://github.com/apollographql/apollo-android-tutorial

 

apollographql/apollo-android-tutorial

The code for the Apollo Android Tutorial. Contribute to apollographql/apollo-android-tutorial development by creating an account on GitHub.

github.com

이제 새 프로젝트를 만들어서 진행을 시작해보겠습니다.

 

build.gradle(:app) 을 세팅하겠습니다.

 

더보기
plugins {
    id("com.apollographql.apollo").version("2.2.2")
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.example.graphql_mvvm"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    apollo {
        generateKotlinModels.set(true)
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

    viewBinding{
        enabled = true
    }

    dataBinding {
        enabled = true
    }

}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation("com.google.android.material:material:1.1.0")
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

    def apolloVersion='2.2.2'
    implementation "com.apollographql.apollo:apollo-runtime:$apolloVersion"
    implementation "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"

    def archLifecycleVersion = '2.1.0-rc01'
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"

    def daggerVersion = '2.23.2'
    implementation "com.google.dagger:dagger:$daggerVersion"
    kapt "com.google.dagger:dagger-compiler:$daggerVersion"
    implementation "com.google.dagger:dagger-android-support:$daggerVersion"
    kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
    kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
    kaptTest "com.google.dagger:dagger-compiler:$daggerVersion"

    def okhttpVersion='4.3.1'
    implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
    implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"

    def glideVersion = '4.11.0'
    implementation "com.github.bumptech.glide:glide:$glideVersion"
    kapt  "com.github.bumptech.glide:glide:$glideVersion"

    def activityKtxVersion = '1.1.0'
    implementation "androidx.activity:activity-ktx:$activityKtxVersion"
    def fragmentKtxVersion = '1.1.0'
    implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"

}

 

이미지를 위해 Glide, DI는 Dagger, 통신 세팅을위해 Okhttp를 추가하고 Apollo 라이브러리를 추가했습니다.

그리고 Apollo에서 GraphQL에 쓰이기 위한 코틀린 파일을 Generation 하기 위해 아래를 추가했습니다.

  apollo {
        generateKotlinModels.set(true)
    }

 

GraphQL에서 서버와 데이터를 통신하기 위해서는 서버측으로 부터 Schema 파일을 다운 받아야 합니다. 저는 아폴로 튜토리얼에서 제공하는 서버를 바탕으로 프로젝트를 진행할 것이기 때문에 아폴로 서버에서 Schema 파일을 다운 받겠습니다. 

 

튜토리얼 2.Add the GraphQL schema 부분에서 터미널로 스키마를 추가하는 명령어가 있습니다. 근데 윈도우 환경에서그 명령어를 그대로 실행하면 에러가 발생하게 되는데요. 윈도우에서는 아래와 같이 실행시키시면 됩니다.(이거때문에 삽질을 했습니다...ㅎ)

gradlew :app:downloadApolloSchema --endpoint=https://apollo-fullstack-tutorial.herokuapp.com --schema=app/src/main/graphql/com/example/rocketreserver/schema.json

 

 위의 과정을 진행하셧 다면 디렉토리 구조가 이렇게 되어있을겁니다. 이 Schema를 보시면 서버에서 정의한 타입 정보들에 대해 볼수 있습니다. 

GraphQL 세팅 구조

 

 

이제 MVVM을 위한 세팅을 진행해 보겠습니다.

Applcaition Injection을 위해 GlobalApplcaition 을 생성하고 Manifest에 등록합니다.

 

open class GlobalApplication : DaggerApplication() {

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerApplicationComponent.factory().create(applicationContext)
    }


    override fun onCreate() {
        super.onCreate()
        instance = this

    }

    companion object {
        private var instance: GlobalApplication? = null

        val globalApplicationContext: GlobalApplication
            get() {
                checkNotNull(instance) {}
                return instance!!
            }

        var token =""
    }

}

 

 

이후 액티비티/프래그먼트의 Injection을 위해 세팅합니다. 뷰모델은 

@Module
abstract class MainModule {

    @ContributesAndroidInjector(modules = [
        ViewModelBuilder::class
    ])
    internal abstract fun mainActivity(): MainActivity

}

 

그리고 함수나 변수들에 대한 Injection을 세팅합니다.

 

@Module(includes = [ApplicationModuleBinds::class])
object ApplicationModule {

    @Qualifier
    @Retention(RUNTIME)
    annotation class ApiServerNetworkSource

    @Qualifier
    @Retention(RUNTIME)
    annotation class ApiRealTimeServerNetworkSource

    @Qualifier
    @Retention(RUNTIME)
    annotation class ApiLocalSource

    @JvmStatic
    @Singleton
    @Provides
    fun provideIoDispatcher() = Dispatchers.IO


    @JvmStatic
    @Singleton
    @Provides
    fun provideOkhttp(tokenIntercepter: TokenIntercepter) : OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(1, TimeUnit.MINUTES)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(tokenIntercepter)
            .addInterceptor(HttpLoggingInterceptor()
                .setLevel(HttpLoggingInterceptor.Level.BODY))
            .build()
    }


    @JvmStatic
    @Singleton
    @Provides
    @ApiServerNetworkSource
    fun provideApolloClient(okHttpClient: OkHttpClient) : ApolloClient {
        return  ApolloClient.builder()
            .okHttpClient(okHttpClient)
            .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com")
            .build()
    }

    @JvmStatic
    @Singleton
    @Provides
    @ApiRealTimeServerNetworkSource
    fun provideRealTimeApolloClient(okHttpClient: OkHttpClient) : ApolloClient {
        return  ApolloClient.builder()
            .okHttpClient(okHttpClient)
            .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql")
            .subscriptionTransportFactory(WebSocketSubscriptionTransport.Factory("wss://apollo-fullstack-tutorial.herokuapp.com/graphql",okHttpClient))
            .build()
    }

}

@Module
abstract class ApplicationModuleBinds {

    @Singleton
    @Binds
    abstract fun bindRepository(repo: DefaultRepository): ApiRepository


}

 

 

Apollo 클라이언트가 2개인데 하나는 Restful 통신이고 2번째는 웹소켓을  이용한 통신을 하기 위한 클라이언트를 만들었습니다.

 

토큰 인터셉터는 통신할때마다 헤더에 직접 토큰을 넣지 않고 한번만  세팅하면 매번 통신할때마다 토큰이 들어가게 설정하기 위해 만들었습니다.

 

@Singleton
class TokenIntercepter @Inject constructor() : Interceptor{
    var token : String = ""
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization",token)
            .build()

        return chain.proceed(request)
    }
}

 

이제 뷰모델에 매개변수를넘기기 위한 ViewModelFactory Class 를 정의합니다.

 

class ViewModelFactory @Inject constructor(
    private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                    break
                }
            }
        }
        if (creator == null) {
            throw IllegalArgumentException("Unknown model class: $modelClass")
        }
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

@Module
internal abstract class ViewModelBuilder {
    @Binds
    internal abstract fun bindViewModelFactory(
        factory: ViewModelFactory
    ): ViewModelProvider.Factory
}

@Target(
    AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

 위와같이 Injection이 세팅되었다면 Applicaition,Activity,Module을 모두 하나의 컴포넌트로 묶어 인젝션 하도록 인터페이스를 만들겠습니다. 

@Singleton
@Component(
    modules = [
        ApplicationModule::class,
        AndroidSupportInjectionModule::class,
        MainModule::class
    ])

interface ApplicationComponent : AndroidInjector<GlobalApplication> {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): ApplicationComponent
    }
}

 

이후 왼쪽의 +버튼을 눌러 Endpoint를 추가합니다.

 

서버와 통신하기 위한 데이터 모델을 정의해야 하는데요. graphql이라는 확장자로 파일을 만들어 정의하면 됩니다. graphql 파일을 만들면 컴파일시 해당 파일에 대한 클래스가 생성되어 따로 DAO파일을 만들필요가 없습니다. 이제 아이템 리스트를 가져오는 파일을 만들겠습니다.

 

아이템정보를 가져오는 graphql

query LaunchList {
  launches {
    cursor
    hasMore
    launches {
      id
      site
      mission{
        name
        missionPatch(size : SMALL)
      }
    }
  }
}

 

위 파일을은 com/example/rocketreserver 아래에 위치시키시면 됩니다.

 

 

다음 포스팅에서는 repository를 세팅하고 실제 통신하는 예제를 만들어보겠습니다.

감사합니다.

댓글