这些笔记将引导您详细了解我的入侵物种赢博体育程序作业的Android解决方案。
为我们的赢博体育程序设置Room数据库的第一步是构造一组Entity类。下面是我的类的代码:
@实体数据类物种(@PrimaryKey() val idspecies: Int, val名称:字符串,val图像:字符串)@实体数据类公园(@PrimaryKey() val idpark: Int = 0, val名称:字符串,val县:字符串)@实体数据类观察(@PrimaryKey(autoGenerate=true) val id: Int = 0, var用户:Int, val公园:Int, val物种:Int, val评论:字符串,val日期:字符串)
事实证明,我们可以同时将这些类用于两个目的。这些类的设置使我们能够在Room数据库中存储物种、公园和观察结果。幸运的是,我们也可以将这些相同的类用于服务器通信。
下一步是准备数据库接口和DAO。这里是这些东西的代码。
@Dao接口InvasiveDao {@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSpecies(species: species) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertPark(park: park) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun inserobservation (observation: observation) @Delete suspend fun removeObservation(observation: observation) @Query(“SELECT * from species ORDER BY name”)suspend fun loadSpecies():List<Species> @Query("SELECT DISTINCT county FROM park ORDER BY county") suspend fun loadCounties(): List<String> @Query("SELECT * FROM park WHERE county=:county ORDER BY name") suspend fun loadCountyParks(county: String): List< park > @Query("SELECT * FROM observation") suspend fun loadObservations(): List< observation >} @Database(entities = [Species::class, park::class, observation::class], version = 1)抽象类InvasiveDatabase: RoomDatabase(){抽象fun invasiveDao():InvasiveDao}
除了一些用于存储实体和在数据库中查找它们的相当明显的方法之外,我还利用了这样一个事实,即我们可以使用一些高级SQL特性来使我们的一些查询更有用。这里最好的例子与公园有关。例如,我在这里设置了一个查询,从公园数据中提取县列表。由于许多县有多个公园,所以我使用SQL SELECT DISTINCT语法提取列表中没有重复的县。我还使用ORDER BY子句将结果按字母顺序排列。拥有县列表将使用户更容易在我的赢博体育程序中找到公园,因为用户可以先选择一个县,然后让赢博体育程序只运行该县的公园的查询。
为了使我们能够与服务器通信,我们还需要一个Retrofit接口。下面是该接口的代码。
数据类User(val iduser: Int = 0, val username: String, val password: String, val realname: String, val phone: String)接口InvasiveApiService {@POST(“用户”)suspend fun newUser(@Body User: User): User @GET(“用户”)suspend fun login(@Query(“用户”)User: String,@Query(“password”)password: String): User @GET(“species”)suspend fun getSpecies(): List< species > @GET(“parks”)suspend fun getParks():List<Park> @POST("observation ") suspend fun postObservation(@Body observation: observation): String}
再一次,我们的赢博体育程序将使用ViewModel类。该类将处理与服务器的通信以及与数据库的通信。
下面的代码显示了类的基本结构以及它的init()方法:
类InvasiveModel(赢博体育程序:赢博体育程序):AndroidViewModel(赢博体育程序){private lateinit var restInterface: InvasiveApiService private lateinit var dao: InvasiveDao val species = mutableStateOf<List< species >>(listOf< species >()) val counties = mutableStateOf<List<String>>(listOf<String>()) val parks = mutableStateOf<List<Park>>(listOf<Park>()) var observations = mutableStateListOf<Observation>() val userid = mutableStateOf(0) init {val db = Room. vardatabaseBuilder(application, InvasiveDatabase::class.java, "invasive").build() dao = db.invasiveDao() val okHttpClient = okHttpClient . builder () .connectTimeout(30, TimeUnit.SECONDS) //连接超时。readtimeout (30, TimeUnit.SECONDS) //读取超时。writetimeout (30, TimeUnit.SECONDS) //写入超时。build() val retrofit:Retrofit = Retrofit. builder () .addConverterFactory(GsonConverterFactory.create()) .baseUrl("https://cmsc106.net/invasive/") .client(okHttpClient) .build() restInterface = Retrofit. create(InvasiveApiService::class.java) loadAllFromDB() if(species.value.isEmpty()) {loadAllFromServer()}}
为了方便我们的组件类,这个模型类将物种、县、县内公园的列表和观察结果存储在状态变量中。
如果您仔细观察这些状态变量,您会注意到其中一些被设置为包含列表的可变状态变量,而另一些则使用mutableStateListOf
这里的另一个复杂的逻辑存在于init()方法中。一旦建立了数据库和服务器的连接,我们必须执行以下步骤:
执行这些步骤的逻辑位于两个助手方法loadAllFromDB()和loadAllFromServer()中。下面是这些方法的代码。
fun loadAllFromDB() {viewModelScope.launch(Dispatchers.IO) {val speciesList = dao. loadspecies () val countiesList = dao. loadcounties () var parksList: List<Park> if(countiesList. isnotempty ()) {parksList = dao。loadCountyParks(county = countiesList[0])} else parksList = listOf<Park>() val observationList = dao.loadObservations() withContext(Dispatchers.Main){物种。value = speciesList values = observationList.toMutableStateList()value = countiesList公园列表value = parksList}}} fun loadAllFromServer() {viewModelScope.launch(dispatcher . io) {try {var speciesList = restInterface.getSpecies() for(sp in speciesList) {dao. insertspecies (sp)} speciesList = dao. loadspecies () var parksList = restInterface.getParks() for(pk in parksList) {dao. insertpark (pk)} val countiesList = dao. loadcounties () parksList = dao. loadcounties()。loadCountyParks(county = countiesList[0]) withContext(Dispatchers.Main){物种。value = speciesList县。value = countiesList公园列表value = parksList}} catch (e:Exception) {e. printstacktrace ()}}
特别注意我们是如何处理loadAllFromServer()中的parks的:
当赢博体育程序启动时,用户将看到的第一个视图是一个着陆屏幕,它提供了对赢博体育程序中其他视图的访问。
赢博体育程序中的其他视图包括一个显示物种名称和图片的图库视图、一个记录观察视图和一个登录视图。下面是这些视图的样子。
注意,图库视图和记录观察视图都有返回箭头,用户可以单击这些箭头返回到登录页面。成功登录或成功创建新用户后,登录视图也会自动将用户返回到登录视图。
我必须在这些视图中实现一些特殊的特性。第一个特殊功能显示在着陆视图上。
为了给用户一些关于赢博体育当前状态的有用反馈,我添加了逻辑,可以根据需要启用或禁用底部的两个按钮。例如,在用户记录了一些观察结果并登录到服务器之后,才启用上传观察结果按钮。同样,一旦用户登录到服务器,登录按钮将被禁用,以让他们知道他们不必登录。
下面是这个视图的代码。
@Composable fun LandingView(vm: InvasiveModel,toLogin:() ->单位,toGallery:() ->单位,toRecord:() ->单位){Column(horizontalAlignment = Alignment。centerhorizontal, verticalArrangement =排列。中心,modifier = modifier . fillmaxsize()){按钮(onClick = {toGallery()}){文本(“物种画廊”)}按钮(onClick = {toRecord()}){文本(“记录观察”)}按钮(onClick = {vm.uploadObservations()}, enabled = vm. observtions . isnotempty () && vm.userid.value != 0){文本(“上传观察”)}按钮(onClick = {toLogin()}, enabled = vm.userid.value == 0){文本(“登录”)}}
启用或禁用底部两个按钮的逻辑是通过向按钮传递一个特殊的启用参数来设置的。我为这个参数使用的值是一个布尔表达式,当我们希望启用按钮时,计算结果为true。
GalleryView包含一些小的专用代码项。
@OptIn(ExperimentalMaterial3Api::class) @Composable fun GalleryView(vm: InvasiveModel, tollanding: () -> Unit) {val speciesList = remember {vm. .species} Scaffold(topBar = {CenterAlignedTopAppBar(title = {Text(" species Gallery",)}), navigationIcon = {IconButton(onClick = tollanding) {Icon(imageVector = icons . automirrored . fill .;)ArrowBack, contentDescription = "Back to Main")}},)},) {innerPadding -> Column(modifier = modifier . verticalscroll (memorberscrollstate ()) .padding(innerPadding)) {val species = speciesList。价值的物种。forEach {sp -> Card() {Box(contentAlignment =对齐。CenterStart, modifier = modifier . fillmaxwidth () .padding(16.dp)) {Row(verticalAlignment = align . centervertical) {AsyncImage(model = sp.image, contentDescription = sp.name, contentScale = contentScale。FillWidth, modifier = modifier .width(200.dp))空格符(modifier = modifier .width(10.dp)) Text(sp.name)}}}}}}}
这里的第一个特殊技巧是在顶部栏中设置一个返回箭头,用户可以使用它返回到登录页面。为了实现这一点,我设置了一个带有顶部栏的Scaffold,然后设置了顶部栏的navigationIcon属性。
下一个技巧涉及保存物种列表及其图片的Column组件。随着图片被添加到列表中,列表的长度变得比屏幕还长。在这种情况下,我们必须设置我们的列滚动,以便用户可以看到赢博体育的条目。我们需要添加一些代码来启用这个功能,因为默认情况下Jetpack Compose中的Columns不会滚动。使Column可滚动的神奇代码是我添加的. verticalscroll()修饰符。
我在这里需要的最后一套技巧涉及到代码,使图像与列表中的物种名称很好地匹配。您可以在显示图片的AsyncImage组件中看到这些设置。我使用contentScale设置和宽度修改器来控制图像的大小。
最后,您应该看一下RecordView中的逻辑。特别要注意县下拉菜单的代码。当用户从菜单中进行选择时,我们到网关,让它获取该县的公园列表。然后,当用户点击下面的公园列表时,只有该县的公园会显示在下拉菜单中。