Android Internet Access - mariamaged/Java-Android-Kotlin GitHub Wiki
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
-bothon-device
and from3rd party JARs
-that give you access to specificprotocols
:HTTP
,XMPP
,SMTP
, and so on.
- 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.
- 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.
-
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"/>
- 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
andlink
.
-
items is a JSON array of objects, where each JSON object represents a single question, with fields like
- 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.
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;
}
- 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.
- 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;
}
}
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);
}
}
-
onCreate():
- We mark that the fragment should be retained, so if the activity undergoes a configuration change, this fragment
will stick around
.
- We mark that the fragment should be retained, so if the activity undergoes a configuration change, this fragment
-
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.
-
onResume() and onPause():
- We register and unregister from the EventBus.
- Our
onQuestionsLoaded()
method will be called when theQuestionsLoadedEvent
is raised byLocalThread
. - 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 simpleListView row
showing the question title. - The ItemsAdapter uses
Html.fromHtml()
to populate the ListView rows, not because Stack OverFlow hands back titles withHTML tags
, but because StackOverFlow back titles withHTML entity references
, and Html.fromHtml() should handle many of those.
-
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
theContract
interface, so we can call theonQuestion()
method on whatever activity that happens to host that fragment.
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
, handingdirect events
raised by itswidgets
(a ListView). -
MainActivity is responsible for handling a events that
trascend
an individual fragment- in this case, itstarts a browser
to view the clicked-upon question.
-
QuestionsFragment is a
- Google has augmented HttpUrlConnection to do more stuff to help developers.
- It automatically uses
GZip
compression on requests, adding the appropriate HTTP header and automaticallydecompressing 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 anHttpResponseCache
implementation of thejava.net.ResponseCache
base class, that can be installed to offer transparent caching of your HTTP requests.
- It automatically uses
- By default, Android will crash your app with a
NetworkOnMainThreadException
if your try to performNetwork I/O
on the main application thread.
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 thejava.net
implementation while maintainingbackwards compatibility
, because its API ismore narrow
. - The
problems
previously experienced on Android with thejava.net
implementation have been largely beenfixed
. - 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
.
- The core Android team is better able to
- 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'sstock HttpClient API
:- Use it when compiling against
SDK 23+
. The library is already there in the target platform.
- Use it when compiling against
- If your objective is to
download some large file
, you may be better served by usingDownloadManager
, 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.
- 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).
- JTwitter bundles the org.json classes in its JAR, which will be superseded by
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?
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 implements its own Http client code, one that offers
many improvements
.
-
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
inAndroid 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
- 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
, andit's short
, use theAsynchronous API
, as it saves you from having tofuss with your own thread
. - If the response requires
significant post-retrieval work
, use your own background thread and use theSynchronous API
.
- If you can work with the
- 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 avariety of configuration options
, such as rules for caching and SSL.- In our case, we just use a
standard configuration
, usingOkHttp's defaults
.
- In our case, we just use a
- 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 vianewCall()
, 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()
orenqueue()
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 thepositive 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 themain application thread
.
-
Many time, when working with HTTP requests, our needs are fairly simple: just retrieve some
JSON
(or other structured data, such asXML
) 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
- This automatically pulls in compatible versions of Gson and OkHttp, courtesy of transitive dependencies.
- Since we are using Gson to parse our JSON, we can use the same
Item
andSOQuestions
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.
- The HTTP
- To do this, we need to create a Java interface with some
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 theHTTP operation
to perform, such as @GET or @POST.- The parameter to the annotation is
path for the request
and anyfixed 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:
- order for whether the results should be ascending (asc) or descending (desc)
-
sort to indicate how the questions should be sorted, such as
creation
to sort by time when the question was posted. - site to indicate what Stack Exchange site we are querying (e.g., stackoverflow).
- The parameter to the annotation is
- The method name can be whatever you want.
- If you have additional query parameters that vary dynamically, you use the
@Query
annotation onString 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.
@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.
- The
- Then, we tell the Retrofit instance to
create()
aninstance
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 aCall
, and we eitherexecute
orenqueue
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()
andonFailure()
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);
}
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:
- Caching those results in memory (or optionally on disk for HTTP requests).
- Displaying
placeholder images
while the real images are being loaded, and displaying error images if there was a problem in loading the image. - Transforming the image, such as
resizing
orcropping
it to fit a certain amount of space. - 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
- 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 isprofile_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 theJSON 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);
}
}
- 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 targetImageView
. - Specify that the image should be resized using
centerCrop()
rules, to- Center the image within the desired size (if it is smaller on one or both axes).
- Crop the image (if it is larger on one or both axes).
- Indicate that we want to put a certain
drawable resource
as theplaceholder()
. - State that we want to show a certain drawable resource in the
ImageView
in case of anerror()
when the image was being loaded. - The statement ends with a call to
into()
, indicating theImageView
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);
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);
}
}