我已经重新实现了在线目录赢博体育程序作为一个基于视图的Android赢博体育程序。该赢博体育程序有三个视图。第一个视图是用户可以登录到服务器或创建新用户帐户的视图。
第二个视图显示目录。
单击顶部栏中的+按钮将用户带到第三个视图,用户可以在其中输入新用户的详细信息。
同样,我们将使用Retrofit库来处理赢博体育服务器交互。这样做的起点是为赢博体育服务器交互设置一个带有方法的接口。
接口DirectoryApiService {@POST(“用户”)有趣的分类列出(@Body用户:用户):调用<字符串> @ get(“用户”)乐趣登录(@Query(“用户”)用户:字符串,@Query(“密码”)密码:字符串):调用<字符串> @ get(“人/{关键}”)有趣getPeople (@ path(“关键”)键:字符串):调用列表< <人> > @POST(“人/{关键}”)有趣postPerson (@ path(“关键”)键:字符串,@Body人:人):调用<人> @DELETE(“人/{关键}/ {id}”)乐趣removePerson (@ path(“关键”)键:字符串,@ path (" id ") id: Int):调用<String>伴侣对象{var directoryService: DirectoryApiService?= null fun getInstance(): DirectoryApiService {if (directoryService == null) {val retrofit = retrofit. builder () .addConverterFactory(GsonConverterFactory.create()) .baseUrl("https://cmsc106.net/directory/") .build() directoryService = retrofit.create(DirectoryApiService::class.java)} return directoryService!!}}
这里有两个重要的区别需要注意。首先,这些方法现在都返回Call对象——下面将详细介绍。第二,我已经将设置服务的代码从ViewModel类移到了这个接口中。
初始化代码位于同伴对象构造中。在Kotlin中,伙伴对象是一种将静态成员变量和静态方法附加到类或接口的方法。在本例中,我使用同伴对象来实现称为单例模式的设计模式。在此模式中,我们设置了特定类型的单个全局实例,并赋予赢博体育程序的每个部分对该对象的访问权。伙伴对象中的directoryService成员变量存储对该单个实例的引用。getInstance()方法负责按需初始化单个对象。
同样,我们的赢博体育程序将使用ViewModel类来提供对服务器的访问。下面是这个类的开头以及它的一个方法:
class DirectoryViewModel: ViewModel() {val userId = MutableLiveData<String>() val error = MutableLiveData<String>() val people = MutableLiveData<List<Person>>() val newPerson = MutableLiveData<Person>() private val service = DirectoryApiService.getInstance() fun newUser(name: String,password: String) {val newUser = User(name,password) val response = service.newUser(newUser) response。enqueue(object: Callback<String>{重载onResponse(call: call <String>, response: response <String>) {if (response. issuccess) {userId.postValue(response.body())}) else{错误。postValue(“新用户失败”)}}重写fun onFailure(call: call <String>, t: Throwable){错误。postValue(“新用户失败”)}})}}
视图模型类存储供赢博体育程序其余部分使用的数据,并提供与服务器通信的方法。这次的不同之处在于,我为成员变量使用了不同的数据类型,服务器代码与我们在Jetpack Compose版本的赢博体育程序中使用的代码略有不同。
在《Jetpack Compose》中,我们大量使用了状态变量。状态变量在普通的基于视图的赢博体育程序中不再可用,所以我已经切换到使用最接近的可用替代方案,实时数据类型。实时数据变量是可以被赢博体育程序的其他部分观察到的变量。当实时数据变量的内容被更新时,它会自动通知我们赢博体育程序中当前正在观察该变量变化的赢博体育部分。在下面的代码中,我们将看到观察这些成员变量变化的变量示例。在这里的代码中,您需要注意的是使用postValue()方法告诉实时数据变量接受一个新值并通知它的观察者。
这里更大的变化是与服务器通信的代码。在Android编程中,我们必须尊重一个基本约束,即赢博体育与服务器的通信都必须在后台线程中进行。在Jetpack Compose版本中,我们通过在协程中运行赢博体育服务器通信代码来处理这个问题。在这里,我们将采用一种不同的方法,以Retrofit提供的Call类的使用为中心。您在上面看到,我们修改了这个赢博体育程序的Retrofit接口,以便从它的每个方法返回Call对象。现在在视图模型中,我们将编写与那些Call对象交互的代码。
下面是我们将如何处理这些Call对象。
排队()
方法。的排队()
方法将此调用置于服务队列。最终,Retrofit管理的后台线程将把这个Call从队列中拉出来,并按照Android的要求在后台运行它。排队()
方法接受一个参数,该参数是一个Callback对象。这个对象反过来用几个成员函数初始化,onResponse ()
和onFailure ()
。当响应从服务器返回或请求失败时,将调用这些回调方法。在我们的赢博体育程序中的第一个视图是一个视图,要求用户登录到服务器上的现有帐户或创建一个新帐户。
以下是该视图视图控制器的完整代码:
类LoginActivity: AppCompatActivity() {lateinit var viewModel: DirectoryViewModel override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_login) viewcompat . setonapplywindowsetslistener (findViewById(R.id.createLayout)) {v, insets -> val systemBars = insets. getinsets (WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars。离开了,systemBars。前,systemBars。viewModel = ViewModelProvider(this)[DirectoryViewModel::class.java] viewModel. userid。观察者{val key = viewModel.userId.value!!val intent = intent (this,ListActivity::class.java) intent. putextra ("Key", Key) startActivity(intent)}) viewModel.error。观察(this,Observer {val text = viewModel.error.value!val duration = Toast。Toast.makeText(this,text,duration).show()}) val name = findViewById<EditText>(R.id.nameEdit) val password = findViewById<EditText>(R.id.passwordEdit) val newUserButton = findViewById<Button>(R.id.newUserButton) val loginButton = findViewById<Button>(R.id.loginButton) newUserButton。setOnClickListener {viewModel.newUser(name.text.toString(),password.text.toString())} loginButton。setOnClickListener {viewModel.login(name.text.toString(),password.text.toString())}}}
这里几乎赢博体育的操作都发生在onCreate()方法中,该方法将在该视图出现时由系统调用。
你可以看到onCreate()中的代码
让我们仔细看看代码,观察一些视图模型的活动数据成员。下面是一个典型的例子:
viewModel.userId。观察者{val key = viewModel.userId.value!!val intent = intent (this,ListActivity::class.java) intent. putextra ("Key", Key) startActivity(intent)})
视图模型的userId成员变量存储了登录用户的用户id。如果查看上一节中创建新用户的示例代码,您将看到该代码在成功将新用户发送到服务器后更新该成员变量。
在这段代码中,我要求观察成员变量的变化。为此,我调用活动数据变量的observe()方法,并向它传递一个用回调初始化的Observer对象。当userId变量改变其值时,将调用该回调。在这个例子中,我想在userId可用时获取它,把它放入一个意图中,并使用那个意图带我们到第二个视图。
赢博体育程序中的第二个视图显示了目录中的人员列表。这是该视图控制器的完整代码。
类ListActivity: AppCompatActivity() {var键:字符串?private lateinit var peopleList: RecyclerView private lateinit var viewModel: DirectoryViewModel private var adapter: PersonAdapter?= null override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_list) viewpat . setonapplywindowinsetslistener (findViewById(R.id.createLayout)) {v, insets -> val systemBars = insets. getinsets (windowinsetspat . type .systemBars()) v. setpadding (systemBars。离开了,systemBars。前,systemBars。右,systemBars.bottom) insets} setSupportActionBar(findViewById(R.id.topbar)) key = intent.getStringExtra(" key ") peopleList = findViewById(R.id.peopleList) peopleList。layoutManager = LinearLayoutManager(this) viewModel = ViewModelProvider(this)[DirectoryViewModel::class.java] viewModel.people。观察(this, Observer {adapter = PersonAdapter(viewModel.people.value!!))) viewModel.getPeople(key!!)} override fun onCreateOptionsMenu(menu: menu ?): Boolean {menuinflter . inflation (r.m uu .directory_menu,menu)返回true} override fun onOptionsItemSelected(item: MenuItem): Boolean {if (item: MenuItem)。itemId == R.id。plus_item & &关键! = null) = {val意图意图(这个,CreateActivity:: class.java) intent.putExtra(“关键”,关键! !)startActivity(意图)}还真}私有内部类PersonHolder(视图:视图):RecyclerView.ViewHolder(视图){val nameTextView: TextView = itemView.findViewById < TextView > (R.id.nameText) val officeTextView: TextView = itemView.findViewById < TextView > (R.id.officeText)}私有内部类PersonAdapter (var人:列表<人>):RecyclerView.Adapter<PersonHolder>() {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonHolder {val view = layoutinflater .充气(R.layout.list_item_people,parent,false) return PersonHolder(view)} override fun getItemCount(): Int{返回people。重载onBindViewHolder(持有人:PersonHolder,位置:Int) {val person = people[position]持有人。赢博体育{nameTextView。text = person.name officeTextView. name文本=人。办公室}}}}
这个控制器管理的视图有两个不同的部分。第一部分是视图顶部的工具栏,它显示标题和单个操作项。第二个也是更重要的部分是一个RecyclerView,它显示目录中的条目列表。在基于视图的Android世界中,推荐使用RecyclerView来显示列表。
这里的onCreateOptionsMenu()和onOptionsItemSelected()方法管理工具栏中的操作。首先必须在res/menus子目录中将其设置为XML菜单资源。第一个方法安装菜单,而第二个方法包含响应菜单选择的代码。因为我们的菜单只包含一个项目,所以这个项目通常会以图标的形式出现在窗口顶部工具栏的右侧。
注意,onOptionsItemSelected()中的代码设置了一个Intent,将我们带到第三个视图,然后启动该Intent。
RecyclerView的设置比较复杂。完全设置它需要三个步骤:
onCreate ()
.onBindViewHolder ()
方法将数据放入列表中:给定一个PersonHolder和列表中的一个位置,我们在人员列表中找到该位置的Person,并将他们的姓名和办公室放入PersonHolder中。在赢博体育程序的最后一个视图中,用户可以为他们的目录创建新条目。以下是该视图视图控制器的完整代码:
类CreateActivity: AppCompatActivity() {var key: String?= null private lateinit var viewModel: DirectoryViewModel override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_create) viewpat . setonapplywindowinsetslistener (findViewById(R.id.createLayout)) {v, insets -> val systemBars = insets. getinsets (windowinsetspat . type .systemBars()) v. setpadding (systemBars。离开了,systemBars。前,systemBars。没错,systemBars.bottom) insets}关键= intent.getStringExtra(“关键”)viewModel = ViewModelProvider(这)[DirectoryViewModel:: class.java] val name = findViewById EditText < > (R.id.newNameText) val办公室= findViewById EditText < > (R.id.newOfficeText) val saveButton = findViewById <按钮> saveButton (R.id.saveButton)。setOnClickListener {val newPerson = Person(name = name.text. tostring (),office=office.text. tostring ()) viewModel。addPerson(key = key!!){viewModel.newPerson}观察(this, Observer {finish()})}}
这里最有趣的代码链接到Save按钮。该代码创建一个新的Person对象,并将该对象传递给视图模型的addPerson()方法。然后,该方法将把此人发布到服务器。当发布成功时,服务器将返回设置了id的Person对象,视图模型将把这个新用户存储在它的newPerson实时数据成员变量中。我还在这里添加了代码来观察该成员变量:当它得到更新时,我将通过调用finish()来响应。那个方法退出这个视图并带我们回到列表视图。
在Jetpack Compose中,视图模型的工作方式与传统的基于视图的赢博体育程序中的工作方式有一个重要的区别。在Jetpack Compose中,我们通常会在赢博体育程序的早期设置一个视图模型对象,然后安排将该视图模型传递给任何需要与之通信的组件。在传统的基于视图的赢博体育程序中,没有很好的机制将视图模型从一个视图传递到另一个视图。我们可以在Intent中放入简单的数据从一个视图传递到另一个视图,但我们不能在Intent中放入更复杂的东西,比如对视图模型的引用。这种限制迫使我们采用不同的策略。
在这个赢博体育程序中,我设置每个视图来创建自己单独的视图模型。为了防止不必要的重复,赢博体育这些视图模型对象将共享用于与服务器通信的同一个服务单例。您将看到我做的另一件事是将重要数据从一个视图模型传递到另一个视图模型。例如,第一个视图中的视图模型将处理用户登录并从服务器获取用户id。当我们让第一个视图转到第二个视图时当我们让第二个视图转到第三个视图时我在intent中传递了那个用户id。在我们到达的每个新视图中,我都会把用户id从意图中拉出来并在做其他事情之前把它放到本地视图模型中。