Giter Club home page Giter Club logo

promise-kotlin's Introduction

GitHub license Build Status GitHub release

Promise.kt

Promise as a pattern is well known in JS world, but it's not so popular among Android folks, maybe because of the fact that we have very powerful RxJava library. But what if you need RxJava just for a single value response (Single) such as single network request and couple transformation operation like: flatMap and map. If this is the case then you should consider Promise pattern that works well for single value response.

What is Promise ?

Wiki defines Promises pattern as:

Promise refer to constructs used for synchronizing program execution in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is yet incomplete.

Technically, it's a wrapper for an async function with result returned via callback. Besides being an async computation wrapper, Promise can also act as continuation monad opening up a new way of solving many problems related to async computations.

Features

This implementation of Promise provides next key-features:

  • cold by default, what means until you explicitly subscribe to it via whenComplete task won't be executed
  • cached, once value computed subsequent calls to whenComplete will not trigger async computation again.
  • parameterized with both value and error: Promise<Int, Error>
  • implemented as a continuation monad, defines bind operator that provides extension point for new custom operators
  • cancelable, Promise can be canceled with triggering action if provided in task declaration via doOnCancel
  • very light weight implementation that offers only core functionality but at the same time flexible enough to extend
  • synchronization lock free, uses CAS/atomic operations only

Quick Start

As a quick start let's imagine the next scenario, we want to fetch user by id first and then fetch user's repositories. Below example that demonstrates how it can be solved with Promise:

 Promise.ofSuccess<Long, IOException>(100)
  .then { userId ->
    Promise<User, IOException> {
      val fetchUser: Call = userService.fetchUserById(userId)
      onCancel {
        // when Promise.cancel() is called this action will be performed
        // to give you a chance cancel task execution and clean up resources
        fetchUser.cancel()
      }

      fetchUser.enqueue(object : Callback {
        override fun onFailure(e: IOException) {
          // notify promise about error
          reject(e)
        }

        override fun onResponse(response: User) {
          // notify promise about success
          resolve(response)
        }
      })
    }
  }.then { user ->
    // chain another promise that will be executed when previous one
    // resolved with success
    Promise<List<Repo>, IOException> {
      val fetchUserRepositories: Call = repoService.fetchUserRepositories(user.id)
      onCancel {
        // when Promise.cancel() is called this action will be performed
        // to give you a chance cancel task execution and clean up resources
        // any chained promises will get a chance to cancel their task
        // if they alread started execution
        fetchUserRepositories.cancel()
      }

      fetchUserRepositories.enqueue(object : Callback {
        override fun onFailure(e: IOException) {
          // notify promise about error
          reject(e)
        }

        override fun onResponse(response: List<Repo>) {
          // notify promise about success
          resolve(response)
        }
      })
    }.map { 
      // chain implicitly new promise with transformation function
      repositories -> user to repositories 
    }
}.whenComplete {
  // subscribe to pipeline of chained promises
  // this call will kick-off promise execution
  when (it) {
    is Promise.Result.Success -> println("User: ${it.value.first} repositories: ${it.value.second}")
    is Promise.Result.Error -> println("Yay, error: ${it.error.message}")
  }
}

Usage

Promise is super easy to use, you should treat them as a task that suppose to be executed sometime in the future but it won't be started unless you subscribe and it promises either be successfully resolved or failed.

Create promise

There are 2 ways to create a Promise, provide a task that will do some job:

Promise<User, RuntimeException> {
  val fetchUserRequest = ...;
  onCancel {
    // will be called if promise has been canceled
    // it is task responsibility to terminate any internal long running jobs
    // and clean up resources here
    fetchUserRequest.cancel()
  }
  
  // do some real job
  val user = fetchUserRequest.execute();
  
  // notify about success
  resolve(user) 
  
  //or report error
  //reject(RuntimeException("Failed"))
}

we just created a Promise with success result type User and error result type RuntimeException. This promise will perform some task to fetch user in some future and notify about result via onSuccess(user) or if task failed onError(RuntimeException("Failed")). Additionally task can register an action to terminate long running job, clean up resources, etc. and it will be called when this Promise is canceled. Keep in mind that this is a cold Promise and it won't start executing task until we explicitly subscribe to it.

The second way to create a Promise is wrapping existing result into promise:

val promise1 = Promise.of("Boom!")
val promise2 = Promise.ofSuccess<String, RuntimeException>("Kaboom!")
val promise3 = Promise.ofError<String, IOException>(IOException("Smth wrong today"))

promise1 will be resolved with success result value Boom1, promise2 will be resolved with success result value Kaboom!. The difference between these two promises is in types: promise1 has type Promise<String, Nothing> that means this promise doesn't know what type of error the result pipeline will be, while promise2 declare that any next chained promise should propagate RuntimeException. The last promise3 resolved with error result IOException("Smth wrong today").

Start promise

To start promise task execution you have to subscribe to it:

Promise<String, RuntimeException> {
  val call = null
  onCancel {
    call.cancel()
  }

  val result = try {
    call.execute() // long running job
  } catch (e: Exception) {
    reject(RuntimeException(e))
    return@Promise
  }
  resolve(result)
}.whenComplete { result: 
  when (it) {
    is Promise.Result.Success -> println("We did it: ${it.value}")
    is Promise.Result.Error -> println("Yay, error: ${it.error.message}")
  }
}

You can call as many whenComplete on the same promise as you want, all will be notified about the promise result but task will be executed only once.

Chain promises

Now the best part, you can build a chain or pipeline of promises by using: then, map, errorThen, mapError:

Promise.ofSuccess<Long, IOException>(100)
  .then { userId ->
    Promise<User, IOException> {
      val fetchUser: Call = userService.fetchUserById(userId)
      onCancel {
        // when Promise.cancel() is called this action will be performed
        // to give you a chance cancel task execution and clean up resources
        fetchUser.cancel()
      }

      fetchUser.enqueue(object : Callback {
        override fun onFailure(e: IOException) {
          // notify promise about error
          reject(e)
        }

        override fun onResponse(response: User) {
          // notify promise about success
          resolve(response)
        }
      })
    }
  }.then { user ->
    // chain another promise that will be executed when previous one
    // resolved with success
    Promise<List<Repo>, IOException> {
      val fetchUserRepositories: Call = repoService.fetchUserRepositories(user.id)
      onCancel {
        // when Promise.cancel() is called this action will be performed
        // to give you a chance cancel task execution and clean up resources
        // any chained promises will get a chance to cancel their task
        // if they alread started execution
        fetchUserRepositories.cancel()
      }

      fetchUserRepositories.enqueue(object : Callback {
        override fun onFailure(e: IOException) {
          // notify promise about error
          reject(e)
        }

        override fun onResponse(response: List<Repo>) {
          // notify promise about success
          resolve(response)
        }
      })
    }.map { 
      // chain implicitly new promise with transformation function
      repositories -> user to repositories 
    }
}

Scheduling

Even though it is up to promise task to decide what thread to use for job execution and notify about result, but sometimes as a consumer you need a way to specify explicitly the thread on which you want receive the result:

val mainThreadExecutor: Executor = ...;
Promise<User, IOException> {
   //do some job
}
  .completeOn(mainThreadExecutor)
  .whenComplete { result: Promise.Result<User, RuntimeException> ->
    when (result) {
      is Promise.Result.Success -> println("User : ${result.value}")
      is Promise.Result.Error -> println("Yay, error: ${result.error.message}")
    }
  }

Now the result will be delivered on mainThreadExecutor. You probably will ask what if I need to be notified on main Android thread, you can easily extend existing functionality by:

fun <T, E> Promise<T, E>.completeOn(handler: Handler): Promise<T, E> {
  return bind { result ->
    Promise<T, E> {
      val canceled = AtomicBoolean()
      onCancel {
        canceled.set(true)
      }
      handler.post {
        if (!canceled.get()) {
          when (result) {
            is Promise.Result.Success -> onSuccess(result.value)
            is Promise.Result.Error -> onError(result.error)
          }
        }
      }
    }
  }
}

This a good example of using bind operation to extend and bring your custom behaviour for this micro-framework.

You can even specify on what thread Promise job should be started:

Promise<User, IOException> {
   //do some job
}
  .startOn(Executors.newSingleThreadExecutor())
  .whenComplete { result: Promise.Result<User, RuntimeException> ->
    when (result) {
      is Promise.Result.Success -> println("User : ${result.value}")
      is Promise.Result.Error -> println("Yay, error: ${result.error.message}")
    }
  }

Now the job inside promise will be started with the executor we just provided.

Groups

There are circunstances where you would like to handle a cluster of promises. Promise.all() receive as argument a collection of promises, it will return on success an array of results and on failure will return the first rejected value (indicated on that promise).

Let's imagine we have a list of users ids and we want to know the latest activity for each user.

val ids = mutableListOf(1, 2, 3, 4)
val promises : mutableListOf(Promises<List<UserActivity>, IOException>)
ids.map{
	val promise = Promise<List<UserActivity>, IOException> {
			//Remember to handle cancel
			try {
				//it is our user id
				val lastActivity = fetchLatestActivity(it).execute()
				resolve(lastActivity)
			} catch (e : IOException) {
				reject(e)
			}
	}
	promises.add(promise)
}
//After ids iteration we have a list of promises, we have to susbcribe now
Promise.all(promises).whenComplete {
	//You can use reserved word it, this is for clarification
	result: Promise.Result<Array<List<UserActivity>>, Int>
	when (result) {
		is Promise.Result.Success<Array<List<UserActivity>>, Int> -> {
			//Every promise success value was a list, we have an array of lists
			val arrayOfLists : <Array<List<UserActivity>> = result.value
			//You now can iterate, save it to the database
		}
		is Promise.Result.Error<Array<List<UserActivity>>, Int> -> {
			//This is the error of the first promise to fail
			Log.d("SHOPIFY_PROMISE", "request fail", result.error)
		}
	}
}

Gradle Integration

implementation 'com.shopify.promises:promises:0.0.9'

Version 0.0.9 and beyond are available through Maven Central. Previous versions remain available through JCenter.

Other API

Additionally this micro-framework provides next util functions:

  • Promise#cancel() signals promise to cancel task execution if it hasn't been completed yet
  • Promise#onResolve(block: (T) -> Unit), Promise#onReject(block: (E) -> Unit) like in Rx it allows to register some action to be performed when this promise resolved with success or failure
  • Promise#onStart(block: () -> Unit) allows to register some action to be performed before promise task execution, useful when some initialization is required
  • Promise.Companion#all(promises: Sequence<Promise>) creates a Promise that will wait until all provided promises are successfully resolved or one of them fails
  • Promise.Companion#any(promises: Sequence<Promise>) creates a Promise that will be resolved as soon as the first of the provided promises resolved or failed

Contributions

We welcome contributions. Please follow the steps in our contributing guidelines.

License

Promise-Kotlin is distributed under MIT license MIT License.

promise-kotlin's People

Contributors

cursedcoder avatar cutiko avatar jaredh avatar sav007 avatar udeyrishi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

promise-kotlin's Issues

Intergration with Kotlin Coroutines

Since Kotlin Coroutines are now no longer experimental, it would be nice to integrate it into the library to extend the behavior of the Deferred<T> object in the library. Plus perhaps give more functionalities specifically targeted towards Android.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.