Spike
A network abstraction layer over Volley, written in Kotlin and inspired by Moya for Swift
Example
Download repository and try the app.
Installation
Add in your build.gradle file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Then add as dependency to yout app/build.gradle
dependencies {
...
implementation 'com.github.dariopellegrini:Spike:v0.23.2'
}
Versions below 0.10 of this library use apache http libraries, that need the following code at the end of the android section in app/build.gradle. From version 0.10 this is not necessary.
android {
...
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
}
}
Usage
This library lets you to split API request's details inside kotlin files, in order to have more control on what each API does and needs. Each file is a sealed class and must implement the interface TargetType. Every detail of each call is selected using a when statement. See this example (TVMazeAPI.kt):
// Each of these data class represents a call
data class GetShows(val query: String): TVMazeTarget()
data class GetShowInformation(val showID: String, val embed: String): TVMazeTarget()
// Following actually don't exists
data class AddShow(val name: String, val coverImage: ByteArray, val token: String): TVMazeTarget()
data class UpdateShow(val showID: String, val name: String, val token: String): TVMazeTarget()
data class DeleteShow(val showID: String, val token: String): TVMazeTarget()
// This is the target sealed class, from which every data class inherits.
sealed class TVMazeTarget: TargetType {
// BaseURL of each call
override val baseURL: String
get() {
return "https://api.tvmaze.com/"
}
// Path of each call
override val path: String
get() {
return when(this) {
is GetShows -> "search/shows"
is GetShowInformation -> "shows/" + showID
is AddShow -> "shows/"
is UpdateShow -> "shows/" + showID
is DeleteShow -> "shows/" + showID
}
}
// Method of each call
override val method: SpikeMethod
get() {
return when(this) {
is GetShows -> SpikeMethod.GET
is GetShowInformation -> SpikeMethod.GET
is AddShow -> SpikeMethod.POST
is UpdateShow -> SpikeMethod.PATCH
is DeleteShow -> SpikeMethod.DELETE
}
}
// Headers of each call
override val headers: Map<String, String>?
get() {
return when(this) {
is GetShows -> mapOf("Content-Type" to "application/json")
is GetShowInformation -> mapOf("Content-Type" to "application/json")
is AddShow -> mapOf("Content-Type" to "application/json", "user_token" to token)
is UpdateShow -> mapOf("Content-Type" to "application/json", "user_token" to token)
is DeleteShow -> mapOf("Content-Type" to "application/json", "user_token" to token)
}
}
// Multipart entries to load multipart form data: this is optional
override val multipartEntities: List<SpikeMultipartEntity>?
get() {
return when(this) {
is GetShows -> null
is GetShowInformation -> null
is AddShow -> listOf(SpikeMultipartEntity("image/jpeg", coverImage, "coverImage", "coverImage.jpg"))
is UpdateShow -> null
is DeleteShow -> null
}
}
// Call's parameters with the labels wanted by backend services
override val parameters: Map<String, Any>?
get() {
return when(this) {
is GetShows -> mapOf("q" to query)
is GetShowInformation -> mapOf("embed" to embed)
is AddShow -> mapOf("name" to name)
is UpdateShow -> mapOf("name" to name)
is DeleteShow -> null
}
}
}
// Optional response closures
Provider
After this the only thing to do is init a SpikeProvider and make a request using the desired instance:
val provider = SpikeProvider<TVMazeTarget>(context)
val request = provider.request(GetShowInformation("1", embed = "cast"), {
response ->
println(response.results.toString())
}, {
error ->
println(error.results.toString())
})
Here response object contains status code, an enum value describing status code, headers in map, result in String and a computed result (see later). Then error contains the same values plus a VolleyError object.
The request is a Volley request and can be canceled as you wish.
There are different constructors for providers:
- Context constructor: init a volley queue using the passed context. By this each provider has its queue.
val provider = SpikeProvider<TVMazeTarget>(context)
- Queue constructor: init a volley queue using a queue passed to it.
val provider = SpikeProvider<TVMazeTarget>(queue)
- Empty constructor: implementing this requires to configure a Spike singleton instance, which contains a queue that is global and shared between each provider.
Spike.instance.configure(context) // called typically in Application file
val provider = SpikeProvider<TVMazeTarget>()
Closure responses
It's possible to deal with network responses in the API file, implementing 2 optional closure variables.
...
override val successClosure: ((String, Map<String, String>?) -> Any?)?
get() = {
result, headers ->
when(this) {
is GetShows -> {
val movieType = object : TypeToken<List<MovieContainer>>() {}.type
Gson().fromJson<List<MovieContainer>>(result, movieType)
}
is GetShowInformation -> {
val movieType = object : TypeToken<Movie>() {}.type
Gson().fromJson<Movie>(result, movieType)
}
is AddShow -> {
val movieType = object : TypeToken<Movie>() {}.type
Gson().fromJson<Movie>(result, movieType)
}
is UpdateShow -> {
val movieType = object : TypeToken<Movie>() {}.type
Gson().fromJson<Movie>(result, movieType)
}
is DeleteShow -> null
}
}
override val errorClosure: ((String, Map<String, String>?) -> Any?)?
get() = { errorResult, _ ->
val errorType = object : TypeToken<TVMazeError>() {}.type
Gson().fromJson<TVMazeError>(errorResult, errorType)
}
Here you can compute the result string from network (making for example a Gson mapping). Result of those closures will be in computedResult inside povider request's closures as a parameter of type Any?.
val provider = SpikeProvider<TVMazeTarget>()
provider.request(GetShowInformation("1", "cast"), {
response ->
// Printing success computed result
println(response.computedResult)
}, {
error ->
// Printing error computed result
println(error.computedResult)
})
Because computedResult is an Any? type, provider can perform a type safety call so that computed results for success and error have specific types.
// Movie and TVMazeError are data classes for TVMaze APIs
val provider = SpikeProvider<TVMazeTarget>()
provider.requestTypesafe<Movie, TVMazeError>(GetShowInformation("1", "cast"), {
response ->
// Printing success computed result Movie? type
println(response.computedResult)
}, {
error ->
// Printing error computed result TVMazeError? type
println(error.computedResult)
})
Coroutines support
Starting by version 0.12 it's possible to use suspending functions to perform provider requests.
CoroutineScope(Dispatchers.Main).launch {
try {
val response = provider.suspendingRequest<Movie>(GetShows("gomorra"))
println("${response.results}")
} catch (e: SpikeProviderException) {
// Exception in case of error, like server error or connection error
// Status code
val statusCode = e.statusCode
// Generics used to have a typesafe computed result call
val errorResponse = e.errorResponse<TVMazeError>()
val computedError = errorResponse?.computedResult // TVMazeError
println("""
$statusCode
$errorResponse
$computedError
""".trimIndent())
}
}
Builder
Version 0.14 supports builders for creating targets and requests.
Target
val target = buildTarget {
baseURL = "http://localhost"
path = "endpoint"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
successClosure = {
result, headers ->
JSONObject(result)
}
errorClosure = {
errorResult, headers ->
JSONObject(errorResult)
}
}
Request
Here is returned a suspending function to use within a coroutine.
CoroutineScope(Dispatchers.Main).launch {
try {
val response = SpikeProvider<TargetType>()
.buildRequest<JSONObject> {
baseURL = "http://localhost/"
path = "tales"
method = SpikeMethod.POST
headers = mapOf("Content-Type" to "application/json")
parameters = mapOf(
"title" to " My tale",
"content" to "This is the tale content",
"author" to "John")
}
Log.i("Success", "${response.computedResult}")
} catch(e: Exception) {
Log.e("Error", "$e")
}
}
Other requests
In order to make requests shorter starting from version 0.16 other types of requests which use global providers are available, with and without coroutines.
CoroutineScope(Dispatchers.Main).launch {
// Function with global provider and typesafe computed result
try {
val response = request<JSONObject> {
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
successClosure = { response, header ->
JSONObject()
}
}
Log.i("Spike", "${response.computedResult}")
} catch(e: SpikeProviderException) {
Log.e("Spike", "$e")
}
// Function with global request
try {
val response = requestAny {
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
}
Log.i("Spike", "${response}")
} catch(e: SpikeProviderException) {
Log.e("Spike", "$e")
}
}
// Callbacks
// Function with global provider and typesafe computed result
request<JSONObject, JSONObject>({
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
successClosure = { response, header ->
JSONObject()
}
errorClosure = { response, header ->
JSONObject()
}
}, onSuccess = { response ->
Log.i("Spike", "${response.computedResult}")
}, onError = { error ->
Log.e("Spike", "${error.computedResult}")
})
// Function with global provider
requestAny({
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
}, onSuccess = { response ->
Log.i("Spike", "${response}")
}, onError = { error ->
Log.e("Spike", "$error")
})
Mapping
Starting from version 0.22, mapping methods based on Gson has been introduced, for success and error responses.
try {
val movies = request<List<Movie>> {
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
}.mapping()
Log.i("Spike", "${movies}") // List<Movie>
} catch(e: SpikeProviderException) {
val error = e.errorResponse<TVMazeError>().mapping() // TVMazeError
Log.e("Spike", "$e")
} catch (e: Exception) {
Log.e("Spike", "$e")
}
By default mapping
function returns null
if a mapping error is thrown.
In order to throw mapping error mappingThrowable
function is available.
Coroutine
Mapping process can be expensive for large body sizes and can block main thread, freezing UI.
To avoid that, suspend mapping function are supported.
try {
val movies = request<List<Movie>> {
baseURL = "https://api.tvmaze.com/"
path = "shows"
method = SpikeMethod.GET
headers = mapOf("Content-Type" to "application/json")
}.suspend.mapping()
Log.i("Spike", "${movies}") // List<Movie>
} catch(e: SpikeProviderException) {
val error = e.errorResponse<TVMazeError>().suspend.mapping() // TVMazeError
Log.e("Spike", "$e")
} catch (e: Exception) {
Log.e("Spike", "$e")
}
Suspend mapping function are executed on Dispatchers.Default
starting from version 0.23.2.
TODO
- Testing.
Author
Dario Pellegrini, pellegrini.dario.1303@gmail.com