我们的下一个示例是一个简单的赢博体育程序,用于存储部门中一组人员的目录。该赢博体育程序存储了一个人员列表,每个人都有名字和办公室。
在本例中,我们将获得更多使用屏幕间导航的经验,我们还将深入了解显示和管理项目列表的技术。在这个场景背后,这个例子也将是我们第一次接触到模型类的使用,这是赢博体育程序的一个架构特性,我们将在未来的例子中广泛使用。
赢博体育程序中的第一个视图显示目录。
单击导航栏中的+按钮将用户带到第二个视图,用户可以在其中输入新用户的详细信息。
这个赢博体育程序的主要数据是一个Person对象列表。
package edu.lawrence.androiddirectory import java.util.UUID数据类Person(val id: UUID = UUID. randomuuid (), val name: String, val office: String)
我们赢博体育程序中的顶层组件是DirectoryApp:
@Composable fun DirectoryApp(initialList: List<Person>,modifier: modifier) {val people = remember {initialList. tomutablestatelist ()} val context = LocalContext。当前val navController = memorbernavcontroller () fun saveChanges() {val gson = gson () val json: String = gson. tojson (people) try {context.openFileOutput("people. tojson ")json, MODE_PRIVATE)。使用{fos -> fos.write(json.toByteArray())}} catch(e: IOException) {e. printstacktrace ()}} fun createPerson(name: String,office: String): Unit {val newPerson = Person(name = name,office = office) people.add(newPerson) navController.navigate("list") saveChanges()} fun removePerson(Person:Person) {people.remove(Person) saveChanges()} NavHost(navController,startDestination="list") {composable(route="list") {ListScreen(people = people,doRemove = {Person ->removePerson(Person)},toCreate = {navController.navigate("create")},modifier = modifier} composable(route="create") {CreateScreen(onSave = {name,office -> createPerson(name,office)}, modifier = modifier)}}
关于这个类首先要注意的是people状态变量。这是我们将存储Person对象列表的地方。这是整个赢博体育程序的主要状态变量。
由于人员列表非常重要,因此该组件还包含一组本地函数,这些函数将帮助我们对列表执行重要操作,例如将列表保存到文件中、添加新人员以及从目录中删除人员。
我们在这个类中没有直接处理的一个细节是设置要处理的人员的初始列表。该列表将以参数的形式传递给DirectoryApp组件。下面是负责设置初始列表和启动赢博体育程序的代码:
类MainActivity: ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) enableEdgeToEdge() var initialList: List<Person> try {val json = openFileInput("people.json"). bufferedreader()。使用{it.readText()} val sType = object: TypeToken<List<Person>>(){}。type initialList = Gson().fromJson<List<Person>>(json, sType)} catch (e: IOException) {initialList = listOf(Person(name =" Acacia Ackles",office ="Steitz 131"), Person(name =" Joe Gregg",office ="Briggs 413"), Person(name =" Kurt Krebsbach",office ="Briggs 411"))} setContent {AndroidDirectoryTheme {DirectoryApp(initialList = initialList,modifier = modifier . fillmaxsize ())}}}}
为了将数据保存到本地文件,我们将把Person对象列表转换为JSON,然后将JSON代码保存在一个名为“people.json”的简单文本文件中。在上面的代码中,您可以看到我们是如何从文件中读取人员列表的,而DirectoryApp组件中的saveChanges()函数演示了如何将人员列表保存到同一个文件中。
在DirectoryApp组件中需要注意的另一件重要的事情是在那里设置的导航结构。我们采用了通常的方法,为赢博体育程序中的每个屏幕使用带有单独路由的NavHost()组件。
我们将为赢博体育程序中的两个屏幕分别设置单独的组件。
当赢博体育程序启动时,用户将看到的第一个屏幕是显示目录列表的屏幕。
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ListScreen(people: List<Person>, doRemove: (Person)->Unit,toCreate: ()->Unit,modifier: modifier) {Scaffold(topBar = {TopAppBar(title = {Text("Directory",)}), actions = {IconButton(onClick = {toCreate()}) {Icon(imageVector = Icons.Default.)。添加,contentDescription = “添加人”)}},)},modifier = modifier . fillmaxsize ()) {innerPadding -> Column(verticalArrangement = Arrangement.排列。顶部,modifier = modifier . fillmaxsize ().padding(innerPadding)) {people。forEach {person -> key(person.id) {PersonItem(person = person,onRemove = doRemove)}}}}}
该组件通过使用Scaffold来设置屏幕。因为我们将在这个赢博体育中做一些导航我们需要确保屏幕配备有一个顶部栏它有一个导航按钮能带我们到第二个屏幕。您可以看到用于设置顶栏的代码嵌入到Scaffold组件的代码中。顶部栏通常显示标题和一个或多个操作按钮。在这种情况下,我们需要设置一个操作按钮,将我们导航到第二个屏幕。
这里需要注意的一件重要事情是,实际的导航结构是在这个组件之上的DirectoryApp中设置的。如果我们想做一些导航,我们需要某种方式告诉DirectoryApp为我们做那个导航。处理这个的方式是DirectoryApp将设置一个特殊的回调函数,它导航到第二个屏幕,然后把那个回调函数作为参数传递给我们,toCreate参数。然后,我们所要做的就是将lambda表达式附加到调用回调函数的操作按钮上——该函数将处理到第二个屏幕的导航。
Scaffold的内容区域是一个简单的Column组件,它将显示人员列表。这里需要注意的一个重要特性是使用key()组件包装Column中的每个项。这对于使“滑动-删除”机制对我们的列正确工作是必要的。key()组件为列中的每个项分配一个唯一的键。幸运的是,我已经为Person类配备了我创建的每个Person的唯一id,因此这就为我们显示的每个人提供了唯一键的完美候选项。
people列表中的每个Person将通过使用自定义PersonItem组件显示出来:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun PersonItem(person: person, onRemove::(人)- >单位){val dismissState = rememberSwipeToDismissBoxState (confirmValueChange ={如果(= = SwipeToDismissBoxValue.EndToStart) {onRemove(人)return@rememberSwipeToDismissBoxState真正}return@rememberSwipeToDismissBoxState假},positionalThreshold ={它* .33f}) SwipeToDismissBox(状态= dismissState修饰符= Modifier.fillMaxWidth () .wrapContentHeight (), backgroundContent = {DismissBackground (dismissState)},content = {Row(modifier = modifier . fillmaxwidth () .background(MaterialTheme.colorScheme.background) .padding(horizontal = 16.dp,vertical=6.dp)) {Text(person.name) Spacer(modifier= modifier .width(20.dp)) Text(person.office)}})} @OptIn(ExperimentalMaterial3Api::class) @Composable fun遣散背景(遣散状态:swipeto遣散boxstate) {val color = if(遣散状态。解散方向== swipeto解散boxvalue . endtostart){颜色。红色}else颜色。透明框(modifier = modifier . fillmaxwidth () .background(color) .padding(top=4.dp,bottom=4.dp,end=6.dp), contentAlignment = align{图标(imageVector = Icon . default。删除,contentDescription = null, tint = Color。白)}}
在最基本的形式中,PersonItem可以简单地实现为一行,其中显示一个包含人员姓名的Text组件、一个Spacer组件和一个显示人员办公室的Text组件。
这个组件的代码如此复杂的原因是,我还想实现您将在列表中看到的另一个常见功能,即滑动删除功能。我希望用户能够通过滑动删除手势从目录中删除条目。要在Jetpack中实现这个功能,Compose将不得不在这里使用一个特殊的组件,一个SwipeToDismissBox。该组件反过来还要求我们设置一个单独的遣散背景组件,当用户向左滑动列表项以启动删除过程时,该组件会显示出来。
swipeto解散盒需要的最后一件事是当用户完成手势时调用的一些代码。如果您仔细查看上面的代码,您将看到在我们设置的结构的关键点上调用onRemove()函数。由于我们位于PersonItem组件中,因此需要有人向我们传递onRemove()函数,我们可以调用该函数来执行删除操作。如果您查看ListScreen组件的代码,您将看到该组件从DirectoryApp获得一个函数,然后它将该函数传递给每个PersonItem以用作remove person函数。
这个赢博体育程序需要的最后一个组件是在赢博体育程序中实现第二个屏幕的组件。
@可组合的乐趣CreateScreen(onSave:(字符串,字符串)->单位,modifier: modifier) {val focusManager = LocalFocusManager。当前val name = remember {mutableStateOf("")} val office = remember {mutableStateOf("")} Column(verticalArrangement = Arrangement。居中,horizontalAlignment =对齐。centerhorizontal, modifier = modifier. fillmaxsize ().padding(10.dp)) {Row(horizontalArrangement = Arrangement。开始,verticalalign = align . bottom) {Text("Name:“)空格符(modifier= modifier .width(10.dp)) TextField(value = Name:”)value, onValueChange = {newValue: String ->名称。value = newValue}, keyboardOptions = keyboardOptions (imeAction = imeAction . done), keyboardActions = keyboardActions (onDone = {focusManager.clearFocus()}))} Spacer(modifier= modifier .height(10.dp)) Row(horizontalArrangement = Arrangement。开始,verticalalign = align . bottom) {Text("Office:“)空格符(modifier= modifier .width(10.dp)) TextField(value = Office:”)value, onValueChange = {newValue: String -> office。value = newValue}, keyboardOptions = keyboardOptions (imeAction = imeAction . done), keyboardActions = keyboardActions (onDone = {focusManager.clearFocus()}))}间距器(modifier= modifier .height(20.dp))按钮(onClick = {onSave(name.value,office.value)}) {Text("Save")}}}
这个屏幕基本上是一个表单,用户可以填写它来创建一个新的目录条目。表单由两个状态变量(name和office)提供支持,这两个状态变量依次链接到TextFields,用户可以在其中输入该信息。在表单的底部是一个“保存”按钮,用户可以单击该按钮将新用户保存到目录中,也可以导航回第一个屏幕。
处理赢博体育这些的onSave()函数将从DirectoryApp传递给我们。如果你回头看看DirectoryApp组件的代码,你会看到这个回调函数是如何设置的。