一、 实验名称
使用Retrofit从互联网获取数据并完成图像显示应用的开发。
二、 参考资料
《Android开发者官方网站:Android 移动应用开发者工具 – Android 开发者 | Android Developers》、第八章课件。
三、 实验目的
练习在Android应用开发中使用Retrofit获取网络数据。
四、 实验内容
1. 核心功能
JSONPlaceholder是一个用于开发和测试的假数据API,提供模拟的RESTful API,API文档 ,例如,通过发送jsonplaceholder.typicode.com/photos?albumId=10就可以id为10的影集的所有图片的JSON数组。的本实验要开发一个项目名称为MyAlbums的图像显示应用,通过Retrofit从JSONPlaceholder获取数据,图像的首页是影集的标题(title)列表。点击标题以后,会显示该影集下的所有图片列表,图片列表样式如下图所示,使用LazyVerticalGrid组件实现图像列表的显示,列数为2。使用Card组件显示列表项,每一个列表项的上部是图片,使用Coil库的 AsyncImage函数显示,图片高度为95dp。列表项的下部是图片的标题,使用Text组件显示,padding为16dp。
2. 设置依赖项
本实验使用 Retrofit 处理网络请求,使用 Coil 加载图片,并使用 kotlinx.serialization 库解析 API 返回的 JSON。将其依赖项添加到 app/build.gradle。
3. 创建界面层
应遵循 Android 应用架构最佳实践为此应用创建界面层。
此层包含 ViewModel 以及用于在屏幕上显示来自 ViewModel 的 UiState 的可组合函数。ViewModel 负责公开屏幕界面状态,处理界面层中的业务逻辑,以及从层次结构的其它层调用业务逻辑。
界面层还包含用户看到并与之互动的视觉元素。在此层中,将决定各种元素如何协同工作,以便打造预想的界面。需要决定颜色、字体和图片的显示方式。
4. 创建数据层
数据层负责从 API 检索图片数据,需要添加影集和图片的数据类、用于管理这些数据的仓库,以及用于从网络检索这些数据的数据源类。
5. 实现依赖项注入
应使用依赖项注入 (DI),以确保应用灵活、稳健且可扩缩。DI 可使应用组件松散耦合,并且更易于测试。实现 DI 时,需要创建应用容器,用于获取应用所需的依赖项。应用容器必须可供整个应用访问。为此,可以将依赖项容器保存在自定义 Application 类中。然后,该类将继承自 Application 类。
按照实验任务书的要求完成以下实验报告:
一、程序代码
1. 从互联网获取数据
(1)在模块的build.gradle.kts文件中添加用于在 Android 应用中支持内嵌 WebView的依赖库:
implementation("androidx.webkit:webkit:1.6.1")
(2)打开manifests目录下的 AndroidManifest.xml配置 文件,在application元素前 添加允许应用访问互联网的 权限:
<uses-permission android:name="android.p ermission.INTERNET" />
(3)在ui包下新建home软件包,在home包下新建HomeScreen.kt文件,编写WebViewCompose可组合函数,调用WebViewCompose函数并传入网址:
//调用WebViewCompose函数并传入网址
@Composable
fun HomeScreen(
modifier: Modifier=Modifier
) {
WebViewCompose("https://baike.deno.dev",modifier=modifier)
}
@Composable
fun WebViewCompose(url: String,modifier: Modifier=Modifier) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.apply {
builtInZoomControls = true
displayZoomControls = false
javaScriptEnabled = true
useWideViewPort = true
loadWithOverviewMode = true
}
webViewClient = WebViewClientCompat()
loadUrl(url)
}
}
)
}
(4)在ui包下新建MyAlbumsApp.kt文件并添加构建标题栏的可组合函数、添加MyAlbumsApp可组合函数构建应用界面:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyAlbumsTopAppBar(scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier) {
CenterAlignedTopAppBar(
scrollBehavior = scrollBehavior,
title = {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall,
)
},
modifier = modifier
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyAlbumsApp() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { MyAlbumsTopAppBar(scrollBehavior = scrollBehavior) }
) {
HomeScreen(
modifier = Modifier.padding(top = it.calculateTopPadding())
)
}
}
(5)在strings.xml文件中修改app_name的值为百科花卉
(6)修改MainActivity.kt文件,修改后的代码如下所示:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyAlbumsTheme {
MyAlbumsApp()
}
}
}
}
2. 使用Retrofit库访问网络
(1)在模块的build.gradle.kts文件中添加Retrofit相关依赖库
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")// Retrofit的Kotlin序列化转换器扩展,允许我们将数据直接序列化为Kotlin对象
implementation("com.squareup.retrofit2:retrofit:2.9.0")// Retrofit是一个用于创建和管理网络请求的库
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")// Kotlinx序列化库的JSON实现,用于处理JSON数据的序列化和反序列化
implementation("io.coil-kt:coil-compose:2.6.0")// Coil是一个用于加载图像的库
(2)在项目的com.example.myAlbums包下新建model软件包创建MyAlbums数据类
@Serializable
data class MyAlbums(
@SerialName(value="itemName")
val name: String,
val description: String="",
@SerialName(value="cover")
val imageUrl: String="",
val link: String="",
val updateTime:String=""
)
(3)在项目的com.example.baikeflower包下新建network软件包下MyAlbumsApiService.kt文件
interface MyAlbumsApiService {
@GET("/item/{name}")
suspend fun getFlowerFromBaike(@Path("name") name: String): MyAlbumsResponse
}
@Serializable
data class MyAlbumsResponse(val status:Int,val message:String,val data: MyAlbums)
(4)在项目的com.example.baikeflower包下新建data软件包下 MyAlbumsRepository.kt文件
interface MyAlbumsRepository {
suspend fun getFlowerFromBaike(name: String): MyAlbums
}
class NetworkMyAlbumsRepository(
private val myAlbumsApiService: MyAlbumsApiService
) : MyAlbumsRepository {
override suspend fun getFlowerFromBaike(name: String): MyAlbums =
myAlbumsApiService.getFlowerFromBaike(name).data
}
(5)在data包下新建AppContainer.kt文件
interface AppContainer {
val myAlbumsRepository: MyAlbumsRepository
}
class DefaultAppContainer : AppContainer {
private val baseUrl = "https://baike.deno.dev"
private val retrofit: Retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(baseUrl)
.build()
private val retrofitService: MyAlbumsApiService by lazy {
retrofit.create(MyAlbumsApiService::class.java)
}
override val myAlbumsRepository: MyAlbumsRepository by lazy {
NetworkMyAlbumsRepository(retrofitService)
}
}
(6)在项目的com.example.baikeflower包下新建MyAlbumsApplication类
class MyAlbumsApplication : Application(){
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
}
3. 管理界面状态
(1)在home包下新建MyAlbumsViewModel.kt文件
sealed interface ApiRequestState {
data object Loading : ApiRequestState
data object Success : ApiRequestState
data object Error : ApiRequestState
}
data class MyAlbumsUiState(
val myAlbums: MyAlbums = MyAlbums("大花紫薇"),
)
class MyAlbumsViewModel(private val myAlbumsRepository: MyAlbumsRepository) :
ViewModel() {
var apiRequestState =
MutableStateFlow<ApiRequestState>(ApiRequestState.Loading)
private set
var myAlbumsUiState = MutableStateFlow(MyAlbumsUiState())
private set
init {
getMyAlbums()
}
fun getMyAlbums() {
viewModelScope.launch {
apiRequestState.update { ApiRequestState.Loading }
try {
val flower: MyAlbums =
myAlbumsRepository.getFlowerFromBaike(myAlbumsUiState.value.myAlbums.name)
myAlbumsUiState.update { currentState ->
currentState.copy(myAlbums = flower.copy(description = flower.description + " ······"))
}
apiRequestState.update { ApiRequestState.Success }
} catch (e: Exception) {
Log.e("BaikeFlowerViewModel", "getBaikeFlower error: ${e.message}")
apiRequestState.update { ApiRequestState.Error }
}
}
}
fun updateMyAlbums() {
getMyAlbums()
}
fun updateFlowerUiState(flower: MyAlbums) {
myAlbumsUiState.update { currentState ->
currentState.copy(myAlbums = flower)
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as MyAlbumsApplication)
val myAlbumsRepository = application.container.myAlbumsRepository
MyAlbumsViewModel(myAlbumsRepository = myAlbumsRepository)
}
}
}
}
4. 实现界面设计
(1)修改HomeScreen.kt文件
package com.example.myalbums.ui.home
import android.webkit.WebView
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.webkit.WebViewClientCompat
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.myalbums.R
import com.example.myalbums.model.MyAlbums
//调用WebViewCompose函数并传入网址
@Composable
fun HomeScreen(
viewModel: MyAlbumsViewModel,
retryAction: () -> Unit,
modifier: Modifier=Modifier
) {
val myAlbumsApiUiState by viewModel.apiRequestState.collectAsState()
val flowerUiState by viewModel.myAlbumsUiState.collectAsState()
val flower = flowerUiState.myAlbums
val scrollState = rememberScrollState()
val focusManager = LocalFocusManager.current
Column(
modifier = modifier
.verticalScroll(scrollState)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
WebViewCompose(url = "https://baike.deno.dev")
Row(modifier=Modifier.padding(horizontal = 16.dp)) {
OutlinedTextField(
label = { Text(text = stringResource(id = R.string.flower_name)) },
value = flower.name,
onValueChange = { viewModel.updateFlowerUiState(flower.copy(name = it)) },
modifier = Modifier.fillMaxWidth()
)
}
Button(
onClick = {
viewModel.updateMyAlbums()
focusManager.clearFocus()
},
) {
Text(text = stringResource(id = R.string.submit))
}
when (myAlbumsApiUiState) {
is ApiRequestState.Loading -> {
AnimatedVisibility(
visible = myAlbumsApiUiState is ApiRequestState.Loading,
enter = expandVertically(),
exit = shrinkVertically()
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
is ApiRequestState.Success -> FlowerInformationScreen(
flowerUiState.myAlbums
)
is ApiRequestState.Error -> ErrorScreen(
retryAction,
modifier = modifier.fillMaxSize()
)
}
}
}
@Composable
private fun FlowerInformationScreen(flower: MyAlbums, modifier: Modifier=Modifier) {
Surface(
shape = MaterialTheme.shapes.large,
modifier = modifier
) {
Column(
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceVariant),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(flower.imageUrl)
.crossfade(true).build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.flower_photo),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
)
Text(
text = flower.description,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom=24.dp))
}
}
}
@Composable
private fun LoadingScreen(modifier: Modifier = Modifier) {
Image(
modifier = modifier.size(200.dp),
painter = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.loading)
)
}
@Composable
private fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
)
Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
Button(onClick = retryAction) {
Text(stringResource(R.string.retry))
}
}
}
@Composable
fun WebViewCompose(url: String,modifier: Modifier=Modifier) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.apply {
builtInZoomControls = true
displayZoomControls = false
javaScriptEnabled = true
useWideViewPort = true
loadWithOverviewMode = true
}
webViewClient = WebViewClientCompat()
loadUrl(url)
}
}
)
}
(2)修改MyAlbumsApp.kt文件的MyAlbumsApp()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyAlbumsApp() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val myAlbumsViewModel: MyAlbumsViewModel =
viewModel(factory = MyAlbumsViewModel.Factory)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { MyAlbumsTopAppBar(scrollBehavior = scrollBehavior) }
) {
HomeScreen(
viewModel = myAlbumsViewModel,
retryAction = myAlbumsViewModel::getMyAlbums,
modifier = Modifier.padding(top = it.calculateTopPadding())
)
}
}
通过Retrofit从JSONPlaceholder获取数据,图像的首页是影集的标题(title)列表。点击标题以后,会显示该影集下的所有图片列表,图片列表样式如下图所示,使用LazyVerticalGrid组件实现图像列表的显示,列数为2。使用Card组件显示列表项,每一个列表项的上部是图片,使用Coil库的 AsyncImage函数显示,图片高度为95dp。列表项的下部是图片的标题,使用Text组件显示,padding为16dp。
二、 实验结果(含程序运行截图)
1. 从互联网获取数据
2.使用Retrofit库访问网络
3. 管理界面状态
4. 实现界面设计
三、 出现问题及解决方法
问题1:从互联网获取数据时,"import androidx.activity.enableEdgeToEdge" 显示错误
分析:这个错误通常意味着你的开发环境(如Android Studio)无法识别或找到 androidx.activity.enableEdgeToEdge 相关的类或函数。这可能是因为以下原因:
(1)项目中没有正确配置或导入 androidx.activity 库。
(2)项目的build.gradle文件中配置的AndroidX依赖版本可能不正确或不兼容。
(3)IDE可能没有正确同步或索引你的项目依赖
解决方法:
确保你的项目已经开启了AndroidX。在你的gradle.properties文件中添加以下两行:(将旧的Support库转换为AndroidX)
android.useAndroidX=true
android.enableJetifier=true
在app模块的build.gradle文件中添加以下依赖,以确保你有正确版本的androidx.activity库:
implementation 'androidx.activity:activity:版本号'
替换“版本号”为你项目中使用的合适版本。Ctrl+Shift+O同步Gradle项目即可
问题2:“import androidx.lifecycle.viewmodel.compose.viewModel显示错误”
分析:这个错误通常意味着你的项目中缺少必要的依赖或者导入的包路径不正确。androidx.lifecycle.viewmodel.compose.viewModel 是 Jetpack Compose 中用于集成 ViewModel 的工具,它在 AndroidX Lifecycle 库中。
解决方法:确保你的项目中已经添加了正确版本的 AndroidX Lifecycle 库依赖。在 build.gradle 文件中添加以下依赖:
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
问题3:运行代码,虚拟机页面出现:百科花卉keeps stopping
分析:代码没有报错,查看Logcat日志。在Android Studio中查看Logcat窗口,可以获取应用程序崩溃时的详细错误信息,帮助定位问题所在。滑到最下面报错位置:
具体报错如下:
java.lang.ClassCastException: android.app.Application cannot be cast to com.example.myalbums.MyAlbumsApplication
这个错误的意思是:试图将一个android.app.Application对象强制转换为com.example.myalbums.MyAlbumsApplication对象,但它们之间并不具有继承关系,因此无法进行强制类型转换。
在MyAlbumsApplication代码中有这样一行
我想要声明MyAlbumsApplication类来继承Application。树藤摸瓜,我去查看这个Application的声明
继承了Applacation的类需要在AndroidManifest.xml文件的application元素中通过android:name属性进行配置:
四、 实验心得
1. 从互联网获取数据
(1)配置项目:在模块的build.gradle.kts文 件中添加用于在 Android 应 用中支持内嵌 WebView的依赖库
(2)在ui包下新建home软件包,在home包下新建HomeScreen.kt文件编写WebViewCompose可组合函数、调用WebViewCompose可组合函数:
(3)在ui包下新建MyAlbumsApp.kt文件并添加构建标题栏的可组合函数、添加MyAlbumsApp可组合函数构建应用界面
(4)修改MainActivity.kt文件:在MainActivity.kt文件中调用MyAlbumsApp()
2. 使用Retrofit库访问网络
(1)添加Retrofit相关依赖库
(2)在项目的com.example.myAlbums包下新建model软件包下MyAlbums数据类,用于接收从通过百度百科API获取的花卉信息,通过 @Serializable注解可以自动把接收到的花卉信息反序列化成MyAlbums类的对象,当接收 到的数据的字段和当前类的字段的名称不一致时,要通过@SerialName注解标注接收到数据的字段名称
(3)在项目的com.example.myAlbums包下新建network软件包,在network 包新建MyAlbumsApiService.kt文件,添加MyAlbumsApiService 接口和用于接收数据的MyAlbumsResponse类,MyAlbumsResponse类的结构要和 getFlowerFromBaike函数返回的数据的结构保持一致。
(4)在项目的com.example.myAlbums包下新建data软件包,在data包下新建 MyAlbumsRepository.kt文件,在该文件中添加仓储接口和接口的实现类,在实现类中需要注入一个MyAlbumsApiService类型的对象.
(5)在data包下新建AppContainer.kt文件,在该文件中添加AppContainer接口和接口的实现 类,用于进行依赖注入,该实现类的实例会在Application中创建,可以注入到ViewModel 对象中
(6)在项目的com.example.myAlbums包下新建MyAlbumsApplication类继承Application 类,在该类中创建AppContainer类型的实例
3. 管理界面状态(在home包下新建MyAlbumsViewModel.kt文件)
(1)创建ApiRequestState密封 接口用来表示网络请求的三种状态:正在连接、请求成功、请求错误;创建 MyAlbumsUiState数据类用来表示界面显示的花卉信息的状态
(2)创建MyAlbumsViewModel 类,在类中添加获取花卉信息的getMyAlbums()函数;添加更新花卉信息的updateMyAlbums()函数、更新界面 花卉信息状态的updateFlowerUiState(flower: MyAlbums)函数和创建ViewModel工厂的 伴生对象,
4. 实现界面设计
(1)在HomeScreen.kt文件中添加生成正在连接网络界面的可组合函数(R.string.loading中loading的值为:正在装载);添加生成网络连接错误界面的可组合函数(R.string.retry的值为重试);添加生成显示花卉信息界面的可组合函数;在FlowerInformationScreen函数的Column可组合项的大括号中添加要显示花卉图片和花卉简介,由coil-compose库的的AsyncImage可组合函数显示图片可自动实现图片的下载、缓存及解码
(2)把loading_img.xml、ic_connection_error.xml、ic_broken_image.xml三个要在界面中用的的图像文件复制到drawable目录下
(3)配置MyAlbumsApplication类
Application类是Android应用程序的入口点,继承并扩展它允许您在整个应用程序范围内设置统一的初始化逻辑、配置或状态,这意味着可以在应用程序启动时执行一些全局设置或初始化操作
继承了Application的类需要在AndroidManifest.xml文件的application元素中通过android:name属性进行配置,代码如下所示:
<application
android: name=". MyAlbumsApplication"