Skip to content

Learn-Android-app/bridge

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

130 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bridge

Bridge is a simple but powerful HTTP networking library for Android. It features a Fluent chainable API, powered by Java/Android's URLConnection classes for maximum compatibility and speed.

Gradle Dependency

First, add JitPack.io to the repositories list in your app module's build.gradle file:

repositories {
    maven { url "https://jitpack.io" }
}

Then, add Bridge to your dependencies list:

dependencies {
    compile 'com.afollestad:bridge:1.6.4'
}

JitPack Badge

Table of Contents

  1. Requests
    1. Basics
    2. URL Format Args
    3. Headers
    4. Bodies
      1. Plain
      2. Forms
      3. MultipartForms
      4. Streaming (Pipe)
      5. Upload Progress
    5. Info Callback
  2. Responses
    1. Headers
    2. Bodies
  3. Error Handling
  4. Async Requests, Duplicate Avoidance, and Progress Callbacks
    1. Example
    2. Duplicate Avoidance
    3. Download Progress Callbacks
  5. Request Cancellation
    1. Cancelling Individual Requests
    2. Cancelling Multiple Requests
      1. All Active
      2. Method, URL/Regex
      3. Tags
    3. Preventing Cancellation
  6. Validators
  7. Global Configuration
    1. Host
    2. Default Headers
    3. Timeouts
    4. Buffer Size
    5. Logging
    6. Validators
  8. Cleanup

Requests

Basics

Here's a very basic GET Request that retrieves Google's home page and saves the content to a String.

Response response = Bridge.client()
    .get("http://www.google.com")
    .response();

if (!response.isSuccess()) {
    // Response code was not 200-300,
    // decide what to do in this case.
} else {
    String responseContent = response.asString();
    // Do something with response
}

This could be shortened to:

String content = Bridge.client()
    .get("http://www.google.com")
    .asString();

Behind the scenes, this is actually performing this:

String content = Bridge.client()
    .get("http://www.google.com")
    .throwIfNotSuccess()
    .response()
    .asString();

The shorter version makes use of throwIfNotSuccess() to automatically throw a BridgeException with the reason REASON_RESPONSE_UNSUCCESSFUL (see the Error Handling section) if Response#isSuccess() returns false.

URL Format Args

When passing query parameters in a URL, you can use format args:

String searchQuery = "hello, how are you?";
String content = Bridge.client()
    .get("http://www.google.com/search?q=%s", searchQuery)
    .asString();
// Do something with response

This is using Java's String.format() method behind the scenes. The searchQuery variable replaces %s in your URL. You can have multiple format args, and they don't all have to be strings (e.g. %d for any number variable). Args that are strings will be automatically URL encoded for you.

Headers

Changing or adding Request headers is pretty simple. You just use the header(String, String) method which can be chained to add multiple headers.

String content = Bridge.client()
    .get("http://www.google.com")
    .header("User-Agent", "BridgeSampleProject")
    .header("CustomHeader", "Hello")
    .asString();
// Do something with response

You can also set a Map of headers:

Map<String, Object> headersMap = new HashMap<>();
headersMap.put("User-Agent", "BridgeSampleProject");
headersMap.put("CustomHeader", "Hello");

String content = Bridge.client()
    .get("http://www.google.com")
    .headers(headersMap)
    .asString();
// Do something with response

Default headers: see the Configuration section for info on how to set default headers that are automatically included in every request.

Bodies

Plain

Here's a basic POST request that sends plain text in the body:

String postContent = "Hello, how are you?";
String response = Bridge.client()
    .post("http://someurl.com/post")
    .body(postContent)
    .asString();

Passing a String will automatically set the Content-Type header to text/plain. The body() method takes other various forms of data, including:

  • Raw byte[] data
  • JSONObject/JSONArray
  • Form
  • MultipartForm
  • Pipe
  • File

Byte arrays are obviously raw data. Passing a JSONObject or JSONArray will automatically set the Content-Type header to application/json. Form, MultipartForm, and Pipe will be discussed in the next few sections. File relies on the Pipe feature, it reads the file and sets the request body to the raw contents.


Forms

Form's are commonly used with PUT/POST requests. They're basically the same thing as query strings with get requests, but the parameters are included in the body of the request rather than the URL.

Here's a basic example of how it's done:

Form form = new Form()
    .add("Username", "Aidan")
    .add("Password", "Hello");

String response = Bridge.client()
    .post("http://someurl.com/login")
    .body(form)
    .asString();

This will automatically set the Content-Type header to application/x-www-form-urlencoded.


MultipartForms

A MultipartForm is a bit different than a regular form. Content is added as a "part" to the request body. The content is included as raw data associated with a content type, allowing you to include entire files. Multipart forms are commonly used in HTML forms (e.g. a contact form on a website), and they can be used for uploading files to a website.

Here's a basic example of how it's done:

MultipartForm form = new MultipartForm()
    .add("Subject", "Hello")
    .add("Body", "Hey, how are you?")
    .add("FileUpload", new File("/sdcard/Download/ToUpload.txt"))
    .add("FileUpload2", "ToUpload2.mp4", Pipe.forFile(new File("/sdcard/Download/ToUpload2.mp4")));

String response = Bridge.client()
    .post("http://someurl.com/post")
    .body(form)
    .asString();

This will automatically set the Content-Type header to multipart/form-data.

Note: MultipartForm has an add() method that accepts a Pipe. This can be used to add parts from streams (see the section below on how Pipe is used). add() for File objects is actually using this indirectly for you.


Streaming (Pipe)

Bridge allows you to stream data directly into a post body:

Pipe pipe = new Pipe() {

    byte[] content = "Hello, this is a streaming example".getBytes();

    @Override
    public void writeTo(@NonNull OutputStream os, @Nullable ProgressCallback progressListener) throws IOException {
        os.write(content);

        // Notify optional progress listener that all data was transferred
        if (progressListener != null)
            progressListener.publishProgress(content.length, content.length);
    }

    @NonNull
    @Override
    public String contentType() {
        return "text/plain";
    }

    @Override
    public int contentLength() throws IOException {
        return content.length;
    }
};

String response = Bridge.client()
    .post("http://someurl.com/post")
    .body(pipe)
    .asString();

Note: when you use a Pipe as a body, the Content-Type header will automatically be set based on what contentType() in the Pipe implementation returns, same for Content-Length. If you want to override these headers, you can change it in the Pipe or manually set the header after setting the body.

Pipe has three static convenience methods that create a pre-designed Pipe instance:

Pipe uriPipe = Pipe.forUri(this, Uri.parse(
    "content://com.example.provider/documents/images/1"));

Pipe filePipe = Pipe.forFile(new File("/sdcard/myfile.txt"));

InputStream is = // ...
Pipe transferPipe = Pipe.forStream(is, "text/plain");

forUri(Context, Uri) will read from a File URI or content URI, so basically any file on an Android device can be read. forFile(File) indirectly uses forUri(Context, Uri) specifically for file:// URIs. forStream(InputStream, String) reads an InputStream and transfers the content into the Pipe, you need to specify a Content-Type value in the second parameter.


Upload Progress

You can monitor upload progress like this:

Bridge.client()
    .post("http://someurl.com/upload")
    .body(Pipe.forUri(this, data.getData()))
    .throwIfNotSuccess()
    .uploadProgress(new ProgressCallback() {
        @Override
        public void progress(Request request, int current, int total, int percent) {
            // Use progress
        }
    })
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            // Use response
        }
    });

Note: this only works effectively with Pipes (streams). If you specify non-stream data, upload is one step and can't be monitored.


Info Callback

You can set an info callback to receive various events, including a connection being established, and request bodies being sent:

Bridge.client()
    .get("https://www.google.com")
    .infoCallback(new InfoCallback() {
        @Override
        public void onConnected(Request request) {
            // Connection to Google established
        }

        @Override
        public void onRequestSent(Request request) {
            // This method is optional to override
            // Indicates request body was sent to Google
        }
    })
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            // Response received from Google
        }
    });

Responses

Headers

Retrieving response headers is simple:

Response response = // ...
String contentType = response.header("Content-Type");

Headers can also have multiple values, separated by commas:

Response response = // ...
List<String> values = response.headerList("header-name");

You can even retrieve a Map containing all of the existing headers. The value portion of each Map entry contains a List<String> which will only have 1 entry unless a header has multiple values:

Response response = // ...
Map<String, List<String>> headers = response.headers();

Since Content-Type and Content-Length are commonly used response headers, there's convenience methods to get these values:

Response response = // ...
String contentType = response.contentType();
int contentLength = response.contentLength();

Bodies

The above examples all use asString() to save response content as a String. There are many other formats that responses can be converted/saved to:

Response response = // ...

byte[] responseRawData = response.asBytes();

// Converts asBytes() to a UTF-8 encoded String.
String responseString = response.asString();

// If you set this to a TextView, it will display HTML formatting
Spanned responseHtml = response.asHtml();

// Cached in the Response object, using this method multiples will reference the same JSONObject.
// This allows your app to not re-parse the JSON if it's used multiple times.
JSONObject responseJsonObject = response.asJsonObject();

// Cached in the Response object, using this method multiples will reference the same JSONArray.
// This allows your app to not re-parse the JSON if it's used multiple times.
JSONArray responseJsonArray = response.asJsonArray();

// Don't forget to recycle!
// Once you use this method once, the resulting Bitmap is cached in the Response object,
// Meaning asBitmap() will always return the same Bitmap from any reference to this response.
Bitmap responseImage = response.asBitmap();

// Save the response content to a File of your choosing
response.asFile(new File("/sdcard/Download.extension"));

// Returns String, Spanned (for HTML), JSONObject, JSONArray, Bitmap, or byte[]
// based on the Content-Type header.
Object suggested = response.asSuggested();

Error Handling

The BridgeException class is used throughout the library and acts as a single exception provider. This helps avoid the need for different exception classes, or very unspecific Exceptions.

If you wanted, you could just display errors to the user using BridgeException#getMessage(). However, BridgeException lets you know exactly what happened before the user sees anything.


The BridgeException#reason() method returns one of a set of constants that indicate why the exception was thrown. If the Exception is for a request, you can retrieve the Request with BridgeException#request(). If the Exception is for a response, you can retrieve the Response with BridgeException#response().

BridgeException e = // ...
switch (e.reason()) {
    case BridgeException.REASON_REQUEST_CANCELLED: {
        Request request = e.request();
        // Used in BridgeExceptions passed to async request callbacks
        // when the associated request was cancelled.
        break;
    }
    case BridgeException.REASON_REQUEST_TIMEOUT: {
        Request request = e.request();
        // The request timed out (self explanatory obviously)
        break;
    }
    case BridgeException.REASON_REQUEST_FAILED: {
        Request request = e.request();
        // Thrown when a general networking error occurs during a request,
        // not including timeouts.
        break;
    }
    case BridgeException.REASON_RESPONSE_UNSUCCESSFUL: {
        Response response = e.response();
        // Thrown by throwIfNotSuccess(), when you explicitly want an
        // Exception be thrown if the status code was unsuccessful.
        break;
    }
    case BridgeException.REASON_RESPONSE_UNPARSEABLE: {
        Response response = e.response();
        // Thrown by the response conversion methods (e.g. asJsonObject())
        // When the response content can't be successfully returned in the
        // requested format. E.g. a JSON error.
        break;
    }
    case BridgeException.REASON_RESPONSE_IOERROR: {
        Response response = e.response();
        // Thrown by the asFile() response converter when the library
        // is unable to save the content to a file.
        break;
    }
    case BridgeException.REASON_RESPONSE_VALIDATOR_FALSE:
    case BridgeException.REASON_RESPONSE_VALIDATOR_ERROR:
        String validatorId = e.validatorId();
        // Discussed in the Validators section
        break;
}

Note: you do not need to handle all of these cases everywhere a BridgeException is thrown. The comments within the above example code indicate where those reasons are generally used.


Async Requests, Duplicate Avoidance, and Progress Callbacks

Example

Here's a basic example of an asynchronous request:

Bridge.client()
    .get("http://www.google.com")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                String content = response.asString();
            }
        }
    });

There's two major advantages to using async requests:

  1. Threads are not blocked. You can execute this code on UI thread (e.g. in an Activity) without freezing or errors.
  2. Duplicate avoidance (discussed below).

Note: You can replace get() with post(), put() or delete() too.

Like synchronous requests, you can use throwIfNotSuccess to receive a BridgeException with the reason REASON_RESPONSE_UNSUCCESSFUL if the status code is not successful:

Bridge.client()
    .get("http://www.google.com")
    .throwIfNotSuccess()
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                String content = response.asString();
            }
        }
    });

Duplicate Avoidance

Duplicate avoidance is a feature in this library that allows you to avoid making multiple requests to the same URL at the same time. For an example, if you were to do this...

Bridge.client()
    .get("http://www.google.com")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response object
            }
        }
    });

Bridge.client()
    .get("http://www.google.com")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response object
            }
        }
    });

...Google would only be contacted once by the library. When that single request is complete, both callbacks would be called at the same time with the same response data.

This lets you be very efficient on bandwidth and resource usage. If this library was being used to load images into ImageView's, you could display 100 ImageView's in a list, make a single request, and immediately populate all 100 ImageView's with the same image at the same time. Check out the sample project to see this in action.

Download Progress Callbacks

The Callback class has an optional progress(Request, int, int, int) method that can be overridden to receive progress updates for response downloading. current is how many bytes have been downloaded, total is how many bytes are available to be downloaded, and percent is the current/total ratio times 100.

Bridge.client()
    .get("http://someurl/bigfile.extension")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null || !response.isSuccess()) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response object
            }
        }

        @Override
        public void progress(Request request, int current, int total, int percent) {
            // Progress updates
        }
    });

Note: progress callbacks are only called if the library is able to determinate the size of the content being downloaded. Generally, this means the requested endpoint needs to return a Content-Length header.

Upload progress callbacks were discussed in Upload Progress section.


Request Cancellation

Cancelling Individual Requests

This library allows you easily cancel requests:

Request request = Bridge.client()
    .get("http://www.google.com")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response
            }
        }
    });

request.cancel();

When a request is cancelled, the BridgeException will not be null (it will say the request was cancelled), and BridgeException#isCancelled() will return true.

Cancelling Multiple Requests

The Bridge singleton allows you to cancel managed async requests.


All Active

This code will cancel all active requests, regardless of method or URL:

Bridge.client()
    .cancelAll();

Method, URL/Regex

The cancelAll(Method, String) allows you to cancel all active requests that match an HTTP method and a URL or regular expression pattern.

This will cancel all GET requests to any URL starting with http:// and ending with .png:

Bridge.client()
    .cancelAll(Method.GET, "http://.*\\.png");

.* is a wildcard in regular expressions, \\ escapes the period to make it literal.

If you want to cancel all requests to a specific URL, you can use Pattern.quote() to specify a regex that matches literal text:

Bridge.client()
    .cancelAll(Method.GET, Pattern.quote("http://www.android.com/media/android_vector.jpg"));

Tags

When making a request, you can tag it with a value:

Bridge.client()
    .get("http://www.google.com")
    .tag("Hello!")
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response
            }
        }
    });

By "a value", I mean literally any type of Object. It could be a String, int, boolean, etc.

You can cancel all requests marked with a specific tag value:

// Add a second parameter with a value of true to cancel un-cancellable requests
Bridge.client()
    .cancelAll("Hello!");

Preventing Cancellation

There are certain situations in which you wouldn't want to allow a request to be cancelled. For an example, your app may make calls to Bridge.client().cancelAll() when an Activity pauses; that way, all requests that were active in that screen are cancelled. However, there may be a Service in your app that's making requests in the background that you would want to maintain. You can make those requests non-cancellable:

Bridge.client()
    .get("http://www.google.com")
    .cancellable(false)
    .request(new Callback() {
        @Override
        public void response(Request request, Response response, BridgeException e) {
            if (e != null) {
                // See the 'Error Handling' section for information on how to process BridgeExceptions
                int reason = e.reason();
            } else {
                // Use the Response
            }
        }
    });

If you tried to make a call to cancel() on this Request, an IllegalStateException would be thrown. If you really want to cancel an un-cancellable request (Request.isCancellable() returns true), you can force it to be cancelled with cancel(true).

Un-cancellable requests will be ignored by Bridge.cancelAll() and the other variants of that method, unless you pass true for the force parameter.


Validators

Validators allow you to provide consistent checking that certain conditions are true for a response.

Here's a quick example:

ResponseValidator validator = new ResponseValidator() {
    @Override
    public boolean validate(@NonNull Response response) throws Exception {
        JSONObject json = response.asJsonObject();
        return json.getBoolean("success");
    }

    @NonNull
    @Override
    public String id() {
        return "custom-validator";
    }
};
try {
    JSONObject response = Bridge.client()
        .get("http://www.someurl.com/api/test")
        .validators(validator)
        .asJsonObject();
} catch (BridgeException e) {
    if (e.reason() == BridgeException.REASON_RESPONSE_VALIDATOR_FALSE) {
        String validatorId = e.validatorId();
        // Validator returned false
    } else if (e.reason() == BridgeException.REASON_RESPONSE_VALIDATOR_ERROR) {
        String validatorId = e.validatorId();
        String errorMessage = e.getMessage();
        // Validator threw an error
    }
}

The validator is passed before the request returns. Basically, the validator will check if a boolean field in the response JSON called success is equal to true. If you had an API on a server that returned true or false for this value, you could automatically check if it's true for every request with a single validator.

You can even use multiple validators for a single request:

ResponseValidator validatorOne = // ...
ResponseValidator validatorTwo = // ...

try {
    JSONObject response = Bridge.client()
        .get("http://www.someurl.com/api/test")
        .validators(validatorOne, validatorTwo)
        .asJsonObject();
} catch (BridgeException e) {
    if (e.reason() == BridgeException.REASON_RESPONSE_VALIDATOR_FALSE) {
        String validatorId = e.validatorId();
        // Validator returned false
    } else if (e.reason() == BridgeException.REASON_RESPONSE_VALIDATOR_ERROR) {
        String validatorId = e.validatorId();
        String errorMessage = e.getMessage();
        // Validator threw an error
    }
}

Note: validators work with async requests too!

Note: you can apply validators to every request in your application with global configuration, which is discussed in the next section.


Global Configuration

Bridge allows you configure various functions globally.

Host

You can set a host that is used as the base URL for every request.

Bridge.client().config()
    .host("http://www.google.com");

With Google's homepage set as the host, the code below would request http://www.google.com/search?q=Hello:

Bridge.client()
    .get("/search?q=%s", "Hello")
    .asString();

Basically, the URL you pass with each request is appended to the end of the host. If you were to pass a full URL (beginning with HTTP) in get() above, it would skip using the host for just that request.

Default Headers

Default headers are headers that are automatically applied to every request. You don't have to do it yourself with every request in your app.

Bridge.client().config()
    .defaultHeader("User-Agent", "Bridge Sample Code")
    .defaultHeader("Content-Type", "application/json")
    .defaultHeader("Via", "My App");

Every request, regardless of the method, will include those headers. You can override them at the individual request level by setting the header as you normally would.

Timeouts

You can configure how long the library will wait until timing out, either for connections or reading:

Bridge.client().config()
    .connectTimeout(10000)
    .readTimeout(15000);

You can set timeouts at the request level too:

Bridge.client()
    .get("http://someurl.com/bigVideo.mp4")
    .connectTimeout(10000)
    .readTimeout(15000)
    .asFile(new File("/sdcard/Download/bigVideo.mp4"));

Buffer Size

The default buffer size is 1024 * 4 (4096). Basically, when you download a webpage or file, the buffer size is how big the byte array is with each pass. A large buffer size will create a larger byte array, which can affect memory usage, but it also increases the pace in which the content is downloaded.

The buffer size can easily be configured:

Bridge.client().config()
    .bufferSize(1024 * 10);

Just remember to be careful with how much memory you consume, and test on various devices.


You can set the buffer size at the request level too:

Bridge.client()
    .get("http://someurl.com/bigVideo.mp4")
    .bufferSize(1024 * 10)
    .asFile(new File("/sdcard/Download/bigVideo.mp4"));

Note: the buffer size is used in a few other places, such as pre-built Pipe's (Pipe#forUri, Pipe#forStream, etc.).

Logging

By default, logging is disabled. You can enable logging to see what the library is doing in your Logcat:

Bridge.client().config()
    .logging(true);

Validators

Validators for individual requests were shown above. You can apply validators to every request in your application:

Bridge.client().config()
    .validators(new ResponseValidator() {
        @Override
        public boolean validate(@NonNull Response response) throws Exception {
            JSONObject json = response.asJsonObject();
            return json.getBoolean("success");
        }

        @NonNull
        @Override
        public String id() {
            return "custom-validator";
        }
    });

Note: you can pass multiple validators into the validators() method just like the individual request version.


Cleanup

When you're done with Bridge (e.g. your app is terminating), you should call the cleanup method to avoid any memory leaks. Your app would be fine without this, but this is good practice and it helps speed up Java's garbage collection.

Bridge.cleanup();

Note: Calling this method will also cancel all active requests for you.

About

A simple but powerful HTTP networking library for Android. It features a Fluent chainable API, powered by Java/Android's URLConnection classes for maximum compatibility and speed.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Java 100.0%