Android Internet Access - mariamaged/Java-Android-Kotlin GitHub Wiki

Android - Internet Access

The expectation is that most, if not all, Android devices will have built-in Internet access.

  • This could be WiFi, cellular data services (EDGE, 3G, etc), or possibly something else entirely.
     
  • If you want, you can drop all the way down to using raw sockets.
  • Or, in between, you can leverage APIs-both on-device and from 3rd party JARs-that give you access to specific protocols: HTTP, XMPP, SMTP, and so on.

DIY HTTP

  • In many cases, your only viable option for accessing some Web service or other HTTP-based resource is to do the request yourself.
  • The Google-endorsed API for doing this nowadays in Android is to use the classic java.net classes for HTTP operation, centered around HTTPUrlConnection.

A Sample Usage of HttpUrlConnection

  • The app has a:
    • Single Activity.
    • Single ListFragment.
  • The app will:
    • Load the latest block of StackOverflow questions.
    • Tagged with android.
  • Using
    • The StackOverFlow public API.
  • Those questions will be shown as in the list, and tapping on a question will bring the Web page for that question in the user's default Web browser.

Asking Permission

  • To do anything with the internet (or a local network) from your app, you need to hold the INTERNET permission.

  • This includes cases where you use things like WebView - if your process needs network access, you need the INTERNET permisison.

    Manifest

<uses-permission android:name = "android.permission.INTERNET"/>

Creating Your Data Model

  • The StackOverFlow Web Service API returns JSON in response to various queries.
  • Hence, we need to create Java classes that mirror the JSON structure.
  • In particular, many of the examples will be using Google's GSON to populate these data models automatically based upon its parsing of the JSON that we receive from the Web service.
     
  • In our case, we are going to use a specific endpoint of the StackOverFlow API, referred to as /questions after the distinguishing portion of the path.
  • The results we get for issuing a GET request for the URL is a JSON structure.
{
   "items": [
{
"question_id": 17196927,
"creation_date": 1371660594,
"last_activity_date": 1371660594,
"score": 0,
"answer_count": 0,
"title": "ksoap2 failing when in 3G",
"tags": ["android", "ksoap2", "3g" ],
"view_count": 2, 
"owner": { 
"user_id": 773259, 
"display_name": "SparK", 
"reputation": 513, 
"user_type": "registered", 
"profile_image": "http://www.gravatar.com/avatar/ 511b37f7c313984e624dd76e8cb9faa6?d=identicon&r=PG", 
"link": "http://stackoverflow.com/users/773259/spark" 
	}, 
"link": "http://stackoverflow.com/questions/17196927/ksoap2-failing-when-in-3g", 
"is_answered": false 
} 
], 
"quota_remaining": 9991, 
"quota_max": 10000, 
"has_more": true 
}
  • We get back a JSON object, where our questions are found under the name of items.
    • items is a JSON array of objects, where each JSON object represents a single question, with fields like title and link.
  • The questions JSON object has an embedded owner JSON object with addition information.
     
  • The key is that, by default, the data members in our Java data model must exactly match the JSON keys for the JSON objects.

Items class

package com.commonsware.android.hurl;

public class Item {
	String title;
	String link;

@Overrride
public String toString() {
	return(title);
}
}
  • However, our Web service does not return the items array directly.
  • items is the key in a JSON object that is the actual JSON returned by StackOverFlow.
  • So we need Java class that contains the data members we need from that outer JSON object, here named SOQuestions.
package com.commonsware.android.hurl;

import java.util.List;

public class SOQuestions {
	List<Item> items;
}

A Thread for Loading

  • We need to do the network I/O on a background thread, so we do not tie up the main application thread.
  • To that end, the sample app has a LoadThread that loads our questions:
package com.commonsware.android.hurl;

import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import com.google.gson.Gson;
import org.greenrobot.eventbus.EventBus;

class LocalThread extends Thread {
	static final String SO_URL = "https://api.stackexchange.com/2.1/questions?"+"order=desc&sort=creation&site=stackoverflow&tagged=android";

@Override
public void run() {
	try {
	HttpURLConnection c = (HttpURLConnection) new URL(SO_URL).openConnection();
	try {
	InputStream in = c.getInputStream();
	BufferedReader reader = new BufferedReader(new InputStreamReader(in));
	SOQuestions questions = new Gson().fromJson(reader, SOQestions.class);	
	reader.close();

	EventBus.getDefault().post(new QuestionsLoadedEvent(questions));							
}
catch(IOException e) {
	Log.e(e.getClass().getSimpleName(), "Exception parsing JSON", e)
finally {
	c.disconnect();
}
}
catch (Exception e) { 
Log.e(getClass().getSimpleName(), "Exception parsing JSON", e); 
}
}
}
  • LocalThread:
    • Creates a HTTPURLConnection by creating a URL for our Stack Over Flow API endpoint and opening a connection.
    • Creates a BufferedReader wrapped around the InputStream for the HTPP connection.
    • Parses the JSON we get back from the HTTP request via a GSON instance, loading the data into an instance of our SOQuestions.
    • Close the BufferedReader (and the InputStream by extension).
    • Post a QuestionsLoadedEvent to greenbot's EventBus, to let somebody know that our questions exist.
    • Log messages into the Logcat in case of errors.

QuestionsLoadedEvent

  • It is a simple wrapper around an SOQuestions instance, serving as an event class form use with EventBus:
package com.commonsware.android.hurl;

public class QuestionsLoadedEvent {
	final SOQuestions questions;

	QuestionsLoadedEvent(SOQuestions questions) {
	this.questions = questions;
}
}

A Fragment for Questions

package com.mariamaged.android.internetaccess;  
  
import android.app.ListFragment;  
import android.os.Bundle;  
import android.text.Html;  
import android.view.View;  
import android.view.ViewGroup;  
import android.widget.ArrayAdapter;  
import android.widget.ListView;  
import android.widget.TextView;  
  
import org.greenrobot.eventbus.EventBus;  
import org.greenrobot.eventbus.Subscribe;  
import org.greenrobot.eventbus.ThreadMode;  
  
import java.util.List;  
  
public class QuestionsFragment extends ListFragment {  
    private boolean loadRequested = false;  
  
    @Override  
  public void onCreate(Bundle savedInstancesState) {  
        super.onCreate(savedInstancesState);  
  
        setRetainInstance(true);  
    }  
  
    @Override  
  public void onViewCreated(View view, Bundle savedInstanceState) {  
        super.onViewCreated(view, savedInstanceState);  
        if (!loadRequested) {  
            loadRequested = true;  
            new LocalThread().start();  
        }  
    }  
  
    @Override  
  public void onResume() {  
        super.onResume();  
        EventBus.getDefault().register(this);  
    }  
  
    @Override  
  public void onPause() {  
        EventBus.getDefault().unregister(this);  
        super.onPause();  
    }  
  
    @Override  
  public void onListItemClick(ListView l, View v, int position, long id) {  
        Item item = ((ItemsAdapter) getListAdapter()).getItem(position);  
        ((Contract) getActivity()).onQuestion(item);  
    }  
  
    @Subscribe(threadMode = ThreadMode.MAIN)  
    public void onQuestionsLoaded(QuestionsLoadedEvent event) {  
        setListAdapter(new ItemsAdapter(event.questions.items));  
    }  
  
    class ItemsAdapter extends ArrayAdapter<Item> {  
        ItemsAdapter(List<Item> items) {  
            super(getActivity(), android.R.layout.simple_list_item_1, items);  
        }  
  
        @Override  
  public View getView(int position, View convertView, ViewGroup parent) {  
            View row = super.getView(position, convertView, parent);  
            TextView title = row.findViewById(android.R.id.text1);  
            title.setText(Html.fromHtml(getItem(position).title));  
  
            return row;  
        }  
    }  
  
    public interface Contract {  
        void onQuestion(Item question);  
    }  
  
  
}
  1. onCreate():
    • We mark that the fragment should be retained, so if the activity undergoes a configuration change, this fragment will stick around.
  2. onViewCreated():
    • We fork the LocalThread thread.
    • Hence, once we have our questions, our retained fragment, will hold onto that model data for us.
    • To avoid duplicating the LocalThread, if a configuration change occurs sometime after our fragment was initially created, we track whether or not we have already requested our data load via a loadRequested flag.
  3. onResume() and onPause():
    • We register and unregister from the EventBus.
    • Our onQuestionsLoaded() method will be called when the QuestionsLoadedEvent is raised by LocalThread.
    • And there we hold into the loaded questions and populate the ListView.
    • We use an ItemAdapter, which knows how to render an Item as a simple ListView row showing the question title.
    • The ItemsAdapter uses Html.fromHtml() to populate the ListView rows, not because Stack OverFlow hands back titles with HTML tags, but because StackOverFlow back titles with HTML entity references, and Html.fromHtml() should handle many of those.
  4. onListItemClick():
    • We find the Items associated with the row that the user clicked on.
    • Then call an onQuestion() method on our hosting activity.
    • This activity needs to implement the Contract interface, so we can call the onQuestion() method on whatever activity that happens to host that fragment.

An Activity for Orchestration

package com.mariamaged.android.internetaccess5;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

import androidx.fragment.app.FragmentActivity;



public class MainActivity extends FragmentActivity implements QuestionsFragment.Contract {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        if(getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(android.R.id.content, new QuestionsFragment())
                    .commit();
        }
    }

    @Override
    public void onQuestion(Item question) {
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(question.link)));
    }
  • Hence, MainActivity is serving in an orchestration rule.
    • QuestionsFragment is a local controller, handing direct events raised by its widgets (a ListView).
    • MainActivity is responsible for handling a events that trascend an individual fragment- in this case, it starts a browser to view the clicked-upon question.

Result

enter image description here
  enter image description here

What Android Brings to the Table

  • Google has augmented HttpUrlConnection to do more stuff to help developers.
    • It automatically uses GZip compression on requests, adding the appropriate HTTP header and automatically decompressing any compressed responses.
    • It uses Sever Name Indication to help work with several HTTPS hosts sharing a single API address.
    • API level 13 (Android 4.0) added an HttpResponseCache implementation of the java.net.ResponseCache base class, that can be installed to offer transparent caching of your HTTP requests.

Testing with StrictMode

  • By default, Android will crash your app with a NetworkOnMainThreadException if your try to perform Network I/O on the main application thread.

What About HttpClient?

Many developers use this, as they prefer the richer API offered by this library over the somewhat clunky approach used by java.net.

This was the more stable option prior to Android 2.3.

  • There are few reasons why this is no longer recommended, for Android 2.3 and beyond:
    • The core Android team is better able to add capabilities to the java.net implementation while maintaining backwards compatibility, because its API is more narrow.
    • The problems previously experienced on Android with the java.net implementation have been largely been fixed.
    • The Apache HttpClient project continuously evolves its API.
      • This means that Android will continue to fall further and further behind the leatses-and-greatest from Apache.
      • As Android insists on maintaining the best possible backward compatibility.
      • And therefore cannot take on newer-but-different HttpClient versions.
    • Google officially deprecated this API in Android 5.1.
    • Google officially removed this API in Android 6.0.

  • If you have a legacy code that uses the HttpClient API, please consider using Apache's standalone edition of HttpClient for Android.
     
  • And, if you cannot do any of that, and you are using Gradle for your builds (e.g., you are using Android Studio's default settings), you can use useLibrary 'org.apache.http.legacy' to the android closure to give you access to Android's stock HttpClient API:
    • Use it when compiling against SDK 23+. The library is already there in the target platform.
        enter image description here
        enter image description here

HTTP via DownloadManager

  • If your objective is to download some large file, you may be better served by using DownloadManager, as it handles a lot of low-level complexities for you.
  • For example,
    • If you start a download on WiFi, and the user leaves the building.
    • And the device fails over to some mobile data, you need to reconnect to the server.
    • And either start the download again or use some content negotiation to pick up from where you left.
    • DownloadManager handles that.

Using Third-Party Libraries

  • To some extent, the best answer is to not write the code yourself, but rather use some existing library that handles both the:
    • Internet I/O.
    • And any required threading.
    • And data parsing.
       
  • This is commonplace when accessing public Web services - either because:
    • The firm behind the Web service has released a library.
    • Or because somebody in the community has released a library for that Web service.
       
  • Examples include:
    • Using JTwitter to access Twitter's API.
    • Using Amazon's library to access various AWS APIs, including S3, SimpleDB, and SQS.
    • Using the Dropbox SDK for accessing DropBox folder and files.

  • However, beyond the classical potential library problems, you may encounter another when it comes to using libraries for accessing Internet services: versioning.
    • JTwitter bundles the org.json classes in its JAR, which will be superseded by Android's own copy, and if the JTwitter version of the classes have a different API, JTwitter would crash.
    • Libraries dependent upon HttpClient might be dependent upon on a version with a different API (e.g., 4.1.1) than is in Android (4.0.2 beta).

SSL

HTTPS - SSL-encrypted HTTP operations.

  • Normally, SSL "just works" by using an https:// URL.
  • Hence, typically, there is little that you need to do to enable simple encryption.
  • In fact, on Android 9.0, by default, you have to use SSL - attempts to use plain HTTP will fail.
  • However, there are other aspects of SSL to consider, including:
    • What if the server is not using an SSL certificate that Android will honor, such as self-signed certificate?
    • What about man-in-the-middle attacks, hacked certificate authorities, and the like?

Using HTTP Client Libraries

Not surprisingly, there are a variety of third-party libraries designed to assist with this.

  • Some are designed to provide access to a specific API.
  • However, others are more general-purpose, designed to make writing HTTP operations a bit easier, by handling things like:
    • Retries (e.g., device failed over from WiFi to mobile data mid-transaction).
    • Threading (e.g., handling doing the Internet work on the background thread for you).
    • Data parsing and marshaling, for well-known formats (e.g., JSON).

OkHttp

OkHttp implements its own Http client code, one that offers many improvements.

  1. Support for SPDY:
    • A Google sponsored enhanced version of HTTP.
    • Going beyond classic HTTP "keep-alive" support to allow for many requests and responses to be delivered over the same socket connection.
    • This, in return, evolved into HTTP/2.
    • Many Google APIs are served by SPDY- or HTTP/2-capable servers, and HTTP/2 is gaining popularity overall.

Note that a version of OkHttp lies behind the standard implementation of HttpUrlConnection in Android 4.4 and higher - this is where Android SDPY support comes from.

Beyond OkHttp wraps up common HTTP performance-improvement patterns, such as:

  • Transparent GZip compression.
  • Response caching to avoid the network completely for repeated requests.
  • Connection pooling (if HTTP/2 is not available).

OkHttp supports Android 2.3 and above.

For Java, the minimum requirement is 1.7.


Dependency

enter image description here

  • OkHttp offer two flavors of HTTP API:
    • Synchronous.
    • Asynchronous.
  • Synchronous call:
    • The call blocks until the HTTP I/O is completed (or, at least, the headers are downloaded).
  • Asynchronous call:
    • That initial pulse of network I/O is handled on the background thread.
  • The general rule of thumb is:
    • If you can work with the raw HTTP response, and it's short, use the Asynchronous API, as it saves you from having to fuss with your own thread.
    • If the response requires significant post-retrieval work, use your own background thread and use the Synchronous API.
       
  • In our case, while we need to parse the JSON using Gson, the Web service response is fairly short, so we can get away with doing that parsing on the main application thread.
package com.mariamaged.android.internetaccess1;  
  
import android.os.Bundle;  
import android.text.Html;  
import android.util.Log;  
import android.view.View;  
import android.view.ViewGroup;  
import android.widget.ArrayAdapter;  
import android.widget.ListView;  
import android.widget.TextView;  
import android.widget.Toast;  
  
import androidx.annotation.NonNull;  
import androidx.annotation.Nullable;  
import androidx.fragment.app.ListFragment;  
  
import com.google.gson.Gson;  
import com.mariamaged.android.internetaccess.Item;  
import com.mariamaged.android.internetaccess.SOQuestions;  
import com.mariamaged.android.internetaccess.QuestionsFragment.Contract;  
  
import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.Reader;  
import java.util.List;  
  
import okhttp3.Call;  
import okhttp3.Callback;  
import okhttp3.OkHttpClient;  
import okhttp3.Request;  
import okhttp3.Response;  
  
public class QuestionsFragment extends ListFragment {  
  
    static final String SO_URL = "https://api.stackexchange.com/2.1/questions?" + "order=desc&sort=creation&site=stackoverflow&tagged=android";  
  
    @Override  
  public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setRetainInstance(true);  
    }  
  
    @Override  
  public void onViewCreated(@NonNull View v, @Nullable Bundle savedInstanceState) {  
        super.onViewCreated(v, savedInstanceState);  
  
        OkHttpClient client = new OkHttpClient();  
        Request request = new Request.Builder().url(SO_URL).build();  
  
        client.newCall(request).enqueue(new Callback() {  
            @Override  
  public void onFailure(Call call, final IOException e) {  
                if (getActivity() != null && !getActivity().isDestroyed()) {  
                    getActivity().runOnUiThread(  
                            () -> Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show());  
                }  
                Log.e(getClass().getSimpleName(), "Exception parsing JSON", e);  
            }  
  
            @Override  
  public void onResponse(Call call, Response response) throws IOException {  
                Reader in = response.body().charStream();  
                BufferedReader reader = new BufferedReader(in);  
  
                SOQuestions questions = new Gson().fromJson(reader, SOQuestions.class);  
  
                reader.close();  
  
                if (getActivity() != null && !getActivity().isDestroyed())  
                    getActivity().runOnUiThread(() -> setListAdapter(new ItemsAdapter(questions.items)));  
  
            }  
        });  
    }  
  
    class ItemsAdapter extends ArrayAdapter<Item> {  
        ItemsAdapter(List<Item> items) {  
            super(getActivity(), android.R.layout.simple_list_item_1, items);  
        }  
  
        @Override  
  public View getView(int position, View convertView, ViewGroup parent) {  
            View row = super.getView(position, convertView, parent);  
            TextView title = row.findViewById(android.R.id.text1);  
            title.setText(Html.fromHtml(getItem(position).title));  
  
            return row;  
        }  
    }  
  
    @Override  
  public void onListItemClick(ListView l, View v, int position, long id) {  
        Item item = ((ItemsAdapter)getListAdapter()).getItem(position);  
  
        ((Contract)getActivity()).onQuestion(item);  
    }  
}
  • We can create an HTTPClient object, which is our gateway to using OkHttp.
  • In this case, we create one per request.
    • That is not necessary- if we are going to make lots of requests, we could create the OkHttpClient request once.
  • You can also use an OkHttpClient.Builder to create OkHttpClient, with a variety of configuration options, such as rules for caching and SSL.
    • In our case, we just use a standard configuration, using OkHttp's defaults.
       
  • We then create a Request object that represents our desired HTTPS request.
  • By default, this will perform a HTTPS GET request, but we could configure the Request.Builder to use a different HTTP method, etc.
     
  • Then, we call a Call object, by asking the OkHttpClient to create a call via newCall(), passing in the Request that describes the call to make.
  • At this point, while the call is configured, it has not begun to do any network I/O.
  • That waits until we call execute() or enqueue() on the Call object.
    • execute() performs that call synchronously.
    • enqueue() performs its asynchronously.
       
  • enqueue() takes a Callback object, describing what to do when we get our result and what to do if there is some sort of (1) connectivity error or a (2) bad HTTP response.
    • onResponse() handles the positive result.
    • onResponse() is called on the background thread, so we cannot update the UI and do not even know if our activity is still alive.
    • So, if we are still attached to the activity and the activity is not destroyed, we use runOnUiThread() to update our ListView on the main application thread.

Retrofit

Many time, when working with HTTP requests, our needs are fairly simple: just retrieve some JSON (or other structured data, such as XML) from some Web service, or perhaps upload some JSON to that Web service.

  • Retrofit accomplishes this through the cunning use of:
    • Annotations.
    • Reflection.
    • OkHttp.

Dependencies

enter image description here

  • This automatically pulls in compatible versions of Gson and OkHttp, courtesy of transitive dependencies.

Create the Interface

  • Since we are using Gson to parse our JSON, we can use the same Item and SOQuestions classes as before.
  • However, we need to tell Retrofit more about where our JSON is coming from.
    • To do this, we need to create a Java interface with some specific Retrofit-supplied annotations, documenting:
      • The HTTP operations that we wish to perform.
      • The path (and, if needed, query parameters) to apply an HTTP operation to
      • The per-request data to configure the HTTP operation, such as the (1) dynamic portions of the path for a REST-style API, or (2) additional query parameters to attach to the URL.
      • What objects should be used for pouring the HTTP response into.
package com.mariamaged.android.internetaccess4;  
  
import com.mariamaged.android.internetaccess.SOQuestions;  
  
import retrofit2.Call;  
import retrofit2.http.GET;  
import retrofit2.http.Query;  
  
  
public interface StackOverflowInterface {  
    @GET("/2.1/questions?order=desc&sort=creation&site=stackoverflow")  
    Call<SOQuestions> questions(@Query("tagged") String tags);  
}
  • Each method in the interface should have an annotation identifying the HTTP operation to perform, such as @GET or @POST.
    • The parameter to the annotation is path for the request and any fixed query parameters.
    • In our case, we are using the path documented by Stack Exchange for retrieving questions (/2.1/questions).
    • Plus, some fixed query parameters:
      1. order for whether the results should be ascending (asc) or descending (desc)
      2. sort to indicate how the questions should be sorted, such as creation to sort by time when the question was posted.
      3. site to indicate what Stack Exchange site we are querying (e.g., stackoverflow).
  • The method name can be whatever you want.
  • If you have additional query parameters that vary dynamically, you use the @Query annotation on String parameters to have them added to the end of the URL.
  • The return type is Call.
    • This works akin to the Call from OkHttp, in that it represents an HTTP call to be made.
  • Curiously, we will never create an implementation of the StackOverflowInterface ourselves.
    • Instead, Retrofit generates one for us, with code that implements our requested behaviours.

Making the Request

@Override  
public void onViewCreated(View v, Bundle savedInstanceState) {  
    super.onViewCreated(v, savedInstanceState);  
  
    Retrofit retrofit = new Retrofit.Builder()  
            .baseUrl("https://api.stackexchange.com")  
            .addConverterFactory(GsonConverterFactory.create())  
            .build();  
  
    StackOverflowInterface so  = retrofit.create(StackOverflowInterface.class);  
    so.questions("android").enqueue(this);  
}
  • We create a Retrofit instance when using Retrofit, by means of a Retrofit.Builder.
  • Here, we prove two bits of configuration:
    • The base URL to use to complete the URLs defined in StackOverflowInterface, and
    • The converter to use to convert our Web service responses into our desired model objects.
       
  • Then, we tell the Retrofit instance to create() an instance of StackOverflowInterface.

Under the covers, Retrofit generates a class that implements StackOverflowInterface, creates an instance of that class and that returns that instance to us.

  • Since that object implements StackOverflowInterface, we can call our questions() method to ask for the Android questions.
    • questions() returns a Call, and we either execute or enqueue the work, just as we would with OkHttp.
    • In this case, the fragment itself implements that interface:
public class QuestionsFragment extends ListFragment implements 
Callback<SOQuestions> {
}
  • And, as with OkHttp, we need to implement onResponse() and onFailure() methods.
  • However, this time, we are called on the main application thread, not a background thread, and so we do not have to fuss with arranging for our UI updates to be done on the main application thread.
  • Plus, onResponse() gets the parsed results directly - we do not need to invoke Gson for that:
@Override  
public void onResponse(Call<SOQuestions> call, Response<SOQuestions> response) {  
  
    com.mariamaged.android.internetaccess.QuestionsFragment qf = new com.mariamaged.android.internetaccess.QuestionsFragment();  
    setListAdapter(qf.new ItemsAdapter(response.body().items));  
}  
  
@Override  
public void onFailure(Call<SOQuestions> call, Throwable t) {  
    Toast.makeText(getActivity(), t.getMessage(), Toast.LENGTH_LONG).show();  
    Log.e(getClass().getSimpleName(), "Exception from Retrofit request to StackOverflow", t);  
}

Picasso

Sometimes, what you want to download is not JSON, or XML, or any sort of structured data.

Sometimes, it is an image.

  • Picasso is a library from Square that is designed to help with asynchronously loading images, whether those images come from:
    • HTTP requests.
    • Local files.
    • A ContentProvider.
  • In addition to doing the loading synchronously, Picasso simplifies many operations on those images, such as:
    1. Caching those results in memory (or optionally on disk for HTTP requests).
    2. Displaying placeholder images while the real images are being loaded, and displaying error images if there was a problem in loading the image.
    3. Transforming the image, such as resizing or cropping it to fit a certain amount of space.
    4. Loading the images directly into an ImageView of your choice.
      • Even handling where that ImageView is recycled.
      • (e.g., part of a row in a ListView, where the user scrolled while an image for that ImageView was still loading, and now another image is destined for that same ImageView when the row was recycled).

Dependencies

enter image description here

Updating the Model

  • Our original data did not include information about the owner.
  • Hence, we need to augment our data model, so Retrofit pulls that information out of the StackOverflow JSON and makes it available to us.
  • To that end, we now have an Owner class, holding onto the one piece of information we need about the owner: the URL to the avatar (a.k.a, "profile image").
package com.mariamaged.android.internetaccess5;  
  
import com.google.gson.annotations.SerializedName;  
  
public class Owner {  
    @SerializedName("profile_image") String profileImage;  
}
  • The JSON key for the Stack Overflow API is profile_image.
    • Underscores are not the conventional way of separating words in Java data member.
    • Java samples usually use the "camelCase".
  • The default behviour of retrofit would require us to name our data member profile_image to match the JSON.
     
  • However, under the covers, retrofit is using Google's Gson to do the mapping from JSON to objects.
  • Gson supports a @SerializedName annotation, to indicate the JSON key to use for this data member.
package com.mariamaged.android.internetaccess5;  
  
public class Item {  
    public String title;  
    public String link;  
    public Owner owner;  
  
    public String toString() {  
        return(title);  
    }  
}

Requesting the Images

  • Using Picasso is extremely simple, as it offers a fluent interface that allows us to set up a request in a single Java statement.
     
  • The statement begins with a call to the static with() method on the Picasso class, where we apply a Context such as our activity for Picasso to use.
  • In between those calls, we can other calls, as with() and most other methods on a Picasso object return the Picasso object itself.
     
  • Indicate that we want to load() an image found at a certain URL, identified by the profileImage data member of the owner class.
  • Say that we want to fit() the image to our target ImageView.
  • Specify that the image should be resized using centerCrop() rules, to
    1. Center the image within the desired size (if it is smaller on one or both axes).
    2. Crop the image (if it is larger on one or both axes).
  • Indicate that we want to put a certain drawable resource as the placeholder() .
  • State that we want to show a certain drawable resource in the ImageView in case of an error() when the image was being loaded.
  • The statement ends with a call to into(), indicating the ImageView into which Picasso should load an image.
Picasso.with(getActivity())
.load(item.owner.profileImage)
.fit()
.centerCrop() 
.placeholder(R.drawable.owner_placeholder) 
.error(R.drawable.owner_error)
.into(icon);

The Rest of the Story

public class ItemsAdapter extends ArrayAdapter<Item> {  
    public ItemsAdapter(List<Item> items) {  
        super(getActivity(), R.layout.row, R.id.title, items);  
    }  
  
    @Override  
  public View getView(int position, View convertView, ViewGroup parent) {  
        View row = super.getView(position, convertView, parent);  
        Item item = getItem(position);  
  
        ImageView icon = row.findViewById(R.id.icon);  
        Picasso.with(getActivity())  
                .load(item.owner.profileImage)  
                .fit()  
                .centerCrop()  
                .placeholder(R.drawable.owner_placeholder)  
                .error(R.drawable.owner_error)  
                .into(icon);  
  
        TextView title = row.findViewById(R.id.title);  
        title.setText(Html.fromHtml(item.title));  
  
        return (row);  
    }  
}

Result

enter image description here

⚠️ **GitHub.com Fallback** ⚠️