参考书籍:Android第一行代码(第二版).郭霖著
在手机端使用HTTP协议和服务器端进行网络交互,并对服务器端返回的数据进行解析。这是Android最常使用的网络技术。
1、WebView
借助WebView控件,可在应用程序中嵌入一个浏览器。
新建一个WebViewTest项目,修改布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
修改主程序:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled(true);//getSettiongs()用于设置一些浏览器属性,这里让WebView支持JavaScript脚本
webView.setWebViewClient(new WebViewClient());//当需要从一个网页跳转到另一个网页是,希望目标网页仍然在当前WebView显示,而不是打开浏览器
webView.loadUrl("http://www.baidu.com");
}
}
还需声明访问网络的权限:
<uses-permission android:name="android.permission.INTERNET"/>
在开始运行之前,需保证模拟器(电脑正常上网)或手机联网。
点击链接还可浏览更多网页。
2、使用HTTP协议访问网络
原理:客户端向服务器发出一条HTTP请求,服务器接收到请求后会返回一些数据给客户端,客户端再对这些数据进行解析和处理就行了。
WebView已经在后台处理好了发送HTTP请求、接收服务响应、解析返回数据及页面展示这几步工作,封装得很好。需手动发送HTTP请求的方式进行深入理解。
(1)使用HttpURLConnection
Android6.0之前发送HTTP请求一般有两种方式:HttpURLConnection和HttpClient(API数量过多、扩展困难等缺点)。Android6.0中HttpClient功能被完全移除。
HttpURLConnection用法:
a、获取HttpURLConnection实例。new一个URL对象,传入目标网络地址,调用openConnection()方法:
URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
b、设置HTTP请求所使用的方法。常用两种:GET(希望从服务器获取数据)和POST(希望提交数据给服务器)。写法:
connection.setRequestMethod("GET");
c、进行自由定制,如设置连接超时、读取超市的毫秒数,及服务器希望得到的一些消息头等。如:
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
d、调用getInputStream()获取服务器返回的输入流,读取输入流:
InputStream in = connection.getInputStream();
e、调用disconnect()方法关闭HTTP连接。如:
connection.disconnect();
例,新建一个NetworkTest项目,修改布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/send_request"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/response_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ScrollView>
LinearLayout>
修改主程序:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
TextView responseText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendRequest = (Button) findViewById(R.id.send_request);
responseText = (TextView) findViewById(R.id.response_text);
sendRequest.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.send_request){
sendRequestWithHttpURLConnection();
}
}
private void sendRequestWithHttpURLConnection(){
//开启线程来发起网络请求
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL("https://hao.360.cn/");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in = connection.getInputStream();
//对获取到的输入流进行读取
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null){
response.append(line);
}
showResponse(response.toString());
} catch (Exception e) {
e.printStackTrace();
}finally {
if (reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null){
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response){
//Android不允许在子线程中进行UI操作,需通过此方法将线程切换到主线程,再更新UI元素
runOnUiThread(new Runnable() {
@Override
public void run() {
//在这里进行UI操作,将结果显示到界面上
responseText.setText(response);
}
});
}
}
注:如网址不正确,会出现NetworkSecurityConfig: No Network Security Config specified, using platform default的提示。
服务器返回的就是这种HTML代码,只是通常浏览器会将代码解析成漂亮的网页后再展示。
如果想提交数据给服务器,只需将HTTP请求方法改成POST,并在获取输入流之前把要提交的数据写出即可。每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开,如提交用户名和密码:
connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
(2)使用OkHttp
网络通信库(Square公司开发),OkHttp项目主页地址:https://github.com/square/okhttp
使用之前,需添加OkHttp库依赖,打开app/buid.gradle,在dependencies闭包中添加如下内容:
compile 'com.squareup.okhttp3:okhttp:3.8.1'
添加此依赖,会自动下载两个库:OkHttp库、Okio库(是前者的通信基础)。
用法:
a、创建OkHttpClient实例,如下:
OkHttpClient client = new OkHttpClient();
b、如想发起一条HTTP请求,需创建Request对象:
Request request = new Request.Builder().url("http://www.baidu.com").build();//build()方法之前可连缀很多其他方法丰富此Request对象
c、调用OkHttpCilent的newCall()方法创建一个Call对象,并调用它的execute()方法发送请求并获取服务器返回的数据:
Response response = client.newCall(request).execute();//Response对象就是服务器返回的数据
String responseData = response.body().string();//得到返回的具体内容
d、如果发起一条POST请求,需先构建RequestBody对象存放待提交的参数:
RequestBody requestBody = new FormBody.Builder().add("username", "admin").add("password", "123456").build();
然后在Request.Builder中调用post()方法,并传入RequestBody对象:
Request request = new Request.Builder().url("http://www.baidu.com").post(requestBody).build();
接下来调用execute()方法发送请求并获取服务器返回的数据即可。
修改MainActivity:
...
public void onClick(View v) {
if (v.getId() == R.id.send_request){
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("https://hao.360.cn/").build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
showResponse(responseData);//将服务器返回的数据显示到界面上
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
与HttpURLConnection实现的功能一样。
3、解析XML格式数据
通常,每个需要访问网络的应用程序都会有一个自己的服务器。在网络上传输数据最常用的格式有两种:XML和JSON。
解析XML格式的数据:
首先需有XML格式数据。搭建最简单的Web服务器(很多供选择,这里使用Apache服务器,官方下载地址:http://httpd.apache.org/download.cgi)
安装步骤详见:http://blog.csdn.net/lanjingling09/article/details/77034964
进入…\Apache\htdocs目录下,新建名为get_data.xml的文件,编辑此文件:
<apps>
<app>
<id>1id>
<name>Google Mapsname>
<version>1.0version>
app>
<app>
<id>2id>
<name>Chromename>
<version>2.0version>
app>
<app>
<id>3id>
<name>Google Playname>
<version>2.3version>
app>
apps>
在浏览器中访问http://127.0.0.1/get_data.xml:
(1)Pull解析方式
解析XML格式数据有很多方式,Pull和SAX解析是常用的两种。
修改MainActivity:
...
public void onClick(View v) {
if (v.getId() == R.id.send_request){
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
**Request request = new Request.Builder().url("http://10.0.2.2/get_data.xml").build();//http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseXMLWithPull(responseData);//解析服务器返回的数据**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void **parseXMLWithPull**(String xmlData){
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
int eventType = xmlPullParser.getEventType();//得到当前解析事件
String id = "";
String name = "";
String version = "";
while (eventType != XmlPullParser.END_DOCUMENT){
//解析事件不为XmlPullParser.END_DOCUMENT,说明解析工作没完成
String nodeName = xmlPullParser.getName();//获取当前结点名字
switch (eventType){
//开始解析某个节点
case XmlPullParser.START_TAG:
if ("id".equals(nodeName)){
id = xmlPullParser.nextText();//获取节点具体内容
}else if ("name".equals(nodeName)){
name = xmlPullParser.nextText();
}else if ("version".equals(nodeName)){
version = xmlPullParser.nextText();
}
break;
//完成解析某个节点
case XmlPullParser.END_TAG:
if ("app".equals(nodeName)){
//解析完一个app节点后打印获取到的内容
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
break;
default:
break;
}
eventType = xmlPullParser.next();//获取下一个解析事件
}
} catch (Exception e) {
e.printStackTrace();
}
}
...
成功解析。
(2)SAX解析方式
比Pull解析复杂一些,但予以更清楚。
通常会新建一个类继承子DefaultHandler,并重写父类5个方法。
新建ContentHandler类继承自DefaultHandler(org.xml.sax.helpers):
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
//在开始XML解析时调用
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
//开始解析某个节点时调用
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
//记录当前节点名
nodeName = localName;
}
//在获取节点内容时调用,会被调用多次
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//根据当前的节点名判断将内容添加到哪一个StringBuilder对象中
if ("id".equals(nodeName)){
id.append(ch, start, length);
}else if ("name".equals(nodeName)){
name.append(ch, start, length);
}else if ("version".equals(nodeName)){
version.append(ch, start, length);
}
}
//完成解析某个节点时调用
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("app".equals(localName)){
Log.d("ContentHandler", "id is " + id.toString().trim());//id\name\version中可能包含回车或换行符,需调用trim()方法除去
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
//最后要将StringBuilder清空,避免影响下一次内容读取
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
//完成整个XML解析时调用
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
修改MainActivity:
...
private void sendRequestWithOkHttp(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://10.0.2.2/get_data.xml").build();//http://10.0.2.2/对于模拟器来说是电脑本机IP地址
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseXMLWithSAX(responseData);//解析服务器返回的数据**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void parseXMLWithSAX(String xmlData){
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
//将ContentHandler的实例设置到XMLReader中
xmlReader.setContentHandler(handler);
//开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch (Exception e) {
e.printStackTrace();
}
}
...
解析结果一样。还有DOM解析方式可用。
4、解析JSON数据
JSON的体积比XML更小,网络传输更省流量,但语义性差不如XML直观。
在…\Apache\htdocs目录中新建一个get_data.json文件:
[{"id":"5","name":"Clash of Clans","version":"5.5"},
{"id":"6","name":"Boom Beach","version":"7.0"},
{"id":"7","name":"Clash Royale","version":"3.5"}]
(1)使用JSONObject
解析JSON数据也有很多方法,可使用官方的JSONObject,谷歌的开源库GSON,或第三方的开源库如Jackson、FastJSON等.
使用JSONObject,修改MainActivity:
...
private void sendRequestWithOkHttp(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
**Request request = new Request.Builder().url("http://10.0.2.2/get_data.json").build();//http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseJSONWithJSONObject(responseData);**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void parseJSONWithJSONObject(String jsonData){
try {
JSONArray jsonArray = new JSONArray(jsonData);//服务器中定义的时JSON数组
for (int i = 0; i < jsonArray.length(); i++){
JSONObject jsonObject = jsonArray.getJSONObject(i);
String id = jsonObject.getString("id");
String name = jsonObject.getString("name");
String version = jsonObject.getString("version");
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
} catch (Exception e) {
e.printStackTrace();
}
}
...
(2)使用GSON
更简单。由于其没被添加到Android官方API中,所以要使用它必须在项目中添加GSON库依赖:
compile 'com.google.code.gson:gson:2.7'
它主要可以将一段JSON格式的字符串自动映射成一个对象(定义一个类对应),不需手动编写代码解析。
如果需要解析的是一段JSON数组,需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中,如:
List people = gson.fromJson(jsonData, new TypeToken<List>(){}.getType());
例,首先新建一个App类:
public class App {
private String id;
private String name;
private String version;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}
修改MainActivity:
...
private void sendRequestWithOkHttp(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://10.0.2.2/get_data.json").build();//http://10.0.2.2/对于模拟器来说是电脑本机IP地址
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parsJSONWithGSON(responseData);**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void parsJSONWithGSON(String jsonData){
Gson gson = new Gson();
List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>(){}.getType());
for (App app : appList){
Log.d("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
...
运行程序,效果一样。
5、网络编程最佳实践
应用程序很可能会在许多地方都使用网络功能,而发送HTTP请求的代码基本相同,所以通常应该把通用的网络操作提取到一个公共类例,并提供一个静态方法。考虑到这种耗时的操作需开启子线程完成且能实时接收到返回的数据,这里使用Java的回调机制来完成数据的获取:
定义一个HttpCallbackListener接口:
public interface HttpCallbackListener {
void onFinish(String response);//当服务器成功响应请求时调用,参数为服务器返回的数据
void onError(Exception e);//当进行网络操作出现错误时调用,参数记录错误的详细信息
}
接着新建一个类(通用网络操作的公共类):
public class HttpUtil {
public static void sendHttpRequest( final String address, final HttpCallbackListener listener){
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
//对获取到的输入流进行读取
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null){
response.append(line);
}
if (listener != null){
//回调onFinish方法
listener.onFinish(response.toString());
}
} catch (Exception e) {
if (listener != null){
//回调onError方法
listener.onError(e);
}
}finally {
if (connection != null){
connection.disconnect();
}
}
}
}).start();
}
}
此类中有一个sendHttpRequest()方法,有两个参数(第二个参数即为定义的HttpCallbackListener接口参数)。
在调用时还需传入HttpCallbackListener实例:
HttpUtil.sendHttpRequest(address, new HtpCallbackListener(){
@Override
public void onFinish(String response){
//在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e){
//在这里对异常情况进行处理
}
});
利用回调机制将响应数据成功返回给调用方。
如果使用OkHttp,在HttpUtil中加入一个sendOkHttpRequest()方法:
public class HttpUtil {
...
public static void sendOkHttpRequest(String address, okhttp3.Callback callback){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(address).build();
client.newCall(request).enqueue(callback);//enqueue方法内部已经开好了子线程
}
}
调用sendOkHttpRequest()方法:
HttpUtil.sendOkHttpRequest("https://www.baidu.com", new okhttp3.Callback(){
@Override
public void onResponse(Call call, Response response) throws IOException{
//得到服务器返回的具体数据
String reponseData = reponse.body().string();
}
@Override
public void onFailure(Call call, IOException e){
//在这里对异常情况进行处理
}
});
不管使用HttpURLConnection还是OkHttp,最终的回调接口都还在子线程中运行,因此不可以在这里执行任何的UI操作,除非借助runOnUiThread()方法进行线程转换。