apollo-android v3.1.0 Release Notes

  • 2022-02-07

    ๐Ÿ”– Version 3.1.0 introduces new APIs for testing, mapping scalars as well a redesigned cache pipeline. ๐Ÿ›  It also contains bugfixes around the @include directives, MemoryCache and GraphQL validation amongst other changes.

    ๐Ÿ“ฆ โš™๏ธ [breaking] Fragment package name and useSchemaPackageNameForFragments (#3775)

    ๐Ÿ“ฆ If you're using packageNamesFromFilePaths(), the package name of generated fragment classes has changed.

    ๐Ÿ“ฆ Different generated types have different package names:

    • Generated types coming from operations are generated based on the operation path
    • Generated types coming from the schema (input objects, custom scalars and enums) are generated based on the schema path

    Previously, fragments were using the schema path which is inconsistent because fragments are not defined in the schema but are executable files, like operations.

    ๐Ÿ”– Version 3.1.0 now uses the same logic for fragments as for operations. To revert to the previous behaviour, you can ๐Ÿ“ฆ use useSchemaPackageNameForFragments:

    apollo {
      useSchemaPackageNameForFragments.set(true)
    }
    

    This is also done automatically if you're using useVersion2Compat(). Moving forward, the plan is to โœ‚ remove useSchemaPackageNameForFragments in favor of setting a custom PackageNameGenerator. If you have use cases ๐Ÿ“ฆ that require useSchemaPackageNameForFragments, please reach out .

    โœ… โœจ [New] QueueTestNetworkTransport (#3757)

    โœ… 3.1.0 introduces QueueTestNetworkTransport to test at the GraphQL layer without needing to run an HTTP server.

    ๐Ÿ”ง To use it, configure your ApolloClient:

    // This uses a QueueTestNetworkTransport that will play the queued responses
    val apolloClient = ApolloClient.Builder()
        .networkTransport(QueueTestNetworkTransport())
        .build()
    

    โœ… You can then use the enqueueTestResponse extension function to specify the GraphQL responses to return:

    val testQuery = GetHeroQuery("001")
    val testData = GetHeroQuery.Data {
      hero = droidHero {
        name = "R2D2"
      }
    }
    apolloClient.enqueueTestResponse(testQuery, testData)
    val actual = apolloClient.query(testQuery).execute().data!!
    assertEquals(testData.hero.name, actual.hero.name)
    

    ๐Ÿคก โœจ [New] MockServerHandler (#3757)

    โœ… If you're testing at the HTTP layer, you can now define your own MockServerHandler to customize how the server is going to answer to requests:

    val customHandler = object : MockServerHandler {
      override fun handle(request: MockRequest): MockResponse {
        return if (/* Your custom logic here */) {
          MockResponse(
              body = """{"data": {"random": 42}}""",
              headers = mapOf("X-Test" to "true"),
          )
        } else {
          MockResponse(
              body = "Internal server error",
              statusCode = 500,
          )
        }
      }
    }
    val mockServer = MockServer(customHandler)
    

    โœจ [New] FetchPolicy.CacheAndNetwork (#3828)

    Previously, FetchPolicys were limited to policies that emitted at most one response. There was a executeCacheAndNetwork() method but it felt asymmetrical. This version introduces FetchPolicy.CacheAndNetwork that can emit up to two responses:

    apolloClient.query(query)
        // Check the cache and also use the network (1 or 2 values can be emitted)
        .fetchPolicy(FetchPolicy.CacheAndNetwork)
        // Execute the query and collect the responses
        .toFlow().collect { response ->
          // ...
        }
    

    โœจ [New] ApolloCall<D>.fetchPolicyInterceptor(interceptor: ApolloInterceptor) (#3743)

    If you need more customized ways to fetch data from the cache or more fine-grained error handling that does not come with the built-in FetchPolicy, you can now use fetchPolicyInterceptor:

    // An, interceptor that will only use the network after getting a successful response
    val refetchPolicyInterceptor = object : ApolloInterceptor {
      var hasSeenValidResponse: Boolean = false
      override fun <D : Operation.Data> intercept(
          request: ApolloRequest<D>,
          chain: ApolloInterceptorChain
      ): Flow<ApolloResponse<D>> {
        return if (!hasSeenValidResponse) {
          CacheOnlyInterceptor.intercept(request, chain).onEach {
            if (it.data != null) {
              // We have valid data, we can now use the network
              hasSeenValidResponse = true
            }
          }
        } else {
          // If for some reason we have a cache miss, get fresh data from the network
          CacheFirstInterceptor.intercept(request, chain)
        }
      }
    }
    
    apolloClient.query(myQuery)
        .refetchPolicyInterceptor(cacheOnlyInterceptor)
        .watch()
        .collect {
          //
        }
    

    โœจ [New] Service.mapScalar Gradle API (#3779)

    You can now use mapScalar to specify your scalar mappings:

    apollo {
      // Replace 
      customScalarsMapping.set(mapOf(
          "Date" to "java.util.Date"
      ))
    
      // With
      mapScalar("Date", "java.util.Date")
    }
    

    mapScalar also works with built-in scalar types so you can map the ID type to a kotlin Long:

    apollo {
      // This requires registering an adapter at runtime with `addCustomScalarAdapter()` 
      mapScalar("ID", "kotlin.Long")
    }
    

    As an optimization, you can also provide the adapter at compile time. This will avoid a lookup at runtime everytime such a scalar is read:

    apollo {
      // No need to call `addCustomScalarAdapter()`, the generated code will use the provided adapter 
      mapScalar("ID", "kotlin.Long", "com.apollographql.apollo3.api.LongAdapter")
    }
    

    For convenience, a helper function is provided for common types:

    apollo {
      // The generated code will use `kotlin.Long` and the builtin LongAdapter 
      mapScalarToKotlinLong("ID")
    
      // The generated code will use `kotlin.String` and the builtin StringAdapter
      mapScalarToKotlinString("Date")
    
      // The generated code will use `com.apollographql.apollo3.api.Upload` and the builtin UploadAdapter
      mapScalarToUpload("Upload")
    }
    

    ๐Ÿšง [Changed] convertApolloSchema and downloadApolloSchema now use paths relative to the root of the project (#3773, #3752)

    Apollo Kotlin adds two tasks to help to manage schemas: convertApolloSchema and downloadApolloSchema. These tasks are meant to be used from the commandline.

    Previously, paths were interpreted using the current working directory with File(path). Unfortunately, this is unreliable because Gradle might change the current working directory in some conditions ( ๐Ÿ‘€ see Gradle#13927 or Gradle#6074 for an example).

    With 3.1.0 and onwards, paths, will be interpreted relative to the root project directory (project.rootProject.file(path)):