1 webview用法
class MainActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AndroidView(factory = { context ->
WebView(context).apply {
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
try {
if (url!!.startsWith("baiduboxapp://")) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
return true
}
} catch (e: Exception) {
return false
}
view?.loadUrl(url!!)
return true
}
}
settings.javaScriptEnabled = true
loadUrl("https://www.baidu.com/")
}
})
}
}
}
}
}
Compose没有WebView控件,使用传统的WebView控件,创建一个WebViewClient对象,用于展示百度首页。loadUrl函数加载百度首页数据。javaScriptEnabled用于加载JavaScript样式
由于baidu有自定义scheme,所以这里做了特殊处理
2 使用http访问网络
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
color = MaterialTheme.colorScheme.background
) {
ShowHttp()
}
}
}
}
}
@Composable
fun ShowHttp() {
var response by remember {
mutableStateOf("")
}
LazyColumn {
item {
Button(
onClick = {
thread {
var conn: HttpURLConnection? = null
try {
val res = StringBuilder()
val url = URL("https://www.baidu.com")
conn = url.openConnection() as HttpURLConnection
conn.connectTimeout = 8000
conn.readTimeout = 8000
val input = conn.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
res.append(it)
}
}
response = res.toString()
Log.i("TAG", "response = $response ")
} catch (e: Exception) {
e.printStackTrace()
} finally {
conn?.disconnect()
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "request")
}
}
item {
Button(
onClick = {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val res = client.newCall(request).execute()
val responseData = res.body?.string()
if (responseData != null) {
response = responseData
}
} catch (e: Exception) {
e.printStackTrace()
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "request from okhttp")
}
}
item {
Text(text = response)
}
}
}
这里创建两个按钮,一个数据展示的空间。两个按钮是两种使用http访问网络的方式,第一种是Java自带的HttpURLConnection相关的API,第二种是使用okhttp这个开源框架。
下面是访问baidu之后的打印界面
3 解析xml数据
网络上的数据经常使用xml或json进行传输,需要学习怎么对xml和json类型数据进行解析
这个使用pull和sax方式解析xml
class MainActivity : ComponentActivity() {
private final val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
XmlPage { sendRequestForXml() }
}
}
}
}
private fun sendRequestForXml() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://192.168.12.148:12345/get_data_xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseXmlDataWithPull(responseData)
}
if (responseData != null) {
parseXmlWithSax(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun parseXmlDataWithPull(responseData: String) {
try {
val factory = XmlPullParserFactory.newInstance()
val xmlPullparser = factory.newPullParser()
xmlPullparser.setInput(StringReader(responseData))
var eventType = xmlPullparser.eventType
var id = ""
var name = ""
var version = ""
while (eventType != XmlPullParser.END_DOCUMENT) {
val nodeName = xmlPullparser.name
when (eventType) {
XmlPullParser.START_TAG -> {
when (nodeName) {
"id" -> id = xmlPullparser.nextText()
"name" -> name = xmlPullparser.nextText()
"version" -> version = xmlPullparser.nextText()
}
}
XmlPullParser.END_TAG -> {
if ("app" == nodeName) {
Log.d(TAG, "id = $id, name = $name, version = $version")
}
}
}
eventType = xmlPullparser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun parseXmlWithSax(responseData: String) {
try {
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader
val handler = ContentHandler()
xmlReader.contentHandler = handler
xmlReader.parse(InputSource(StringReader(responseData)))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@Composable
fun XmlPage(
sendRequest: () -> Unit
) {
LazyColumn {
item {
Button(
onClick = {
sendRequest()
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "GetXml")
}
}
}
}
SAX方式解析需要继承DefaultHandler并重写其中的方法
class ContentHandler : DefaultHandler() {
private var nodeName = ""
private lateinit var id: StringBuilder
private lateinit var name: StringBuilder
private lateinit var version: StringBuilder
override fun startDocument() {
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
override fun startElement(
uri: String,
localName: String,
qName: String,
attributes: Attributes
) {
nodeName = localName
}
override fun characters(ch: CharArray, start: Int, length: Int) {
when (nodeName) {
"id" -> id.appendRange(ch, start, length)
"name" -> name.appendRange(ch, start, length)
"version" -> version.appendRange(ch, start, length)
}
Log.d("ContentHandler", "id = $id")
Log.d("ContentHandler", "name = $name")
Log.d("ContentHandler", "version = $version")
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
if ("app" == localName) {
Log.d("endElement", "id = $id")
Log.d("endElement", "name = $name")
Log.d("endElement", "version = $version")
Log.d("ContentHandler", "id is $id, name is $name, version is $version")
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
override fun endDocument() {
}
}
点击之后会打印
id = 1, name = Google Maps, version = 1.0
id = 2, name = Chrome, version = 2.1
id = 3, name = Google Play, version = 3.2
4 解析Json数据
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sendRequestForJson()
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
}
}
}
}
private fun sendRequestForJson() {
thread {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://192.168.12.148:12345/get_data_json")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseJsonWithJsonObject(responseData)
parseJsonWithGson(responseData)
}
}
}
private fun parseJsonWithJsonObject(responseData: String) {
try {
val jsonArray = JSONArray(responseData)
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val id = jsonObject.getString("id")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d(
"parseJsonWithJsonObject",
"id = ${id.trim()}, name = ${name.trim()}, version = ${version.trim()}"
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun parseJsonWithGson(responseData: String) {
val gson = Gson()
val typeOf = object : TypeToken<List<App>>() {}.type
val appList = gson.fromJson<List<App>>(responseData, typeOf)
for (app in appList) {
Log.d(
"parseJsonWithGson", "id = ${app.id.trim()}, name = ${app.name.trim()} " +
", version = ${app.version.trim()}"
)
}
}
}
class App(val id: String, val name: String, val version: String)
使用JSONArray和Gson解析,Gson可以直接解析成对象。
打印如下
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
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
5 使用回调
由于网络请求是耗时的操作,在子线程中操作,无法准确知道结果什么时候返回,所以可以通过回调的方式来返回结果。
HttpCallbackListener
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e: Exception)
}
HttpUtils
object HttpUtils {
private const val TAG = "HttpUtils"
fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
thread {
var connect: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connect = url.openConnection() as HttpURLConnection
connect.connectTimeout = 8000
connect.readTimeout = 8000
val inputStream = connect.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
reader.use {
reader.forEachLine {
response.append(it)
}
}
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
listener.onError(e)
} finally {
connect?.disconnect()
}
}
}
fun sendHttpRequest(address: String, callback: okhttp3.Callback) {
thread {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
}
fun parseXmlWithPull(response: String): String {
try {
val factory = XmlPullParserFactory.newInstance()
val parser = factory.newPullParser()
parser.setInput(StringReader(response))
var eventType = parser.eventType
val responseData = StringBuilder()
var id = ""
var name = ""
var version = ""
while (eventType != XmlPullParser.END_DOCUMENT) {
val nodeName = parser.name
when (eventType) {
XmlPullParser.START_TAG -> {
when (nodeName) {
"id" -> id = nodeName
"name" -> name = nodeName
"version" -> version = nodeName
}
}
XmlPullParser.END_TAG -> {
if ("app" == nodeName) {
val text = "id = ${id.trim()}, name = ${name.trim()}," +
" version = ${version.trim()}\n"
Log.d(TAG, text)
responseData.append(text)
}
}
}
eventType = parser.next()
}
return responseData.toString()
} catch (e: Exception) {
e.printStackTrace()
return ""
}
}
val localIpv4Address: String?
get() {
val en = NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val netInterface = en.nextElement()
val enIpAddress = netInterface.inetAddresses
while (enIpAddress.hasMoreElements()) {
val inetAddress = enIpAddress.nextElement()
if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
return inetAddress.hostAddress!!.toString()
}
}
}
return null
}
}
MainActivity
const val TAG = "MainActivity"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RequestPage()
}
}
}
}
}
@Composable
fun RequestPage() {
var result by remember {
mutableStateOf("")
}
LazyColumn {
result = ""
item {
Button(
onClick = {
val listener = object : HttpCallbackListener {
override fun onFinish(response: String) {
result = "HttpURLConnection data: \n"
result += HttpUtils.parseXmlWithPull(response)
}
override fun onError(e: Exception) {
Log.d(TAG, "onError: ")
result = "HttpURLConnection request failed"
}
}
val ip = HttpUtils.localIpv4Address
val url = "http://$ip:12345/get_data_xml"
HttpUtils.sendHttpRequest(url, listener)
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "request for xml with HttpURLConnection")
}
}
item {
Button(
onClick = {
result = ""
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "onFailure: ")
result = "okhttp request failed"
}
override fun onResponse(call: Call, response: Response) {
result = "okhttp data: \n"
result += HttpUtils.parseXmlWithPull(response.body?.string().toString())
}
}
val ip = HttpUtils.localIpv4Address
val url = "http://$ip:12345/get_data_xml"
HttpUtils.sendHttpRequest(url, callback)
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "request for xml with okhttp")
}
}
item {
Text(text = result)
}
}
}
这里展示了HttpURLConnection和okhttp使用回调的方式,HttpURLConnection需要自己创建回调接口,okhttp则有自带的callback接口。
6 Retrofit
6.1 Retrofit使用
Retrofit基于以下几点设计:
- 同一款应用程序中所发起的网络请求绝大多数指向的是同一个服务器域名
- 服务器提供的接口通常是可以根据功能来归类的
- 开发者肯定更加习惯于“调用一个接口,获取它的返回值”这样的编码方式
class App(val id: String, val name: String, val version: String)
创建一个App类用于存储数据,Retrofit可以通过Gson直接将xml数据解析成对象进行存储
interface AppService {
@GET("get_data_json")
fun getAppData(): Call<List<App>>
}
创建一个AppService 接口,定义一个函数做网络请求的入口,使用GET注解表示一个是get类型的请求,由于Retrofit可以设置baseurl,所以这里只需要设置相对的资源路径
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NetWorkDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
GetXmlData()
}
}
}
}
}
@Composable
fun GetXmlData() {
var xmlData by remember {
mutableStateOf("")
}
LazyColumn {
item {
Button(
onClick = {
val ip = HttpUtils.localIpv4Address
val retrofit = Retrofit.Builder()
.baseUrl("http://$ip:12345/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback<List<App>> {
override fun onResponse(
call: Call<List<App>>,
response: Response<List<App>>
) {
val result = StringBuilder()
val list = response.body()
if (list != null) {
for (app in list) {
result.append(Gson().toJson(app).toString() + "\n")
}
}
xmlData = result.toString()
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
xmlData = "request failed"
}
})
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "get xml data")
}
}
item {
Text(text = xmlData)
}
}
}
首先创建Retrofit对象,设置baseurl,并设置Gson作为转换工具。
然后创建AppService子类对象,调用getAppData方法,并调用enqueue开始发起网络请求。后面传入一个回调作为参数,请求的response返回后直接触发回调
请求后效果如下:
6.2 其他请求方式
如果需要复杂参数传递,可以参考
// GET http://example.com/<page>/get_data.json
interface ExampleService {
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call<Data>
}
如果存在页面参数,可以通过传入一个int值并使用Path注解修饰
// GET http://example.com/get_data.json?u=<user>&t=<token>
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}
这个是带参数查询的写法
// DELETE http://example.com/data/<id>
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id") id: String): Call<ResponseBody>
}
这个是delete请求
// POST http://example.com/data/create {"id": 1, "content": "The description for this data."}
interface ExampleService {
@POST("data/create")
fun createData(@Body data: Data): Call<ResponseBody>
}
这个是post请求
// GET http://example.com/get_data.json
// User-Agent: okhttp
// Cache-Control: max-age=0
interface ExampleService {
@Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
@GET("get_data.json")
fun getData(): Call<Data>
}
这种是静态的Header中添加数据
interface ExampleService {
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String): Call<Data>
}
这种是动态的Header中添加数据
6.3 最佳实践
val ip = HttpUtils.localIpv4Address
val retrofit = Retrofit.Builder()
.baseUrl("http://$ip:12345/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
这段代码可以优化,创建一个ServiceCreator 类
object ServiceCreator {
private const val BASE_URL = "http://192.168.12.148:12345"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
使用泛型,并创建一个内联函数,使用reified修饰,可以访问泛型的真实类型来进一步简化
// val appService = ServiceCreator.create(AppService::class.java)
val appService = ServiceCreator.create<AppService>()
没有加入inline函数可以调用上面的,加入inline之后,更为简化