完成艺术空间(ArtSpace)应用的开发

一、         实验名称

 完成艺术空间(ArtSpace)应用的开发。

二、         参考资料

《Android开发者官方网站:Android 移动应用开发者工具 – Android 开发者  |  Android Developers》、第3、4、5章课件。

三、         实验目的

通过完成ArtSpace应用,构建您自己的数字艺术空间,打造一款可用来展示一系列艺术作品的应用,如下图所示:

image.png

通过本应用的开发,达到以下实验目的:

1.      练习在 Android 应用中使用模型驱动的架构模式。

2.      练习使用 ViewModel管理界面状态。

3.      练习在 Android 应用中与界面和状态进行交互。

四、         实验内容

1. 使用可组合项构建静态界面

创建低保真度原型

低保真度(以下简称“低保真”)原型是指一个简单的模型或绘图,可让人对应用的外观有一个基本的了解。

创建低保真原型:

1.       想一想,要在 Art Space 应用中展示哪些内容,以及目标受众群体是谁。

2.       在您的首选媒介上,添加应用的组成元素。需要考虑的一些元素包括:

·        艺术作品图片

        ·艺术作品相关信息(例如作品名称、艺术家和发表年份)

        ·任何其他元素,例如能让应用具有互动性、变得动态的按钮。

3.       将这些元素放到不同位置,评估一下不同的视觉效果。不需要立刻达到完美的效果,您完全可以在当前先选定一项设计,以后再反复改进。

注意:有一些原则有助于为用户提供更好的设计,而这不在此项目的讨论范围之内。如需了解详情,请参阅了解布局

4.       您可能会想出一种低保真度设计,如下图所示:

image.png

 1. 界面模型中的占位符元素有助于直观呈现最终产品。

将设计转换为代码

如需借助原型来将您的设计转换为代码,请执行以下操作:

1.       确定构建应用所需的界面元素。

例如,在您制作的设计示例中,您需要在代码中添加一个 Image 可组合项、两个 Text 可组合项和两个 Button 可组合项。

2.       找出应用的不同逻辑区块,并确定它们之间的界限。

此步骤可帮助您将屏幕划分为小型可组合项,并思考可组合项的层次结构。

在本示例中,您可以将界面划分为三个区块:

·        艺术作品墙

·        艺术作品说明

·        显示控制器

您可以使用布局可组合项(例如 Row 或 Column 可组合项)排列各个部分。

image.png

 2. 在区块周围绘制边界有助于开发者直观地理解可组合项。

3.       在应用中找出各个包含多个界面元素的区块,然后为其绘制边界。

这些边界有助于您了解相应部分中不同元素之间的关联。

image.png

 3. 为文本和按钮绘制的边界越多,越有助于开发者排列各个可组合项。

现在,您可以轻松了解如何使用布局可组合项来排列 Text 和 Button 等可组合项。

关于您可能使用的各种可组合项的一些说明:

·Row  Column 可组合项。不妨在 Row  Column 可组合项中使用各种不同的 horizontalArrangement  verticalAlignment 参数进行实验,找到符合您当前设计的参数设置。

·Image 可组合项。别忘了填写 contentDescription 参数。如前一个 Codelab 中所述,TalkBack 使用 contentDescription 参数来支持应用的无障碍功能。如果 Image 可组合项仅用于装饰目的,或者存在描述 Image 可组合项的 Text 元素,您可以将 contentDescription 参数设置为 null

·Text 可组合项。您可以尝试在 fontSizetextAlign  fontWeight 中使用各种不同的值来设置文本样式,还可以使用 buildAnnotatedString 函数为单个 Text 可组合项应用多种样式。

·Surface 可组合项。您可以针对 Modifier.border 尝试在 ElevationColor  BorderStroke 中使用各种不同的值,以在 Surface 可组合项中创建不同的界面。

·间距和对齐。您可以使用 Modifier 参数(例如 padding  weight)来帮助排列可组合项。

注意:如果是简单的应用,您可以为各个界面元素设置单独的样式。不过,随着您添加越来越多的界面,这种做法会造成负担。Compose 已实现 Material Design,有助于保持设计一致性。我们会在日后学习的单元中详细了解 Material Design 和 Material 主题设置。您可以参阅 Compose 中的 Material 主题设置,了解有关详情。

4.       在模拟器中或在 Android 设备上运行应用。

image.png

 4. 此应用显示的是静态内容,但用户还无法与其互动。

2. 让应用具有互动性

确定用户互动方式

以数字化方式构建 Art Space 应用的好处在于,您可以让该应用为用户提供动态的互动体验。在最初的设计中,您构建了两个按钮供用户互动。不过别忘了,这是您自己的 Art Space 应用!因此您可以根据需要更改设计以及用户与应用的互动方式。现在,请花点时间想一想,您希望用户如何与应用互动,以及应用该如何对这类互动做出响应。以下是一些您可以在应用中添加的可能互动方式:

·在用户点按按钮时,显示下一幅或上一幅艺术作品。

·在用户滑动时,让显示的艺术作品快进到下一个作品专辑。

·在用户长按按钮时,显示用于了解其他信息的提示。

注意:Compose 支持多种手势和动画,可让您的应用具有互动性。我们会在日后学习的单元中详细了解动画。您可以参阅手势,详细了解各个高级主题。

为动态元素创建状态

处理界面的相应部分,使之能在用户点按按钮时显示下一幅或上一幅艺术作品:

1.       首先,确定需要在用户互动时变化的界面元素。

在本例中,这类界面元素是艺术作品、艺术作品名称、艺术家和年份。

2.       如有必要,使用 MutableState 对象来创建各个动态界面元素的状态。

3.       记得将硬编码值替换为已定义的 states

注意:虽然目前每个动态界面元素都只使用一个状态,但就代码可读性和应用的性能而言,这可能不是最高效的做法。您可以将相关元素作为一个实体组合在一起,然后将该实体声明为单一状态。我们会在日后学习的单元中了解如何使用 Collection 和 Data 类。了解完这些概念后,您可以回到这个项目,运用学到的概念重构您的代码。

编写互动方式的条件逻辑

1.       想一想在用户点按按钮时需要触发的行为,不妨从 Next 按钮着手。

当用户点按 Next 按钮时,应该会看到序列中的下一幅艺术作品。目前,可能很难确定接下来要显示哪一幅艺术作品。

2.       为每幅艺术作品添加序数形式的标识符(即 ID),从 1 开始。

现在很明显,下一幅艺术作品指向的是序列中带有下一个 ID 的艺术作品。

因为您没有无限多的艺术作品,所以您可能还需要确定在显示系列中的最后一幅艺术作品时 Next 按钮的行为。一种常见的行为是:在显示最后一幅艺术作品后,返回显示第一幅艺术作品。

3.       先编写伪代码,以捕获不含 Kotlin 语法的代码逻辑。

如果有三幅要显示的艺术作品,则针对 Next 按钮的行为逻辑编写的伪代码可能如以下代码段所示:

if (current artwork is the first artwork) {
    // Update states to show the second artwork.
}
else if (current artwork is the second artwork) {
    // Update states to show the third artwork.
}
else if (current artwork is the last artwork) {
   // Update state to show the first artwork.
}

4.       将伪代码转换为 Kotlin 代码。

您可以使用 when 语句(而不是 if else 语句)来构建条件逻辑,以便在管理大量艺术作品时,提高代码的可读性。

5.       如需在用户点按按钮时执行此逻辑,请将其放入 Button 可组合项的 onClick() 参数中。

6.       重复相同的步骤,为 Previous 按钮构建逻辑。

7.       运行应用,然后点按按钮,确认这些按钮是否会切换显示上一幅或下一幅艺术作品。

3. 挑战:针对不同屏幕尺寸构建应用

Android 的优势之一是,它支持多种设备和屏幕尺寸,这意味着您构建的应用可以覆盖广泛的受众群体,并能以多种方式使用。为了确保向所有用户提供最佳体验,建议您在打算支持的设备上测试您的应用。例如,在当前的示例应用中,您最初可能是针对移动设备的纵向模式设计、构建并测试的应用。不过,您的某些用户可能会发现,在更大屏幕的横屏模式下使用您的应用时,可获得愉快的使用体验。

虽然平板电脑并不是此应用主要支持的设备,但是当用户在更大的屏幕上使用该应用时,您仍然需要确保它能够正常运行。

使用平板电脑来测试您的应用在尺寸较大屏幕上的显示效果:

1.       如果您没有 Android 平板电脑设备,请创建 Android 虚拟设备 (AVD)

2.       在平板电脑 AVD 的横屏模式下构建并运行应用。

3.       目测检查是否存在无法接受的错误,例如某些界面元素被截断、没有对齐或按钮的互动方式不符合预期。

image.png

 5. 应用需进行修改,才能在采用更大屏幕的设备上正确显示。

4.       修改代码,以修复发现的所有 bug。如需相关指南,请参阅大屏应用质量基本兼容性指南

5.       再次在平板电脑和手机上测试应用,以确保相应 bug 在这两类设备上均已得到修复。

image.png

 6. 现在,应用在大屏幕上的显示效果良好。

注意:您可能会发现,许多支持平板电脑和手机的应用在不同外形规格上的显示效果可能会有所不同。之所以出现这种差异,通常是因为应用针对不同屏幕尺寸支持不同的布局。如需了解详情,请参阅支持不同的屏幕尺寸

4. 功能实现

(1)   新建一个名为ArtSpace的Compose项目,项目的主要目录结构如下图所示:

image.png

 

(2)   在data包下新建ArtPicture数据类,用来封装图片信息,内容如下:

package com.example.artspace.data
 
data class ArtPicture(val id: Int, val title: String, val artist: String, val year: String, val image: Int) {
}

(3)  在data.local包下新建LocalArtPictureDataProvider.kt文件,该文件提供本地的图像数据,内容如下:

package com.example.artspace.data.local
 
import com.example.artspace.R
import com.example.artspace.data.ArtPicture
 
object LocalArtPictureDataProvider {
    val allArtPictures =
        listOf(
            ArtPicture(1, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_01),
            ArtPicture(2, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_02),
            ArtPicture(3, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_03),
            ArtPicture(4, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_04),
            ArtPicture(5, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_05),
            ArtPicture(6, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_06),
            ArtPicture(7, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_07),
            ArtPicture(8, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_08),
            ArtPicture(9, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_09),
            ArtPicture(10, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_10),
        )
    val defaultArtPicture = allArtPictures.first()
}

(4)           在data包下新建用来获取数据的仓储库接口ArtPictureRepository,接口中函数的返回值是Flow流,表示函数会异步返回一个ArtPicture类型的数据,内容如下:

package com.example.artspace.data

import kotlinx.coroutines.flow.Flow

interface ArtPictureRepository {
    fun getDefaultArtPicture(id: Int): Flow<ArtPicture>
    fun getAllArtPictures():Flow<List<ArtPicture>>
}

(5)           在data包下新建仓储库接口的实现类,在函数中使用flow块来定义一个流,其中通过emit函数发数据,内容如下:

package com.example.artspace.data

import com.example.artspace.data.local.LocalArtPictureDataProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class ArtPictureRepositoryImpl : ArtPictureRepository {
    override fun getDefaultArtPicture(id: Int): Flow<ArtPicture> = flow {
            emit(LocalArtPictureDataProvider.defaultArtPicture)
        }
    override fun getAllArtPictures(): Flow<List<ArtPicture>> = flow {
            emit(LocalArtPictureDataProvider.allArtPictures)
        }
}

(6)         在ui.home包下新建ArtSpaceHomeViewModel.kt文件,在该文件中添加用来管理界面状态的ArtSpaceHomeViewModel类和表示界面状态的class ArtSpaceHomeState类,内容如下:

package com.example.artspace.ui.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.artspace.data.ArtPicture
import com.example.artspace.data.ArtPictureRepository
import com.example.artspace.data.ArtPictureRepositoryImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

/**
 * 艺术空间首页的ViewModel类,负责管理界面状态和数据的获取。
 *
 * @param artPictureRepository 艺术图片仓库接口,用于获取艺术图片数据,默认实现为ArtPictureRepositoryImpl。
 */
class ArtSpaceHomeViewModel(private val artPictureRepository: ArtPictureRepository = ArtPictureRepositoryImpl()) :
    ViewModel() {
    // 界面状态的StateFlow,用于向UI组件提供数据和状态。
    private val _uiState = MutableStateFlow(ArtSpaceHomeState())
    val uiState: StateFlow<ArtSpaceHomeState> = _uiState
    // 当前显示图片的索引。
    private var currentIndex: Int = 0
    /**
     * 监听艺术图片数据,出现错误时更新状态显示错误信息,成功时更新状态显示图片列表和第一张图片。
     */
    private fun observeArtPictures() {
        viewModelScope.launch {
            artPictureRepository.getAllArtPictures()
                .catch { error ->
                    _uiState.value=_uiState.value.copy(error = error.message ?: "不知道的错误!")
                }
                .collect { artPictures ->
                    if (artPictures.isEmpty()) {
                        _uiState.value=_uiState.value.copy(error = "没有图片可用!")
                    } else {
                        currentIndex = 0
                        _uiState.value=_uiState.value.copy(
                            artPictures = artPictures,
                            currentPicture = artPictures.first()
                        )
                    }
                }
        }
    }

    // 类初始化时调用observeArtPictures观察图片数据。
    init {
        observeArtPictures()
    }

    /**
     * 切换到下一张图片。
     */
    fun nextPicture() {
        viewModelScope.launch {
            if (_uiState.value.artPictures.isEmpty()) return@launch

            val nextPictureIndex = (currentIndex + 1) % _uiState.value.artPictures.size
            currentIndex = nextPictureIndex
            _uiState.value = _uiState.value.copy(currentPicture = _uiState.value.artPictures[nextPictureIndex])
        }
    }

    /**
     * 切换到上一张图片。
     */
    fun previousPicture() {
        viewModelScope.launch {
            if (_uiState.value.artPictures.isEmpty()) return@launch

            val previousPictureIndex = if (currentIndex == 0) {
                _uiState.value.artPictures.size - 1
            } else {
                currentIndex - 1
            }
            currentIndex = previousPictureIndex
            _uiState.value = _uiState.value.copy(currentPicture = _uiState.value.artPictures[previousPictureIndex])
        }
    }
}

/**
 * 艺术空间首页的状态数据类,包含图片列表、当前图片和错误信息。
 *
 * @param artPictures 艺术图片列表。
 * @param currentPicture 当前显示的图片。
 * @param error 错误信息,如果有错误发生。
 */
data class ArtSpaceHomeState(
    val artPictures: List<ArtPicture> = emptyList(),
    val currentPicture: ArtPicture? = null,
    val error: String? = null,
)

(7)         修改res目录下values子目录中strings.xml文件,定义界面要显示的文字内容,修改后的文件内容如下:

<resources>
    <string name="app_name">Art Space</string>
    <string name="error_message">出错了:</string>
    <string name="previous_text">前一幅</string>
    <string name="next_text">后一幅</string>
</resources>

(8)           在ui.home包下新建ArtSpaceHome.kt文件,在该文件中创建构建界面的可组合函数,内容如下:

package com.example.artspace.ui.home

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.artspace.R
import com.example.artspace.data.ArtPicture
/**
 * 艺术空间首页界面。
 *
 * @param artSpaceHomeState 页面状态,包含图片信息和错误状态等。
 * @param viewModel 页面的ViewModel,用于处理逻辑和数据。
 */
@Composable
fun ArtSpaceHome(artSpaceHomeState: ArtSpaceHomeState, viewModel: ArtSpaceHomeViewModel) {
    Column(
        modifier = Modifier.padding(horizontal = 20.dp)
    ) {
        // 获取当前展示的图片,如果没有则默认显示第一张
        val currentPicture =
            artSpaceHomeState.currentPicture ?: artSpaceHomeState.artPictures.first()
        // 如果有错误信息,则展示错误信息
        if (artSpaceHomeState.error != null) {
            Text(
                text = "${stringResource(id = R.string.error_message)}${artSpaceHomeState.error}",
                style = MaterialTheme.typography.headlineLarge,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(top = 50.dp)
            )
        } else {
            // 展示图片和相关信息
            Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
            PictureFrame(
                currentPicture,
                Modifier
                    .weight(1f)
                    .fillMaxWidth()
            )
            PictureInfo(currentPicture, Modifier.height(IntrinsicSize.Min))
            Spacer(Modifier.height(20.dp))
            // 展示上一张和下一张图片的按钮
            PictureAction(
                onPreviusClick = viewModel::previousPicture,
                onNextClick = viewModel::nextPicture,
                modifier = Modifier.height(IntrinsicSize.Min)
            )
            Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
        }
    }
}

/**
 * 展示图片。
 *
 * @param artPicture 要展示的图片信息。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureFrame(artPicture: ArtPicture, modifier: Modifier = Modifier) {
    Box(modifier = modifier) {
        Card(
            modifier = Modifier.align(alignment = Alignment.Center),
            shape = RoundedCornerShape(0.dp),
            elevation = CardDefaults.cardElevation(10.dp),
            colors = CardDefaults.cardColors(Color.White)
        ) {
            Image(
                modifier = Modifier.padding(20.dp),
                painter = painterResource(id = artPicture.image),
                contentDescription = null
            )
        }
    }
}

/**
 * 展示图片的详细信息。
 *
 * @param artPicture 要展示的图片信息。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureInfo(artPicture: ArtPicture, modifier: Modifier = Modifier) {
    Box(modifier = modifier) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.LightGray),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                modifier = Modifier.padding(vertical = 10.dp),
                style = MaterialTheme.typography.titleLarge,
                text = "${artPicture.title} (${artPicture.id})"
            )
            Row(modifier = Modifier.padding(bottom = 10.dp)) {
                Text(
                    modifier = Modifier.padding(end = 10.dp),
                    fontWeight = FontWeight.Bold,
                    text = artPicture.artist
                )
                Text(text = "(${artPicture.year})")
            }

        }
    }
}

/**
 * 展示上一张和下一张图片的按钮。
 *
 * @param onPreviusClick 点击上一张图片的回调。
 * @param onNextClick 点击下一张图片的回调。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureAction(
    onPreviusClick: () -> Unit,
    onNextClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier = modifier) {
        Row(
            modifier = Modifier
                .padding(12.dp)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(onClick = onPreviusClick) {
                Text(text = stringResource(id = R.string.previous_text))
            }
            Button(onClick = onNextClick) {
                Text(text = stringResource(id = R.string.next_text))
            }
        }
    }
}

/**
 * 艺术空间首页的预览界面。
 */
@Preview
@Composable
fun PreArtSpaceHome() {
    val viewModel: ArtSpaceHomeViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ArtSpaceHome(uiState, viewModel)
}

(9)           修改MainActivity类,修改后的代码内容如下:

class MainActivity : ComponentActivity() {
    private val viewModel: ArtSpaceHomeViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            ArtSpaceTheme {
                val uiState by viewModel.uiState.collectAsStateWithLifecycle()
                ArtSpaceHome(artSpaceHomeState = uiState,viewModel)
            }
        }
    }
}

(10)       运行项目,运行结果如下图所示:

image.png

(11)       完善项目,使翻页按钮具有校验功能,当前图片如果是第一张图片,前一幅按钮为不可用;当前图片如果是最后一张图片,后一幅按钮为不可用(提示:在PictureAction可组合函数中添加两个参数:previewEnbled: Boolean = false,nextEnbled: Boolean = true并非给Button的enabled参数;ArtSpaceHomeState类添加两个状态值val isFirst: Boolean = true, val isLast: Boolean = false用来判断图片是不是第一张或最后一张;修改ArtSpaceHomeViewModel类中切换图片的方法,使其实现新的业务逻辑)。完善后的项目运行结果如下图所示:

image.png

 

5. 实验报告

一、程序代码

1. 在data包下新建ArtPicture数据类,用来封装图片信息,内容如下:

package com.example.artspace.data

data class ArtPicture(val id: Int, val title: String, val artist: String, val year: String, val image: Int) {

}

2. 在data.local包下新建LocalArtPictureDataProvider.kt文件,该文件提供本地的图像数据,内容如下:

package com.example.artspace.data.local



import com.example.artspace.R

import com.example.artspace.data.ArtPicture



object LocalArtPictureDataProvider {

    val allArtPictures =

        listOf(

            ArtPicture(1, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_01),

            ArtPicture(2, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_02),

            ArtPicture(3, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_03),

            ArtPicture(4, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_04),

            ArtPicture(5, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_05),

            ArtPicture(6, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_06),

            ArtPicture(7, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_07),

            ArtPicture(8, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_08),

            ArtPicture(9, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_09),

            ArtPicture(10, "西藏印象", "江新华", "2023", R.drawable.jiangxinhua20230414_10),

                )

    val defaultArtPicture = allArtPictures.first()

}

3. 在data包下新建用来获取数据的仓储库接口ArtPictureRepository,接口中函数的返回值是Flow流,表示函数会异步返回一个ArtPicture类型的数据,内容如下:

package com.example.artspace.data

import kotlinx.coroutines.flow.Flow

interface ArtPictureRepository {
    fun getDefaultArtPicture(id: Int): Flow<ArtPicture>
    fun getAllArtPictures():Flow<List<ArtPicture>>
}

4. 在data包下新建仓储库接口的实现类,在函数中使用flow块来定义一个流,其中通过emit函数发数据,内容如下:

package com.example.artspace.data

import com.example.artspace.data.local.LocalArtPictureDataProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class ArtPictureRepositoryImpl : ArtPictureRepository {
    override fun getDefaultArtPicture(id: Int): Flow<ArtPicture> = flow {
            emit(LocalArtPictureDataProvider.defaultArtPicture)
        }
    override fun getAllArtPictures(): Flow<List<ArtPicture>> = flow {
            emit(LocalArtPictureDataProvider.allArtPictures)
        }
}

 5. 在ui.home包下新建ArtSpaceHomeViewModel.kt文件,在该文件中添加用来管理界面状态的ArtSpaceHomeViewModel类和表示界面状态的class ArtSpaceHomeState类,内容如下:

package com.example.artspace.ui.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.artspace.data.ArtPicture
import com.example.artspace.data.ArtPictureRepository
import com.example.artspace.data.ArtPictureRepositoryImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch



/**
 * 艺术空间首页的ViewModel类,负责管理界面状态和数据的获取。
 *
 * @param artPictureRepository 艺术图片仓库接口,用于获取艺术图片数据,默认实现为ArtPictureRepositoryImpl。
 */
class ArtSpaceHomeViewModel(private val artPictureRepository: ArtPictureRepository = ArtPictureRepositoryImpl()) :
    ViewModel() {
    // 界面状态的StateFlow,用于向UI组件提供数据和状态。
    private val _uiState = MutableStateFlow(ArtSpaceHomeState())
    val uiState: StateFlow<ArtSpaceHomeState> = _uiState
    // 当前显示图片的索引。
    private var currentIndex: Int = 0
    /**
     * 监听艺术图片数据,出现错误时更新状态显示错误信息,成功时更新状态显示图片列表和第一张图片。
     */
    private fun observeArtPictures() {
        viewModelScope.launch {
            artPictureRepository.getAllArtPictures()
                .catch { error ->
                    _uiState.value=_uiState.value.copy(error = error.message ?: "不知道的错误!")
                }
                .collect { artPictures ->
                    if (artPictures.isEmpty()) {
                        _uiState.value=_uiState.value.copy(error = "没有图片可用!")
                    } else {
                        currentIndex = 0
                        _uiState.value=_uiState.value.copy(
                            artPictures = artPictures,
                            currentPicture = artPictures.first()
                        )
                    }
                }
        }
    }

    // 类初始化时调用observeArtPictures观察图片数据。
    init {
        observeArtPictures()
    }

    /**
     * 切换到下一张图片。
     */
    fun nextPicture() {
        viewModelScope.launch {
            if (_uiState.value.artPictures.isEmpty()) return@launch

            val nextPictureIndex = (currentIndex + 1) % _uiState.value.artPictures.size
            currentIndex = nextPictureIndex
            _uiState.value = _uiState.value.copy(currentPicture = _uiState.value.artPictures[nextPictureIndex])
        }
    }

    /**
     * 切换到上一张图片。
     */
    fun previousPicture() {
        viewModelScope.launch {
            if (_uiState.value.artPictures.isEmpty()) return@launch

            val previousPictureIndex = if (currentIndex == 0) {
                _uiState.value.artPictures.size - 1
            } else {
                currentIndex - 1
            }
            currentIndex = previousPictureIndex
            _uiState.value = _uiState.value.copy(currentPicture = _uiState.value.artPictures[previousPictureIndex])
        }
    }
}

/**
 * 艺术空间首页的状态数据类,包含图片列表、当前图片和错误信息。
 *
 * @param artPictures 艺术图片列表。
 * @param currentPicture 当前显示的图片。
 * @param error 错误信息,如果有错误发生。
 */
data class ArtSpaceHomeState(
    val artPictures: List<ArtPicture> = emptyList(),
    val currentPicture: ArtPicture? = null,
    val error: String? = null,
)

6. 修改res目录下values子目录中strings.xml文件,定义界面要显示的文字内容,修改后的文件内容如下:

<resources>
    <string name="app_name">Art Space</string>
    <string name="error_message">出错了:</string>
    <string name="previous_text">前一幅</string>
    <string name="next_text">后一幅</string>
</resources>

7. 在ui.home包下新建ArtSpaceHome.kt文件,在该文件中创建构建界面的可组合函数,内容如下:

package com.example.artspace.ui.home

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.artspace.R
import com.example.artspace.data.ArtPicture
/**
 * 艺术空间首页界面。
 *
 * @param artSpaceHomeState 页面状态,包含图片信息和错误状态等。
 * @param viewModel 页面的ViewModel,用于处理逻辑和数据。
 */
@Composable
fun ArtSpaceHome(artSpaceHomeState: ArtSpaceHomeState, viewModel: ArtSpaceHomeViewModel) {
    Column(
        modifier = Modifier.padding(horizontal = 20.dp)
    ) {
        // 获取当前展示的图片,如果没有则默认显示第一张
        val currentPicture =
            artSpaceHomeState.currentPicture ?: artSpaceHomeState.artPictures.first()
        // 如果有错误信息,则展示错误信息
        if (artSpaceHomeState.error != null) {
            Text(
                text = "stringResource(id=R.string.errormessage){artSpaceHomeState.error}",
                style = MaterialTheme.typography.headlineLarge,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(top = 50.dp)
            )
        } else {
            // 展示图片和相关信息
            Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
            PictureFrame(
                currentPicture,
                Modifier
                    .weight(1f)
                    .fillMaxWidth()
            )
            PictureInfo(currentPicture, Modifier.height(IntrinsicSize.Min))
            Spacer(Modifier.height(20.dp))
            // 展示上一张和下一张图片的按钮
            PictureAction(
                onPreviusClick = viewModel::previousPicture,
                onNextClick = viewModel::nextPicture,
                modifier = Modifier.height(IntrinsicSize.Min)
            )
            Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
        }
    }
}

/**
 * 展示图片。
 *
 * @param artPicture 要展示的图片信息。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureFrame(artPicture: ArtPicture, modifier: Modifier = Modifier) {
    Box(modifier = modifier) {
        Card(
            modifier = Modifier.align(alignment = Alignment.Center),
            shape = RoundedCornerShape(0.dp),
            elevation = CardDefaults.cardElevation(10.dp),
            colors = CardDefaults.cardColors(Color.White)
        ) {
            Image(
                modifier = Modifier.padding(20.dp),
                painter = painterResource(id = artPicture.image),
                contentDescription = null
            )
        }
    }
}

/**
 * 展示图片的详细信息。
 *
 * @param artPicture 要展示的图片信息。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureInfo(artPicture: ArtPicture, modifier: Modifier = Modifier) {
    Box(modifier = modifier) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.LightGray),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                modifier = Modifier.padding(vertical = 10.dp),
                style = MaterialTheme.typography.titleLarge,
                text = "artPicture.title({artPicture.id})"
            )
            Row(modifier = Modifier.padding(bottom = 10.dp)) {
                Text(
                    modifier = Modifier.padding(end = 10.dp),
                    fontWeight = FontWeight.Bold,
                    text = artPicture.artist
                )
                Text(text = "(${artPicture.year})")
            }

        }
    }
}

/**
 * 展示上一张和下一张图片的按钮。
 *
 * @param onPreviusClick 点击上一张图片的回调。
 * @param onNextClick 点击下一张图片的回调。
 * @param modifier 组件修饰符。
 */
@Composable
fun PictureAction(
    onPreviusClick: () -> Unit,
    onNextClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier = modifier) {
        Row(
            modifier = Modifier
                .padding(12.dp)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(onClick = onPreviusClick) {
                Text(text = stringResource(id = R.string.previous_text))
            }
            Button(onClick = onNextClick) {
                Text(text = stringResource(id = R.string.next_text))
            }
        }
    }
}

/**
 * 艺术空间首页的预览界面。
 */
@Preview
@Composable
fun PreArtSpaceHome() {
    val viewModel: ArtSpaceHomeViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ArtSpaceHome(uiState, viewModel)
}

8. 修改MainActivity类,修改后的代码内容如下:

class MainActivity : ComponentActivity() {
    private val viewModel: ArtSpaceHomeViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            ArtSpaceTheme {
                val uiState by viewModel.uiState.collectAsStateWithLifecycle()
                ArtSpaceHome(artSpaceHomeState = uiState,viewModel)
            }
        }
    }
}

二、  实验结果(含程序运行截图)

    1. 运行项目,运行结果如下图所示:

 

新的图片

 2. 完善项目,使翻页按钮具有校验功能,

(1)当前图片如果是第一张图片,前一幅按钮为不可用;

新的图片

(2)当前图片如果是最后一张图片,后一幅按钮为不可用

新的图片

三、 出现问题及解决方法

问题1:在第一张照片时不可判定为第一张

 解决方法:

1. 在ArtSpaceHome.kt文件中的 ArtSpaceHome 函数通过

(1)//获取当前照片currentPicture 是否与 第一张照片(artPictures列表中的第一个元素)相同,如果相同,则isFirst为true,否则为false。
val isFirst:Boolean= currentPicture == artSpaceHomeState.artPictures.first()

(2)//获取当其照片currentPicture 是否与 最后一张照片(artPictures列表中的最后一个元素)相同,如果相同,则isList为true,否则为false。
val isLast:Boolean=currentPicture == artSpaceHomeState.artPictures.last()

2. 在展示上一张和下一张图片的按钮 PictureAction 中判断当前照片不是第一张或最后一张

previewEnbled=!isFirst,nextEnbled=!isLast

3. 在PictureActionk可组合函数中

(1)定义两个参数

previewEnbled: Boolean = false,
nextEnbled: Boolean = true

(2)在Box中的Button按钮判断当前参数是否为第一幅

enabled = previewEnbled

则显示按钮颜色为白色

Button(onClick = onPreviusClick, enabled = previewEnbled) {
   Text(text = stringResource(id = R.string.previous_text) ,
       color = if (previewEnbled) Color.White else Color.Black
   )
}

(3)在Box中的Button按钮判断当前参数是否为最后一幅

enabled = nextEnbled

则显示按钮颜色为白色

Button(onClick = onNextClick, enabled = nextEnbled) {
   Text(text = stringResource(id = R.string.next_text),
       color = if (nextEnbled) Color.White else Color.Black)
}

四、  实验心得

    使用可组合项构建静态界面:创建低保真度原型;将设计转换为代码。

    让应用具有互动性:确定用户互动方式;为动态元素创建状态;编写互动方式的条件逻辑

    挑战:针对不同屏幕尺寸构建应用

    ArtPicture数据类,用来封装图片信息;LocalArtPictureDataProvider.kt文件提供本地的图像数据;新建用来获取数据的仓储库接口ArtPictureRepository,接口中函数的返回值是Flow流,表示函数会异步返回一个ArtPicture类型的数据;新建仓储库接口的实现类,在函数中使用flow块来定义一个流,其中通过emit函数发数据;新建ArtSpaceHomeViewModel.kt文件,在该文件中添加用来管理界面状态的ArtSpaceHomeViewModel类和表示界面状态的class ArtSpaceHomeState类;修改res目录下values子目录中strings.xml文件,定义界面要显示的文字内容;新建ArtSpaceHome.kt文件,在该文件中创建构建界面的可组合函数,

1.练习在 Android 应用中使用模型驱动的架构模式:

    模型驱动的架构(Model-Driven Architecture, MDA)是一种软件开发方法,它侧重于使用模型来描述软件系统的各个方面。在 Android 应用开发中,模型驱动的架构意味着将应用的数据模型作为核心,驱动其他应用组件的设计和实现。

    要在Android应用中使用模型驱动的架构模式,可以选择MVC(Model-View-Controller)、MVP(Model-View-Presenter)或MVVM(Model-View-ViewModel)等模式。这些模式都可以帮助组织和管理应用的代码、逻辑和数据流。以下是一些练习使用模型驱动的架构模式的步骤:

(1)理解模式原理:

    MVC:模型(Model)负责数据和业务逻辑,视图(View)负责显示用户界面,控制器(Controller)负责处理用户输入并协调Model和View。

    MVP:模型(Model)同样负责数据和业务逻辑,视图(View)只负责显示用户界面并与用户交互,主持人(Presenter)处理业务逻辑并协调Model和View。

    MVVM:模型(Model)负责数据和业务逻辑,视图(View)负责显示用户界面,视图模型(ViewModel)作为Model和View之间的桥梁,处理业务逻辑并更新View。

(2)创建项目结构:

    根据所选模式,创建相应的项目文件和目录结构。包括Model、View、Controller/Presenter/ViewModel等目录。

(3)实现Model层:

    创建Model类,定义数据结构和数据操作方法。例如,定义数据库访问、网络请求等。

(4)实现View层:

    创建View组件,用于显示用户界面。在View组件中,只处理与用户的交互和显示数据,不处理业务逻辑。

(5)实现Controller/Presenter/ViewModel层:

    根据所选模式,创建相应的Controller、Presenter或ViewModel类。在这些类中实现业务逻辑,包括处理用户输入、数据验证、数据转换等。

2. 练习使用 ViewModel管理界面状态。

    在Android开发中,ViewModel 是 MVVM(Model-View-ViewModel)架构模式中的一个关键组件,用于管理界面(View)的状态和业务逻辑。ViewModel 的主要目的是将数据持久化,即使在配置更改(如屏幕旋转)时也能保持数据的状态。它存储和管理与UI相关的数据,并且可以在数据发生变化时更新UI。

以下是练习使用 ViewModel 管理界面状态的一些步骤:

(1)创建 ViewModel 类:

    继承 ViewModel 类来创建你的 ViewModel。类中定义与界面状态相关的数据和方法。

    // 定义数据

    // 提供获取数据的方法

    // 提供修改数据的方法

(2)在 Activity 或 Fragment 中使用 ViewModel:

    在 Activity 或 Fragment 中,使用 ViewModelProviders 或 ViewModelProvider 来获取 ViewModel 的实例。

        // 获取 ViewModel 实例

        // 设置按钮点击事件

            // 更新UI 

        // 初始化UI

        // 根据 ViewModel 中的数据更新UI

(2)在Activity或Fragment中使用ViewModel

    在Activity或Fragment中,需要获取ViewModel的实例,并使用它来更新和观察数据。

3. 练习在 Android 应用中与界面和状态进行交互。

    在Android应用中,与界面和状态进行交互是常见的任务,通常涉及到监听用户输入、更新UI元素以及管理应用的状态。

(1) 创建数据模型(Model)

首先,创建一个简单的数据模型来表示应用中的状态。

(2)创建ViewModel

    创建一个ViewModel类,它将包含我们要在界面上显示的数据和更新这些数据的方法。

    // 使用LiveData来包装我们的数据模型

    // 提供一个方法来更新用户数据

(3)创建Activity或Fragment

    在Activity或Fragment中,将初始化ViewModel,观察数据变化,并相应地更新UI。

        // 获取ViewModel实例

        // 观察数据变化

                // 更新UI

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本系统的研发具有重大的意义,在安全性方面,用户使用浏览器访问网站时,采用注册和密码等相关的保护措施,提高系统的可靠性,维护用户的个人信息和财产的安全。在方便性方面,促进了校园失物招领网站的信息化建设,极大的方便了相关的工作人员对校园失物招领网站信息进行管理。 本系统主要通过使用Java语言编码设计系统功能,MySQL数据库管理数据,AJAX技术设计简洁的、友好的网址页面,然后在IDEA开发平台中,编写相关的Java代码文件,接着通过连接语言完成与数据库的搭建工作,再通过平台提供的Tomcat插件完成信息的交互,最后在浏览器中打开系统网址便可使用本系统。本系统的使用角色可以被分为用户和管理员,用户具有注册、查看信息、留言信息等功能,管理员具有修改用户信息,发布寻物启事等功能。 管理员可以选择任一浏览器打开网址,输入信息无误后,以管理员的身份行使相关的管理权限。管理员可以通过选择失物招领管理,管理相关的失物招领信息记录,比如进行查看失物招领信息标题,修改失物招领信息来源等操作。管理员可以通过选择公告管理,管理相关的公告信息记录,比如进行查看公告详情,删除错误的公告信息,发布公告等操作。管理员可以通过选择公告类型管理,管理相关的公告类型信息,比如查看所有公告类型,删除无用公告类型,修改公告类型,添加公告类型等操作。寻物启事管理页面,此页面提供给管理员的功能有:新增寻物启事,修改寻物启事,删除寻物启事。物品类型管理页面,此页面提供给管理员的功能有:新增物品类型,修改物品类型,删除物品类型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值