A simple and easy to use Android library for handling REST API or RESTFul API. It is part of the Fulton family.
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.swarmnyc:fulton-android:$version'
}
Many operations for Android need android.context.Context. Therefore, Fulton-Android needs to be initialized before it can do anything.
// it can be initialized on create of an application
class MyApplication : Application() {
override fun onCreate() {
Fulton.init(this)
}
}
// or on create of an activity
class MainActivity : Activity() {
override fun onCreate() {
Fulton.init(this)
}
}
Fulton-Android uses Promise-Like to handle its async http request. It is very similar to Promise of JavaScript which is very easy to use. For Example,
FooApiClient().barList().then { list ->
// convert List<Bar> to List<BarViewModel>
list.map { BarViewModel(it) }
}.thenUi { vmList ->
// update UI
}.catchUi { error ->
// show error
}
Each Api Call returns a Promise object. From there, you can use .then
to do stuff that doesn't require the UI thread or use .thenUi
to update UI. Also, use .catch
or .catchUi
to handle errors.
There are many other features. See PromiseKt for more information.
ApiClient is an abstract class that handles REST APIs. You can use one ApiClient to handle any requests, but we recommend creating different ApiClients for different API routes. For Example,
/* ProductApiClient handles all API calls of Product */
class ProductApiClient : ApiClient() {
override val urlRoot = "https://api.your-domain.com/products"
fun search(keyword:String, limit:Int) : Promise<List<Product>> {
return request {
// the request becomes GET https://api.your-domain.com/products/search?keyword=${keyword}&limit=${limit}
paths("latest")
query("keyword" to keyword, "limit" to limit)
// Java has problems for Generic, so if the return Type has generics
// The generics have be manually defined.
// Like there, List<Product>, News have to be defined.
resultTypeGenerics(Product::class.java)
}
}
fun create(product:Product) : Promise<Product> {
return request {
// the request becomes POST https://api.your-domain.com/products
method = Method.Post
body(product)
}
}
}
The following two functions are the basic functions to make HTTP calls.
fun <T> request(init: Request.() -> Unit): Promise<T>
, The parameter is a block that you can initialize the Request object inside of.fun <T> request(req: Request): Promise<T>
, The parameter is a Request object.
We recommend that you use the first function because it is easier to use. For example,
fun foo() : Promise<Bar> {
return request { // "this" is a Request object
// initialize the Request object inside the lambda function, like
paths("bar")
}
}
// this serves the same result as the above example
fun foo() : Promise<Bar> {
val req = Request()
req.returnType = Boo::class.java
req.paths("bar")
return request(req)
}
See wiki/ApiClient for more detail.
FultonApiClient is an abstract class and extends from ApiClient. It is fully designed to handle the request and response for Fulton Server which is a RESULTFul server based on Express. For example,
/* ProductApiClient handles all API calls of Product */
class ProductApiClient : FultonApiClient(){
override val urlRoot = "https://api.your-domain.com/products"
fun listProduct() : Promise<ApiManyResult<Product>> {
return list()
}
}
val productApiClient = ProductApiClient()
productApiClient.listAllProducts().thenUi { result ->
listAdapter.add(result.data)
}
These five methods matches the RESTFul convention:
fun <T> list(queryParams: QueryParams?, init: (Request.() -> Unit)?): Promise<ApiManyResult<T>>
fun <T> detail(id: Any, queryParams: QueryParams?, init: (Request.() -> Unit)?): Promise<T>
fun <T> create(entity: T, init: (Request.() -> Unit)?): Promise<T>
fun <T> update(id: Any, entity: T, init: (Request.() -> Unit)?): Promise<Unit>
fun delete(id: Any, init: (Request.() -> Unit)?): Promise<Unit>
You can use FultonApiClient (even though the API Server doesn't use Fulton Server) as long as the API Server meets these specifications.
- It is RESTFul.
- the body of request for "create" and "update"
{ data: T }
- the body of response for "list"
{ data: T[], pagination?: { size:number, index:number, total:number}, error?: FultonError }
- the body of response for "detail" and "create"
{ data: T, error?: FultonError }
See wiki/FultonApiClient for more detail.
QueryParams are the query parameters that are accepted by Fulton-Server which provide you with a flexible way to query data.
It has 5 parts:
- filter
- sort
- projection, to define whether columns should return or not
- includes, to define whether api should extra data or not
- pagination
fun listProduct(keyword:String) : Promise<ApiManyResult<Product>> {
return list {
queryParams {
filter {
"category" to "book"
"title" to json {
"like" to keyword
}
}
sort {
desc("releaseDate")
ase("title")
}
projection {
show("price")
hide("discount")
}
includes {
add("factory")
}
pagination {
index = 1
size = 100
}
}
}
}
// or
val qp = queryParams {
...
}
fun listProduct(qp:queryParams) : Promise<ApiManyResult<Product>> {
return list(qp)
}
// it generates this query string ?filter[category]=book&filter[title][$like]=$keyword&sort=releaseDate-,title&includes=factory&pagination[index]=1&pagination[size]=100
See wiki/QueryParams for more detail.
FultonApiClient can handle authentication itself. Currently, Fulton supports bearer token. There are two ways to set the token:
- Fulton.context.identityManager.token or fultonApiClient.context.identityManager.token Put the token on the global context or on its own context. For example,
fun login(username:String, password:String) : Promise<Unit> {
return request {
paths("login")
body("username" to username, "password" to password)
}.then {
context.identityManager.token = it
}
}
- IdentityApiClient Fulton-Android provides IdentityApiClient which matches the authentication of Fulton-Server. You can extend the class and use it that way. For example,
class MyIdentityApiClient : IdentityApiClient() {
override val urlRoot = "https://api.your-domain.com/auth"
}
val identityApiClient = MyIdentityApiClient()
identityApiClient.login(username, password)
Once the access token has settled, all the coming API calls have Authorization bearer ${token}
in their request headers.
See wiki/Identity for more detail.
ApiClient can use cached data for speeding up. The cache logics are:
- storing response body for GET method for given time (the default value is 5 mins), while the key is the url.
- When another GET request calls, ApiClient checks if there is data matched to the same url and if it hasn't expired. Then ApiClient returns the data immediately instead of making a real Request.
There are many options to change cache settings:
- set default cache duration
// change the option when initialization
Fulton.init(this) {
defaultCacheDurationMs = 60_000 // set the default cache duration to 1 minute
}
// or after initialization
Fulton.context.defaultCacheDurationMs = 60_000
- set cache duration for each request
fun getData() : Promise<Data> {
return request {
cacheDurationMs = 0 // if the duration is 0 means no cache for this request.
}
}
- set the CacheManager
Fulton-Android provides two managers,
- SqlCacheManager, the default cache manager which stores data in its database.
- VoidCacheManager, the cache manager does nothing, but it can be used in Test.
You also can customize your own cache manager. For example,
class MyCacheManager : CacheManager {
override fun add(cls: String, url: String, durationMs: Int, data: ByteArray) {
...
}
override fun <T> get(url: String, type: Type): T? {
...
}
override fun clean(cls: String?) {
...
}
}
// to set the cache manager
Fulton.init(this) {
cacheManager = MyCacheManager()
}
Fulton-Android has three places to catch the errors by request:
- apiClient.onError (error: Throwable): Boolean With the error handler on ApiClient, the return value tells us if the error is handled or not. If the value is "true", it means the error is handled, so .catch or Fulton.context.errorHandler won't be invoked. For example,
class MyApiClient : ApiClient() {
override fun onError(error: Throwable): Boolean {
... // do some things
return error is MyException
}
}
- .catch or .catchUi Every request returns a Promise object. It supports using .catch and .catchUi to catch errors. For example,
FooApiClient().barList().then {
...
}.catch {
// handle the error in normal thread
}.catchUi
// handle the error in UI thread
}
- global error handler You can add a global error handler to catch all unhandled errors from your requests. For example:
Fulton.init(this) {
errorHandler = { error ->
// do some things, like logging the error or show a dialog
}
}
There are two ways to avoid errors through the global error handler:
- handle errors on apiClient.onError which was mentioned above, or
- set request.shouldSendErrorToErrorHandler = false
For example,
fun getData() : Promise<Data> {
return request {
shouldSendErrorToErrorHandler = false
}
}
Fulton.context.errorHandler only catches errors from ApiClient. If errors are from .then, .thenUi or other promise methods, the errors are not caught, so these will go to Promise.uncaught. For example:
FooApiClient().barList().then {
throw error1
}.catch {
// error1 is caught there
throw error2
}
FooApiClient().barList().then {
throw error3
}
Promise.uncaughtError = {
// error2 and error3 are caught there
}
FultonContext stores options and the need for other objects like ApiClient. There is a global FultonContext created on Fulton.init. By default, it is used if you don't give a specific context. ApiClient and FultonApiClient will use the global context. However, Each ApiClient and FultonApiClient can have its own context. For example:
use default FultonContext class
// create a default FultonContext by Fulton.createDefaultContext
class MyApiClient(context: FultonContext): ApiClient(context)
{
....
}
fun foo() {
val myContext = Fulton.createDefaultContext(this) {
...
}
val myApiClient = MyApiClient(myContext)
}
// or you can create your own context
class MyContext : FultonContext {
....
}
class MyApiClient(): ApiClient(MyContext())
{
....
}
This feature only supports if the devices is Android 7.0 or above. Also, it stops monitoring if the app go to background and starts monitoring if the app come back to foreground.
If you want to make a simple request, you can use Fulton.request. For example:
Fulton.request<Foo> {
// provide every parameters that request needs
urlRoot = "http://api.your-domain.com"
paths("bar")
resultType = Foo::class.java
}.then {
...
}
Fulton-Android supports a way to mock responses. For example:
// once requestExecutorMock is not null, ApiClient will use it to execute the request
Fulton.context.requestExecutorMock = object : RequestExecutor {
override fun execute(req: Request, callback: RequestCallback) {
when (req.url){
"https://api.your-domain.com/foo"-> {
// return a list
callback(req, Response(200, listOf(Foo(), Foo())))
}
"https://api.your-domain.com/bar"->{
// return a object
callback(req, Response(200, Foo()))
}
}
}
}
MyApiClient().foo().then {
// the result from the MockRequestExecutor object
}
Fulton.context.requestExecutorMock = null // to cancel mocking
We've provided a sample app to show you how we use Fulton-Android with others libraries, as seen below:
See sample for more information.